1. 进程调度概述:进程与线程的区别、调度器的作用、调度策略的分类

进程调度是操作系统内核最核心的功能之一,它决定了哪个任务在何时获得 CPU 的使用权。理解进程与线程的本质区别、调度器的职责以及调度策略的分类,是深入掌握 Linux 内核抢占机制的前提。

1.1 进程与线程的区别

在 Linux 内核视角下,进程是资源分配的最小单位,拥有独立的地址空间、文件描述符表、信号处理表等资源;线程是 CPU 调度的最小单位,共享所属进程的地址空间和资源。Linux 对线程的实现采用“轻量级进程”模型,即线程本质上是一个与父进程共享资源的进程。

对比维度 进程 (Process) 线程 (Thread)
资源拥有 独立的地址空间、堆栈、文件描述符 共享所属进程的地址空间和资源
调度单位 内核调度实体 (task_struct) 内核调度实体 (task_struct),共享 mm_struct
创建开销 较大,需复制或写时拷贝 (COW) 地址空间 较小,仅创建新的 task_struct 和栈
上下文切换 涉及地址空间切换 (TLB 刷新) 不涉及地址空间切换,开销更低
隔离性 高,一个进程崩溃不影响其他进程 低,一个线程崩溃可能导致整个进程退出
Linux 实现 clone() 系统调用,不设置 CLONE_VM clone() 系统调用,设置 CLONE_VM | CLONE_THREAD
内核视角提示: 在 Linux 内核中,进程和线程统一由 task_struct 结构体表示,调度器不区分“进程”和“线程”,只调度 task_struct 实体。区别在于 task_struct 中的 mm 指针是否指向共享的地址空间。

1.2 调度器的作用

调度器 (Scheduler) 是内核中负责选择下一个要运行的任务的子系统。其核心职责包括:

  • 任务选择: 从就绪队列 (runqueue) 中按照特定策略选出下一个要执行的进程。
  • 上下文切换: 保存当前进程的 CPU 寄存器状态,恢复新进程的寄存器状态。
  • 负载均衡: 在多核系统中,将任务均匀分配到各个 CPU 核心上,避免某些核心过载而其他核心空闲。
  • 时间片管理: 为进程分配 CPU 时间片,并在时间片耗尽时触发重新调度。
  • 抢占决策: 判断是否允许当前进程被更高优先级的进程抢占。
调度器核心数据结构: 每个 CPU 核心维护一个 struct rq (runqueue),其中包含多个调度类 (sched_class) 的队列,如 CFS (完全公平调度) 的红黑树、实时任务的优先级队列等。
Linux 调度器核心架构 核心调度器 (core.c) stop_sched_class dl_sched_class rt_sched_class fair_sched_class 每个 CPU 的 Runqueue (struct rq) 包含: cfs_rq (红黑树) | rt_rq (优先级位图) | dl_rq (最早截止时间优先) 以及: nr_running, curr, idle, prev_prio, 负载统计等 上下文切换 (context_switch) 调度类优先级: stop > deadline > realtime > fair > idle

1.3 调度策略的分类

Linux 内核支持多种调度策略,每种策略适用于不同场景。调度策略通过 task_struct 中的 policy 字段指定,内核根据策略将任务分配到对应的调度类中。

调度策略 调度类 优先级范围 适用场景 时间片
SCHED_NORMAL (CFS) fair_sched_class 0~39 (nice 值) 普通用户进程、后台服务 动态权重分配
SCHED_BATCH fair_sched_class 0~39 (nice 值) 批处理任务、非交互式计算 更长,减少抢占
SCHED_IDLE idle_sched_class 最低 低优先级后台任务 仅当 CPU 空闲时运行
SCHED_FIFO rt_sched_class 1~99 (实时优先级) 实时任务,先入先出 无时间片,直到主动让出
SCHED_RR rt_sched_class 1~99 (实时优先级) 实时任务,轮转调度 固定时间片 (100ms)
SCHED_DEADLINE dl_sched_class 0~2^63-1 (截止时间) 硬实时任务,最早截止时间优先 根据 runtime/period 计算
抢占相关注意: 实时策略 (SCHED_FIFO/SCHED_RR) 和 deadline 策略 (SCHED_DEADLINE) 的进程在就绪状态下会立即抢占普通进程。如果实时进程出现死循环,可能导致系统卡死。内核提供了 sched_rt_runtime_us 和 sched_rt_period_us 参数来限制实时进程的总 CPU 使用时间。

1.4 调度策略的选择与切换

用户态程序可以通过 sched_setscheduler() 系统调用或 chrt 命令行工具修改进程的调度策略和优先级。内核在 __setscheduler() 函数中执行策略切换逻辑:

// 内核调度策略切换核心逻辑 (简化)
static void __setscheduler(struct task_struct *p, const struct sched_attr *attr)
{
    int policy = attr->sched_policy;

    // 检查权限:设置实时策略需要 CAP_SYS_NICE
    if (rt_policy(policy) && !capable(CAP_SYS_NICE))
        return -EPERM;

    // 更新 task_struct 中的策略字段
    p->policy = policy;

    // 将任务从旧调度类的就绪队列中移除
    dequeue_task(rq, p, DEQUEUE_SAVE);

    // 设置新的优先级/权重
    p->prio = effective_prio(p);
    p->normal_prio = normal_prio(p);

    // 将任务重新入队到新调度类的就绪队列
    enqueue_task(rq, p, ENQUEUE_RESTORE);

    // 如果新策略优先级更高,触发抢占检查
    check_preempt_curr(rq, p, 0);
}
关键点: 调度策略的切换涉及“出队-改参-入队”三步操作,整个过程在关闭本地中断 (local_irq_disable) 的情况下完成,以保证就绪队列数据结构的原子性。切换后如果新进程优先级高于当前运行进程,会立即触发抢占。
调度策略分类与抢占关系 普通进程 SCHED_NORMAL / BATCH / IDLE 实时进程 SCHED_FIFO / SCHED_RR Deadline 进程 SCHED_DEADLINE 可被抢占 可被抢占 内核抢占 (Kernel Preemption) PREEMPT_NONE / PREEMPT_VOLUNTARY / PREEMPT / PREEMPT_RT 抢占触发条件 1. 高优先级进程变为可运行 (wake_up_new_task / try_to_wake_up) 2. 当前进程时间片耗尽 (scheduler_tick) 3. 中断/异常返回时检查 TIF_NEED_RESCHED 标志 抢占优先级: Deadline > Realtime (FIFO/RR) > Normal (CFS) > Idle

1.5 调度策略选择实践建议

  • 普通服务器/桌面: 使用默认的 SCHED_NORMAL (CFS),内核自动平衡交互性和吞吐量。
  • 批处理/科学计算: 使用 SCHED_BATCH,减少上下文切换开销,提升缓存命中率。
  • 音视频播放/低延迟应用: 使用 SCHED_FIFO 或 SCHED_RR,优先级设为 50~80,确保及时响应。
  • 工业控制/自动驾驶: 使用 SCHED_DEADLINE,精确控制任务的截止时间和执行预算。
  • 后台维护任务: 使用 SCHED_IDLE,避免影响前台交互体验。
查看当前进程调度策略: 使用 ps -eo pid,comm,policy,prio,nice,rtprio 或 chrt -p <pid> 命令。策略字段显示为:NORMAL=TS, BATCH=B, IDLE=ID, FIFO=FF, RR=RR, DEADLINE=DL。