父子进程和 fork
前言
在前文从冯诺依曼体系到进程中,我们认识了进程的概念。在操作系统中,进程是程序执行的基本单位。Linux下进程是由一个个的task_struct组织起来的。通过结构体管理进程,其中包含进程的所有属性。本文将从进程的标识符入手,深入探讨如何通过系统调用创建子进程,并分析其背后的深层次原理。
本文介绍 Linux 进程标识符 PID 的获取与管理方法,深入解析 fork 系统调用创建子进程的原理,涵盖父子进程代码共享、写时拷贝技术及返回值机制。同时阐述进程调度器的工作原理及 Bash 执行命令时的 fork 流程,结合代码示例帮助理解进程并发与资源管理。

在前文从冯诺依曼体系到进程中,我们认识了进程的概念。在操作系统中,进程是程序执行的基本单位。Linux下进程是由一个个的task_struct组织起来的。通过结构体管理进程,其中包含进程的所有属性。本文将从进程的标识符入手,深入探讨如何通过系统调用创建子进程,并分析其背后的深层次原理。
Linuxtask_struct(PID)fork每个进程都有一个唯一的进程标识符(PID),用于在系统中唯一标识该进程。
在Linux中,可以通过ps命令查看进程的 PID。
命令:ps
选项:aux等
ps -ef # 显示所有进程的详细信息
ps aux # 显示所有用户的所有进程,重在用户
# 我们一般上使用 ps aux 查看系统内所有的进程
ps aux | head -1 && ps aux | grep proc | grep -v grep
ps aux 演示:
我们一般在进行 grep 进程时,会过滤出 grep 命令本身,因为 grep 命令也是一个进程。
隐藏掉 grep 关键字的命令:ps aux | head -1 && ps aux | grep proc | grep -v grep
&& 表示左边的命令执行完,紧接着执行右边的命令。左边的命令执行成功,右边的命令也要执行成功。ps aux | head -1 && ps aux | grep proc 该命令得到的结果会同时显示 proc 进程和 grep 进程。
grep 进程,可以使用管道,对上述命令的结果再进行 grepps aux | head -1 && ps aux | grep proc | grep -v grep
-v 选项配合管道,在已有的结果中反向匹配 grep,可以隐藏掉 grep 关键字命令:kill [选项] PID
常用选项:-9
功能:
kill PID 是温柔的杀掉这个进程kill -9 PID 向指定 PID 的某个进程发送 9 信号,暴力的杀掉这个进程演示:
kill -9 760776 # 强制终止 PID 为 760776 的进程
kill -9 760776 的结果如下:
当使用不带 -9 选项的 kill PID 命令时,默认会向目标进程发送 SIGTERM 信号(信号编号为 15)。与 kill -9(发送 SIGKILL 信号)的强制终止不同,SIGTERM 是一种更'友好'的终止方式,(信号部分之后的文章会介绍,) 目前介绍区别如下:
kill PID(默认发送 SIGTERM)的行为:
signal() 或 sigaction()),它可以自定义对 SIGTERM 的响应(如延迟退出或忽略信号)。kill -9 强制终止。kill -9 PID(发送 SIGKILL)的行为:
使用建议:
kill PID(SIGTERM)kill -9(SIGKILL)扩展知识:
kill -lSIGHUP (1):挂起(重新加载配置)SIGINT (2):中断(同 Ctrl+C)SIGTERM (15):终止SIGKILL (9):强制终止通过合理选择信号,可以更安全地管理系统进程。
Linux 操作系统中,描述系统进程的 task_struct 是用双向链表组织的PID 属性存在于进程的task_struct中ps aux 的本质作用,相当于遍历 task_struct 的链表,拿到所有进程的相关属性,打印出来,供我们查看 PID操作系统不相信任何用户,不能让用户通过
task_struct.pid的方式通过结构体直接访问PID。因此操作系统一定对外提供了系统调用接口,供用户访问task_struct内描述进程的相关属性。
在 Linux 中,可通过系统 getpid() 或 getppid() 调用获取进程的 PID:
pid_t getpid():获取当前进程的 PID。PID 也可以被获取到pid_t getppid():获取父进程的 PID。pid_t,本质是 int 的类型别名。typedef int pid_tgetpid 代码演示
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
pid_t pid = getpid();
pid_t ppid = getppid();
// pid_t 本质是有符号整数
while (1) {
printf("I am a process, my ID is %d, my parent ID is %d\n", pid, ppid);
sleep(1);
}
}
ps aux 配合 grep 命令,制作一个系统内进程监控脚本,监控系统内进程的信息。while :; do
ps axj | head -1
ps aux | grep proc | grep -v grep | grep -v .vscode
sleep 1
done
sleep 1:每秒执行一次
ps axj | head -1 ; ps aux | grep proc | grep -v grep | grep -v .vscode:过滤我们的 proc 进程,不显示 grep 命令本身的进程和 .vscode 远程连接的进程
我们可以手动在终端中执行以上命令。也可以将以上内容保存在一个后缀为.sh的文件中,这里我选择保存为一个 sh 文件,并命名为monitor.sh
运行效果如下:
保存后,为当前文件增加执行权限后并执行
chmod u+x monitor.sh # 增加执行权限
bash monitor.sh # 执行脚本
proc 进程ps aux 查看到的 PID 及 PPID 和 getpid() 和 getppid() 获取到的内容完全一致!!!PID 来对进程进行管理了观察以下现象,注意proc 进程的 PID
PID 只保证在每次运行期间有效,下次启动,操作系统为该进程分配的 PID 可能会变化,这是正常的。PID 一直是 761739,这里的父进程是什么进程?761739 在这里就是我们的命令行解释器 bash!我们来查看 PID 为 761739 的进程
ps aux | head -1 && ps aux | grep 761739
Bash(命令行解释器)本身是一个进程,用户执行的命令(如 ls、./a.out 等)均为 Bash 的子进程。Bash 的 PID 在单次登录会话中固定,但重新登录后会变化。
我们在命令行解释器 bash 中,执行的所有指令 (包括命令和./执行的程序) 的父进程,就是 bash 本身
Linux 系统会单独为我们创建一个 bash 进程,即为我们创建一个命令行解释器进程,帮我们在显示器中打印出命令行终端bash 都会为我们创建进程,这些程序都是 bash 的子进程bash 只负责命令行解释,具体执行出问题时,只会影响对应的子进程。因此在同一登陆状态下,指令和程序的父进程 PID 不变,也就是 bash 的 PID 不变bash 有一个 PID,多开多个 bash,会有多个 bashPID。./ 操作执行程序或者命令时,就是操作系统为我们创建了一个进程,在操作系统上运行
父子进程关系特点:
wait 系统调用回收资源。init 进程(PID=1)接管。以上我们介绍了获取父子进程
PID相关的知识,那么我们用户可否创建一个进程呢
我们目前已知的创建进程的方式,
./exe 操作,执行程序或者执行命令时,就是操作系统为我们创建了一个进程,在操作系统上运行。这是指令级别创建进程这种方式是我们手动创建进程,那么可否在程序运行时创建进程呢?
Linux 内核为我们提供了系统调用fork(),用于为当前进程创建一个子进程。这是代码级别创建进程
fork是Linux中创建子进程的核心系统调用,可以在程序运行时创建进程,其独特的行为常引发初学者的困惑
使用 man 手册查看 fork 函数的用法
man 2 fork
函数名和功能:fork
child process),当前调用进程为父进程(parent process)头文件:<sys/types.h> 和 <unistd.h>
参数类型:void 无需传参
返回值:类型为 pid_t,这里 pid_t 是 int 的类型别名。typedef int pid_t
根据文档介绍,
-1,并设置 errnoPIDfork 的简单使用:
int main() {
printf("before: only one line\n");
fork();
printf("after: only one line\n");
sleep(1);
}
这里我们不免会有疑惑???
==fork 之后的代码,执行了两次!!!==这是为什么?
先给出结论:fork 之后的代码,父子进程是共享的,既然父子进程各有一份代码 (共享),那 fork 之后的代码执行了两次就可以说得通了
再看如下代码和现象:
./proc 是当前进程,fork 之后,现象是:代码被一分为二了,两个循环各自在执行,父子进程各执行一个循环。这说明,id == 0 和 id > 0 同时成立了,且根据进程的 PID,我们可以得出:
fork 之后当前进程是父进程fork 创建的bash 进程以上种种现象,我们不免产生很多疑问???
为什么 fork 要给子进程返回 0,给父进程返回子进程的 pid 呢?为什么父子进程的返回值不同呢?
一个函数,怎么会有两个返回值,如何做到返回两次呢?
变量 id 接收 fork 的返回值,为什么一个变量可以有两个不同的值?
fork 函数内究竟做了什么?
这些问题我们后文会一一解答
fork 的翻译,分支,分叉,表示我们的代码在 fork 这里要进行分叉。task_struct。fork 返回后,父子进程从同一位置继续执行,但通过返回值区分逻辑分支。**结论:**一般而言,fork 之后的代码,父子共享
解答这个问题,先思考我们为什么要创建子进程?
fork 返回值要不同为什么要父子进程要分别返回不同的值?
fork 之后,可以根据不同的返回值区分父子进程,来让父子进程执行不同的代码片段**为什么给子进程返回 0,给父进程返回子进程的 PID?**因为:
PID 来区分不同的子进程。
PID,用 PID 来标识子进程的唯一性,方便直接通过不同的 PID,对不同的子进程直接做控制0 简化逻辑判断。
标识子进程,只需要在父进程内判断fork 的返回值是否 PID == 0 即可标识子进程。子进程得到父进程的 PID,只需要调用 getppid() 函数即可。原因如下:
原因如下:
fork 创建子进程,也就是内存中多了一个进程,Linux 操作系统会为该子进程创建一个 task_structtask_struct 和代码和数据。创建子进程,操作系统层面只创建了子进程的 task_struct,子进程没有自己的代码和数据,只能和父进程共享同一份代码。因此 fork 之后,父子进程代码共享。
fork 之后,父子进程共享后续的代码。
fork之后父子进程执行的代码一样,那我们为什么要创建子进程呢?我们的目的就是为了让父子进程协同起来分别做不同的事情。因此需要想办法让父子进程执行不同的代码块。fork函数具有不同的返回值,就是为了让父子进程执行不同的代码块而设计的。那么
fork如何设计实现了以上功能?
代码示例:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("begin: 我是一个进程,pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id == 0) {
// 子进程分支
while (1) {
printf("我是子进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
} else if (id > 0) {
// 父进程分支 成功时,子进程的 PID > 0 返回到父进程
while (1) {
printf("我是父进程,pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
} else {
// error
}
return 0;
}
fork 有不同的返回值?一个函数如何做到返回两次解释如下:
return 时,这个函数的核心功能就已经完成了。fork 是一个函数,内部的实现是要创建子进程。fork 函数做的关键步骤:
returntask_struct,且可以被 CPU 调度。此时在临界执行 return 之前,父子进程的 task_struct(PCB) 都已存在了,父子进程也都已存在。return 执行之前父子进程均已创建完成,且可以被 CPU 调度。创建子进程后,父子进程代码共享,return 语句也属于代码,因此父子进程共享 return 语句。CPU 分别调度父子进程,父进程和子进程分别执行了 return,就实现了一个函数返回两次
fork在即将执行return之前,创建子进程的工作已经完成了,子进程也允许被 CPU 调度了,之后的return代码父子进程共享,父和子进程分别执行return,因此fork函数就实现了返回两次
综上我们可以总结出 fork 内部究竟做了什么:
任何平台下,进程在运行时具有独立性!进程的运行互不干扰
以上称为父子进程之间,数据层面的写时拷贝,写时拷贝保证了父子进程间数据的独立性,避免了父或子进程修改数据后对子或父进程的数据产生影响
fork 之后父子进程共享代码段,但数据段通过写时拷贝(COW——Copy-On-Write)技术实现高效复制。
原理:
优点:
fork 的执行效率return 时,return 是对 id 做写入。return 时,直接对 id 做写入;子进程 return 时,对 id 做写时拷贝问题:fork() 后父子进程谁先运行?
当调用 fork() 创建子进程后,父子进程会被同时放入操作系统的就绪队列中等待执行。它们的运行顺序由操作系统的进程调度器决定,用户无法预测或干预。这是操作系统的核心设计原则之一:调度器尽可能保证公平性,而非确定性。因此 fork() 后父子进程谁先运行,无法确定,取决于调度器。
调度器的工作原理
常见的调度算法
| 算法 | 特点 |
|---|---|
| 先来先服务 (FCFS) | 简单,但可能导致'饥饿'现象 |
| 时间片轮转 (RR) | 每个进程分配固定时间片,公平性强,适合交互式系统 |
| 优先级调度 | 高优先级进程优先执行,需防止低优先级进程'饿死' |
| 多级反馈队列 | 结合时间片和优先级,动态调整进程优先级,兼顾响应时间和吞吐量 |
为什么单核 CPU 能'同时'运行数百个进程?
fork() 后,父子进程进入就绪队列,可能发生以下情况:
bash也是通过fork创建子进程的
Bash 执行命令的流程
当在 Bash 中输入命令(如 ls -l)时,Bash 的底层操作如下:
fork():创建子进程
Bash)的内存、文件描述符、环境变量等。exec():加载新程序
exec() 系统调用,用目标程序(如 /bin/ls)替换当前内存空间。wait():父进程等待Bash(父进程)调用 wait() 阻塞自身,直到子进程结束并回收资源。wait(),子进程退出后会成为僵尸进程(Zombie),占用系统资源。进程是操作系统资源分配的基本单位,而 fork 作为创建子进程的核心机制,通过代码共享、写时拷贝和调度器的协同工作,实现了高效的多任务管理。理解父子进程的关系、fork 的'分流'特性以及调度器的随机性,是掌握进程管理的关键。无论是 bash 执行命令时的 隐式 fork,还是程序内显式创建子进程,本质都在通过进程的独立性完成并发任务。写时拷贝技术平衡了性能与资源隔离,而调度器的公平策略让单核 CPU 也能营造'并行'假象。希望本文为你揭开了进程与 fork 的神秘面纱

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML 转 Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online