【Linux】进程状态、僵尸与孤儿、进程调度

进程并不是"要么在跑,要么没在跑"这样简单二分。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) 的思想仍然是理解调度设计的重要铺垫。

相关推荐
罗西的思考3 小时前
机器人 / 强化学习】HIL-SERL:人类在环驱动的具身智能进化框架
人工智能·算法·机器学习
CSharp精选营5 小时前
关系型 vs 非关系型:从原理到选型,一文搞定数据库核心分类
数据结构·nosql·关系型数据库·非关系型数据库·技术选型
美团技术团队6 小时前
LongCat 开源 VitaBench 2.0:长期动态智能体基准新标杆
人工智能·算法
To_OC1 天前
LC 207 课程表:刚学图论那会儿,我连这是拓扑排序都没看出来
javascript·算法·leetcode
To_OC1 天前
LC 208 实现 Trie 前缀树:曾被名字劝退,写完发现是送分题
javascript·算法·leetcode
BadBadBad__AK1 天前
线段树维护区间 k 次方和
c++·数学·算法·stl
AlfredZhao1 天前
生产环境里,为什么不建议把普通端口直接暴露到公网?
linux·https·443·80
_清歌2 天前
DSpark 深度解读:DeepSeek-V4 如何用「半自回归」把推理速度提升 85%
算法
统计实现局2 天前
SVD 的三步走:双对角化、Givens 收敛、排序
算法