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 (完全公平调度) 的红黑树、实时任务的优先级队列等。
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) 的情况下完成,以保证就绪队列数据结构的原子性。切换后如果新进程优先级高于当前运行进程,会立即触发抢占。
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。