ZeroOne AI
← 返回文章列表

Linux系统篇(十二)——进程(二):深入剖析 Linux 进程:状态变迁、优先级及调度切换逻辑

👁 1
分类:工业互联网

深入 Linux 进程内核:六种进程状态与变迁、僵尸孤儿进程、优先级机制,以及 O(1) 调度器的位图与队列原理

项目背景

在 Linux 系统中,进程就像一个个独立的"工作单元",系统能否高效运转,取决于对进程的管理能力。进程此刻在做什么?能不能使用 CPU?为什么有的进程 kill -9 都杀不死?为什么系统会出现"僵尸"?

这些问题都指向三大核心主题:进程状态进程优先级调度切换。本文深入 Linux 内核视角,讲解六种进程状态的变迁、僵尸/孤儿进程的来龙去脉、优先级的作用边界,以及 O(1) 调度器如何用位图与队列实现高效调度。

技术方案

1. 进程状态

进程状态是操作系统对进程所处运行阶段的标识,描述进程此刻在做什么、能否使用 CPU、是否在等待资源。内核会根据进程行为与资源情况自动切换状态,调度器也依据状态决定是否给进程分配 CPU。

Linux 内核中状态的定义如下:

#define TASK_RUNNING        0   // R 运行/就绪态
#define TASK_INTERRUPTIBLE  1   // S 可中断睡眠
#define TASK_UNINTERRUPTIBLE 2  // D 不可中断睡眠
#define __TASK_STOPPED      4   // T 进程被暂停
#define __TASK_TRACED       8   // T 被调试器跟踪
/* 位于 tsk->exit_state */
#define EXIT_ZOMBIE         16  // Z 僵尸进程
#define EXIT_DEAD           32  // 进程彻底消亡

ps -al 即可查看进程的当前状态。

运行状态 R

这是进程唯一能在 CPU 上执行的状态:

#include <stdio.h>
#include <unistd.h>

int main()
{
    int ret = fork();
    if (ret == 0)
    {
        // 子进程:每秒打印一次,主要处于睡眠
        while (1)
        {
            printf("我是一个子进程:%d\n", getpid());
            sleep(1);
        }
    }
    else if (ret > 0)
    {
        // 父进程:空转死循环,持续占用 CPU,处于 R 状态
        while (1) {}
    }
    return 0;
}

2. 睡眠(阻塞)状态

睡眠状态指进程主动暂停执行、放弃 CPU,等待某个事件发生才能被唤醒。分为两种:

可中断睡眠 S: 进程处于等待状态,可被信号唤醒,也可被等待的事件唤醒,不占用 CPU,属于正常阻塞。scanfsleepreadwrite 这类"等事件"的系统调用默认都会让进程进入 S 状态,可用 kill -9 杀掉。

不可中断睡眠 D: 进程处于深度等待状态,忽略所有信号,只能被等待的硬件/IO 事件唤醒,用于保证原子操作。典型场景是等待磁盘读写:写磁盘属于硬件操作,内核必须保证这次 I/O 原子性完成,不能中途被打断,因此进程进入 D 状态——此时连 kill -9 都杀不死。

#include <stdio.h>

int main()
{
    printf("我的进程:%d\n", getpid());
    while (1)
    {
        int x;
        scanf("%d", &x);   // 等待输入期间进程处于 S 状态
        printf("ok\n");
    }
}

3. 暂停状态 T

进程被外部信号强制挂起,完全停止执行、不占用 CPU、保留现场,直到收到恢复信号才继续运行。例如调试器打断点跟踪(__TASK_TRACED)就属于这一大类。

4. 僵尸进程与孤儿进程

僵尸进程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        return 1;
    }
    else if (pid == 0)
    {
        // 子进程:运行 2 秒后直接退出,成为僵尸
        printf("子进程 PID: %d 即将退出\n", getpid());
        sleep(2);
        exit(0);
    }
    else
    {
        // 父进程:持续运行,不回收子进程
        while (1)
        {
            printf("父进程 PID: %d 运行中\n", getpid());
            sleep(2);
        }
    }
    return 0;
}

孤儿进程:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main()
{
    pid_t pid = fork();
    if (pid < 0)
    {
        perror("fork");
        return 1;
    }
    else if (pid == 0)
    {
        // 子进程:持续运行,父进程退出后被 init 收养
        while (1)
        {
            printf("子进程 PID: %d, 父进程 PID: %d\n", getpid(), getppid());
            sleep(2);
        }
    }
    else
    {
        // 父进程:休眠 1 秒后退出
        sleep(1);
        printf("父进程即将退出\n");
        exit(0);
    }
    return 0;
}

孤儿进程 vs 僵尸进程对比:

对比项孤儿进程僵尸进程
形成原因父进程先退出,子进程存活子进程先退出,父进程存活且不回收
进程状态R / SZ
管理方被 PID=1 进程收养父进程持有 PCB,无人主动回收
资源占用正常占用系统资源仅占用进程表项,无 CPU/内存占用
危害性无害大量堆积会导致无法创建新进程

系统架构

进程优先级

进程优先级是操作系统决定哪个进程优先获得 CPU 时间片的数值/权重:数值越小 → 优先级越高 → 越先被执行

优先级调整:top 界面按 r(renice),输入要修改的进程标识符与修改值即可调整。

优先级为什么有极限:

  1. 防止无限抢占:若普通进程优先级可无限调高,会挤掉内核与关键系统进程的执行时间,导致系统卡顿甚至崩溃;
  2. 保障调度公平:若没有下限,低优先级进程可能永远得不到 CPU,被"饿死";
  3. 内核模型固定区间:普通进程 nice 值固定为 -2019,实时进程调度优先级固定为 199,这是调度算法的固有约束。

最终权限值: 最终权限值 = 进程优先级(默认 80)+ nice 值。例如:80 + 19 = 99,80 + (-20) = 60。

进程切换

为什么需要切换? 即使某个进程死循环,其他进程依然能执行——现代操作系统实现抢占式多任务,CPU 不会被单个进程独占。在时间片轮转调度(RR)中,每个就绪进程分到固定长度的时间片(如 10ms~100ms),时间片耗尽即切换。进程切换解决两个核心问题:并发执行(多进程看起来同时在运行)与资源隔离(每个进程都感觉独占 CPU)。

上下文是什么? 进程切换本质是保存与恢复进程上下文——即进程的"运行快照":

切换完整流程(进程 A 让出 CPU 给进程 B):

  1. 保存现场:把进程 A 所有寄存器值保存到它的 struct task_struct(PCB)中;
  2. 恢复现场:从进程 B 的 task_struct 中把之前保存的寄存器值恢复到 CPU;
  3. 继续执行:CPU 从进程 B 的 EIP 指向的指令继续执行。

一句话总结:"把进程 A 的状态打包存起来,再把进程 B 的状态加载进 CPU"

进程调度

进程切换是"换人上 CPU"的动作,调度器则是决定"下一个该谁上"的决策者。目标是:公平性(每个进程都能分到 CPU)、效率(CPU 尽量不空闲)、低延迟(交互式进程响应快)。

调度队列与优先级: 用户态看到的 nice 值(-20~19)会映射成内核动态优先级。调度器维护多个队列:

调度器优先从高优先级队列挑选进程运行,活动队列调度完毕的进程链入过期队列,两者此消彼长;活动队列为空时与过期队列交换,继续下一轮调度。

O(1) 调度算法

早期 Linux 把进程优先级分为 0~139 共 140 个等级,核心数据结构如下:

nr_active: 记录当前队列中就绪进程总数,调度器据此做负载统计与调度决策(判断队列是否为空、是否需要负载均衡),本质是一个计数器。

bitmap[5](位图数组): 5 × 32 = 160 位,足以覆盖 140 个优先级,是典型的"用空间换时间"设计。每一位代表一个优先级:为 1 表示该优先级队列有就绪进程,为 0 表示队列为空。调度器直接找 bitmap 中第一个为 1 的位,即可 O(1) 定位最高优先级。

queue[140](队列数组): 数组下标 0~139 对应优先级(数值越小优先级越高),每个 queue[i] 是存放优先级为 i 的就绪进程链表。调度器根据 bitmap 找到最高优先级 k 后,直接取 queue[k] 的第一个进程运行即可。

Linux 2.6 内核中 O(1) 调度器的核心数据结构:

struct rq {
    spinlock_t lock;
    unsigned long nr_running;          // 运行队列中进程数
    unsigned long raw_weighted_load;
    unsigned long long nr_switches;    // 切换次数统计
    unsigned long nr_uninterruptible;  // 不可中断睡眠进程数
    unsigned long expired_timestamp;
    struct task_struct* curr, * idle;  // 当前进程与空闲进程
    struct mm_struct* prev_mm;

    struct prio_array* active, * expired, arrays[2];  // 活动/过期队列
    int best_expired_prio;
    atomic_t nr_iowait;
};

struct prio_array {
    unsigned int nr_active;                        // 就绪进程计数
    DECLARE_BITMAP(bitmap, MAX_PRIO + 1);          // 优先级位图
    struct list_head queue[MAX_PRIO];              // 按优先级分组的队列数组
};

实施过程

完整的进程观察与实验流程可以按以下步骤操作:

  1. 观察状态:编译运行"父子进程"示例,用 ps -al 查看父子进程分别处于 R、S 还是 Z 状态;
  2. 制造僵尸:运行"子进程退出、父进程不回收"的示例,用 ps -al 观察 Z 状态进程;
  3. 制造孤儿:运行"父进程先退出"的示例,观察子进程的 PPID 变为 1(被 init/systemd 收养);
  4. 调整优先级top 中按 r 调整进程 nice 值,观察调度行为变化;
  5. 理解调度:结合 O(1) 调度器的位图与队列结构,理解高优先级进程为何能更快获得 CPU。

应用价值

SEO关键词

Linux, 进程状态, 僵尸进程, 孤儿进程, 进程优先级, 进程调度, 上下文切换, O(1)调度器

评论(0