如何选择和切换下一个进程 —— Linux进程调度原理与代码全解
文章链接支持新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
京东购买链接
目录
- 进程调度的基础概念
- 进程、线程、任务结构体的关系梳理
- sched_entity:调度的核心粒度
- 进程选择(挑选下一个进程)的核心逻辑
- schedule() 函数源码流程解析
- 进程切换的全过程
- 调度器的主流算法(以CFS为例)
- 内核抢占与调度点
- 重要结构体/函数剖析
- 实战调试与打印技巧
- 常见场景与误区
- 小结与延伸阅读
1. 进程调度的基础概念
操作系统的本质就是“在有限的CPU资源下,合理高效地运行多个进程/线程”。而调度器(scheduler)就是操作系统内核中,负责决定“哪个进程/线程上CPU、何时切换、怎样切换”的关键机制。
调度包括两个核心问题:
- 选择(Select):在众多可运行的进程中,挑选一个最合适的“下一个”。
- 切换(Switch):完成从当前进程到下一个进程的上下文切换。
Linux 内核通过一整套高效的数据结构、调度算法,以及软硬件协同手段,实现了公平、实时、低延迟的多任务调度。
2. 进程、线程、任务结构体的关系梳理
2.1 任务的本质:task_struct
Linux中,“进程/线程”统一用task_struct
结构体表示。每个正在运行或可运行的进程(线程)都有一个唯一的task_struct
实例。
核心内容包括:
- 进程ID(PID)、状态
- 进程优先级、调度策略
- 进程地址空间、文件描述符表
- 调度实体(sched_entity)(重点)
2.2 调度实体 sched_entity
sched_entity
结构体不是独立存在的,而是作为 task_struct
的成员。其设计是为了支撑“调度算法”的实现。
- 一个 task_struct 一定包含一个 sched_entity
- sched_entity 记录了与调度相关的权重、vruntime、执行时长等信息
- 多线程(如CFS调度组)时,group也用sched_entity,体现层级关系
简化结构:
struct task_struct {
...
struct sched_entity se;
...
};
3. sched_entity:调度的核心粒度
sched_entity
承载着调度算法的“可公平比对”信息。以CFS为例,每个进程都对应一个sched_entity,它被组织在红黑树(rb_tree)中。
关键成员:
vruntime
:当前实体的虚拟运行时间(衡量公平性的核心)exec_start
、sum_exec_runtime
:累积的执行时间load_weight
:权重,影响进程被选中的频率run_node
:红黑树节点
作用:
- 让调度器用统一、抽象的“实体”来比较、排序、挑选任务
- 屏蔽了task_struct中与调度无关的内容
4. 进程选择(挑选下一个进程)的核心逻辑
4.1 选择下一个进程,实际上是选下一个sched_entity
以CFS调度器为例,所有处于TASK_RUNNING
状态的进程,其sched_entity
会被插入红黑树中。
调度器每次调度时,都会:
- 在红黑树上寻找
vruntime
最小的sched_entity
- 找到后,定位其对应的
task_struct
(即要切换的进程)
流程简述:
- 红黑树排序,最“公平”(vruntime最少)的排最左
- 选中最左侧节点,对应进程“被调度上CPU”
4.2 pick_next_task() 的作用
-
pick_next_task()
就是实现“挑选下一个进程”的主力函数 -
典型代码(位于
kernel/sched/fair.c
):static struct task_struct *pick_next_task_fair(struct rq *rq) { struct sched_entity *se = pick_next_entity(cfs_rq); // se对应的task_struct就是要切换的进程 return task_of(se); }
-
多调度器共存(CFS/RT/Deadline),由
sched_class
多态分发
5. schedule() 函数源码流程解析
schedule()
是调度器的核心入口函数,位于 kernel/sched/core.c
,每当需要进行调度时都会调用。
精简流程如下:
- 关闭抢占(preempt_disable),保证调度期间不会被再次抢占
- 调用
__schedule()
,进入调度主循环 __schedule()
里会遍历调度类,执行pick_next_task()
选择下一个进程- 若当前与下一个进程不同,则调用
context_switch()
完成上下文切换 - 恢复抢占(preempt_enable)
源码片段简析:
asmlinkage __visible void __sched schedule(void)
{
preempt_disable();
__schedule(SM_NONE);
preempt_enable();
}
__schedule()
里核心就是pick_next_task()
+ context_switch()
。
6. 进程切换的全过程
进程切换(Context Switch)并不是一行代码就能搞定,而是一个完整的CPU资源归还与重分配过程:
- 保存当前进程的上下文(CPU寄存器、栈指针、PC等)
- 更新当前task_struct的状态
- 切换到下一个task_struct(切换页表、切换栈等)
- 恢复下一个进程的上下文
- CPU开始执行新进程
Linux通过switch_to(prev, next, last)
完成底层切换,底层多是汇编代码。
整个切换过程由schedule() -> __schedule() -> context_switch() -> switch_to()
贯穿实现。
7. 调度器的主流算法(以CFS为例)
CFS(Completely Fair Scheduler)完全公平调度器:
- 以vruntime为核心公平度指标
- 所有进程调度实体组织成红黑树
- 每次调度挑选vruntime最小的进程(最“亏”的进程)
- 保证所有进程“获得CPU时间总量大致公平”
- 权重影响vruntime增速,优先级高的进程“变慢”
常见调度类:
- CFS(普通进程)
- RT(实时进程)
- DEADLINE(实时调度)
8. 内核抢占与调度点
内核抢占(Preemption):
- 在关键信号量、锁保护区域,或关中断期间禁止抢占,保证数据一致性
preempt_disable()
/preempt_enable()
手动控制临界区- 通常在
do_exit()
、sys_fork()
、定时器中断、I/O中断等“调度点”主动触发调度
调度点(Schedule Point):
- 某些内核API、系统调用、内核定时器、用户主动让出CPU(如
sched_yield()
)都会触发调度
9. 重要结构体/函数剖析
9.1 task_struct
进程/线程的描述符,贯穿生命周期的核心对象。包含进程状态、调度信息、资源占用等几乎所有重要属性。
9.2 sched_entity
调度算法核心的数据结构。承担公平度计算、优先级、累计运行时间等功能,是调度粒度的具体化。
9.3 pick_next_task()
选择下一个task_struct,分调度器多态分发(CFS、RT、Deadline)。
9.4 context_switch()
负责底层上下文切换,最终实现CPU寄存器、内核栈、页表切换。
9.5 need_resched()
判断当前是否需要调度(被抢占、主动让出等)
10. 实战调试与打印技巧
10.1 打印调度流程
-
在
schedule()
、pick_next_task()
、context_switch()
等关键位置增加pr_info
打印:pr_info("[SCHED] schedule(): cpu=%d, prev=%s, next=%s\n", smp_processor_id(), prev->comm, next->comm);
-
打印
vruntime
、PID等,观察调度公平性
10.2 跟踪fork和进程切换
-
在
kernel_clone
/copy_process
中添加打印,记录父子进程、栈地址等:pr_info("[FORK] Parent: %s(%d), Child: %s(%d)\n", current->comm, current->pid, child->comm, child->pid);
10.3 使用 ftrace/perf/strace 追踪调度
- ftrace支持调度事件(
sched_switch
、sched_wakeup
等),可用来分析调度行为
11. 常见场景与误区
- “用户进程和内核进程切换”:本质都是
task_struct
,调度器只关心“可运行的任务”,不区分“用户/内核进程”。 - “为什么需要 sched_entity?”:用于抽象和统一调度算法,支撑多层级(如调度组)。
- “能否只靠task_struct做调度?”:task_struct包含太多与调度无关信息,不便于算法抽象和性能优化。
- “选定sched_entity是否等价于选定进程?”:完全等价,sched_entity和task_struct是一一对应的。
- “抢占期间调度安全吗?”:必须通过preempt_disable/enable保证临界区调度安全,防止数据竞争。
12. 小结与延伸阅读
12.1 小结
- 进程调度的实质:选中下一个sched_entity,其对应的task_struct被切换到CPU
- 关键路径:schedule() → pick_next_task() → context_switch()
- 核心结构体:task_struct、sched_entity
- 调度算法:CFS红黑树公平调度,RT、Deadline等多调度类协作
- 调试利器:pr_info打印、ftrace、perf、源码instrument
- 常见误区:调度只认“可运行的任务”,task_struct与sched_entity一一对应
12.2 延伸阅读与建议
- 推荐阅读Linux内核源码
kernel/sched/core.c
、kernel/sched/fair.c
- 结合实际硬件场景,动手在开发板/虚拟机上实验、打印、调试
- 推荐书籍:《Linux内核设计与实现》《深入理解Linux内核》《Yocto项目实战教程:高效定制嵌入式Linux系统》
文章链接支持新书:《Yocto项目实战教程:高效定制嵌入式Linux系统》
京东购买链接视频教程请关注 B 站:“嵌入式 Jerry”