进程和fork
- 父子进程和fork
- github地址
- 前言
- [1. 进程的标识符PID](#1. 进程的标识符PID)
-
- [1.1 查看系统内所有的进程](#1.1 查看系统内所有的进程)
- [1.2 kill杀掉进程](#1.2 kill杀掉进程)
- [1.3 获取进程的PID](#1.3 获取进程的PID)
- [1.4 bash与父子进程](#1.4 bash与父子进程)
- [2. 创建进程与fork](#2. 创建进程与fork)
-
- [2.1 fork创建子进程](#2.1 fork创建子进程)
- [2.2 fork困惑的解释](#2.2 fork困惑的解释)
-
- [0. fork的工作原理](#0. fork的工作原理)
- [1. 为什么给子进程返回0,给父进程返回子进程PID](#1. 为什么给子进程返回0,给父进程返回子进程PID)
- [2. 一个函数如何做到返回两次?如何理解?](#2. 一个函数如何做到返回两次?如何理解?)
-
- [2.1 为什么父子进程共享fork之后的代码](#2.1 为什么父子进程共享fork之后的代码)
- [2.2 理解一个函数做到返回两次](#2.2 理解一个函数做到返回两次)
- [2.3 fork函数内做了什么?](#2.3 fork函数内做了什么?)
- [3. 一个id变量里面怎么会有两个值](#3. 一个id变量里面怎么会有两个值)
-
- [3.1 写时拷贝](#3.1 写时拷贝)
- [3.2 fork中的写时拷贝](#3.2 fork中的写时拷贝)
- [3. 进程与调度器](#3. 进程与调度器)
- [4. bash与fork](#4. bash与fork)
- [5. 结语](#5. 结语)
父子进程和fork
github地址
前言
在前文从冯诺依曼体系到进程中,我们认识了进程的概念。在操作系统中,进程是程序执行的基本单位 。Linux
下进程是由一个个的task_struct
组织起来的。Linux
通过task_struct
结构体管理进程,其中包含进程的所有属性。本文将从进程的标识符(PID)
入手,深入探讨如何通过fork
系统调用创建子进程,并分析其背后的深层次原理。
1. 进程的标识符PID
每个进程都有一个唯一的进程标识符(PID)
,用于在系统中唯一标识该进程。
1.1 查看系统内所有的进程
在Linux
中,可以通过ps
命令查看进程的PID
命令 :ps
选项 :ajx
等
bash
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
ps ajx演示:

我们一般在进行grep
进程时,会过滤出grep
命令本身,因为grep
命令也是一个进程
隐藏掉grep
关键字的命令:ps ajx | head -1 && ps ajx | grep proc | grep -v grep
&&
表示左边的命令执行完,紧接着执行右边的命令 。左边的命令执行成功,右边的命令也要执行成功。ps ajx | head -1 && ps ajx | grep proc
该命令得到的结果会同时显示proc
进程和grep
进程。- 如果我们不想显示
grep
进程,可以使用管道 ,对上述命令的结果再进行grep
ps ajx | head -1 && ps ajx | grep proc | grep -v grep
-v
选项配合管道 ,在已有的结果中反向匹配grep ,可以隐藏掉grep
关键字
- 如果我们不想显示
1.2 kill杀掉进程
命令 :kill [选项] PID
常用选项 :-9
功能:
kill PID
是温柔的杀掉这个进程kill -9 PID
向指定PID
的某个进程发送9
信号,暴力的杀掉这个进程
演示:
bash
kill -9 760776 # 强制终止PID为760776的进程

kill -9 760776
的结果如下:
当使用不带 -9
选项的 kill PID
命令时,默认会向目标进程发送 SIGTERM
信号(信号编号为 15)。与 kill -9
(发送 SIGKILL 信号)的强制终止不同,SIGTERM
是一种更"友好"的终止方式,(信号部分之后的文章会介绍,)目前介绍区别如下:
kill PID
(默认发送 SIGTERM)的行为:
- 允许进程优雅退出
进程收到 SIGTERM 后,可以执行清理操作(如保存数据、关闭文件、释放资源等),然后自行终止。 - 进程可以捕获或忽略 SIGTERM
如果进程代码中注册了信号处理函数(例如通过signal()
或sigaction()
),它可以自定义对 SIGTERM 的响应(如延迟退出或忽略信号)。 - 可能无法立即终止进程
如果进程因代码缺陷、死锁或无限循环无法响应 SIGTERM,它可能不会退出。此时需手动使用kill -9
强制终止。
kill -9 PID
(发送 SIGKILL)的行为:
- 强制立即终止进程
SIGKILL 信号无法被进程捕获或忽略,操作系统会直接终止进程,不给进程任何清理的机会。 - 可能导致资源泄漏
进程无法执行清理操作,可能导致临时文件残留、内存未释放、文件句柄未关闭等问题。 - 适用于"无响应"的进程
当进程对 SIGTERM 无响应时,SIGKILL 是最后手段。
使用建议:
- 优先使用默认的
kill PID
(SIGTERM)
给进程机会优雅退出,避免数据丢失或资源泄漏。 - 仅在必要时使用
kill -9
(SIGKILL)
例如进程完全卡死、僵尸进程或恶意进程等情况。
扩展知识:
-
查看所有信号:
kill -l
-
常用信号:
SIGHUP
(1):挂起(重新加载配置)SIGINT
(2):中断(同 Ctrl+C)SIGTERM
(15):终止SIGKILL
(9):强制终止
通过合理选择信号,可以更安全地管理系统进程。
1.3 获取进程的PID

Linux
操作系统中,描述系统进程的task_struct
是用双向链表组织的ps ajx
的作用,相当于遍历task_struct
的链表,拿到所有进程的相关属性 ,打印出来,供我们查看PID
- 既然可以拿到所有进程 的相关属性,那么对于一个特定进程的PID,应当也是可以获取到的
操作系统不相信任何用户,不能让用户通过
task_struct.pid
的方式通过结构体直接访问PID
。因此操作系统一定对外提供了系统调用接口,供用户访问task_struct
内描述进程的相关属性。
在Linux
中,可通过系统getpid()
或getppid()
调用获取进程的PID
:
pid_t getpid()
:获取当前进程的PID。- 进程都是被创建出来的,一个进程除了有自己的PID,也有自己的父进程 ,父进程的
PID
也可以被获取到 pid_t getppid()
:获取父进程的PID。- 返回值类型均为
pid_t
,本质是int
的类型别名。typedef int pid_t
getpid代码演示
cpp
#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 precess, my ID is %d, my parent ID is %d\n", pid, ppid);
sleep(1);
}
}
- 我们可以手动将
ps ajx
配合grep
命令,制作一个系统内进程监控脚本,监控系统内进程的信息。
bash
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
远程连接的进程 -
我们可以手动在终端中执行以上命令。也可以将以上内容保存在一个后缀为
.sh
的文件中,这里我选择保存为一个sh文件 ,并命名为monitor.sh
-
保存后,为当前文件增加执行权限后并执行
bashchmod u+x monitor.sh # 增加执行权限 bash monitor.sh # 执行脚本
-
-
运行效果如下:

- 这样就实现了系统内运行进程的实时监控,我们利用该脚本来监测我们启动的进程

- 可以看到:
- 进程启动前,检测脚本未检测到任何内容
- 进程启动后 ,检测脚本就检测到了我们启动的
proc
进程 - 且
ps ajx
查看到的PID
及PPID
和getpid()
和getppid()
获取到的内容完全一致!!!
- 这样我们就可以通过
PID
来对进程进行管理了
1.4 bash与父子进程
观察以下现象,注意proc进程的PID

- 一个程序,多次启动时PID在变。

-
一个程序,多次启动时PID在变。。
- 这是因为:
PID
只保证在每次运行期间有效,下次启动,操作系统为该进程分配的PID
可能会变化,这是正常的。
- 这是因为:
-
一个程序,多次启动时的PID在变,其父进程的PID一直不变。这是为什么?父进程的
PID
一直是761739
,这里的父进程是什么进程? -
我们来查看
PID
为761739
的进程bashps ajx | head -1 && ps ajx | grep 761739
-
这里可以清楚的看到,
761739
在这里就是我们的命令行解释器bash
!
Bash
(命令行解释器)本身是一个进程,用户执行的命令(如ls
、./a.out
等)均为Bash的子进程
。Bash
的PID
在单次登录会话中固定 ,但重新登录后会变化。
我们在命令行解释器bash
中,执行的所有指令(包括命令和./执行的程序
)的父进程 ,就是bash
本身
-
每次重新登陆
xshell
时,Linux
系统会单独为我们创建一个bash
进程,即为我们创建一个命令行解释器进程,帮我们在显示器中打印出命令行终端 -
我们在命令行解释器中,执行或输入的所有指令和程序,
bash
都会为我们创建进程,这些程序都是bash
的子进程
bash
只负责命令行解释,具体执行出问题时,只会影响对应的子进程。因此在同一登陆状态下,指令和程序的父进程PID
不变,也就是bash
的PID
不变 -
一个
bash
有一个PID
,多开多个bash
,会有多个bash
PID。进程主要维护父子关系
./
操作执行程序或者命令时,就是操作系统为我们创建了一个进程,在操作系统上运行
父子进程关系特点:
- 父进程负责管理子进程。
- 子进程退出后,父进程需要通过
wait
系统调用回收资源。 - 若父进程先退出,子进程会成为"孤儿进程",由
init
进程(PID=1
)接管。
2. 创建进程与fork
2.1 fork创建子进程
以上我们介绍了获取父子进程
PID
相关的知识,那么我们用户可否创建一个进程呢
我们目前已知的创建进程的方式,
./exe
操作,执行程序或者执行命令 时,就是操作系统为我们创建了一个进程,在操作系统上运行
这种方式是我们手动创建进程,那么可否在程序运行时创建进程呢?
Linux
内核为我们提供了系统调用 fork()
,用于为当前进程创建一个子进程。
fork
是Linux
中创建子进程的核心系统调用,其独特的行为常引发初学者的困惑
使用man 手册查看fork函数的用法
bash
man 2 fork

-
函数名和功能 :
fork
- 为当前调用fork的进程创建一个子进程。
- 新进程为子进程 (
child process
),当前调用进程为父进程 (parent process
) - 子进程复制父进程的代码、数据、堆栈和打开的文件描述符。
-
头文件 :
<sys/types.h>
和<unistd.h>
-
参数类型 :
void
无需传参 -
返回值 :类型为
pid_t
,这里pid_t
是int
的类型别名。typedef int pid_t
-
根据文档介绍,
- 创建子进程失败时 ,在原进程中返回
-1
,并设置errno
- 成功时 :
- 在父进程中返回子进程的
PID
- 在子进程中返回0
- 在父进程中返回子进程的
- 创建子进程失败时 ,在原进程中返回
fork
的简单使用:
cpp
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之后一定存在两个进程,存在两个执行流
以上种种现象,我们不免产生很多疑问???
-
- 为什么
fork
要给子进程返回0
,给父进程返回子进程的pid
呢?为什么父子进程的返回值不同呢?
- 为什么
-
- 一个函数,怎么会有两个返回值,如何做到返回两次呢?
-
- 变量
id
接收fork
的返回值,为什么一个变量可以有两个不同的值?
- 变量
-
fork
函数究竟做了什么?
这些问题我们后文会一一解答
2.2 fork困惑的解释

- 结合
fork
的翻译,分支,分叉,代表我们的代码在fork
这里要进行分叉。
0. fork的工作原理
- 创建子进程的PCB :内核为子进程分配新的
task_struct
。 - 复制父进程上下文 :子进程继承父进程的代码段、数据段、堆栈和文件描述符表。
- 分流执行 :
fork
返回后,父子进程从同一位置继续执行,但通过返回值区分逻辑分支。
1. 为什么给子进程返回0,给父进程返回子进程PID
一般而言,fork之后的代码,父子共享
解答这个问题,先思考我们为什么要创建子进程?
- 是为了让父子进程协作,执行不同的事情 ,因此需要想办法让父子进程执行不同的代码块 。为了让父子进程协同,执行不同的执行流,就设计了
fork
返回值要不同
为什么要父子进程要分别返回不同的值?
- 是为了在
fork
之后,可以根据不同的返回值区分父子进程,来让父子进程执行不同的代码片段 - 返回不同的返回值,是为了区分。让不同的进程执行流,执行不同的代码块
为什么给子进程返回0,给父进程返回子进程的PID?
-
一个子进程只会有一个父进程,一个父进程可能会有多个子进程
-
父进程需要管理多个子进程 ,通过
PID
区分不同子进程。- 父进程需要区分不同的子进程 :给父进程返回子进程的
PID
,用来标识子进程的唯一性,方便直接通过不同的PID
,对不同的子进程直接做控制
- 父进程需要区分不同的子进程 :给父进程返回子进程的
-
子进程只需确认自身身份 ,返回
0
简化逻辑判断。- 子进程只有一个父进程 。
标识子进程
,只需要在父进程内判断fork的返回值 是否PID == 0
即可标识子进程。子进程得到父进程的PID
,只需要调用getppid()
函数即可。
- 子进程只有一个父进程 。
2. 一个函数如何做到返回两次?如何理解?
2.1 为什么父子进程共享fork之后的代码
原因如下:

原因如下:
-
进程 = 内核数据结构 + 代码和数据
-
fork
创建子进程,也就是内存中多了一个进程,Linux
操作系统会为该子进程创建一个task_struct
-
创建子进程前,该进程有自己的
task_struct
和代码和数据。创建子进程,操作系统层面 只创建了子进程的task_struct
,子进程没有自己的代码和数据,只能和父进程共享同一份代码 。数据呢?(下文解释) -
因此fork之后,父子进程共享后续的代码
fork之后,父子进程共享后续的代码。
fork
之后父子进程执行的代码一样,那我们为什么要创建子进程呢?我们的目的就是为了让父子进程协同起来分别做不同的事情。因此需要想办法让父子进程执行不同的代码块
。让fork
函数具有不同的返回值就是为此设计的。那么
fork
如何设计实现了以上功能?
2.2 理解一个函数做到返回两次
代码示例:
cpp
#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
是一个函数,内部的实现是要创建子进程 。return之前,父子进程都已存在,各自有独立的task_struct
且可以被CPU调度 。此时在临界执行return
之前 ,父子进程的task_struct(PCB)
都已存在了,父子进程也都已存在。
return
执行之前父子进程均已创建完成,且可以被CPU
调度。创建子进程后,父子进程代码共享,return
语句也属于代码,因此父子进程共享return语句。CPU
分别调度父子进程 ,父子进程分别执行了return
,就实现了一个函数返回两次
fork
在即将执行return
之前,创建子进程的工作已经完成了,子进程也允许被CPU调度 了,之后的return
代码父子进程共享,父和子进程分别执行return
,因此fork
函数就实现了返回两次
2.3 fork函数内做了什么?
综上我们可以总结出fork
内部究竟做了什么:
- 创建子进程
- 为子进程创建PCB
- 用父进程的字段初始化子进程
- 让父子进程实现代码共享
- ...其他工作
3. 一个id变量里面怎么会有两个值
任何平台下,进程在运行时具有独立性!进程的运行互不干扰
- 一个id变量里面有两个值的现象,实际是通过写时拷贝实现的

-
子进程刚被创建时,代码和数据都是父子进程共享的
代码加载到内存中后,是不可修改的 ,因此代码直接父子共享。 -
子进程在读取父进程的数据时,数据也是共享的。当子进程尝试修改父进程的数据时,操作系统为子进程新分配一块内存空间,让子进程对待修改数据的拷贝进行修改 ,修改多少内容,就申请多少空间,从而不会影响到父进程的数据,该过程称为写时拷贝
以上称为父子进程之间,数据层面的写时拷贝,写时拷贝保证了父子进程间数据的独立性,避免了父或子进程修改数据后对子或父进程的数据产生影响
3.1 写时拷贝
fork
之后父子进程共享代码段,但数据段通过写时拷贝(COW------Copy-On-Write)技术实现高效复制。
原理:
- 子进程创建时,与父进程共享物理内存。
- 当子进程尝试修改内存时,内核会为该内存页创建副本,确保修改独立。
优点:
- 减少内存全量复制开销。
- 提高
fork
的执行效率
3.2 fork中的写时拷贝

return
时,return
是对id
做写入。- return语句父子进程共享 ,父子进程分别return。
- 父进程
return
时,直接对id
做写入 ,子进程return
时,对id
做写时拷贝
3. 进程与调度器
问题:fork()
后父子进程谁先运行?
当调用 fork()
创建子进程后,父子进程会被同时放入操作系统的就绪队列中等待执行。它们的运行顺序由操作系统的进程调度器决定,用户无法预测。这是操作系统的核心设计原则之一:调度器保证公平性,而非确定性 。因此fork()
后父子进程谁先运行,无法确定,取决于调度器。
调度器的工作原理
-
进程的组织形式
所有进程的PCB(进程控制块)以数据结构(如链表、队列、树等)组织在内存中,例如:-
就绪队列:等待CPU时间的进程
-
阻塞队列:等待I/O或其他资源的进程
-
-
调度器的核心任务
调度器从就绪队列中选择一个进程分配CPU时间,具体流程如下:
-
触发时机:时钟中断、进程阻塞、进程退出等。
-
选择算法:通过调度算法(如时间片轮转、优先级调度)选择下一个进程。
-
上下文切换:保存当前进程的CPU状态(寄存器、程序计数器等),加载目标进程的上下文。
-
-
常见的调度算法
算法 特点 先来先服务 (FCFS) 简单,但可能导致"饥饿"现象 时间片轮转 (RR) 每个进程分配固定时间片,公平性强,适合交互式系统 优先级调度 高优先级进程优先执行,需防止低优先级进程"饿死" 多级反馈队列 结合时间片和优先级,动态调整进程优先级,兼顾响应时间和吞吐量
为什么单核CPU能"同时"运行数百个进程?
-
并发假象
调度器通过快速切换进程(纳秒级时间片)营造"并行"假象。例如:
-
进程A运行5ms → 切换到进程B运行5ms → 切换回进程A......
-
人类无法感知微小的时间片切换,因此感觉多个进程在"同时"运行。
-
-
父子进程的竞争
在
fork()
后,父子进程进入就绪队列,可能发生以下情况:-
父进程先获得CPU时间(常见,因父进程可能处于活跃状态)。
-
子进程先获得CPU时间(可能因父进程被阻塞或时间片耗尽)。
-
父子进程交替执行(取决于调度策略和系统负载)。
-
4. bash与fork
bash
也是通过fork
创建子进程的
Bash执行命令的流程
当在Bash
中输入命令(如 ls -l
)时,Bash
的底层操作如下:
-
fork()
:创建子进程-
子进程复制父进程(
Bash
)的内存、文件描述符、环境变量等。 -
优化技术:写时复制 (Copy-On-Write),仅在数据被修改时复制内存,减少开销。
-
-
exec()
:加载新程序- 子进程调用
exec()
系统调用,用目标程序(如/bin/ls
)替换当前内存空间。
- 子进程调用
-
wait()
:父进程等待
-
Bash
(父进程)调用wait()
阻塞自身,直到子进程结束并回收资源。 -
若未调用
wait()
,子进程退出后会成为僵尸进程(Zombie),占用系统资源。
5. 结语
进程是操作系统资源分配的基本单位,而 fork
作为创建子进程的核心机制,通过代码共享、写时拷贝和调度器 的协同工作,实现了高效的多任务管理。理解父子进程的关系、fork
的"分流"特性以及调度器的随机性,是掌握进程管理的关键。无论是 bash
执行命令时的隐式 fork
,还是程序内显式创建子进程 ,本质都在通过进程的独立性完成并发任务。写时拷贝 技术平衡了性能与资源隔离,而调度器的公平策略 让单核 CPU 也能营造"并行"假象。希望本文为你揭开了进程与 fork
的神秘面纱
以上就是本文的所有内容了,如果觉得文章写的不错,还请留下免费的赞和收藏,也欢迎各位大佬在评论区交流
分享到此结束啦
一键三连,好运连连!