文章目录
- 一、进程
-
- [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 > 0和pid == 0两个条件,让if的两个分支都执行了?
这背后的核心原因,是 fork()函数并非普通的函数调用------它会创建一个新的进程,并且会有两次返回 。我们之前对代码执行流程的理解,都是基于单个进程 的线性执行,而fork()打破了这个逻辑,我们需要从进程复制与独立执行的角度重新理解这段代码。
3. 为什么 fork() 会返回两次?
当程序执行到pid_t pid = fork();这一行时,操作系统会做以下几件关键的事:
- 创建子进程 :操作系统会复制当前的父进程(包括进程的内存空间、代码段、数据段、寄存器状态等),生成一个全新的子进程。
- 两个进程独立运行 :从这一行代码开始,父进程和子进程会作为两个完全独立的进程,同时继续执行后续的代码。
- 不同的返回值 :
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参数)。
- 指定回收某个 PID 的子进程(
五、其他进程创建接口: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相关)。
七、练习
(一)基础概念
-
进程概念回顾:什么是进程?进程和程序的本质区别是什么?OS管理进程的核心数据结构是什么(以Linux为例)?
-
进程属性基础:Linux中进程的PID、PPID分别代表什么?如何在终端查看一个进程的PID和PPID?
-
子进程基础:什么是子进程?子进程和父进程的关系是什么?父进程退出后,子进程会变成什么进程?
-
fork函数基础:Linux中创建子进程的核心系统调用函数是什么?这个函数的最特殊的特点是什么(返回值层面)?
(二)进阶题
-
fork函数返回值 :调用
fork()后,为什么会有两个返回值?父进程和子进程分别拿到的返回值是什么?如果fork()调用失败,返回值是什么? -
多进程执行逻辑:创建多进程时,多个子进程的执行顺序是由什么决定的?OS的调度器在其中起到了什么作用?
-
系统接口关联 :除了
fork(),Linux中还有哪些创建进程的相关系统调用(比如vfork()、exec系列函数)?fork()和vfork()的核心区别是什么? -
进程退出:父进程如何等待子进程退出?如果父进程不等待子进程,会产生什么问题?
(三)编程题
-
基础题 :编写一个C/C++程序,使用
fork()创建一个子进程,父进程打印自身PID和子进程PID,子进程打印自身PID和父进程PID。 -
思考题 尝试在代码中验证"子进程复制父进程地址空间":在
fork()前定义一个全局变量i=0,父进程将i改为10,子进程打印i的值,观察结果并解释原因。 -
进阶题:编写一个C/C++程序,创建3个子进程,每个子进程打印自己的PID和"我是第X个子进程",父进程等待所有子进程退出后打印"所有子进程已退出"。
Doro又又又带着小花🌸来啦!🌸超级奖励🌸给坚持看到这里的你!能沉下心把Linux进程这部分核心知识啃完,你真的超棒的~ 如果你觉得这篇博客把晦涩的进程概念讲得清晰易懂,帮你理解了了fork创建子进程、进程回收这些知识点的,别忘了动动小手点个【点赞】和【收藏】呀!这样后续复习进程相关知识点时,就能快速找到这份干货满满的笔记啦~
赶紧和Doro一起关注这个博主吧!他悄悄的告诉了Doro说他后续会持续更新Linux系统编程、进程通信、线程等系列干货内容的,把复杂的技术点拆解得明明白白,一步步夯实编程基础~ 另外,文中练习题的详细答案已经放在评论区咯!如果做练习时遇到困惑,或者对进程知识点有任何疑问、想法,都欢迎在评论区留言讨论,Doro会吧每一个回复都告诉博主的,和大家一起交流学习~ 我们下期干货再见!🌸
