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

文章目录

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

代码演示

cpp 复制代码
#include <iostream>
#include <unistd.h>  // 必须包含的系统头文件
using namespace std;

int main() {
    // 每次运行 PID 会动态分配,但 PPID 通常为启动进程的 bash 终端 PID
    cout << "当前进程 PID: " << getpid() << "  父进程 PPID: " << getppid() << endl;
    return 0;
}

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. 函数原型与返回值

cpp 复制代码
#include <unistd.h>
pid_t fork();  // 返回值类型为 pid_t(本质是整数类型)

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

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

2. fork() 的用法

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

cpp 复制代码
#include <iostream>
#include <unistd.h> //系统头文件

using namespace std;

int main()
{
	pid_t pid = fork();// 类型pid_t
  	if(pid < 0)
  	{
   		cerr << "Fork Failed!" << endl;
  	}
  	else if(pid > 0)
	{
		//这里是父进程逻辑
		cout << "我是父进程 我的PID = " << getpid() << " 我的子进程的PID = " << pid << endl;
	}
	else
	{
		//这里是子进程的逻辑
		cout << "我是子进程 我的PID = " << getpid()  << " 我的父进程的PID = " << getppid() << endl;
	}
	return 0;
}

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

复制代码
我是父进程 我的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. 父子进程的核心关系

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

代码验证数据独立性

cpp 复制代码
#include <iostream>
#include <unistd.h>
using namespace std;

int main() {
    int i = 0;  // 全局/局部变量均遵循写时复制规则
    pid_t pid = fork();

    if (pid < 0) {
        cerr << "fork 创建子进程失败!" << endl;
    } else if (pid == 0) {
        // 子进程逻辑:未修改 i,读取初始值
        cout << "我是子进程(PID: " << getpid() << "),i 的值:" << i << endl;
    } else {
        // 父进程逻辑:修改 i 的值为 10
        i = 10;
        cout << "我是父进程(PID: " << getpid() << "),i 的值:" << i << endl;
    }
    return 0;
}

运行结果

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

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

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

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

1. 循环创建多子进程

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

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

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>  // wait() 函数头文件
using namespace std;

int main() {
    int child_num = 3;  // 计划创建的子进程数
    for (int i = 1; i <= child_num; i++) {
        pid_t pid = fork();

        if (pid < 0) {
            cerr << "fork 创建子进程失败!" << endl;
            exit(1);  // 创建失败直接退出
        } else if (pid == 0) {
            // 子进程逻辑:打印自身编号和 PID,执行后立即退出
            cout << "我是第 " << i << " 个子进程,PID: " << getpid() << endl;
            exit(0);  // 关键:子进程退出,避免进入下一次循环
        }
    }

    // 父进程逻辑:等待所有子进程退出,避免僵尸进程
    for (int i = 0; i < child_num; i++) {
        wait(NULL);  // 阻塞等待任意一个子进程退出,回收资源
    }
    cout << "所有子进程已退出,父进程(PID: " << getpid() << ")结束" << endl;

    return 0;
}

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会吧每一个回复都告诉博主的,和大家一起交流学习~ 我们下期干货再见!🌸

相关推荐
白昼流星!2 小时前
C++ 封装的经典实践:从立方体到点圆关系的面向对象思考
c++
leiming62 小时前
c++ 利用模板创建一个可以储存任意类型数据的数组类
开发语言·c++·算法
无敌最俊朗@2 小时前
音视频C++开发进阶指南
开发语言·c++·音视频
EthanLifeGreat2 小时前
VSCode ssh远程到低内核版本Linux失败原因分析
linux·ide·vscode
一枚正在学习的小白2 小时前
prometheus监控mysql服务
linux·运维·mysql·prometheus
charlee442 小时前
Ubuntu 下配置 SFTP 服务并实现安全数据共享
linux·ubuntu·sftp·freefilesync
tuokuac2 小时前
Linux的目录结构
linux·运维·服务器
梦仔生信进阶2 小时前
【Linux基础】Linux磁盘空间管理之批量删除文件
linux
MarkHD2 小时前
智能体在车联网中的应用:第6天 核心工具链与仿真世界:从零构建车联网开发环境——Linux Ubuntu与命令行精要指南
linux·运维·ubuntu