【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) 的思想仍然是理解调度设计的重要铺垫。

相关推荐
郝学胜-神的一滴2 小时前
力扣 662 :二叉树最大宽度
java·数据结构·c++·python·算法·leetcode·职场和发展
仙俊红2 小时前
反射到底解决什么问题?
java·开发语言
2301_764441332 小时前
基于Stackelberg博弈的分散式库存模型
python·算法·数学建模
着迷不白2 小时前
七、Linux网络管理
服务器·网络·php
大阳1232 小时前
ARM.9(RGBLCD,PWM)
c语言·开发语言·汇编·单片机·嵌入式硬件·pwm·rgblcd
Urbano2 小时前
工业及物流工装制作流程与各工序自动化替代方案
运维·自动化
加油码2 小时前
Linux IO 多路转接详解:从 select、poll 到 epoll
linux·c++
是一个Bug2 小时前
Nginx 与 API Gateway:从“小区门卫”到“商场总服务台”
运维·nginx·gateway
syagain_zsx2 小时前
Linux进程控制学习总结(2/2)
linux·运维·学习