
大家好,欢迎来到 huangjin007_ 的博客
⭐ 个人主页:huangjin007_
🔥 文章收录专栏:Linux 内功修炼手册(系统篇)
总会有一些坚持
能从冰封的土地里
培育出十万朵怒放的蔷薇
Linux 系统篇(十六) ------ 僵尸进程、孤儿进程、进程优先级
文章目录
- [Linux 系统篇(十六) ------ 僵尸进程、孤儿进程、进程优先级](#Linux 系统篇(十六) —— 僵尸进程、孤儿进程、进程优先级)
-
- [一、僵尸进程(Zombie Process)](#一、僵尸进程(Zombie Process))
-
- [1.1 什么是僵尸进程?为什么它叫"僵尸"?](#1.1 什么是僵尸进程?为什么它叫“僵尸”?)
- [1.2 创建一个持续 30 秒的僵尸进程](#1.2 创建一个持续 30 秒的僵尸进程)
- [1.3 僵尸进程的危害](#1.3 僵尸进程的危害)
-
- [1.3.1 为什么说僵尸进程会导致内存泄漏?](#1.3.1 为什么说僵尸进程会导致内存泄漏?)
- [1.3.2 内核对象申请与 Slab 分配器](#1.3.2 内核对象申请与 Slab 分配器)
- [1.4 如何避免僵尸进程?](#1.4 如何避免僵尸进程?)
- [二、孤儿进程(Orphan Process)](#二、孤儿进程(Orphan Process))
-
- [2.1 什么是孤儿进程?](#2.1 什么是孤儿进程?)
- [2.2 观察孤儿进程的收养过程](#2.2 观察孤儿进程的收养过程)
- [2.3 孤儿进程 vs 僵尸进程](#2.3 孤儿进程 vs 僵尸进程)
- 三、进程优先级(Priority)
-
- [3.1 为什么需要进程优先级?](#3.1 为什么需要进程优先级?)
- [3.2 查看系统进程的优先级:`ps -l` 命令](#3.2 查看系统进程的优先级:
ps -l命令) - [3.3 深入理解 `PRI` 和 `NI`](#3.3 深入理解
PRI和NI) -
- [3.3.1 什么是 PRI?](#3.3.1 什么是 PRI?)
- [3.3.2 什么是 NI(nice)?](#3.3.2 什么是 NI(nice)?)
- [3.3.3 PRI 和 NI 的关系](#3.3.3 PRI 和 NI 的关系)
- [3.4 如何修改进程的优先级(调整 nice 值)](#3.4 如何修改进程的优先级(调整 nice 值))
- [3.5 补充重要概念:竞争性、独立性、并行、并发](#3.5 补充重要概念:竞争性、独立性、并行、并发)
- [3.6 深入理解 nice 值的"饥饿问题"](#3.6 深入理解 nice 值的“饥饿问题”)
- [3.7 进程与权限:一个小知识](#3.7 进程与权限:一个小知识)
- 四、面试问题
- 结语:
一、僵尸进程(Zombie Process)
1.1 什么是僵尸进程?为什么它叫"僵尸"?
想象你正走在路上,一个人突然从你身边跑过,却脚下一滑,摔倒在地,猝死了。虽然他已经没有了生命体征,但因为死因不明,他的遗体和随身物品(对应子进程的 task_struct、退出码和信号)必须原封不动地保留在原地,任何人不得擅自收尸。这个"等待验尸、尸体尚未处理"的阶段,就是 Linux 中的 僵尸状态。
你马上报警,警察赶到现场:拍照、登记身份信息、查明死因(对应父进程调用 waitpid 获取子进程的退出信息)。一切登记完毕,警察才挥手让救护车把遗体拉走,此时才真正进入死亡状态(X 状态),所有相关资源(task_struct)被释放。
如果警察一直不来(父进程始终不调用 wait),遗体就会一直留在原地,占用公共空间------这正是僵尸进程的危害所在。
在 Linux 中,僵尸进程 就是指:一个子进程已经退出(结束运行),但它的父进程还没有调用 wait() 或 waitpid() 来读取这个子进程的退出状态 。此时,子进程虽然已经不再执行任何代码,但它仍然保留在内核的进程表中,占据着一个 task_struct 结构体,处于 Z(Zombie)状态。
用更准确的话来说:
- 进程退出后,它的退出状态(例如退出码、是否被信号杀死等)需要被保存下来,以便父进程查询。
- 这些信息保存在内核的
task_struct(进程控制块,PCB)中。- 只要父进程没有读取这些信息,这个
task_struct就不会被释放,进程就一直处于僵尸状态。
"僵尸"这个名字很形象:它已经"死"了(退出执行),但它的"尸体"(进程表项)还留在系统里,没有被"收尸"(回收)。
1.2 创建一个持续 30 秒的僵尸进程
我们写一段简单的 C 代码来亲自制造一个僵尸进程。
c
#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); // 父进程睡 30 秒,期间不回收子进程
}
else
{
// 子进程分支
printf("child[%d] is begin Z...\n", getpid());
sleep(5); // 子进程睡 5 秒后退出
exit(EXIT_SUCCESS);
}
return 0;
}
代码解析:
fork()会创建一个新进程。在父进程中返回子进程的 PID(>0),在子进程中返回 0,出错返回 -1。- 父进程进入
id > 0分支,打印自己的 PID 后,调用sleep(30)休眠 30 秒。- 子进程进入
else分支,打印自己的 PID 后,调用sleep(5)休眠 5 秒,然后调用exit(EXIT_SUCCESS)正常退出。
预期结果 :子进程在 5 秒后退出,但父进程还在睡觉,没有读取子进程的退出状态,因此子进程会变成僵尸进程,持续 25 秒(直到父进程醒来退出或调用 wait)。
步骤:
- 将上述代码保存为
zombie.c。- 编译:
gcc zombie.c -o zombie- 在一个终端运行
./zombie。- 在另一个终端中,快速执行
ps -l或ps -al | grep zombie来观察进程状态。
观察输出:
运行程序后,立即在另一个终端执行:
bash
while true; do ps -o pid,ppid,state,comm -C zombie; sleep 1; done
或者简单使用:
bash
ps -al | grep zombie
你会看到类似下面的输出(具体 PID 可能不同):

注意第三行:状态列显示为 Z,并且命令名后有 <defunct>(已失效)。这就证明我们成功制造了一个僵尸进程。
1.3 僵尸进程的危害
你可能觉得,反正子进程已经退出了,只是留个记录而已,能有多大危害?但实际上,僵尸进程的危害是实实在在的,主要体现在内存泄漏上。
1.3.1 为什么说僵尸进程会导致内存泄漏?
- 每个进程在内核中都有一个对应的
task_struct结构体。这个结构体非常大,包含了进程的所有信息:PID、状态、优先级、内存管理信息、文件描述符表、信号处理函数等等。- 当进程退出时,大部分资源(如用户空间内存、打开的文件等)会被立即释放。但是,
task_struct本身不会立即释放,因为内核需要保留其中的退出状态(如退出码)供父进程查询。- 如果父进程一直不调用
wait(),那么这个task_struct就会一直占据内核内存。- 如果一个父进程创建了大量子进程,但从不回收它们,就会产生大量僵尸进程,导致内核内存被耗尽,系统性能下降,甚至无法创建新进程。
1.3.2 内核对象申请与 Slab 分配器
这里涉及一个更深层的知识点:内核是如何管理 task_struct 的?
- 创建进程时,内核需要从内存中申请一块空间来存放
task_struct。- 进程退出且未被回收时,这个
task_struct处于"僵尸态",占用内存但无法使用。- 当父进程调用
wait()回收子进程后,task_struct被释放。- 为了提高效率,内核并不会把释放的
task_struct直接归还给操作系统,而是放入一个叫做 Slab 分配器 的缓存中。下次创建新进程时,可以直接从缓存中取出一个task_struct结构体复用,而不需要重新分配内存。这种机制叫做"数据结构对象的缓存"。
所以,虽然僵尸进程的 task_struct 最终会被回收,但在回收之前,它一直占据着内存,是一种内存泄漏。尤其是那些常驻内存的进程(如服务器进程),如果它们不断产生僵尸子进程而不回收,内存泄漏会越来越严重。
面试常问 :僵尸进程会导致内存泄漏吗?为什么?回答要点:是的,因为退出状态保存在 task_struct 中,父进程不读取就一直占用内核内存。
1.4 如何避免僵尸进程?
避免僵尸进程的关键在于父进程要及时回收子进程。方法主要有:
- 父进程调用
wait()或waitpid()等待子进程结束并获取其状态。- 父进程显式忽略
SIGCHLD信号(signal(SIGCHLD, SIG_IGN)),这样子进程退出后内核会自动回收,不会产生僵尸进程。- 使用双
fork()技巧:父进程先fork一个子进程,子进程再fork一个孙进程,然后子进程立即退出,孙进程被 1 号进程收养,由 1 号进程负责回收。这样原父进程就不需要关心孙进程的回收了。
这些方法会在后续讲解时详细展开,目前只需要知道"父进程必须负责回收"即可。
二、孤儿进程(Orphan Process)
2.1 什么是孤儿进程?
与僵尸进程相反,孤儿进程 是指:父进程先于子进程退出,导致子进程失去了父进程,变成了"孤儿"。
那么问题来了:父进程死了,子进程的退出状态谁来读取?如果没有进程来回收,子进程退出后不也会变成僵尸吗?
Linux 的设计非常巧妙:当一个进程的父进程退出时,它的子进程会被自动"领养"给 1 号进程(init 或 systemd) 。1 号进程是系统启动的第一个进程,负责管理系统中的所有进程,它会定期调用 wait() 来回收这些被领养的孤儿进程,因此孤儿进程不会变成僵尸。
所以,孤儿进程虽然暂时失去了生父,但会有一个"养父"(1 号进程)负责它的善后工作。
2.2 观察孤儿进程的收养过程
c
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {
// 子进程
printf("I am child, pid : %d, ppid : %d\n", getpid(), getppid());
sleep(10); // 子进程睡 10 秒,确保父进程已经退出
printf("child after sleep, ppid : %d\n", getppid()); // 打印新的父进程PID
exit(0);
}
else {
// 父进程
printf("I am parent, pid: %d\n", getpid());
sleep(3); // 父进程只睡 3 秒,然后退出
exit(0);
}
return 0;
}
运行并观察:
- 编译运行程序。
- 父进程在 3 秒后退出。
- 子进程在 10 秒后退出。在父进程退出后、子进程退出前,子进程的父进程 ID 会变成 1(或 systemd 的 PID,通常是 1)。
预期输出(可能略有不同):

看到最后一行,子进程的 PPID 变成了 1,说明它已经被 1 号进程收养。
2.3 孤儿进程 vs 僵尸进程
| 特征 | 僵尸进程 | 孤儿进程 |
|---|---|---|
| 产生条件 | 子进程退出,父进程还在运行但不调用 wait() |
父进程先退出,子进程还在运行 |
| 状态 | 子进程处于 Z 状态,占用进程表 |
子进程正常运行,只是父进程变成 1 号进程 |
| 危害 | 导致内存泄漏(task_struct 不被释放) |
一般无危害,因为 1 号进程会负责回收 |
| 处理者 | 需要父进程主动回收 | 由 1 号进程(init/systemd)自动收养并回收 |
| 本质 | 父进程"不作为" | 父进程"提前离场" |
三、进程优先级(Priority)
3.1 为什么需要进程优先级?
在多任务操作系统中,CPU 的数量是有限的(通常只有几个核心),而同时运行的进程可能成百上千。CPU 必须决定"先执行谁,后执行谁"。进程优先级就是决定进程获取 CPU 资源先后顺序的规则。
- 优先级高的进程:优先获得 CPU 时间,运行得更快、更及时。
- 优先级低的进程:需要等待,可能延迟较久。
3.2 查看系统进程的优先级:ps -l 命令
在 Linux 或 Unix 系统中,使用 ps -l 命令可以查看当前终端相关进程的详细信息。例如:
bash
ps -l
输出类似:

重点关注以下几列:
- UID:进程执行者的用户 ID,表示这个进程属于哪个用户。
- PID:进程 ID,进程的唯一标识。
- PPID:父进程 ID,表示这个进程是由哪个进程创建的。
- PRI :进程的优先级(Priority),数值越小,优先级越高,越早被执行。
- NI:进程的 nice 值,是优先级的修正数值。
3.3 深入理解 PRI 和 NI
3.3.1 什么是 PRI?
PRI 就是进程的实际优先级。在 Linux 中,普通进程的 PRI 默认值是 80 , PRI 值越小,进程越优先被调度。
3.3.2 什么是 NI(nice)?
NI 是 nice 值,表示进程的"谦让程度"。nice 的英文原意是"友好的、和善的"。在 Linux 中,nice 值越大,表示进程越"谦让",愿意把 CPU 让给别人,所以它的优先级就越低。反之,nice 值越小(甚至为负数),表示进程越"霸道",优先级越高。
nice 值的取值范围是 -20 到 19,共 40 个级别。
3.3.3 PRI 和 NI 的关系
很多人会混淆 PRI 和 NI,以为它们是同一个东西。其实不是。NI 只是 PRI 的一个修正量,它们的关系如下:
PRI(new) = PRI(old) + NI
但更准确的表述是,在 Linux 的普通进程调度中:
实际优先级 PRI = 默认优先级(80) + nice 值
也就是说,普通进程的 PRI 默认是 80,然后加上 NI 值得到最终的 PRI。
- 如果
NI = 0,则PRI = 80。- 如果
NI = -20(最小 nice 值,最高优先级),则PRI = 60(80 - 20 = 60,数值更小,优先级更高)。- 如果
NI = 19(最大 nice 值,最低优先级),则PRI = 99(80 + 19 = 99,数值更大,优先级更低)。
注意:为什么说"普通进程默认 80"?因为 Linux 将优先级分为两个范围:
- 实时优先级 :范围 0~99,数值越小优先级越高。实时进程用于对时间要求非常严格的任务(如音频处理、工业控制)。实时进程的
PRI值通常在 0~99 之间,并且它们的调度策略不同。- 普通优先级 :范围 100~139,对应
PRI显示为 80~119(实际上内核内部值映射后,ps显示的PRI默认是 80)。普通进程是我们通常创建的那些进程。
当我们用 ps -l 看到 PRI 为 80 时,就是普通进程的默认优先级。我们只需要记住:普通进程的 PRI 范围大约是 80~99,通过调整 nice 值(-20~19)来改变。

面试常问 :PRI 和 NI 的区别?答:PRI 是实际优先级,NI 是 nice 值,是对优先级的修正。公式:PRI = 80 + NI。NI 越小,优先级越高。
3.4 如何修改进程的优先级(调整 nice 值)
调整进程优先级本质上就是修改它的 nice 值。有三种常用方式:
方法一:使用 top 命令动态修改
top 是 Linux 下常用的性能监控工具。进入 top 界面后,你可以修改某个运行中进程的 nice 值。
步骤:
- 在终端输入
top并回车。- 按下
r键(renice 的缩写)。- 提示
PID to renice:,输入你想要修改的进程 PID,回车。- 提示
Renice PID to value:,输入新的 nice 值(范围 -20 到 19),回车。
注意:普通用户只能调高 nice 值(降低优先级),只有 root 用户可以调低 nice 值(提高优先级)。
方法二:使用 nice 和 renice 命令
-
nice命令:在启动进程时指定 nice 值。bashnice -n -5 ./myprogram # 以 nice 值 -5 启动 myprogram如果不指定,默认 nice 值为 0。
-
renice命令:修改已经运行进程的 nice 值。bashrenice -10 -p 1234 # 将 PID 为 1234 的进程 nice 值改为 -10
方法三:使用系统调用(编程方式)
在 C 程序中,可以使用以下系统调用来获取或设置进程的优先级:
c
#include <sys/time.h>
#include <sys/resource.h>
int getpriority(int which, int who);
int setpriority(int which, int who, int prio);
which:指定操作对象类型,例如PRIO_PROCESS(进程)、PRIO_PGRP(进程组)、PRIO_USER(用户)。who:对应which的 ID,如果为 0 表示当前进程/进程组/用户。prio:新的 nice 值(范围 -20 到 19)。- 返回值:
getpriority返回当前 nice 值(可能为负数,所以要用errno判断错误),setpriority成功返回 0,失败返回 -1。
示例:获取并设置当前进程的 nice 值
c
#include <stdio.h>
#include <sys/resource.h>
#include <sys/time.h>
#include <errno.h>
int main()
{
int prio = getpriority(PRIO_PROCESS, 0);
printf("Current nice value: %d\n", prio);
// 尝试将 nice 值设置为 5(降低优先级)
if (setpriority(PRIO_PROCESS, 0, 5) == -1)
{
perror("setpriority");
return 1;
}
prio = getpriority(PRIO_PROCESS, 0);
printf("New nice value: %d\n", prio);
return 0;
}
3.5 补充重要概念:竞争性、独立性、并行、并发
- 竞争性 :系统中有大量进程,但 CPU 资源非常有限(甚至只有 1 个),所以进程之间必然存在竞争关系。为了更高效地完成任务,合理分配资源,操作系统引入了优先级机制,让重要的进程优先运行。
- 独立性:多个进程同时运行,每个进程都认为自己独占所有资源(内存、CPU 等),互不干扰。这是通过虚拟内存、时间片轮转等机制实现的。独立性保证了进程之间不会因为对方的错误而崩溃。
- 并行(Parallelism) :当系统有多个 CPU 核心时,多个进程可以在同一时刻真正地同时运行,每个核心运行一个进程。这叫做并行。
- 并发(Concurrency):当系统只有一个 CPU 核心(或核心数少于进程数)时,操作系统通过快速切换进程,让每个进程都执行一小段时间(时间片),从宏观上看好像多个进程在同时运行,但微观上任意时刻只有一个进程在执行。这叫做并发。
类比:
- 并行:多个窗口同时服务多个顾客(每个窗口一个顾客)。
- 并发:一个窗口,营业员快速轮换服务多个顾客,每个顾客服务几秒钟就切换,看起来像同时服务。
它们与优先级的关系:在并发环境下,调度器根据优先级决定哪个进程先获得时间片;在并行环境下,优先级也决定了哪个进程优先被分配到空闲的 CPU 核心。
3.6 深入理解 nice 值的"饥饿问题"
前面提到,nice 值越小,优先级越高。如果某个进程的 nice 值设置得非常低(例如 -20),它就会获得大量 CPU 时间。如果系统中同时有很多低优先级进程(nice 值很高,如 19),那么这些低优先级进程可能很长时间得不到 CPU 资源,导致进程饥饿(Starvation)。
进程饥饿是指:一个进程长期无法获得所需的资源(如 CPU 时间),导致无法推进。在极端情况下,低优先级进程可能永远无法运行,就像一个排队的人总是被插队,永远轮不到他。
因此,Linux 的调度器(如 CFS,完全公平调度器)并不是简单的"优先级高就完全独占",而是会保证每个进程都能获得一定的 CPU 时间,只是高优先级进程获得的时间片更多。这样既保证了高优先级任务的响应速度,又避免了低优先级任务被饿死。
注意:普通用户只能调高自己进程的 nice 值(降低优先级),而不能调低 nice 值(提高优先级),这是为了防止普通用户抢占系统资源。只有 root 用户才能设置负的 nice 值。
3.7 进程与权限:一个小知识
你可能知道 Linux 文件有权限设置,比如读、写、执行,并且文件有所有者(UID)。但是,系统是怎么知道"谁"在访问文件的呢?
答案是:进程 。进程是资源访问的主体。当你运行一个程序时,这个程序对应的进程会携带你的用户身份(UID)。当进程去访问文件时,内核根据进程的 UID 来判断是否有权限。所以,实际上是进程在代表用户访问资源。
理解这一点很重要:进程的优先级、权限等属性都是记录在 task_struct 中的,包括它的 UID、GID、优先级等。当进程尝试打开文件时,内核会检查进程的凭证来决定是否允许访问。
四、面试问题
Q1:什么是僵尸进程?如何产生?如何避免?
A:子进程退出后,父进程没有调用 wait() 回收,导致子进程的 task_struct 保留在进程表中,处于 Z 状态。避免方法:父进程调用 wait/waitpid,或忽略 SIGCHLD 信号,或使用双 fork。
Q2:僵尸进程有什么危害?
A:导致内核内存泄漏,因为 task_struct 一直占用内存。大量僵尸进程会耗尽系统资源。
Q3:什么是孤儿进程?它会被谁收养?
A:父进程先退出,子进程还在运行,子进程成为孤儿进程。它会被 1 号进程(init/systemd)收养并回收。
Q4:PRI 和 NI 的区别?
A:PRI 是进程的实际优先级,数值越小优先级越高。NI 是 nice 值,是对优先级的修正量。普通进程的 PRI = 80 + NI,NI 范围 -20~19。
Q5:如何调整进程优先级?
A:可以使用 top 命令(按 r 输入 PID 和 nice 值)、nice/renice 命令,或在代码中使用 setpriority 系统调用。
Q6:并行和并发有什么区别?
A:并行是多个进程在多个 CPU 核心上同时运行;并发是多个进程在一个 CPU 上通过时间片轮转快速切换,宏观上同时运行。
Q7:nice 值设置不当会导致什么问题?
A:如果某个进程 nice 值过低(优先级高),可能导致其他低优先级进程得不到 CPU 时间,产生饥饿。现代调度器会有公平机制来缓解。
结语:
今天的内容到这里就结束了,希望你能有所收获~
干货整理到手抖,觉得有用的话,赏个三连回回血?__(:ᗤ」ㄥ)_ _