进程并不是"要么在跑,要么没在跑"这样简单二分。Linux 内核定义了几种状态,理解它们对分析系统负载和排查问题很有帮助。
目录
[1. 六种进程状态](#1. 六种进程状态)
[2. 僵尸进程:危害与预防](#2. 僵尸进程:危害与预防)
[3. 孤儿进程:当父进程先退出](#3. 孤儿进程:当父进程先退出)
[4. 进程优先级与 nice 值](#4. 进程优先级与 nice 值)
[5. 竞争性、独立性、并行与并发](#5. 竞争性、独立性、并行与并发)
[6. Linux 2.6 的 O(1) 调度算法](#6. Linux 2.6 的 O(1) 调度算法)
1. 六种进程状态
在 Linux 内核源码中,进程状态用一个位图表示,常见的有:
-
R(running):正在运行或就绪,在运行队列中等待调度。
-
S(sleeping):可中断睡眠,等待某个事件(如 I/O 完成、信号)。
-
D(disk sleep):不可中断睡眠,通常等待 I/O 结束。这种状态下的进程不能被信号打断。
-
T(stopped):进程被暂停,通常由 SIGSTOP 信号引起,可用 SIGCONT 恢复。
-
t(tracing stop):调试期间被跟踪暂停。
-
Z(zombie):僵尸状态,进程已结束但父进程尚未回收。
-
X(dead):死亡状态,短暂存在,不会出现在进程列表中。
ps aux 的 STAT 列会显示这些缩写。如果一个进程长期处于 D 状态,往往意味着 I/O 竞争或硬件问题,需要关注。
2. 僵尸进程:危害与预防
当子进程先退出,而父进程没有调用 wait() 或 waitpid() 来回收子进程的退出状态,子进程就会进入 Z 状态,成为僵尸。
僵尸进程虽然不占用 CPU 和内存数据段,但它的 task_struct 结构必须保留(否则父进程再也拿不到退出码),所以会占用内核内存。如果父进程不断创建子进程且不回收,僵尸进程积少成多,最终会耗尽 PID 资源,系统无法创建新进程。
下面这段代码故意创建了一个持续 30 秒的僵尸进程:
cpp
int main() {
pid_t id = fork();
if (id == 0) {
// 子进程
printf("child exit\n");
exit(0);
} else {
// 父进程不 wait,休眠 30 秒
sleep(30);
}
return 0;
}
用 ps aux 观察,会看到子进程状态为 Z。解决办法很简单------父进程及时调用 wait。这也是为什么在编写多进程程序时,必须设计好子进程退出的回收逻辑。
3. 孤儿进程:当父进程先退出
如果父进程先于子进程退出,子进程就成了"孤儿"。孤儿进程会被 init(systemd)进程领养,由 init 负责回收。所以孤儿进程不会变成僵尸,它在设计上是安全的。
cpp
int main() {
pid_t id = fork();
if (id == 0) {
printf("child: pid=%d, ppid=%d\n", getpid(), getppid());
sleep(10);
printf("child: new ppid=%d\n", getppid());
} else {
printf("parent: pid=%d\n", getpid());
sleep(3);
// 父进程先退出
}
return 0;
}
运行程序观察,子进程的 ppid 在父进程退出后会变成 1。这种机制保证了资源一定被回收。
4. 进程优先级与 nice 值
Linux 是一个分时系统,进程之间存在竞争。优先级的引入让重要任务能优先获得 CPU。在 ps -l 的输出中:
-
PRI:进程当前优先级,值越小越优先。
-
NI :nice 值,用于修正优先级,取值范围
-20~19。
PRI(new) = PRI(old) + NI。普通用户只能增大 nice 值(让自己更"谦让"),降低优先级;只有 root 可以减小 nice 值,提升优先级。
修改 nice 值的常用方法:
bash
# 启动程序时指定 nice
nice -n 10 ./myproc
# 调整已运行进程的 nice
renice -n 5 -p PID
# 在 top 中按 r,输入 PID 和新 nice 值
开发者在调优多进程服务时,适当地降低后台批处理任务的 nice 值,可以有效保证交互任务的响应速度。
5. 竞争性、独立性、并行与并发
多进程环境有几个关键性质:
-
竞争性:进程争抢 CPU、内存等资源,优先级就是竞争策略的体现。
-
独立性:每个进程拥有自己的地址空间,互不干扰。
-
并行:多个 CPU 同时执行不同进程,真正的同时。
-
并发:单 CPU 通过快速切换,让多个进程在一段时间内都有推进,宏观上像同时执行。
并发的核心就是进程切换,也就是上下文切换。CPU 保存当前进程的寄存器状态,再从下一个进程的 task_struct 中恢复上下文,整个过程由内核调度程序触发。时间片耗尽、I/O 阻塞等都会触发切换。
6. Linux 2.6 的 O(1) 调度算法
Linux 2.6 内核实现了一个经典的 O(1) 调度器,核心数据结构是 runqueue,每个 CPU 有一个。runqueue 包含两个数组:活动队列 和过期队列,每个数组有 140 个链表头(0~139 对应优先级)。
-
普通进程优先级 100~139,实时优先级 0~99。
-
活动队列:存放时间片尚未用完的进程。
-
过期队列:存放时间片已耗尽、等待重新分配的进程。
调度时,通过位图快速找到第一个非空队列(即最高优先级的非空队列),从链表头取出一个进程执行。当活动队列变空时,交换活动队列和过期队列的指针,过期队列变成新的活动队列,时间片重新计算。
位图查找 + 链表取当前,使得查找最高优先级进程的时间复杂度为 O(1),进程数量增加不影响调度开销。这便是 O(1) 调度器名称的由来。虽然新版本内核已经用 CFS(完全公平调度器)替代了它,但 O(1) 的思想仍然是理解调度设计的重要铺垫。