2. 进程状态与调度实体:TASK_RUNNING、TASK_INTERRUPTIBLE 等状态,task_struct 与 sched_entity 结构体解析
进程状态是内核调度器判断“谁可以运行”的核心依据。Linux 内核使用一组明确定义的宏来表示进程的瞬时状态,这些状态直接决定了进程是否被放入运行队列、是否响应信号、以及是否可以被唤醒。同时,调度器并不直接操作庞大的 task_struct,而是通过内嵌的 sched_entity 结构体来管理调度实体。理解这两层结构是进行调度性能调优的基础。
2.1 进程状态全景图
Linux 进程状态定义在 include/linux/sched.h 中,每个进程在同一时刻只能处于一种状态。以下 SVG 展示了五种主要状态及其转换路径:
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 运行队列之间的层级关系:
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 状态与调度实体的交互流程
当进程状态发生变化时,调度器会执行相应的入队或出队操作。以下是一个典型的调度流程:
- 进程创建:
fork()系统调用创建新进程,task_struct被分配,sched_entity初始化,状态设为TASK_RUNNING,调用enqueue_task()将实体加入 CFS 运行队列。 - 时间片耗尽: 时钟中断触发
scheduler_tick(),更新当前进程的vruntime,检查是否需要抢占。如果需要,设置TIF_NEED_RESCHED标志。 - 进程睡眠: 进程调用
schedule(),状态从TASK_RUNNING切换为TASK_INTERRUPTIBLE,调用dequeue_task()从运行队列移除。 - 进程唤醒: 等待的事件发生(如 I/O 完成),调用
wake_up_process(),状态恢复为TASK_RUNNING,调用enqueue_task()重新加入运行队列。 - 进程退出: 调用
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%。
- 状态切换 会触发调度器的入队/出队操作,这是性能分析的关键切入点。