深入理解 Linux 进程:从概念、fork 创建到内核状态
Linux 进程是操作系统资源分配的基本单位。文章辨析程序、进程与操作系统的关系,详解 PCB(task_struct)结构体及其包含的标识符、状态、内存指针等关键信息。通过 fork 系统调用演示父子进程创建原理,包括写时拷贝机制及返回值规则。重点解析 Linux 内核中七种进程状态(R、S、D、T、t、X、Z),结合实操代码说明状态切换条件及孤儿进程、僵尸进程的处理逻辑,帮助读者掌握进程管理的底层原理与实战技巧。

Linux 进程是操作系统资源分配的基本单位。文章辨析程序、进程与操作系统的关系,详解 PCB(task_struct)结构体及其包含的标识符、状态、内存指针等关键信息。通过 fork 系统调用演示父子进程创建原理,包括写时拷贝机制及返回值规则。重点解析 Linux 内核中七种进程状态(R、S、D、T、t、X、Z),结合实操代码说明状态切换条件及孤儿进程、僵尸进程的处理逻辑,帮助读者掌握进程管理的底层原理与实战技巧。

在讲进程之前,先明确 3 个容易混淆的概念,避免从一开始就踩坑:
结合 Linux 操作系统的核心逻辑:OS 的核心是'管理',管理进程的本质是——先描述进程,再组织进程,这也是本文的核心主线,后续所有知识点(fork 创建、状态管理)都围绕这条主线展开。

进程 = 内核数据结构 (task_struct) + 自己的程序代码和数据
既然 OS 要管理进程,首先得'认识'进程——就像老师管理学生,需要先记录每个学生的姓名、学号、成绩等信息,包括我们日常描述一个人时也需要先描述他的属性,OS 管理进程,也需要一个'信息记录表',这就是 PCB(Process Control Block,进程控制块)。
基本概念:
Linux 操作系统下的 PCB:task_struct,这也是 Linux 下描述进程的结构体。task_struct 是 Linux 内核的一种数据结构类型,它会被装载到 RAM(内存) 里并且包含着进程的信息。在 Linux 系统中,PCB 的具体实现就是 task_struct 结构体(面试高频考点,务必记住),每个进程都有且仅有一个 task_struct,它包含了进程的所有属性,OS 通过操作 task_struct 来管理进程,而非直接操作进程本身。
我们不需要记住 task_struct 的所有字段,但需要掌握以下几类核心信息,这是理解进程创建(fork),调度和状态的关键:
PID是进程唯一标识符(比如 3239), 用来区分系统中的所有进程;PPID 是父进程 ID(比如 3238), 记录该进程由哪个进程创建(比如 bash 进程创建了我们的 test 进程,fork 创建的子进程的 PPID 就是父进程的 PID,这些在后续都会慢慢体现出来)CPU 切换进程时,会通过这个字段恢复进程的执行进度。I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。CPU 时间,打开的文件描述符、等。
系统中会同时运行成百上千个进程,也就会有成百上千个 task_struct,OS 不可能零散地管理这些结构体,必须用高效的数据结构将它们'组织起来'——就像老师用班级名单(链表)管理学生,OS 用链表或红黑树管理 task_struct。
注意:我们在前面的学习中都先使用链表的结构来理解就行了,后续再扩展讲解。

/proc 系统文件夹查看

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
while (1) {
printf("我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
}
return 0;
}
理解了'描述进程、组织进程'的逻辑后,我们来看 Linux 中最基础的进程创建方式 —— fork 系统调用。fork 是创建子进程的核心接口,也是理解'父子进程关系''写时拷贝'的关键,更是衔接 PCB 和进程状态的重要知识点。
下图中涉及的代码在下面会有演示:

fork 的本质是【复制父进程,创建子进程】-- 调用 fork() 后,操作系统会复制 (写时拷贝) 当前进程 (父进程) task_struct,虚拟地址空间,页表等所有资源,生成一个新的进程 (子进程,只有部分会有所修改),最终系统中会同时存在两个进程:原来的父进程和新创建的子进程。
核心记住一句话 (面试高频):fork 有且仅有一个调用,但有两个返回值,这是 fork 最特殊,最容易混淆的点。

结合前文 'OS 管理进程 = 描述 + 组织',fork 创建子进程的 3 个核心步骤(底层逻辑,理解即可):
补充关键衔接点(对应虚拟地址知识点,也是后面问题回答的关键): 我们后续会提到'C/C++ 看到的地址都是虚拟地址',fork 创建子进程时,父子进程的虚拟地址完全相同,但物理地址默认共享、修改时分离 —— 这就是「写时拷贝」机制,简单说:父子进程未修改数据时,共享同一块物理内存;当任意一方修改数据时,OS 会为修改方分配新的物理内存,避免相互影响。
✅️三个关键问题:(下图中解答)
1. fork 为什么给子进程返回 0,给父进程返回子进程的 pid? 2. 为什么同一个函数会返回两次? 3. 为什么同一个变量可以同时满足两个条件,又 == 0,又 >= 0(这里先简单讲解,后续的学习中还会有更详细的解释)


为什么 fork 会有两个返回值 (补充讲解,图中其实也有): 不是 fork 调用了两次,而是【fork 创建子进程后,父进程和子进程会同时继续执行 fork () 之后的代码】,因此 fork () 的返回值会被两个进程分别接收,形成'一个调用,两个返回值'。
两个返回值的规则(核心,必须会,配合上图理解):
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
printf("fork 之前:我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid());
fork();
printf("fork 之后:我是一个进程:pid: %d, ppid: %d\n", getpid(), getppid());
sleep(1);
return 0;
}

图中涉及的代码可以好好看一下:

fork 创建子进程后,父子进程并非一直处于'运行'状态 —— 它们会随着资源分配和 OS 调度,在不同状态之间切换(比如进程等待键盘输入时,会从运行态切换到睡眠态)。很多初学者会混淆'进程状态'和'进程是否在运行',其实 Linux 内核定义的进程状态,是基于 '进程是否能被 CPU 调度' 来划分的。
在讲述 Linux 内核中的进程状态之前,我们先来了解一下操作系统的进程状态


下图中所用到的计算代码:
#include <stdio.h>
#include <unistd.h>
struct obj {
int a;
int b;
char c;
double d;
};
int main() {
struct obj x;
printf("&x: %p, &(x.a): %p\n", &x, &(x.a));
long long offset = (long long)&(((struct obj*)0)->d);
long long start = (long)(long)&(x.d) - offset;
printf("offset: %d\n", offset);
printf("addr 对比:%p,%p\n", &x, start);
return 0;
}

思考:内核为什么要这样实现链表



在 Linux 内核中,进程状态通过 task_struct _array 数组定义(kernel 源码如下),共 7 种状态,但我们只需要重点掌握前面 6 种即可(X 状态几乎不可见)
/*---------------------------------------------------
*The task state array is a strange "bitmap" of
*reasons to sleep. Thus "running" is zero, and
*you can test for combinations of others with
*simple bit tests.
*-----------------------------------------------------*/
static const char*const task_state_array[] = {
"R (running)", /* 0:运行态 */
"S (sleeping)", /* 1:可中断睡眠态 */
"D (disk sleep)", /* 2:不可中断睡眠态 */
"T (stopped)", /* 4:停止态 */
"t (tracing stop)", /* 8:追踪停止态 */
"X (dead)", /* 16:死亡态 */
"Z (zombie)", /* 32:僵尸态 */
};
| 状态标识 | 状态名称 | 含义描述 |
|---|---|---|
R | 运行状态 | 进程正在运行或位于运行队列中 |
S | 睡眠状态(可中断) | 进程在等待事件完成,可被信号中断 |
D | 磁盘休眠状态(不可中断) | 进程通常等待 I/O 操作完成,不可被信号中断 |
T | 停止状态 | 进程被 SIGSTOP 信号暂停,可通过 SIGCONT 信号恢复运行 |
t | 追踪停止状态 | 进程正在被追踪调试而停止(如使用 ptrace) |
X | 死亡状态 | 进程已终止,仅作为返回状态,不会在任务列表中显示 |
Z | 僵尸状态 | 进程已终止,但其父进程尚未读取其退出状态,残留于进程表中 |
⚠️关键提醒:内核中的状态是'位图'形式(通过比特位表示),但是我们无需关系底层的实现,只需要理解每种状态的含义,触发条件和切换逻辑,结合实操来理解学习即可。
./code.exe,fork 创建父子两个进程,用 ps aux | grep code.exe 查看进程状态,两个进程大概率都是 R 态 —— 如果此时 CPU 只有一个核心,两个 R 态进程会交替占用 CPU,切换时通过 task_struct 中的程序计数器恢复执行进度。kill -9 信号唤醒)。code.c,在子进程中调用 sleep (10),执行后用 ps 查看,子进程状态就是 S—— 因为子进程在等待 10 秒后被唤醒,此时不占用 CPU 资源。补充:S 状态也叫'浅度睡眠',是 Linux 中最常见的状态(比如我们打开的浏览器、QQ,大部分时间都处于 S 状态,只有当我们操作时,才会切换到 R 态)。



SIGSTOP 信号(比如 kill -SIGSTOP 3873(kill -19 3873),3873 是 fork 创建的子进程 PID);SIGCONT 信号(比如 kill -SIGCONT 3873(kill -18 3873),唤醒进程,切换到 R 态)。./code,用 ps aux | grep code 获取子进程 PID(比如 3873);kill -SIGSTOP 3873,再次查看,子进程状态变为 T;kill -SIGCONT 3873,再次查看,子进程状态恢复为 R。
✅️ 补充:前后台进程 eg:
./myproc(让程序在前台运行)
./myproc &(让程序在后台运行)


补充一个循环查看的指令 (上面用到了):

task_struct(PCB)不会被释放,处于'僵尸'状态。CPU 和内存资源(除了 task_struct 本身);kill 命令杀死(因为进程已经退出,只是 PCB 未被释放);
案例 1:(上图中用过的)
#include <stdio.h>
#include <unistd.h>
int main() {
printf("我是父进程:%d\n", getpid());
pid_t id = fork();
if (id == 0) {
// child
int cnt = 5;
while (cnt--) {
sleep(1);
printf("我是子进程,我正在运行:%d, ppid: %d\n", getpid(), getppid());
}
printf("我是子进程,我退出了:%d, ppid: %d\n", getpid(), getppid());
} else if (id > 0) {
// father
while (1) {
sleep(1);
printf("我是父进程:%d, ppid: %d\n", getpid(), getppid());
}
}
return 0;
}
案例 2:(补充的)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main() {
pid_t id = fork();
// 创建子进程
if (id < 0) {
perror("fork");
// fork 失败
return 1;
} else if (id > 0) {
// 父进程
printf("parent[%d] is sleeping...\n", getpid());
sleep(30);
// 父进程睡眠 30 秒,不回收子进程
} else {
// 子进程
printf("child[%d] is begin Z...\n", getpid());
sleep(5);
// 子进程睡眠 5 秒后退出
exit(EXIT_SUCCESS);
}
return 0;
}
gcc zombie_test.c -o zombie_test && ./zombie_testwhile :; do ps aux | grep zombie_test | grep -v grep; doneps aux / ps axj 命令Linux 与操作系统进程切换的对比图:

init/systemd 进程领养,也由 init/systemd 进程回收。
Linux 进程的核心逻辑,说到底还是操作系统'先描述、再组织'的管理思想 —— 从 task_struct 封装进程属性,到 fork 复制父进程创建子进程,再到内核定义的七种状态流转,最后通过 ps、top 等命令实操落地,每一步都离不开底层原理与实际应用的结合。对于初学者而言,不必死记硬背源码和状态定义,更重要的是抓住三条主线:① 描述进程(task_struct);② 创建进程(fork 的两个返回值、写时拷贝);③ 管理进程状态(六大核心状态 + 切换逻辑)。吃透这三条主线,就能打通 Linux 进程学习的第一关。如果大家在实操中遇到 fork 创建失败、进程状态识别等问题,欢迎交流,也可以收藏本文反复复盘,愿我们都能在 Linux 底层学习中稳步前行,从'会用'走向'懂原理'。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 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
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online