进程和 fork
1. 进程的标识符 PID
每个进程都有一个唯一的进程标识符(PID),用于在系统中唯一标识该进程。
1.1 查看系统内所有的进程
在 Linux 中,可以通过 ps 命令查看进程的 PID。
Linux 进程标识符 PID 用于唯一标识进程。通过 ps 命令可查看所有进程,kill 命令可终止进程。fork 系统调用创建子进程,父子进程共享代码段,数据段采用写时拷贝技术保证独立性。调度器决定进程运行顺序,支持并发执行。Bash 执行命令时隐式调用 fork 创建子进程。理解这些机制有助于掌握操作系统多任务管理。

每个进程都有一个唯一的进程标识符(PID),用于在系统中唯一标识该进程。
在 Linux 中,可以通过 ps 命令查看进程的 PID。
命令:ps
选项:ajx 等
ps -ef # 显示所有进程的详细信息
ps aux # 显示所有用户的所有进程,重在用户
# 我们一般上使用 ps ajx 查看系统内所有的进程
ps ajx # 查看系统内所有的进程 ./proc # 以运行 proc 进程为例
ps ajx | head -1 && ps ajx | grep proc
ps ajx | head -1 && ps ajx | grep proc | grep -v grep
隐藏掉 grep 关键字的命令:ps ajx | head -1 && ps ajx | grep proc | grep -v grep
&& 表示左边的命令执行完,紧接着执行右边的命令。左边的命令执行成功,右边的命令也要执行成功。ps ajx | head -1 && ps ajx | grep proc 该命令得到的结果会同时显示 proc 进程和 grep 进程。
ps ajx | head -1 && ps ajx | grep proc | grep -v grep
-v 选项配合管道,在已有的结果中反向匹配 grep,可以隐藏掉 grep 关键字。命令:kill [选项] PID
常用选项:-9
功能:
kill PID 是温柔的杀掉这个进程。kill -9 PID 向指定 PID 的某个进程发送 9 信号,暴力的杀掉这个进程。演示:
kill -9 760776 # 强制终止 PID 为 760776 的进程
当使用不带 -9 选项的 kill PID 命令时,默认会向目标进程发送 SIGTERM 信号(信号编号为 15)。与 kill -9(发送 SIGKILL 信号)的强制终止不同,SIGTERM 是一种更友好的终止方式。
kill PID(默认发送 SIGTERM)的行为:
kill -9 强制终止。kill -9 PID(发送 SIGKILL)的行为:
使用建议:
kill PID(SIGTERM):给进程机会优雅退出,避免数据丢失或资源泄漏。kill -9(SIGKILL):例如进程完全卡死、僵尸进程或恶意进程等情况。扩展知识:
kill -l通过合理选择信号,可以更安全地管理系统进程。
Linux 操作系统中,描述系统进程的 task_struct 是用双向链表组织的。
ps ajx 的本质作用,相当于遍历 task_struct 的链表,拿到所有进程的相关属性,打印出来,供我们查看 PID。操作系统不相信任何用户,不能让用户通过 task_struct.pid 的方式通过结构体直接访问 PID。因此操作系统一定对外提供了系统调用接口,供用户访问 task_struct 内描述进程的相关属性。
在 Linux 中,可通过系统 getpid() 或 getppid() 调用获取进程的 PID:
pid_t getpid():获取当前进程的 PID。pid_t getppid():获取父进程的 PID。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 ajx 配合 grep 命令,制作一个系统内进程监控脚本,监控系统内进程的信息。
while :; do
ps axj | head -1 ;
ps ajx | grep proc | grep -v grep | grep -v .vscode;
sleep 1;
done
sleep 1:每秒执行一次。ps axj | head -1 ; ps ajx | grep proc | grep -v grep | grep -v .vscode:过滤我们的 proc 进程,不显示 grep 命令本身的进程和 .vscode 远程连接的进程。保存后,为当前文件增加执行权限后并执行:
chmod u+x monitor.sh # 增加执行权限
bash monitor.sh # 执行脚本
可以看到:
观察以下现象,注意 proc 进程的 PID。
我们来查看 PID 为 761739 的进程:
ps ajx | head -1 && ps ajx | grep 761739
Bash(命令行解释器)本身是一个进程,用户执行的命令(如 ls、./a.out 等)均为 Bash 的子进程。Bash 的 PID 在单次登录会话中固定,但重新登录后会变化。
我们在命令行解释器 bash 中,执行的所有指令(包括命令和./执行的程序)的父进程,就是 bash 本身。
./操作执行程序或者命令时,就是操作系统为我们创建了一个进程,在操作系统上运行。
父子进程关系特点:
以上我们介绍了获取父子进程 PID 相关的知识,那么我们用户可否创建一个进程呢?
我们目前已知的创建进程的方式:
./exe 操作,执行程序或者执行命令时,就是操作系统为我们创建了一个进程,在操作系统上运行。这是指令级别创建进程。这种方式是我们手动创建进程,那么可否在程序运行时创建进程呢?
Linux 内核为我们提供了系统调用 fork(),用于为当前进程创建一个子进程。这是代码级别创建进程。
fork 是 Linux 中创建子进程的核心系统调用,可以在程序运行时创建进程,其独特的行为常引发初学者的困惑。
使用 man 手册查看 fork 函数的用法
man 2 fork
函数名和功能:fork
头文件:<sys/types.h> 和 <unistd.h>
参数类型:void 无需传参
返回值:类型为 pid_t,这里 pid_t 是 int 的类型别名。typedef int pid_t
根据文档介绍,
fork 的简单使用:
int main() {
printf("before: only one line\n");
fork();
printf("after: only one line\n");
sleep(1);
}
这里我们不免会有疑惑???
==fork 之后的代码,执行了两次!!!==这是为什么?
先给出结论:fork 之后的代码,父子进程是共享的,既然父子进程各有一份代码 (共享),那 fork 之后的代码执行了两次就可以说得通了。
再看如下代码和现象:
以上种种现象,我们不免产生很多疑问???
这些问题我们后文会一一解答。
结合 fork 的翻译,分支,分叉,表示我们的代码在 fork 这里要进行分叉。
**结论:**一般而言,fork 之后的代码,父子共享。
解答这个问题,先思考我们为什么要创建子进程?
为什么要父子进程要分别返回不同的值?
为什么给子进程返回 0,给父进程返回子进程的 PID?
因为:
原因如下:
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;
}
解释如下:
return 执行之前父子进程均已创建完成,且可以被 CPU 调度。创建子进程后,父子进程代码共享,return 语句也属于代码,因此父子进程共享 return 语句。CPU 分别调度父子进程,父进程和子进程分别执行了 return,就实现了一个函数返回两次。
fork 在即将执行 return 之前,创建子进程的工作已经完成了,子进程也允许被 CPU 调度了,之后的 return 代码父子进程共享,父和子进程分别执行 return,因此 fork 函数就实现了返回两次。
综上我们可以总结出 fork 内部究竟做了什么:
任何平台下,进程在运行时具有独立性!进程的运行互不干扰。
数据有可能会被修改,代码是相同的且不可被修改。一个进程修改了数据,由于代码是相同的,可能会影响到另一个进程的运行。因此父子进程不能共享同一份数据。
一个 id 变量里面有两个值的现象,实际是通过写时拷贝实现的。
子进程刚被创建时,代码和数据都是父子进程共享的。 代码加载到内存中后,是不可修改的,因此代码直接父子共享。
子进程在读取父进程的数据时,数据也是共享的。当子进程尝试修改父进程的数据时,操作系统为子进程新分配一块内存空间,让子进程对待修改数据的拷贝进行修改,要修改多少内容,就申请多少空间,从而不会影响到父进程的数据,该过程称为写时拷贝。
以后子进程只要修改父进程的数据,操作系统都会给子进程在内存中新拷贝一份,供子进程修改。
以上称为父子进程之间,数据层面的写时拷贝,写时拷贝保证了父子进程间数据的独立性,避免了父或子进程修改数据后对子或父进程的数据产生影响。
fork 之后父子进程共享代码段,但数据段通过写时拷贝(COW——Copy-On-Write)技术实现高效复制。
原理:
优点:
问题:fork() 后父子进程谁先运行?
当调用 fork() 创建子进程后,父子进程会被同时放入操作系统的就绪队列中等待执行。它们的运行顺序由操作系统的进程调度器决定,用户无法预测或干预。这是操作系统的核心设计原则之一:调度器尽可能保证公平性,而非确定性。因此 fork() 后父子进程谁先运行,无法确定,取决于调度器。
调度器的工作原理
常见的调度算法
| 算法 | 特点 |
|---|---|
| 先来先服务 (FCFS) | 简单,但可能导致饥饿现象 |
| 时间片轮转 (RR) | 每个进程分配固定时间片,公平性强,适合交互式系统 |
| 优先级调度 | 高优先级进程优先执行,需防止低优先级进程饿死 |
| 多级反馈队列 | 结合时间片和优先级,动态调整进程优先级,兼顾响应时间和吞吐量 |
为什么单核 CPU 能同时运行数百个进程?
bash 也是通过 fork 创建子进程的
Bash 执行命令的流程
当在 Bash 中输入命令(如 ls -l)时,Bash 的底层操作如下:
fork():创建子进程
exec():加载新程序
wait():父进程等待
进程是操作系统资源分配的基本单位,而 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