1. 调度概述:什么是进程调度?调度器的角色与位置
说到操作系统,调度这事儿,我估计是每个搞底层开发的人都绕不开的坎儿。
进程调度,说白了就是操作系统决定「下一个该谁上CPU跑」的过程。你想想看,一台电脑可能同时开着浏览器、IDE、音乐播放器,但CPU核心就那么几个。谁先跑?谁后跑?跑多久?这就是调度要解决的问题。
1.1 调度器的核心角色
调度器(Scheduler)是操作系统内核里的一个关键模块。它的任务很简单——在多个就绪进程之间,选一个出来执行。
我个人习惯把调度器比作「CPU的分配官」。它手里拿着一个名单(就绪队列),每次CPU空闲了,它就从名单里挑一个人进去干活。
调度器的三个核心职责:
- 选择:从就绪队列中选一个进程
- 切换:保存当前进程的上下文,恢复新进程的上下文
- 分配:把CPU时间片分配给选中的进程
嗯,这里要注意一点。调度器不是一直在运行的。它只在特定时机被触发——比如进程主动让出CPU、时间片用完、或者有更高优先级的进程醒来。
1.2 调度器在系统中的位置
调度器到底待在哪儿?我画了一张图,你看一眼就明白了。
从图上你能看到,调度器位于内核层,夹在用户进程和CPU之间。它手里管着两个关键队列:就绪队列(等着上CPU的)和等待队列(在等I/O或其他资源的)。
1.3 调度发生的时机
调度器什么时候干活?我总结了一下,主要有这么几个时机:
| 触发时机 | 说明 | 常见场景 |
|---|---|---|
| 进程主动阻塞 | 进程等待I/O或资源 | read()、sleep() |
| 时间片耗尽 | 进程用完了分配的CPU时间 | 时钟中断触发 |
| 进程主动让出 | 进程调用yield() | 用户态主动放弃 |
| 高优先级进程就绪 | 优先级更高的进程被唤醒 | 中断处理完成 |
| 进程终止 | 当前进程执行完毕 | exit()调用 |
一个小经验:我在做嵌入式系统时发现,很多人以为调度只在时间片用完时才发生。其实不是。进程主动让出CPU的情况非常常见,尤其是在I/O密集型的应用里。你写一个网络服务器,大部分时间进程都在等网络数据,这时候调度器频繁介入,选别的进程上CPU。
1.4 调度器的两种工作模式
调度器干活的方式,其实就两种:
- 非抢占式调度:进程自己决定什么时候让出CPU。它不跑完或者不主动阻塞,别人就别想上。
- 抢占式调度:操作系统有「强制手段」。时间片到了,或者有更紧急的进程来了,直接把你踢下去。
这两种模式的区别,我后面会专门展开讲。这里先记住一个核心点:非抢占式靠自觉,抢占式靠制度。
注意:我曾经在一个实时系统项目里吃过亏。当时用了非抢占式调度,结果有个进程写了个死循环,整个系统直接卡死。从那以后,我对非抢占式调度就多留了个心眼——它简单,但风险也大。
1.5 调度器与上下文切换
调度器选好下一个进程后,还得干一件事——上下文切换。
说白了,就是把当前进程的「现场」保存下来(寄存器、程序计数器、栈指针等),再把新进程的「现场」恢复上去。这个过程是有开销的,我见过一些优化不好的系统,光上下文切换就占了CPU时间的10%以上。
// 上下文切换的伪代码示意
void context_switch(PCB *current, PCB *next) {
// 保存当前进程的上下文
save_registers(current->context);
save_stack_pointer(current->context);
save_program_counter(current->context);
// 切换到内核栈
switch_kernel_stack(next);
// 恢复新进程的上下文
restore_registers(next->context);
restore_stack_pointer(next->context);
restore_program_counter(next->context);
// 跳转到新进程执行
jump_to_user_mode(next);
}
你看这段代码,虽然只有几行,但每次切换都要做这些操作。所以调度频率不能太高,否则CPU全花在切换上了。但也不能太低,否则响应太慢。这个平衡,就是调度器设计的核心难题。
1.6 小结
进程调度,说白了就是操作系统里那个「谁上谁下」的决策机制。调度器是执行者,它站在内核层,盯着就绪队列,随时准备把下一个进程送上CPU。
我个人觉得,理解调度器的位置和角色,是搞懂整个调度系统的第一步。你想想看,没有调度器,进程之间就是一片混乱——谁抢到CPU谁跑,那系统还怎么稳定?
嗯,这一章就到这里。调度器的基本概念清楚了,后面我们再深入看看它具体怎么选、怎么切。
公众号:蓝海资料掘金营,微信deep3321