news 2026/4/23 5:08:13

Linux 进程核心解析 fork()详解 多进程的创建与回收 C++

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux 进程核心解析 fork()详解 多进程的创建与回收 C++

文章目录

  • 一、进程
    • 1. task_struct 与核心标识符
    • 2. 系统调用获取进程 ID
    • 3. 终端查看进程 ID
    • 4. 进程和程序的区别
  • 二、/proc 目录
    • 1. 核心查看方式
    • 2. 两个关键软链接
  • 三、fork() 进程的创建
    • 1. 函数原型与返回值
    • 2. fork() 的用法
    • 3. 为什么 fork() 会返回两次?
    • 4. 父子进程的核心关系
  • 四、多进程创建、调度与回收
    • 1. 循环创建多子进程
    • 2. 多进程的执行顺序
    • 3. 进程退出与资源回收
  • 五、其他进程创建接口:fork() vfork() exec 系列
    • 1. 核心接口对比
    • 2. fork() 与 vfork() 的关键区别(高频考点)
  • 六、总结
    • 1. 核心概念图谱
    • ==3. 编程实战要点==
  • 七、练习
    • (一)基础概念
    • (二)进阶题
    • (三)编程题


一、进程

在 Linux 中,进程是代码段 + 数据段 + PCB(进程控制块),是操作系统进行资源分配和调度的基本单位

1. task_struct 与核心标识符

Linux 内核通过struct task_struct(即 PCB)结构体完整描述进程,它存储了进程的所有关键属性,是 OS 管理进程的核心数据结构。其中最基础且关键的两个属性的是:

  • PID (Process ID):进程的唯一标识符,相当于进程的“身份证号”,用于 OS 区分不同进程。
  • PPID (Parent Process ID):父进程的标识符,标记当前进程的创建者,体现进程间的父子亲缘关系。

2. 系统调用获取进程 ID

在代码中,可通过以下系统调用接口直接获取进程的 PID 和 PPID,需包含头文件<unistd.h>

函数头文件功能
getpid()<unistd.h>获取当前进程的 PID
getppid()<unistd.h>获取当前进程的父进程 PID

代码演示

#include<iostream>#include<unistd.h>// 必须包含的系统头文件usingnamespacestd;intmain(){// 每次运行 PID 会动态分配,但 PPID 通常为启动进程的 bash 终端 PIDcout<<"当前进程 PID: "<<getpid()<<" 父进程 PPID: "<<getppid()<<endl;return0;}

3. 终端查看进程 ID

除了代码获取,还可通过终端命令直接查看进程的 PID 和 PPID,最常用的命令是:

  • ps -ef:查看系统所有进程的完整信息(包括 PID、PPID、进程状态等)。

4. 进程和程序的区别

可以这样生动的理解:
程序“躺在磁盘上的说明书”:
是静态的指令集合(比如.exe.out文件),只占存储资源,不运行、不占用CPU/内存等系统资源,没有生命周期。

进程“按照说明书干活的工人”:
是程序的一次动态执行实例,会加载程序的指令和数据到内存,占用CPU、内存、PID等系统资源,有“创建→运行→等待→退出”的完整生命周期。

二者的核心对应关系:

  • 一个程序可以对应多个进程(比如多次打开同一个浏览器,就是一个浏览器程序对应多个浏览器进程)
  • 一个进程也能切换执行的程序(比如通过exec函数,shell进程可以切换执行ls程序)。

二、/proc 目录

Linux 提供了特殊的虚拟文件系统/proc,它不占用实际磁盘空间,而是实时映射内核中的进程数据,是查看进程动态信息的核心途径。

所以/proc 不是磁盘类别的文件,本质是内核数据的接口

1. 核心查看方式

当进程启动后,系统会自动在/proc目录下创建一个以该进程PID命名的文件夹,所有与该进程相关的信息都存储在这个文件夹中:

  • 查看命令:ls /proc/[PID](将[PID]替换为我们要查的实际进程 ID,如ls /proc/1234)。

2. 两个关键软链接

在每个/proc/[PID]目录下,有两个极具实用价值的软链接,清晰区分了“程序本体”和“运行环境”:

  • cwd(Current Working Directory):指向进程当前的工作目录
    • 应用场景:代码中使用相对路径(如fopen("test.txt", "w"))操作文件时,文件会默认创建在cwd对应的目录下,而非程序所在目录。
  • exe(Executable):指向进程对应的二进制程序文件的绝对路径。
    • 核心区别:exe定位的是“程序本身在哪里”,cwd定位的是“程序在哪个目录下运行”。例如:/usr/bin/ls程序的exe/usr/bin/ls,但cwd可能是/home/user(取决于运行时的目录)。

三、fork() 进程的创建

fork()是 Linux 创建子进程的核心系统调用,也是进程编程的重中之重,因为其具有“一次调用、两次返回”的特性

1. 函数原型与返回值

#include<unistd.h>pid_tfork();// 返回值类型为 pid_t(本质是整数类型)

fork()的返回值是区分父子进程的关键,不同返回值对应不同进程身份:

  • 返回 0:当前执行流程处于子进程中(子进程没有子进程,故返回 0 标识自身)。
  • 返回 >0 的整数:当前执行流程处于父进程中,返回值即为新创建子进程的 PID(父进程需通过 PID 管理子进程)。
  • 返回 -1:创建子进程失败(如系统进程数达到上限),需在代码中处理错误。

2. fork() 的用法

我们来编写一个C/C++程序,使用fork()创建一个子进程,父进程打印自身PID和子进程PID,子进程打印自身PID和父进程PID,直观的理解fork()的用法和其返回值的特点。

#include<iostream>#include<unistd.h>//系统头文件usingnamespacestd;intmain(){pid_t pid=fork();// 类型pid_tif(pid<0){cerr<<"Fork Failed!"<<endl;}elseif(pid>0){//这里是父进程逻辑cout<<"我是父进程 我的PID = "<<getpid()<<" 我的子进程的PID = "<<pid<<endl;}else{//这里是子进程的逻辑cout<<"我是子进程 我的PID = "<<getpid()<<" 我的父进程的PID = "<<getppid()<<endl;}return0;}

这段代码中乍一看和我们之前的代码没什么区别,但是我们将这个代码跑起来之后我们会发现控制台打印的信息是:

我是父进程 我的PID = 1234 我的子进程的PID = 1235 我是子进程 我的PID = 1235 我的父进程的PID = 1234

那么看输出我们就会发现,为什么一个程序if的两个路径都可以走??这明显不符合我们的之前的理解,明明pid是一个变量,怎么可能同时满足pid > 0pid == 0两个条件,让if的两个分支都执行了?

这背后的核心原因,是fork()函数并非普通的函数调用——它会创建一个新的进程,并且会有两次返回。我们之前对代码执行流程的理解,都是基于单个进程的线性执行,而fork()打破了这个逻辑,我们需要从进程复制与独立执行的角度重新理解这段代码。

3. 为什么 fork() 会返回两次?

当程序执行到pid_t pid = fork();这一行时,操作系统会做以下几件关键的事:

  1. 创建子进程:操作系统会复制当前的父进程(包括进程的内存空间、代码段、数据段、寄存器状态等),生成一个全新的子进程
  2. 两个进程独立运行:从这一行代码开始,父进程和子进程会作为两个完全独立的进程,同时继续执行后续的代码
  3. 不同的返回值fork()函数会分别给父进程和子进程返回不同的值:
    • 父进程返回子进程的PID(一个大于0的整数);
    • 子进程返回0;
    • 如果创建失败,只给父进程返回-1(子进程不会被创建)。

我们把代码的执行过程拆分成步骤,就能清晰看到整个逻辑:

步骤1:父进程执行到fork()前
此时只有一个父进程(假设PID为1234),代码执行到pid_t pid = fork();这一行,准备调用fork()

步骤2:fork()创建子进程,产生两个执行流
操作系统复制父进程,生成子进程(PID为1235)。此时:

  • 父进程的执行流fork()返回子进程的PID(1235),因此pid变量的值是1235(>0)。
  • 子进程的执行流fork()返回0,因此pid变量的值是0。

步骤3:两个进程分别执行后续的if逻辑

  • 父进程:因为pid > 0,进入else if(pid > 0)分支,打印父进程PID和子进程PID。
  • 子进程:因为pid == 0,进入else分支,打印子进程PID和父进程PID。

这两个进程的执行是并行的(具体执行顺序由操作系统的进程调度器决定,可能父进程先执行,也可能子进程先执行),所以我们会在控制台看到两个分支的输出结果。

如果把进程比作一个正在读剧本(代码)的演员:

  • fork()之前,只有一个演员(父进程)在读剧本;
  • fork()发生时,突然复制出一个一模一样的新演员(子进程),两个演员拿着相同的剧本;
  • fork()这一行开始,两个演员继续读剧本,但他们会根据导演(操作系统)给的不同提示(fork()的返回值),做出不同的动作(执行不同的分支)。

4. 父子进程的核心关系

  • 代码共享:父子进程共用一套代码段(只读),不会重复存储,节省内存资源。
  • 数据独立:初始时数据完全一致,但一旦某一方修改数据,写时复制机制会触发,为修改方开辟独立内存,双方数据互不干扰。

代码验证数据独立性

#include<iostream>#include<unistd.h>usingnamespacestd;intmain(){inti=0;// 全局/局部变量均遵循写时复制规则pid_t pid=fork();if(pid<0){cerr<<"fork 创建子进程失败!"<<endl;}elseif(pid==0){// 子进程逻辑:未修改 i,读取初始值cout<<"我是子进程(PID: "<<getpid()<<"),i 的值:"<<i<<endl;}else{// 父进程逻辑:修改 i 的值为 10i=10;cout<<"我是父进程(PID: "<<getpid()<<"),i 的值:"<<i<<endl;}return0;}

运行结果

我是父进程(PID: 1234),i 的值:10 我是子进程(PID: 1235),i 的值:0

结论:父进程修改i后,子进程的i仍为初始值 0,验证了父子进程数据独立的特性。

四、多进程创建、调度与回收

实际开发中一定需创建多个子进程处理并发任务,需掌握正确的创建逻辑、调度规则及资源回收机制,避免僵尸进程等问题。

1. 循环创建多子进程

循环调用fork()时,需注意:子进程会继承父进程的循环变量,若不及时退出,子进程会继续创建“孙子进程”,导致子进程数呈指数增长(如循环 3 次可能创建 7 个子进程)。

创建 3 个子进程的正确代码

#include<iostream>#include<unistd.h>#include<sys/wait.h>// wait() 函数头文件usingnamespacestd;intmain(){intchild_num=3;// 计划创建的子进程数for(inti=1;i<=child_num;i++){pid_t pid=fork();if(pid<0){cerr<<"fork 创建子进程失败!"<<endl;exit(1);// 创建失败直接退出}elseif(pid==0){// 子进程逻辑:打印自身编号和 PID,执行后立即退出cout<<"我是第 "<<i<<" 个子进程,PID: "<<getpid()<<endl;exit(0);// 关键:子进程退出,避免进入下一次循环}}// 父进程逻辑:等待所有子进程退出,避免僵尸进程for(inti=0;i<child_num;i++){wait(NULL);// 阻塞等待任意一个子进程退出,回收资源}cout<<"所有子进程已退出,父进程(PID: "<<getpid()<<")结束"<<endl;return0;}

2. 多进程的执行顺序

  • 核心规则:多个子进程的执行顺序由OS 的 CPU 调度器决定,与创建顺序无关,属于“抢占式调度”(板书“多进程顺序由 OS 调度器决定,不确定”)。
  • 现象:每次运行程序,子进程的打印顺序可能不同,这是正常现象,若需固定顺序,需使用信号、管道等同步机制。

3. 进程退出与资源回收

(1)两种特殊进程

  • 孤儿进程:父进程先于子进程退出,子进程会被 1 号init进程(或systemd进程)领养,由领养进程负责回收资源,不会造成资源泄漏。
  • 僵尸进程:子进程退出后,父进程未调用wait()waitpid()回收其退出状态,子进程的 PCB 会残留在内核中,占用 PID 等系统资源,长期积累会导致系统资源耗尽。

(2)父进程回收子进程的核心接口

  • wait(NULL):阻塞等待任意一个子进程退出,回收其资源,无法指定回收某个子进程。
  • waitpid(pid_t pid, int *status, int options):更灵活的回收接口,支持:
    • 指定回收某个 PID 的子进程(pid参数)。
    • 非阻塞回收(options设为WNOHANG)。
    • 获取子进程的退出状态(通过status参数)。

五、其他进程创建接口:fork() vfork() exec 系列

除了fork(),Linux 还提供了其他进程创建相关的系统调用,需明确其核心区别:

1. 核心接口对比

接口功能描述核心特性
fork()创建子进程,复制父进程地址空间写时复制(独立地址空间),父子进程执行顺序不确定
vfork()创建子进程,共享父进程地址空间共享内存,子进程先执行,父进程挂起至子进程exec或退出
exec系列(execl/execv等)在当前进程中加载新程序,替换原有代码和数据不创建新进程,仅替换进程的代码段和数据段,PID 保持不变
clone()底层通用接口,可创建进程或线程灵活控制资源共享程度(如线程共享地址空间,进程独立)

2. fork() 与 vfork() 的关键区别(高频考点)

对比维度fork()vfork()
地址空间写时复制,父子进程独立完全共享父进程地址空间
执行顺序由调度器决定,不确定子进程优先执行,父进程挂起
数据修改修改数据触发写时复制,不影响父进程修改数据直接改变父进程内存,易引发问题
适用场景通用进程创建场景子进程创建后立即调用exec加载新程序(避免数据冲突)

六、总结

1. 核心概念图谱

进程 = 代码段 + 数据段 + PCB(task_struct) ↓ ↓ ↓ 只读共享 写时复制 存储进程属性(PID/PPID/状态等)
  • 进程 vs 程序:程序是静态的指令集合(如.out文件),进程是动态的执行过程(有生命周期)。
  • OS 管理进程的核心:通过遍历 PCB 链表,实现进程的调度、资源分配和状态管理。

3. 编程实战要点

  • 创建多子进程时,子进程需及时exit(),避免创建“孙子进程”。
  • 父进程必须回收子进程资源,防止僵尸进程。
  • 区分相对路径和绝对路径的使用场景(与cwd相关)。

七、练习

(一)基础概念

  1. 进程概念回顾:什么是进程?进程和程序的本质区别是什么?OS管理进程的核心数据结构是什么(以Linux为例)?

  2. 进程属性基础:Linux中进程的PID、PPID分别代表什么?如何在终端查看一个进程的PID和PPID?

  3. 子进程基础:什么是子进程?子进程和父进程的关系是什么?父进程退出后,子进程会变成什么进程?

  4. fork函数基础:Linux中创建子进程的核心系统调用函数是什么?这个函数的最特殊的特点是什么(返回值层面)?

(二)进阶题

  1. fork函数返回值:调用fork()后,为什么会有两个返回值?父进程和子进程分别拿到的返回值是什么?如果fork()调用失败,返回值是什么?

  2. 多进程执行逻辑:创建多进程时,多个子进程的执行顺序是由什么决定的?OS的调度器在其中起到了什么作用?

  3. 系统接口关联:除了fork(),Linux中还有哪些创建进程的相关系统调用(比如vfork()exec系列函数)?fork()vfork()的核心区别是什么?

  4. 进程退出:父进程如何等待子进程退出?如果父进程不等待子进程,会产生什么问题?

(三)编程题

  1. 基础题:编写一个C/C++程序,使用fork()创建一个子进程,父进程打印自身PID和子进程PID,子进程打印自身PID和父进程PID。

  2. 思考题尝试在代码中验证“子进程复制父进程地址空间”:在fork()前定义一个全局变量i=0,父进程将i改为10,子进程打印i的值,观察结果并解释原因。

  3. 进阶题:编写一个C/C++程序,创建3个子进程,每个子进程打印自己的PID和“我是第X个子进程”,父进程等待所有子进程退出后打印“所有子进程已退出”。

Doro又又又带着小花🌸来啦!🌸超级奖励🌸给坚持看到这里的你!能沉下心把Linux进程这部分核心知识啃完,你真的超棒的~ 如果你觉得这篇博客把晦涩的进程概念讲得清晰易懂,帮你理解了了fork创建子进程、进程回收这些知识点的,别忘了动动小手点个【点赞】和【收藏】呀!这样后续复习进程相关知识点时,就能快速找到这份干货满满的笔记啦~

赶紧和Doro一起关注这个博主吧!他悄悄的告诉了Doro说他后续会持续更新Linux系统编程、进程通信、线程等系列干货内容的,把复杂的技术点拆解得明明白白,一步步夯实编程基础~ 另外,文中练习题的详细答案已经放在评论区咯!如果做练习时遇到困惑,或者对进程知识点有任何疑问、想法,都欢迎在评论区留言讨论,Doro会吧每一个回复都告诉博主的,和大家一起交流学习~ 我们下期干货再见!🌸

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/23 16:12:11

5个理由告诉你为什么Lepton是最佳代码片段管理器

5个理由告诉你为什么Lepton是最佳代码片段管理器 【免费下载链接】Lepton &#x1f4bb; Democratizing Snippet Management (macOS/Win/Linux) 项目地址: https://gitcode.com/gh_mirrors/le/Lepton 在现代软件开发中&#xff0c;一个优秀的代码片段管理器能够显著提升…

作者头像 李华
网站建设 2026/4/23 14:43:44

Langchain-Chatchat版本回退功能:误操作后的内容恢复方法

Langchain-Chatchat 版本回退功能&#xff1a;误操作后的内容恢复方法 在企业级知识库系统日益普及的今天&#xff0c;一个看似微小的操作失误&#xff0c;可能引发连锁反应——比如误删关键文档、错误重建索引导致问答失准&#xff0c;甚至因模型升级失败使整个服务陷入“有问…

作者头像 李华
网站建设 2026/4/21 16:07:02

华东师大:深耕AI与艺术设计教育 构建教学新模式

在人工智能技术迅猛发展的今天&#xff0c;其与艺术设计教育的深度融合正不断拓展教学与创作的边界。华东师范大学设计学院毛溪教授正是这一交叉领域教育创新的先行者与推动者。她以“人工智能与艺术设计”为核心&#xff0c;构建了从高校通识教育到中小学创新实践、从课程建设…

作者头像 李华
网站建设 2026/4/23 16:05:28

Alacritty在WSL2中的终极渲染修复指南:告别模糊和闪烁

Alacritty在WSL2中的终极渲染修复指南&#xff1a;告别模糊和闪烁 【免费下载链接】alacritty A cross-platform, OpenGL terminal emulator. 项目地址: https://gitcode.com/GitHub_Trending/al/alacritty 如果你在WSL2环境中使用Alacritty终端时遇到了字体模糊、字符错…

作者头像 李华
网站建设 2026/4/15 17:57:40

弧焊机器人节气设备

在摩托车制造业的焊接环节&#xff0c;机器人焊接技术成了行业新趋势。不管是大规模生产还是专业零部件制造&#xff0c;工业机器人的应用已很普遍。但在这些高效精准的自动化焊接背后&#xff0c;保护气体使用效率低是个长期被忽视的问题。摩托车车架的复杂构造&#xff0c;要…

作者头像 李华
网站建设 2026/4/20 6:58:58

Langchain-Chatchat备份与恢复策略:保障知识库数据安全

Langchain-Chatchat备份与恢复策略&#xff1a;保障知识库数据安全 在企业加速推进智能化转型的今天&#xff0c;越来越多组织选择部署本地化的大语言模型&#xff08;LLM&#xff09;问答系统来管理内部知识资产。尤其在金融、医疗和法律等对数据隐私高度敏感的行业&#xff0…

作者头像 李华