跳到主要内容
深入理解 Linux 进程:从概念、fork 创建到内核状态 | 极客日志
C
深入理解 Linux 进程:从概念、fork 创建到内核状态 Linux 进程是操作系统资源分配的基本单位。文章辨析程序、进程与操作系统的关系,详解 PCB(task_struct)结构体及其包含的标识符、状态、内存指针等关键信息。通过 fork 系统调用演示父子进程创建原理,包括写时拷贝机制及返回值规则。重点解析 Linux 内核中七种进程状态(R、S、D、T、t、X、Z),结合实操代码说明状态切换条件及孤儿进程、僵尸进程的处理逻辑,帮助读者掌握进程管理的底层原理与实战技巧。
宁静 发布于 2026/2/5 更新于 2026/5/31 1.2K 浏览深入理解 Linux 进程
一、厘清 3 个核心概念(避坑第一步)
1.1 避坑点前置了解
在讲进程之前,先明确 3 个容易混淆的概念,避免从一开始就踩坑:
程序 :静态的指令集合(比如我们写的 test.c 编译后的./test 文件),不占用系统资源,只是存放在磁盘上的二进制文件;
进程 :运行中的程序,是操作系统进行资源分配和调度的基本单位(比如执行./test 后,系统中就会多一个 test 进程),占用 CPU、内存等资源;
操作系统与进程的关系 :操作系统(OS)是'进程管家',负责创建、管理、调度和销毁进程,而进程是 OS 管理的'对象'。
结合 Linux 操作系统的核心逻辑:OS 的核心是'管理',管理进程的本质是——先描述进程,再组织进程,这也是本文的核心主线,后续所有知识点(fork 创建、状态管理)都围绕这条主线展开。
1.2 进程的基本概念
课本概念 :程序的一个执行实例,正在执行的程序等。
内核观点 :担当分配系统资源(CPU 时间,内存)的实体。
我们来理解 :进程 = 内核数据结构 (task_struct) + 自己的程序代码和数据
二、描述进程:PCB 进程控制块(Linux 中是 task_struct)
2.1 认识进程:PCB 基本概念
既然 OS 要管理进程,首先得'认识'进程——就像老师管理学生,需要先记录每个学生的姓名、学号、成绩等信息,包括我们日常描述一个人时也需要先描述他的属性,OS 管理进程,也需要一个'信息记录表',这就是 PCB(Process Control Block,进程控制块)。
基本概念 :
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
Linux 操作系统下的 PCB:task_struct,这也是 Linux 下描述进程的结构体。
task_struct 是 Linux 内核的一种数据结构类型,它会被装载到 RAM(内存) 里并且包含着进程的信息。
在 Linux 系统中,PCB 的具体实现就是 task_struct 结构体(面试高频考点,务必记住),每个进程都有且仅有一个 task_struct,它包含了进程的所有属性,OS 通过操作 task_struct 来管理进程,而非直接操作进程本身。
2.2 task_struct 包含的核心内容(不用死记,理解即可) 我们不需要记住 task_struct 的所有字段,但需要掌握以下几类核心信息,这是理解进程创建(fork),调度和状态的关键:
标识符(PID/PPID) :进程的'身份证'。PID是进程唯一标识符(比如 3239), 用来区分系统中的所有进程;PPID 是父进程 ID(比如 3238), 记录该进程由哪个进程创建(比如 bash 进程创建了我们的 test 进程,fork 创建的子进程的 PPID 就是父进程的 PID,这些在后续都会慢慢体现出来)
进程状态 :记录当前进程的地址空间(虚拟地址空间)和 页表,关联进程占用的内存资源 (代码和数据)。
程序计时器(PC) :记录进程下一条要执行的指令地址 。比如进程正在执行第 7 行代码,程序计数器就存储第 8 行代码的地址,CPU 切换进程时,会通过这个字段恢复进程的执行进度。
内存指针 :指向进程的地址空间,包括程序代码和进行相关数据的指针,还有其他进程共享的内存块的指针
优先级 :相对于其他进程的优先级。
上下文数据 :进程执行时处理器的寄存器中的数据
I/O 状态信息 :包括显示的 I/O 请求,分配给进程的 I/O 设备和被进程使用的文件列表。
记账信息 :可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他资源 :比如进程占用的 CPU 时间,打开的文件描述符、等。
更加具体的信息后面还会再慢慢讲到的,这里大家先理解即可。
三、组织进程:如何管理海量 task_struct?
3.1 OS 用链表管理 task_struct 系统中会同时运行成百上千个进程,也就会有成百上千个 task_struct,OS 不可能零散地管理这些结构体,必须用高效的数据结构将它们'组织起来'——就像老师用班级名单(链表)管理学生,OS 用链表或红黑树管理 task_struct 。
链表 :适合进程数量较少的场景,通过 task_struct 中的 next/prev 指针,将所有进程的 task_struct 串联起来,遍历效率适中;
红黑树 :适合进程数量较多的场景(比如服务器上的上千个进程),红黑树的查找、插入、删除效率是 O(log n),远高于链表,Linux 内核在进程较多时会自动切换为红黑树管理。
核心目的 :让 OS 能快速找到需要调度的进程(比如找到优先级最高的进程),同时也能快速管理 fork 创建的子进程(将子进程的 task_struct 加入链表 / 红黑树)。
注意 :我们在前面的学习中都先使用链表的结构来理解就行了,后续再扩展讲解。
3.2 查看进程
进程的信息可以通过 /proc 系统文件夹查看
大多数进程信息同样可以使用 top 和 ps 这些用户级工具来获取
#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 ;
}
四、fork 创建进程(核心实操 + 底层原理,衔接 PCB) 理解了'描述进程、组织进程'的逻辑后,我们来看 Linux 中最基础的进程创建方式 —— fork 系统调用。fork 是创建子进程的核心接口,也是理解'父子进程关系''写时拷贝'的关键,更是衔接 PCB 和进程状态的重要知识点。
4.1 fork 的核心作用 fork 的本质是【复制父进程,创建子进程】-- 调用 fork() 后,操作系统会复制 (写时拷贝) 当前进程 (父进程) task_struct,虚拟地址空间,页表等所有资源,生成一个新的进程 (子进程,只有部分会有所修改),最终系统中会同时存在两个进程:原来的父进程和新创建的子进程。
核心记住一句话 (面试高频) :fork 有且仅有一个调用,但有两个返回值 ,这是 fork 最特殊,最容易混淆的点。
4.2 fork 的底层原理 (PCB/struct task_struct) 结合前文 'OS 管理进程 = 描述 + 组织' ,fork 创建子进程的 3 个核心步骤(底层逻辑,理解即可):
父进程调用 fork () 系统调用,OS 接收请求后,首先复制父进程的 task_struct(PCB) :将父进程的 PID、程序计数器、内存指针等属性复制一份,修改子进程的 PID(分配新的唯一 ID),设置子进程的 PPID = 父进程 PID,初始化子进程的状态(默认进入 R 态,等待 CPU 调度);
复制父进程的虚拟地址空间和页表:子进程的虚拟地址空间布局和父进程完全一致(比如代码段、堆区、栈区的地址范围相同),页表也会复制父进程的映射关系(此时父子进程的虚拟地址→物理地址映射完全一致);
OS 将子进程的 task_struct 加入进程链表 / 红黑树(组织进程),完成子进程的创建,此时父进程和子进程同时进入运行队列,等待 CPU 调度。
补充关键衔接点(对应虚拟地址知识点,也是后面问题回答的关键):
我们后续会提到'C/C++ 看到的地址都是虚拟地址',fork 创建子进程时,父子进程的虚拟地址完全相同,但物理地址默认共享、修改时分离 —— 这就是「写时拷贝」机制,简单说:父子进程未修改数据时,共享同一块物理内存;当任意一方修改数据时,OS 会为修改方分配新的物理内存,避免相互影响。
4.3 fork 的核心特性以及几个关键问题
4.3.1 关键问题
✅️三个关键问题:(下图中解答)
1. fork 为什么给子进程返回 0,给父进程返回子进程的 pid? 2. 为什么同一个函数会返回两次? 3. 为什么同一个变量可以同时满足两个条件,又 == 0,又 >= 0(这里先简单讲解,后续的学习中还会有更详细的解释)
为什么 fork 会有两个返回值 (补充讲解,图中其实也有):
不是 fork 调用了两次,而是【fork 创建子进程后,父进程和子进程会同时继续执行 fork () 之后的代码】,因此 fork () 的返回值会被两个进程分别接收,形成'一个调用,两个返回值'。
父进程中,fork () 的返回值 = 子进程的 PID(正数):父进程通过这个返回值,识别自己创建的子进程;
子进程中,fork () 的返回值 = 0(零):子进程通过返回值 0,识别自己是子进程;
若 fork 返回负数(<0):表示子进程创建失败(比如系统资源不足,无法分配新的 task_struct)。
4.3.2 实操代码 #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 ;
}
五、Linux 内核中的进程状态(核心重点,必懂必练) fork 创建子进程后,父子进程并非一直处于'运行'状态 —— 它们会随着资源分配和 OS 调度,在不同状态之间切换(比如进程等待键盘输入时,会从运行态切换到睡眠态)。很多初学者会混淆'进程状态'和'进程是否在运行',其实 Linux 内核定义的进程状态,是基于 '进程是否能被 CPU 调度' 来划分的。
5.1 操作系统的进程状态(重点看下面这些图来理解) 在讲述 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 ;
}
5.2 先看 Linux 内核中的进程状态(直接看源码,逐句解读) 在 Linux 内核中,进程状态通过 task_struct _array 数组定义(kernel 源码如下),共 7 种状态,但我们只需要重点掌握前面 6 种即可(X 状态几乎不可见)
static const char *const task_state_array[] = {
"R (running)" ,
"S (sleeping)" ,
"D (disk sleep)" ,
"T (stopped)" ,
"t (tracing stop)" ,
"X (dead)" ,
"Z (zombie)" ,
};
状态标识 状态名称 含义描述 R运行状态 进程正在运行或位于运行队列中 S睡眠状态(可中断) 进程在等待事件完成,可被信号中断 D磁盘休眠状态(不可中断) 进程通常等待 I/O 操作完成,不可被信号中断 T停止状态 进程被 SIGSTOP 信号暂停,可通过 SIGCONT 信号恢复运行 t追踪停止状态 进程正在被追踪调试而停止(如使用 ptrace) X死亡状态 进程已终止,仅作为返回状态,不会在任务列表中显示 Z僵尸状态 进程已终止,但其父进程尚未读取其退出状态,残留于进程表中
⚠️关键提醒 :内核中的状态是'位图'形式(通过比特位表示),但是我们无需关系底层的实现,只需要理解每种状态的含义,触发条件和切换逻辑,结合实操来理解学习即可。
5.3 七种核心进程状态详解(结合 fork 实操,一看就懂)
5.3.1 R 状态(running,运行状态)【高频误解点】
核心含义 :不是'正在 CPU 上运行',而是'要么正在 CPU 上运行,要么在运行队列中等待 CPU 调度'。
举例 :我们执行 ./code.exe,fork 创建父子两个进程,用 ps aux | grep code.exe 查看进程状态,两个进程大概率都是 R 态 —— 如果此时 CPU 只有一个核心,两个 R 态进程会交替占用 CPU,切换时通过 task_struct 中的程序计数器恢复执行进度。
切换条件 :
进入 R 态 :进程被创建后(比如 fork 创建子进程)、睡眠态进程等待的事件完成(比如等待的文件读取完成)、停止态进程被唤醒(比如收到 SIGCONT 信号);
退出 R 态 :时间片耗尽(CPU 给进程分配的运行时间到了)、进程主动放弃 CPU(比如调用 sleep 函数)、被高优先级进程抢占。
5.3.2 S 状态(sleeping,可中断睡眠态)【最常见状态】
核心含义 :进程等待某个事件完成(比如等待键盘输入、等待文件读取、等待网络数据),此时进程不占用 CPU,处于'睡眠'状态,可以被信号中断 (比如用 kill -9 信号唤醒)。
举例 :我们修改 code.c,在子进程中调用 sleep (10),执行后用 ps 查看,子进程状态就是 S—— 因为子进程在等待 10 秒后被唤醒,此时不占用 CPU 资源。
补充 :S 状态也叫'浅度睡眠',是 Linux 中最常见的状态(比如我们打开的浏览器、QQ,大部分时间都处于 S 状态,只有当我们操作时,才会切换到 R 态)。
5.3.3 D 状态(disk sleep,不可中断睡眠状态)【高危状态】
核心含义 :进程正在等待 I/O 操作完成(比如从磁盘读取大文件、向磁盘写入数据),此时进程不占用 CPU,不能被信号中断 (即使发送 kill -9 信号,也无法杀死进程)。
关键提醒 :D 状态是'保护态'—— 比如进程正在向磁盘写入数据,如果此时强行中断进程,会导致磁盘数据损坏,所以 Linux 内核禁止中断 D 状态的进程。
举个例子 :我们写一个 fork 创建子进程、子进程拷贝大文件的程序,执行后用 ps 查看,子进程大概率会显示 D 状态 —— 直到文件拷贝完成(I/O 操作结束),进程才会切换到 R 态或 S 态。我们下面图中演示的时候会使用 dd 的一些操作来观察
5.3.4 T 状态(stopped,停止状态)【可手动控制】
核心含义 :进程被暂停运行,不占用 CPU 资源,也不等待任何事件,只能通过特定信号唤醒。
触发条件 :
进入 T 态 :向进程发送 SIGSTOP 信号(比如 kill -SIGSTOP 3873(kill -19 3873),3873 是 fork 创建的子进程 PID);
退出 T 态 :向进程发送 SIGCONT 信号(比如 kill -SIGCONT 3873(kill -18 3873),唤醒进程,切换到 R 态)。
实操案例(基于 code) :
执行 ./code,用 ps aux | grep code 获取子进程 PID(比如 3873);
执行 kill -SIGSTOP 3873,再次查看,子进程状态变为 T;
执行 kill -SIGCONT 3873,再次查看,子进程状态恢复为 R。
✅️ 补充:前后台进程
eg :./myproc(让程序在前台运行)
5.3.5 t 状态(tracing stop,追踪停止态)【调试相关】
核心含义 :进程被调试器(比如 gdb)追踪时的停止状态,和 T 状态类似,但仅在调试场景下出现。
举个例子 :用 gdb 调试 code 程序,设置断点后运行(r),进程会停在断点处,此时用 ps 查看,进程状态就是 t — 直到我们执行'下一步'(n 命令),进程才会继续运行。
5.3.6 X 状态(dead,死亡状态)【了解即可】
核心含义 :进程彻底退出,task_struct(PCB)被 OS 释放,是进程的最终状态。
关键提醒 :X 状态只是一个'返回状态',不会出现在 ps 命令的进程列表中 —— 因为进程退出后,PCB 会被立即释放,OS 不会保留该进程的任何信息。
5.3.7 Z 状态(zombie,僵尸状态)【面试高频,重点掌握】
核心含义 :子进程已经退出,但父进程没有读取子进程的退出状态(比如父进程一直在睡眠,没有调用 wait 函数),此时子进程的 task_struct(PCB)不会被释放,处于'僵尸'状态。
关键特点 :
僵尸进程不占用 CPU 和内存资源(除了 task_struct 本身);
僵尸进程无法被 kill 命令杀死 (因为进程已经退出,只是 PCB 未被释放);
危害 :如果父进程创建了大量子进程(比如循环 fork)且不回收,会导致大量僵尸进程占用 PCB 资源,造成内存泄漏 (下图中会讲到)。
#include <stdio.h>
#include <unistd.h>
int main () {
printf ("我是父进程:%d\n" , getpid());
pid_t id = fork();
if (id == 0 ) {
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 ) {
while (1 ) {
sleep(1 );
printf ("我是父进程:%d, ppid: %d\n" , getpid(), getppid());
}
}
return 0 ;
}
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main () {
pid_t id = fork();
if (id < 0 ) {
perror("fork" );
return 1 ;
} else if (id > 0 ) {
printf ("parent[%d] is sleeping...\n" , getpid());
sleep(30 );
} else {
printf ("child[%d] is begin Z...\n" , getpid());
sleep(5 );
exit (EXIT_SUCCESS);
}
return 0 ;
}
编译运行 :gcc zombie_test.c -o zombie_test && ./zombie_test
另开一个终端监控 :while :; do ps aux | grep zombie_test | grep -v grep; done
观察结果 :5 秒后,子进程状态变为 Z(显示,表示僵尸进程),直到父进程 30 秒后醒来,子进程才会被回收 —— 这就是 fork 创建子进程后,未处理退出状态导致的僵尸进程问题。
5.4 进程状态查看和切换总结(一张图看懂,不用死记)
进程状态查看 :ps aux / ps axj 命令
补充 :可以结合管道使用,上面的一些示例中用到过,大家可以看看
选项 :
a:显示一个终端所有的进程,包括其他用户的进程。
x:显示没有控制终端的进程,例如后台运行的守护进程。
j:显示进程归属的进程组 ID,会话 ID,父进程 ID,以及与作业控制相关的信息。
u:以用户为中心的格式显示进程信息,提供进程的详细信息,如用户,CPU 和内存使用情况等
5.5 孤儿进程的概念理解(被 1 号领养)
父进程如果提前退出,那么子进程后退出,进入 Z 之后,该如何处理呢?
父进程先退出,子进程就称之为'孤儿进程'
孤儿进程被 1 号 init/systemd 进程领养,也由 init/systemd 进程回收。
结语 Linux 进程的核心逻辑,说到底还是操作系统'先描述、再组织'的管理思想 —— 从 task_struct 封装进程属性,到 fork 复制父进程创建子进程,再到内核定义的七种状态流转,最后通过 ps、top 等命令实操落地,每一步都离不开底层原理与实际应用的结合。对于初学者而言,不必死记硬背源码和状态定义,更重要的是抓住三条主线:① 描述进程(task_struct);② 创建进程(fork 的两个返回值、写时拷贝);③ 管理进程状态(六大核心状态 + 切换逻辑)。吃透这三条主线,就能打通 Linux 进程学习的第一关。如果大家在实操中遇到 fork 创建失败、进程状态识别等问题,欢迎交流,也可以收藏本文反复复盘,愿我们都能在 Linux 底层学习中稳步前行,从'会用'走向'懂原理'。
相关免费在线工具 Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
HTML转Markdown 将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
JSON 压缩 通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
JSON美化和格式化 将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online