2. 进程状态与调度时机

进程调度是内核最核心的活动之一,而理解进程状态模型与调度触发时机是掌握调度器架构的前提。本章将深入剖析 Linux 内核中进程的七种状态及其转换关系,并详细说明调度器在哪些关键路径上被唤醒执行。

2.1 进程状态模型

Linux 内核使用 task_struct 中的 state 字段来标识进程当前所处的状态。该字段是一个位掩码,允许进程同时处于多个状态(如 TASK_INTERRUPTIBLE | TASK_KILLABLE)。

核心原则: 只有状态为 TASK_RUNNING 的进程才会被放入运行队列(runqueue)参与调度;其他状态均表示进程正在等待某种资源或事件。

2.1.1 状态定义与语义

状态宏 数值 含义 典型场景
TASK_RUNNING 0x0000 可运行状态,进程正在 CPU 上执行或等待被调度 普通用户进程、内核线程执行中
TASK_INTERRUPTIBLE 0x0001 可中断睡眠,等待资源时可被信号唤醒 等待 I/O 完成、等待 socket 数据
TASK_UNINTERRUPTIBLE 0x0002 不可中断睡眠,等待资源时忽略信号 等待磁盘 I/O、等待内核锁
__TASK_STOPPED 0x0004 进程被停止执行 收到 SIGSTOP/SIGTSTP 信号
__TASK_TRACED 0x0008 进程被调试器跟踪 ptrace 系统调用、gdb 调试
EXIT_DEAD 0x0010 进程已退出,等待父进程回收 调用 do_exit() 后
EXIT_ZOMBIE 0x0020 僵尸状态,进程已死但 task_struct 未释放 父进程未调用 wait() 前
TASK_KILLABLE 0x0001 | 0x0002 可杀睡眠,等同于 UNINTERRUPTIBLE 但可被致命信号唤醒 NFS 文件系统等待、某些锁等待
经验提示: TASK_KILLABLE 是 Linux 2.6.25 引入的优化状态,它解决了 TASK_UNINTERRUPTIBLE 进程无法被 kill 的问题,同时保留了不可中断睡眠的语义。内核开发者应优先使用 TASK_KILLABLE 替代纯 TASK_UNINTERRUPTIBLE。

2.1.2 状态转换关系

进程状态转换遵循严格的规则,下图展示了核心状态之间的迁移路径:

进程核心状态转换图 TASK_RUNNING TASK_INTERRUPTIBLE TASK_UNINTERRUPTIBLE __TASK_STOPPED __TASK_TRACED EXIT_ZOMBIE 等待资源(可中断) 等待资源(不可中断) 资源就绪/信号唤醒 资源就绪 SIGSTOP ptrace 附加 SIGCONT ptrace 分离 do_exit() 说明:实线箭头表示主动状态迁移,虚线箭头(未画出)表示调度器在时钟中断中的被动抢占

2.1.3 特殊状态:TASK_KILLABLE 与 TASK_IDLE

  • TASK_KILLABLE:通过 set_current_state(TASK_KILLABLE) 设置,底层是 TASK_UNINTERRUPTIBLE | TASK_WAKEKILL。当进程等待 NFS 回复或某些内核锁时,如果收到致命信号(SIGKILL),内核会强制唤醒进程并返回 -EINTR。
  • TASK_IDLE:用于空闲线程(idle thread),表示 CPU 完全空闲。该状态不会参与调度,仅在 cpu_idle_loop() 中使用。
避坑指南: 不要在内核模块中直接修改 current->state,应使用 set_current_state() 宏。该宏包含内存屏障,确保状态修改与唤醒操作之间的顺序正确,避免出现"唤醒丢失"问题。

2.2 调度发生的时机

调度器并非持续运行,而是在特定"调度点"被触发。Linux 内核定义了三种主要的调度时机:时钟中断、进程阻塞和主动调度。

2.2.1 调度时机分类

调度时机 触发源 调度类型 典型入口函数
时钟中断 硬件定时器(HPET/APIC timer) 抢占式调度 scheduler_tick()
进程阻塞 系统调用(read/wait/sleep) 自愿调度 schedule()
主动调度 显式调用 cond_resched() 或 yield() 自愿调度 __schedule()
进程唤醒 资源就绪、信号传递 抢占式调度(若唤醒的进程优先级更高) try_to_wake_up()
中断返回 硬件中断处理完成后返回用户态 抢占式调度 preempt_schedule_irq()

2.2.2 时钟中断调度流程

时钟中断是抢占式调度的核心驱动。每个 CPU 的本地 APIC 定时器以固定频率(HZ)产生中断,触发以下流程:

  1. 中断处理:CPU 保存上下文,跳转到 timer_interrupt() 处理函数。
  2. 更新时间记账:调用 update_process_times(),更新当前进程的 utime/stime 和虚拟运行时间。
  3. 触发调度器 tick:调用 scheduler_tick(),检查当前进程的时间片是否耗尽。
  4. 设置重调度标志:如果当前进程需要被抢占,设置 TIF_NEED_RESCHED 标志。
  5. 中断返回时调度:在 ret_from_intr 路径中检查 TIF_NEED_RESCHED,调用 schedule()。
时钟中断触发调度流程图 硬件定时器中断 timer_interrupt() update_process_times() scheduler_tick() 设置 TIF_NEED_RESCHED 关键路径 中断返回前检查 TIF_NEED_RESCHED 调用 schedule()

2.2.3 进程阻塞调度

当进程执行系统调用需要等待资源时(如 read() 等待磁盘数据),内核会主动调用 schedule() 让出 CPU。这是最自然的调度时机,称为"自愿调度"。

代码示例: 进程阻塞调度典型路径
// fs/read_write.c - 简化版 read 系统调用
ssize_t ksys_read(unsigned int fd, char __user *buf, size_t count)
{
    struct fd f = fdget_pos(fd);
    // ... 参数检查 ...
    
    // 如果文件没有数据可读,进入等待队列
    if (file->f_op->read) {
        // 调用具体文件系统的 read 实现
        ret = file->f_op->read(file, buf, count, &pos);
    }
    
    // 如果返回 -EAGAIN 且文件是非阻塞模式,则可能进入睡眠
    if (ret == -EAGAIN && !(file->f_flags & O_NONBLOCK)) {
        // 将当前进程加入等待队列
        DEFINE_WAIT(wait);
        add_wait_queue(&file->f_wqh, &wait);
        
        // 设置进程状态为 TASK_INTERRUPTIBLE
        set_current_state(TASK_INTERRUPTIBLE);
        
        // 主动让出 CPU
        schedule();
        
        // 被唤醒后继续执行
        remove_wait_queue(&file->f_wqh, &wait);
        // 重新尝试读取
    }
    
    fdput_pos(f);
    return ret;
}

2.2.4 主动调度与 cond_resched()

内核提供了 cond_resched() 函数,允许长时间运行的内核代码路径主动让出 CPU,避免导致系统响应延迟。该函数在以下场景中特别重要:

  • 长循环遍历:如遍历大量内存页、链表扫描。
  • 内核锁持有时间过长:在释放锁后调用 cond_resched() 给其他进程机会。
  • 实时系统:确保低优先级任务不会长时间阻塞高优先级任务。
实现细节: cond_resched() 的实现非常轻量——它首先检查 TIF_NEED_RESCHED 标志,如果未设置则立即返回,开销仅为一次内存读取和条件跳转。因此可以在性能敏感的路径中安全使用。
// kernel/sched/core.c - cond_resched 实现
int __sched cond_resched(void)
{
    if (should_resched(0)) {
        preempt_disable();
        __cond_resched();
        preempt_enable();
        return 1;
    }
    return 0;
}

// 检查是否需要重新调度
static inline int should_resched(int preempt_offset)
{
    return unlikely(tif_need_resched());
}

2.2.5 抢占式调度与内核抢占

Linux 内核支持三种抢占模式,决定了调度器在哪些时机可以抢占当前进程:

抢占模式 配置选项 抢占时机 适用场景
无抢占 CONFIG_PREEMPT_NONE 仅在内核返回用户态时 服务器、批处理系统
自愿抢占 CONFIG_PREEMPT_VOLUNTARY 在 cond_resched() 和显式抢占点 桌面系统
完全抢占 CONFIG_PREEMPT 任何内核代码路径(除原子上下文) 实时系统、嵌入式
注意事项: 完全抢占模式会显著增加内核的调度延迟不确定性,因为任何内核代码都可能被中断。在实时系统中,需要配合 PREEMPT_RT 补丁使用,该补丁将自旋锁替换为可睡眠的互斥锁,确保抢占安全。

2.3 调度时机总结

综合以上分析,调度器在以下五个关键路径上被触发:

  1. 时钟中断:周期性检查,实现时间片轮转和公平调度。
  2. 进程阻塞:资源等待时的自愿让出,效率最高。
  3. 主动调度:内核代码显式让出 CPU,用于长路径。
  4. 进程唤醒:高优先级进程被唤醒时立即抢占当前进程。
  5. 中断返回:从中断上下文返回时检查抢占标志。