跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++

Linux C++ 多进程编程:fork 函数与进程管理机制

Linux 多进程编程通过 fork 系统调用实现进程复制,利用写时复制(COW)机制优化性能。文章涵盖进程基本概念、内存空间划分、状态转换及 PID 标识。深入解析 fork 函数返回值逻辑、父子进程并发执行、进程退出方式差异。讨论僵尸进程与孤儿进程的产生原因及处理方案,介绍 wait 和 waitpid 资源回收机制,帮助理解操作系统进程调度与隔离原理。

疯疯癫癫发布于 2026/3/16更新于 2026/6/1922 浏览
Linux C++ 多进程编程:fork 函数与进程管理机制

Linux/C++ 多进程篇

前言

你有没有想过,为什么你的程序只能一件事一件事地处理?就像一个忙碌的服务员,面对众多顾客,却只能一次服务一个人,其他人只能排队等待。

但在现实生活中,我们常常需要同时处理多件事情:

浏览器同时加载多个网页 服务器同时响应成百上千的客户端请求 视频播放器在播放视频的同时,还能下载后续内容

这背后,靠的就是程序的'分身术'——多进程编程。今天,我们从最底层的内存世界出发,揭开 C++ 多进程'分身术'的神秘面纱。

多进程理论基础

一、为什么要引入多进程

想象一下,你是一个人(单进程),正在厨房里做饭(处理业务)。

  • 现状:你切菜、炒菜、洗碗一把抓。
  • 风险:如果你不小心切到了手(程序崩溃/段错误),或者被一道复杂的菜难住死住了(死循环),整个厨房就瘫痪了,客人吃不上饭,甚至房子都可能着火(服务宕机)。

这时候,'分身术'出现了。
你喊了一声'变!',瞬间变出了另一个你(子进程)。

  • 老大(父进程):负责站在门口迎客、指挥调度、监控老二的状态。
  • 老二(子进程):专门负责进厨房炒菜。

核心收益:
如果老二在厨房里被油烫伤了(子进程崩溃),老大毫发无损!老大可以立刻再变出一个'老三'继续炒菜,客人甚至感觉不到中间停顿了。

这就是多进程存在的第一原动力:隔离与容错。

二、多进程相关概念

  • 进程是程序的一次执行过程,有一定的生命周期,包含了创建态、就绪态、执行态、挂起态、死亡态
  • 进程是计算机资源分配的基本单位,系统会给每个进程分配 0--4G 的虚拟内存,其中 0--3G 是用户空间,3--4G 是内核空间

其中多个进程中 0--3G 的用户空间是相互独立的,但是,3--4G 的内核空间是相互共享的
用户空间细分为:栈区、堆区、静态区

  • 进程的调度机制:时间片轮询上下文切换机制

我们可以看到下图中,我们的 CPU 不停的向我们运行队列中的程序分配时间片,让程序轮流来完成任务,这就是我们的时间片轮询上下文切换机制。

文章配图

  • 并发和并行的区别

并发:针对于单核 CPU 系统在处理多个任务时,使用相关的调度机制,实现多个任务进行细化时间片轮询时,在宏观上感觉是多个任务同时执行的操作,同一时刻,只有一个任务在被 CPU 处理
并行:是针对于多核 CPU 而言,处理多个任务时,同一时间,每个 CPU 处理的任务之间是并行的,实现的是真正意义上多个任务同时执行的

三、进程的内存管理

此处我们就用一图来带大家快速了解我们进程中的内存管理。

文章配图

我们先从左往右来看,我们的操作系统是会通过物理内存来进行映射产生虚拟内存,而我们的虚拟内存中又分为3g 的用户空间与1g 的内核空间。从图中也不难看出,我们的用户空间其实就是我们之前经常所讲的那些内存分配的占用空间,而内核空间其实就是我们操作其他内核代码与数据存储的地方。

  • 物理内存:内存条上(硬件上)真正存在的存储空间
  • 虚拟内存:程序运行后,通过内存映射单元,将物理内存映射出 4G 的虚拟内存,供进程使用

四、进程与程序的区别

  • 进程:是动态的,进程是程序的一次执行过程,是有生命周期的,进程会被分配 0--3G 的用户空间,进程是在内存上存着的
  • 程序:是静态的,没有所谓的生命周期,程序存储在磁盘设备上的二进制文件

文章配图

五、进程的种类

进程一共有三种:交互进程、批处理进程、守护进程

特性交互进程批处理进程守护进程
与终端关系必须关联一个终端(控制终端)通常不关联终端(或与终端无关)脱离终端,无控制终端
用户交互实时交互,等待用户输入无交互,按预设脚本执行无直接用户交互,响应系统请求
运行方式前台或后台(但前台居多)后台运行(作业队列)后台运行,常驻内存
生命周期用户会话期间,用户退出则结束作业执行完即结束系统启动到关闭,长期运行
调度优先级通常较高(响应性要求)可低可高,取决于作业调度通常后台优先级,但重要服务可调高
例子bash、vim、topat作业、batch作业、g++编译任务sshd、httpd、cron

六、进程 PID

那有了进程后,我们的进程之间该怎么区分谁是大哥,谁是二弟呢?这时候就靠我们的进程号 PID 了。

  1. PID(Process ID): 进程号,进程号是一个大于等于 0 的整数值,是进程的唯一标识,不可能重复。
  2. PPID(Parent Process ID): 父进程号,系统中允许的每个进程,都是拷贝父进程资源得到的
  3. 在 linux 系统中的 /proc 目录下的数字命名的目录其实都是一个进程

文章配图

当然,我们的 PID 肯定是有获取方法的,我们待会就会讲到。

七、特殊的进程

  • 0 号进程(idle):他是由 linux 操作系统启动后运行的第一个进程,也叫空闲进程,当没有其他进程运行时,会运行该进程。他也是 1 号进程和 2 号进程的父进程
  • 1 号进程(init):他是由 0 号进程创建出来的,这个进程会完成一些硬件的必要初始化工作,除此之外,还会收养孤儿进程
  • 2 号进程(kthreadd):也称调度进程,这个进程也是由 0 号进程创建出来的,主要完成任务调度问题
  • 孤儿进程:当前进程还正在运行,其父进程已经退出了。

说明:每个进程退出后,其分配的系统资源应该由其父进程进行回收,否则会造成资源的浪费

  • 僵尸进程:当前进程已经退出了,但是其父进程没有为其回收资源

八、linux 中有关进程的指令

  • ps 指令:能够查看当前运行的进程相关属性

    • ps -ef: 能够显示进程之间的关系
      • UID:用户 ID 号
      • PID:进程号
      • PPID:父进程号
      • STIME:开始运行的时间
      • TTY:如果是问号表示这个进程不依赖于终端而存在
      • CMD:名称
    • ps -ajx: 能够显示当前进程的状态
      • PGID:进程组 ID
      • SID:会话组 ID
      • STAT:进程的状态
    • ps -aux: 可以查看当前进程对 CPU 和内存的占用率
      • %CPU:CPU 占用率
      • %MEM:内存占用率
  • top 指令:动态查看进程的相关属性

    此处,表里面的内容其实是会实时变化的,所以叫做动态查看。

  • kill 指令:发送信号的指令

    kill -信号号 进程号

    可以通过指令:kill -l 查看能够发送的信号有哪些

文章配图

  1. 从图可知一共可以发射 62 个信号,而前 32 个是稳定信号,后面剩余的是不稳定信号
  2. 常用的信号
    • SIGHUP:当进程所在的终端被关闭后,终端会给运行在当前终端的每个进程发送该信号,默认结束进程
    • SIGINT:中断信号,当用户键入 ctrl + c 时发射出来
    • SIGQUIT:退出信号,当用户键入 ctrl + \ 时发送,退出进程
    • SIGKILL:杀死指定的进程
    • SIGSEGV:当指针出现越界访问时,会发射,表示段错误
    • SIGPIPE:当管道破裂时会发送该信号
    • SIGALRM:当定时器超时后,会发送该信号
    • SIGSTOP:暂停进程,当用户键入 ctrl+z 时发射
    • SIGTSTP:也是暂停进程
    • SIGUSR1、SIGUSR2:留给用户自定义的信号,没有默认操作
    • SIGCHLD:当子进程退出后,会向父进程发送该信号
  3. 有两个特殊信号:SIGKILL 和 SIGSTOP,这两个信号既不能被捕获,也不能被忽略
  • pidof:查看进程的进程号

    pidof 进程名

文章配图

九、进程状态的切换

  • 可以通过 man ps 进行查看进程的状态

文章配图

进程主状态:
D uninterruptible sleep (usually IO) 不可中断的休眠态,通常是 IO 操作
R running or runnable (on run queue) 运行态
S interruptible sleep (waiting for an event to complete) 可中断的休眠态
T stopped by job control signal 停止态
t stopped by debugger during the tracing 调试时的停止态
W paging (not valid since the 2.6.xx kernel) 已经弃用的状态
X dead (should never be seen) 死亡态
Z defunct ("zombie") process, terminated but not reaped by its parent 僵尸态

附加态:
< high-priority (not nice to other users) 高优先级进程
N low-priority (nice to other users) 低优先级进程
L has pages locked into memory (for real-time and custom IO) 锁在内存中的进程
s is a session leader 会话组组长
l is multi-threaded (using CLONE_THREAD, like NPTL pthreads do) 包含多线程的进程

  • is in the foreground process group 前台运行的进程
  • 状态切换的实例

文章配图

  • 进程主要状态的转换(五态图)

文章配图

多进程的实现

进程的创建过程,是子进程通过拷贝父进程得到的,新进程的创建直接拷贝父进程的资源,只需改变很少部分的数据即可,保留了父进程的大部分的数据信息(遗传基因),所以这个拷贝过程,系统通过一个函数 fork 来自动完成

📖 进程的创建:fork

函数原型pid_t fork(void);
头文件unistd.h
功能通过拷贝父进程得到一个子进程
参数说明无
返回值成功在父进程中得到子进程的 pid,在子进程中的到 0,失败返回 -1 并置位错误码

文章配图

那如果有多个 fork 呢?大家可以先猜一下,下图程序运行后我们的进程总共有多少个,会是 4 个吗?如果有人觉得是 4 个,那不妨想想,我们的程序是不是从第 9 行开始创建新的进程,那么新进程将会从第 10 行开始运行。那么这时候,你还会觉得是 4 个进程吗?此时我们的进程其实就是 8 个。

文章配图

文章配图

如果不关注返回值的话,有 n 个 fork,会产生 2^n 个进程

那么,fork 函数的返回值在上面的表格中我们也说过了,现在我们来看看关注返回值的情况

#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main(int argc, const char *argv[]){
    pid_t pid = -1; //对于父进程而言会得到大于 0 的数字,对于子进程而言会得到 0
    pid = fork(); //创建一个子进程,父进程会将返回值赋值给父进程中的 pid 变量
                  //子进程会将返回值赋值给子进程中的 pid 变量
    printf("pid = %d\n", pid);
    //对 pid 进程判断
    if(pid > 0){ //父进程要做执行的代码
        printf("我是父进程\n");
    }else if(pid == 0){ //子进程要执行的代码
        printf("我是子进程\n");
    }else{
        perror("fork error");
        return -1;
    }
    while(1);
    return 0;
}

对于上述的程序,我们的结果如下

文章配图

那我们现在就可以进行父子进程的并发执行案例了

#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
int main(int argc, const char *argv[]){
    pid_t pid = -1;
    pid = fork(); //创建一个子进程,父进程会将返回值赋值给父进程中的 pid 变量
                  //子进程会将返回值赋值给子进程中的 pid 变量
    //对 pid 进程判断
    if(pid > 0){
        while(1){ //父进程要做执行的代码
            printf("我是父进程 1111\n");
            sleep(1);
        }
    }else if(pid == 0){
        while(1){ //子进程要执行的代码
            printf("我是子进程\n");
            sleep(1);
        }
    }else{
        perror("fork error");
        return -1;
    }
    return 0;
}

值得一提的是,我们可以看看我们执行的结果:

文章配图

📖 父子进程号的获取:getpid、getppid

函数原型pid_t getpid(void);pid_t getppid(void)
头文件sys/types.h
unistd.h
sys/types.h
unistd.h
功能获取当前进程的进程号获取当前进程的父进程 pid 号
参数说明无无
返回值当前进程的进程号当前进程的父进程 pid

📖 进程退出:exit/_exit

上述两个函数都可以完成进程的退出,区别是在退出进程时,是否刷新标准 IO 的缓冲区

exit属于库函数,使用该函数退出进程时,会刷新标准 IO 的缓冲区后退出

_exit属于系统调用(内核提供的函数),使用该函数退出进程时,不会刷新标准 IO 的缓冲区

函数原型void exit(int status);void _exit(int status);
头文件stdlib.hstdlib.h
功能退出当前进程,并刷新当前进程打开的标准 IO 的缓冲区退出当前进程,不刷新当前进程打开的标准 IO 的缓冲区
参数说明进程退出时的状态,会将改制 与 0377 进行位与运算后,返回给回收资源的进程进程退出时的状态,会将改制 与 0377 进行位与运算后,返回给回收资源的进程
返回值无无

📖 进程资源回收:wait、waitpid

有两个函数可以完成对进程资源的回收

wait:阻塞回收任意一个子进程的资源函数

waitpid:可以阻塞,也可以非阻塞完成对指定的进程号进程资源回收

函数原型waitwaitpid
头文件sys/types.h
sys/wait.h
sys/types.h
sys/wait.h
功能阻塞回收子进程的资源可以阻塞也可以非阻塞回收指定进程的资源
参数说明接收子进程退出时的状态,获取子进程退出时的状态与 0377 进行位与后的结果,如果不愿意接收,可以填 NULL参数 1:进程号
    >0:表示回收指定的进程,进程号位 pid(常用)
    =0:表示回收当前进程所在进程组中的任意一个子进程
    =-1:表示回收任意一个子进程(常用)
    <-1:表示回收指定进程组中的任意一个子进程,进程组 id 为给定的 pid 的绝对值

参数 2:
        接收子进程退出时的状态,获取子进程退出时的状态与 0377 进行位与后的结果,如果不愿 意接收,可以填 NULL

参数 3:是否阻塞
        0:表示阻塞等待
        WNOHANG:表示非阻塞
返回值成功返回回收资源的那个进程的 pid 号,失败返回 -1 并置位错误码>0: 返回的是成功回收的子进程 pid 号
=0:表示本次没有回收到子进程
=-1:出错并置位错误码
#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main(int argc, const char *argv[]){
    pid_t pid = -1; //定义用于存储进程号的变量
    //创建进程
    pid = fork();
    if(pid > 0) { //父进程程序代码
        //输出当前进程号、子进程号、父进程号
        printf("self pid=%d, child pid = %d, parent pid = %d\n", getpid(), pid, getppid());
        sleep(8); //休眠 3 秒
        //wait(NULL); //回收子进程资源,只有回收了子进程资源后,父进程才继续向后执行
        waitpid(-1, NULL, WNOHANG); //非阻塞回收子进程资源
        printf("子进程资源已经回收\n");
    } else if(pid == 0) { //子进程程序代码
        //输出当前进程的进程号、父进程进程号
        printf("self pid = %d, parent pid = %d\n", getpid(), getppid());
        //提出子进程
        printf("11111111111111111111111111111111111");
        //没有加换行,不会自动刷新
        sleep(3);
        exit(EXIT_SUCCESS); //刷新缓冲区并退出进程
        //_exit(EXIT_SUCCESS); //不刷新缓冲区退出进程
    } else {
        perror("fork error");
        return -1;
    }
    while(1); //防止进程退出
    return 0;
}

下图为运行效果

文章配图

📖 僵尸进程和孤儿进程

  • 孤儿进程:当前进程还正在运行,其父进程已经退出了。

每个进程退出后,其分配的系统资源应该由其父进程进行回收,否则会造成资源的浪费

我们不妨来看看,父进程不回收资源会发生什么

#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main(int argc, const char *argv[]){
    pid_t pid = fork();
    if(pid > 0){
        sleep(5);
        exit(EXIT_SUCCESS);
    }else if(pid == 0){
        while(1){
            printf("我是子进程\n");
            sleep(1);
        }
    }else{
        perror("fork error");
        return -1;
    }
    return 0;
}

文章配图

可以看到在 Linux 中,孤儿不会流落街头。它们会被系统的'福利院院长'—— init 进程 (PID 1) 收养。init 进程会定期帮它们收尸,所以孤儿进程通常无害。

  • 僵尸进程:当前进程已经退出了,但是其父进程没有为其回收资源
#include<iostream>
#include<cstdio>
#include<cstring>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main(int argc, const char *argv[]){
    pid_t pid = fork();
    if(pid > 0){
        while(1){
            printf("我是父进程\n");
            sleep(1);
        }
    }else if(pid == 0){
        sleep(5);
        exit(EXIT_SUCCESS);
    }else{
        perror("fork error");
        return -1;
    }
    return 0;
}

效果如下:

文章配图

深度解析:fork 中的黑魔法

那么学完应用方面,再给大家提一点点细枝末节。

我相信很多初学者以为 fork() 是把整个程序从头到尾复制了一遍,就像复印机一样。
**那你就错了,**如果每次分身都完整复制几 GB 的内存,电脑早就卡死了。

操作系统(OS)使用了一个极其聪明的机制:写时复制(Copy-On-Write, COW)。

通俗解释 COW:

  1. 分身瞬间:
    • 父进程和子进程共享同一块物理内存。
    • OS 只是把这块内存标记为'只读',并给父子进程各自发了一本新的'地图'(页表)。
    • **此时,没有发生任何数据拷贝!**速度极快,几乎瞬间完成。
  2. 读取数据时:
    • 父子进程去读变量 int a = 10;。
    • 因为大家读的都是同一块只读内存,完全没问题,继续共享。
  3. 修改数据时(关键点!):
    • 假设子进程说:'我要把 a 改成 20'。
    • OS 拦截了这个操作(触发缺页中断)。
    • OS 心想:'你要改?行,那我单独给你复印一份这一页内存,你在你的副本上随便改,别影响你爸。'
    • 只有在这时,真正的内存拷贝才发生。

文章配图

那么现在再来想这么一个问题:fork 之后,父子进程的内存关系吗?如果我在 fork 前定义了一个全局变量,fork 后子进程改了它,父进程会变吗?

这个问题的核心就在于理解 Copy-On-Write (写时复制) 机制。

首先,当调用 fork() 时,操作系统并没有立即在物理内存中复制一份父进程的数据。父子进程在初始阶段是共享同一块物理内存页的,只不过这些页被标记为了只读。此时,如果父子进程都只是读取那个全局变量,它们访问的是同一个物理地址,效率极高。
其次,关键在于写入。当子进程尝试修改这个全局变量时,CPU 会触发一个缺页异常(Page Fault)。操作系统内核捕获到这个异常后,意识到子进程要'写'了,于是它会真正地为子进程分配一块新的物理内存,将原数据复制过去,然后让子进程在新的内存页上修改。而父进程的页表依然指向原来的物理内存。
所以我们的结论就出来了:读操作:父子进程共享物理内存,互不影响(因为都没改)。写操作:一旦发生写操作,内存即刻分离。子进程修改变量,绝对不会影响父进程的值,因为它们此刻已经指向了不同的物理地址。
这种机制既保证了进程间的隔离性(数据安全),又极大优化了性能(避免了不必要的拷贝)。这也是为什么在 C++ 后端中,即使创建大量进程,系统开销也是可控的原因。

结语

今天我们揭开了 C++ 多进程'分身术'的面纱:

  • fork() 是咒语。
  • COW 是省钱的秘诀。
  • 返回值 是身份的证明。
  • wait 是负责任的态度。

但这只是开始。两个独立的进程,如果想互相传个话(比如父进程告诉子进程'该下班了'),或者传个大文件,该怎么办?它们不能直接读对方的内存,那就要用到更有趣的 IPC(进程间通信)技术了:管道、消息队列、共享内存,我们下一篇文章将会带来这些内容的讲解

目录

  1. Linux/C++ 多进程篇
  2. 前言
  3. 多进程理论基础
  4. 一、为什么要引入多进程
  5. 二、多进程相关概念
  6. 三、进程的内存管理
  7. 四、进程与程序的区别
  8. 五、进程的种类
  9. 六、进程 PID
  10. 七、特殊的进程
  11. 八、linux 中有关进程的指令
  12. 九、进程状态的切换
  13. 多进程的实现
  14. 📖 进程的创建:fork
  15. 📖 父子进程号的获取:getpid、getppid
  16. 📖 进程退出:exit/_exit
  17. 📖 进程资源回收:wait、waitpid
  18. 📖 僵尸进程和孤儿进程
  19. 深度解析:fork 中的黑魔法
  20. 结语
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 空洞卷积(Dilated Convolution)网络架构及 PAMAP2 数据集实战
  • DeepSeek R1 与 GPT 的区别及实战应用技巧
  • Llama-Factory 是否支持 RLHF?现状与实践路径
  • 低代码平台Python插件开发指南
  • SpringAI Agent 开发实战:基于 Skills 的代码评审实践
  • ROS2 使用 slam_toolbox 进行激光雷达建图
  • 前端地图 SDK 集成指南:高德百度腾讯 Google Maps 初始化与配置
  • OpenClaw 中 web_search 与 web_fetch 最佳实践速查
  • Vue入门到精通:从零开始学Vue
  • 高精度算法详解:大整数加减乘除实现与原理
  • 基于 SpringBoot 的 SSM 小区失物招领系统设计
  • Visual C++ Redistributable 安装问题排查与修复指南
  • 2024 大模型从业者的至暗时刻
  • 基于 Java 的实体店综合管理系统设计与实现
  • LLM 对齐方案升级:WizardLM、BackTranslation 与 Self Alignment
  • 基于Llama-Factory/Qwen2.5-1.5b自定义数据集LoRA微调实战【PPO/RLHF/训练/评估】
  • CarelessWhisper:将 Whisper 改造为低延迟因果流式语音识别模型
  • 生成式人工智能与大语言模型在医疗保健领域的全面融合路线图
  • C++ 技术面试常见问题解析(三)
  • 基于 Leaflet 和天地图的长沙免费运动场所 WebGIS 可视化

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online