2. 进程状态与调度实体:TASK_RUNNING、TASK_INTERRUPTIBLE 等状态,task_struct 与 sched_entity 结构体解析

进程状态是内核调度器判断“谁可以运行”的核心依据。Linux 内核使用一组明确定义的宏来表示进程的瞬时状态,这些状态直接决定了进程是否被放入运行队列、是否响应信号、以及是否可以被唤醒。同时,调度器并不直接操作庞大的 task_struct,而是通过内嵌的 sched_entity 结构体来管理调度实体。理解这两层结构是进行调度性能调优的基础。

2.1 进程状态全景图

Linux 进程状态定义在 include/linux/sched.h 中,每个进程在同一时刻只能处于一种状态。以下 SVG 展示了五种主要状态及其转换路径:

Linux 进程状态转换图 Linux 进程状态转换图 TASK_RUNNING TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE TASK_STOPPED TASK_DEAD (ZOMBIE) 等待事件/信号 等待 I/O SIGSTOP 唤醒 I/O 完成 SIGCONT exit() 说明:TASK_RUNNING 并不代表 CPU 正在执行,而是表示进程处于“可运行”状态,等待调度器选择。 TASK_DEAD 状态在父进程未回收时表现为僵尸进程(ZOMBIE)。

2.2 核心状态详解

状态宏 数值 含义 调度行为 典型场景
TASK_RUNNING 0 进程可运行(就绪或正在运行) 放入 CFS 运行队列,参与调度 用户态程序执行、内核线程活跃
TASK_INTERRUPTIBLE 1 可中断睡眠,等待资源或事件 移出运行队列,不参与调度 等待 socket 数据、等待锁
TASK_UNINTERRUPTIBLE 2 不可中断睡眠,通常等待 I/O 移出运行队列,不参与调度 磁盘 I/O、NFS 挂载等待
TASK_STOPPED 4 进程暂停执行 移出运行队列,不参与调度 收到 SIGSTOP/SIGTSTP
TASK_TRACED 8 被调试器跟踪暂停 移出运行队列,不参与调度 ptrace 断点命中
EXIT_ZOMBIE 16 进程已终止,但 PCB 未回收 不参与调度,仅保留 task_struct 父进程未调用 wait()
EXIT_DEAD 32 进程完全消亡 不参与调度,即将释放 父进程已回收
调优要点: 如果系统中 TASK_UNINTERRUPTIBLE 状态的进程数量持续偏高(例如超过 10 个),通常意味着 I/O 子系统存在瓶颈。可以通过 ps -eo state,pid,comm | grep '^D' 快速排查。

2.3 task_struct:进程控制块

task_struct 是 Linux 内核中最大的结构体之一,位于 include/linux/sched.h。它包含了进程的所有元数据。以下是与调度直接相关的关键字段:

// 精简自 include/linux/sched.h (Linux 6.x)
struct task_struct {
    // 进程状态
    volatile long           state;          // TASK_RUNNING, TASK_INTERRUPTIBLE 等
    unsigned int            flags;          // PF_* 标志位,如 PF_FROZEN

    // 调度实体(核心)
    struct sched_entity     se;             // CFS 调度实体
    struct sched_rt_entity  rt;             // 实时调度实体
    struct sched_dl_entity  dl;             // Deadline 调度实体

    // 调度策略与优先级
    unsigned int            policy;         // SCHED_NORMAL, SCHED_FIFO, SCHED_RR 等
    int                     prio;           // 动态优先级
    int                     static_prio;    // 静态优先级(nice 值映射)
    int                     normal_prio;    // 常规优先级

    // CPU 与 cgroup 关联
    int                     on_cpu;         // 当前是否在 CPU 上运行
    int                     nr_cpus_allowed; // 允许的 CPU 数量
    struct cpumask          cpus_mask;      // CPU 亲和性掩码

    // 统计信息
    u64                     utime;          // 用户态时间
    u64                     stime;          // 内核态时间
    u64                     start_time;     // 进程启动时间

    // 进程关系
    struct task_struct      *parent;        // 父进程
    struct list_head        children;       // 子进程链表
    struct list_head        sibling;        // 兄弟进程链表
};
关键理解: 一个进程可以同时拥有三个调度实体(se、rt、dl),但同一时刻只会根据 policy 字段激活其中一个。例如,普通进程使用 se,实时进程使用 rt。

2.4 sched_entity:调度器的核心抽象

调度器不直接操作 task_struct,而是操作内嵌的 sched_entity。这种设计使得调度器可以统一调度进程和进程组(如 cgroup)。sched_entity 结构体定义在 include/linux/sched.h 中:

// 精简自 include/linux/sched.h (Linux 6.x)
struct sched_entity {
    // CFS 运行队列节点
    struct rb_node          run_node;       // 红黑树节点
    struct list_head        group_node;     // 组调度链表节点

    // 权重与负载
    unsigned int            weight;         // 调度权重(由 nice 值决定)
    unsigned int            inv_weight;     // 权重的倒数(用于快速计算)

    // 虚拟运行时(vruntime)
    u64                     vruntime;       // 虚拟运行时间,CFS 调度核心
    u64                     prev_sum_exec_runtime; // 上次调度时的总执行时间

    // 执行时间统计
    u64                     sum_exec_runtime; // 总执行时间
    u64                     exec_start;     // 当前调度周期开始时间
    u64                     nr_migrations;  // 跨 CPU 迁移次数

    // 组调度相关
    struct sched_entity     *parent;        // 父调度实体(cgroup 层级)
    struct cfs_rq           *cfs_rq;        // 所属 CFS 运行队列
    struct cfs_rq           *my_q;          // 如果该实体是任务组,则指向其自己的运行队列
};

2.5 task_struct 与 sched_entity 的关系

以下 SVG 展示了 task_struct、sched_entity 与 CFS 运行队列之间的层级关系:

task_struct 与 sched_entity 结构关系图 task_struct 与 sched_entity 结构关系图 struct task_struct state: TASK_RUNNING policy: SCHED_NORMAL prio: 120 struct sched_entity se; struct sched_entity vruntime: 1234567890 weight: 1024 (nice=0) sum_exec_runtime: 56789 cfs_rq: 0xffff88812345678 内嵌 struct cfs_rq (运行队列) rb_root: tasks_timeline (红黑树根) 挂入 说明:调度器通过操作 sched_entity 的 vruntime 和 weight 来决定调度顺序, 而不直接访问 task_struct 的其他字段,实现了调度逻辑与进程管理的解耦。

2.6 调度权重与 nice 值的映射

CFS 调度器使用权重(weight)来表示进程的优先级,权重越高,进程获得的 CPU 时间比例越大。权重与 nice 值的映射关系由内核预定义数组决定:

nice 值 权重 (weight) CPU 时间比例(相对 nice=0) 典型场景
-20 88761 约 86.7 倍 实时音频处理
-10 11051 约 10.8 倍 交互式应用
0 1024 1 倍(基准) 普通用户进程
10 335 约 0.33 倍 后台批处理任务
19 15 约 0.015 倍 低优先级清理任务
性能陷阱: 不要随意将大量进程设置为 nice=-20。虽然单个进程获得更多 CPU,但会导致其他进程严重饥饿,反而降低系统整体吞吐量。在数据库服务器或 Web 服务器上,建议保持 nice=0,通过 cgroup 进行更精细的配额控制。

2.7 状态与调度实体的交互流程

当进程状态发生变化时,调度器会执行相应的入队或出队操作。以下是一个典型的调度流程:

  1. 进程创建: fork() 系统调用创建新进程,task_struct 被分配,sched_entity 初始化,状态设为 TASK_RUNNING,调用 enqueue_task() 将实体加入 CFS 运行队列。
  2. 时间片耗尽: 时钟中断触发 scheduler_tick(),更新当前进程的 vruntime,检查是否需要抢占。如果需要,设置 TIF_NEED_RESCHED 标志。
  3. 进程睡眠: 进程调用 schedule(),状态从 TASK_RUNNING 切换为 TASK_INTERRUPTIBLE,调用 dequeue_task() 从运行队列移除。
  4. 进程唤醒: 等待的事件发生(如 I/O 完成),调用 wake_up_process(),状态恢复为 TASK_RUNNING,调用 enqueue_task() 重新加入运行队列。
  5. 进程退出: 调用 do_exit(),状态变为 EXIT_ZOMBIE,调度器不再管理该实体,等待父进程回收。
// 调度核心路径示意(伪代码)
void schedule(void) {
    struct task_struct *prev, *next;
    struct sched_entity *se;
    struct cfs_rq *cfs_rq;

    prev = current;
    // 如果当前进程不再可运行,出队
    if (prev->state != TASK_RUNNING) {
        dequeue_task(cfs_rq, &prev->se, 0);
    }

    // 选择下一个进程(核心函数)
    next = pick_next_task(cfs_rq);
    // 更新上下文
    context_switch(prev, next);
}
调优观察点: 使用 perf sched 或 trace-cmd 可以跟踪进程状态切换的完整链路。重点关注 sched_switch 事件中的 prev_state 字段,如果大量切换来自 TASK_UNINTERRUPTIBLE,说明 I/O 等待是主要瓶颈。

2.8 本章小结

  • 进程状态 决定了进程是否参与调度,TASK_RUNNING 是唯一允许调度的状态。
  • task_struct 是进程的完整描述,包含调度策略、优先级、统计信息等。
  • sched_entity 是调度器的操作单元,通过 vruntime 和 weight 实现公平调度。
  • 权重映射 是 CFS 公平性的数学基础,nice 值每变化 1,权重变化约 10%。
  • 状态切换 会触发调度器的入队/出队操作,这是性能分析的关键切入点。