Linux 进程调度
Linux 进程调度
进程调度是操作系统的核心功能,它决定了哪个进程在何时使用 CPU。Linux 内核的调度器经过多次演变,如今已成为一个高效、复杂且支持多种需求的模块。
一、进程分类
进程分为实时进程和普通进程。实时进程是直接与用户交互的,需要实时响应。而普通进程不需要实时响应,例如压缩文件、视频编码解码等。
Linux 将进程分为两大类,调度器对它们采取不同的策略:
-
实时进程 (Real-Time Processes)
- 特点:对响应时间和 deadlines(最后期限)有严格要求。用于需要确定性行为的任务。
- 子类:
- SCHED_FIFO (先进先出):最高优先级的进程会一直运行,直到它主动放弃 CPU 或被更高优先级的实时进程抢占。
- SCHED_RR (轮流调度):在相同优先级的实时进程之间分配时间片。当一个进程用完它的时间片后,会被放到同优先级队列的末尾,让下一个进程运行。
- 优先级范围:0 到 99 (数值越大,优先级越高)。
-
普通进程 (Normal Processes)
- 特点:绝大多数用户进程都属于此类。它们没有严格的实时性要求,调度器的目标是公平性和良好的整体系统吞吐量,而不是低延迟。
- 默认调度策略:SCHED_NORMAL (在用户空间通过
nice命令设置时也称为 SCHED_OTHER)。 - 调度器:由 完全公平调度器 (CFS) 负责管理。
二、上下文切换 (Context Switch)
- 背景:CPU 通过时间片 (Time Slice) 技术模拟“同时”运行多个进程。一个进程运行一个时间片后,调度器会切换到另一个进程。
- 定义:上下文切换是指将 CPU 从一个正在运行的进程切换到另一个就绪进程时所执行的操作。
- 过程:
- 保存上下文:将当前进程的执行状态(称为上下文,Context)保存到其进程控制块 (PCB) 中。这包括程序计数器、CPU 寄存器、页表信息等。
- 加载上下文:从下一个要运行进程的 PCB 中加载其上下文到 CPU 的寄存器和相关系统中。
- 切换地址空间:切换内核栈和虚拟内存地址空间(MMU 页表)。
- 开销:上下文切换本身需要 CPU 时间,是一种开销。过于频繁的切换会导致系统效率降低,因为 CPU 花在“管理进程”上的时间多于“执行进程”的时间。
三、调度算法演变
以下几种算法是调度理论的基础,现代调度器(如 CFS)的灵感来源于它们:
-
FIFO (First-In, First-Out) / FCFS (First-Come, First-Served)
- 描述:最简单的算法,先来的进程先运行,直到完成。
- 缺点:护航效应 (Convoy Effect)。一个长进程会阻塞后面所有短进程,平均等待时间很长。
-
SJF (Shortest Job First) / STF (Shortest Time First)
- 描述:选择预计运行时间最短的进程先运行。
- 优点:理论上平均等待时间最优。
- 缺点:非抢占式。如果长进程已经运行,后来的短进程也必须等待。无法预知每个作业的运行时间。
-
STCF (Shortest Time-to-Completion First) / PSJF (Preemptive SJF)
- 描述:抢占式的 SJF。当一个新进程到达时,如果它的预计运行时间比当前剩余运行进程的剩余时间还短,就抢占 CPU。
- 特点:解决了 SJF 的护航问题,是理论上的一个飞跃。
-
RR (Round Robin, 轮询)
- 描述:为每个进程分配一个时间片 (Quantum)。进程轮流运行一个时间片,如果没完成就排到就绪队列的末尾。
- 优点:公平性好,响应时间短,适合交互式系统。
- 缺点:时间片设置是关键。太短导致切换开销大;太长则退化为 FIFO,响应性变差。
Linux 的 CFS 并没有直接采用以上任何一种,而是基于一个更抽象的概念: 公平分配 CPU 时间。
四、进程队列
Linux 内核为每种调度策略(如 SCHED_FIFO, SCHED_RR, SCHED_NORMAL)维护着不同的队列。对于普通进程,CFS 使用一个红黑树 (Red-Black Tree) 而不是一个简单的 FIFO 队列来管理可运行进程。
- 红黑树:一种自平衡的二叉搜索树。
- 键 (Key):CFS 使用
vruntime(虚拟运行时间) 作为键。 - 作用:树最左侧的节点(拥有最小
vruntime的进程)就是下一个最应该被调度的进程。这使得选择下一个进程的操作非常高效 (O(log N))。
五、进程优先级及 nice
Linux 为普通进程提供了两种优先级:
-
静态优先级 (Static Priority)
- 由用户或程序通过
nice值设置。 - 范围:-20 到 +19 (默认值为 0)。
nice值越高,优先级越低。(,,,可以理解为好人没好报) - “静态”意味着它通常不会由内核改变,除非用户显式修改。
- 由用户或程序通过
-
动态优先级 (Dynamic Priority)
- 由调度器在静态优先级的基础上计算而来,是调度器实际使用的优先级。
- CFS 会根据进程的交互性进行微调。例如,一个长时间等待 I/O 的交互式进程在被唤醒时可能会获得一个临时的优先级提升,以便更快地响应用户,从而提供更好的用户体验。
六、调度器的演变与 CFS
- O(n) 调度器:内核维护一个全局的进程运行队列。每次调度时,它需要遍历整个队列(
n个进程)来找出优先级最高且可运行的进程。进程数量n很大时,性能瓶颈非常明显。 - O(1) 调度器:计算机中位运算最快,所以将0-139优先级映射成一个bitmap,如果这个优先级上有进程,这个位就被置为1。解决了遍历问题,调度选择时间变为常数时间,与进程数量无关。
- CFS (Completely Fair Scheduler, 完全公平调度器)
- 引入:从 Linux 2.6.23 内核开始成为默认的普通进程调度器。
- 设计理念:不是分配时间片,而是分配 CPU 使用比例。目标是让所有可运行进程在一段时间内都能公平地获得其应得的 CPU 时间份额。
- 实现:使用红黑树数据结构,以
vruntime(虚拟运行时间)为键值。总是选择vruntime最小的进程来运行。 vruntime的精妙之处:它不是一个简单的计时器。一个高优先级(低nice值)的进程,其vruntime会增长得慢,因此它会在红黑树中保持靠左的位置,从而更频繁地被调度到,获得更多CPU时间。这完美地实现了按权重分配。
总结对比
| 特性 | O(n) 调度器 | O(1) 调度器 | CFS 调度器 |
|---|---|---|---|
| 调度复杂度 | O(n) | O(1) | O(log N) (因红黑树) |
| 核心数据结构 | 一个全局链表 | Per-CPU 优先级数组 + Bitmap | Per-CPU 红黑树 |
| 公平性 | 一般 | 较好(但启发式算法复杂且易出错) | 极好(数学模型保证) |
| 设计目标 | 简单 | 扩展性、响应速度 | 绝对公平、可预测性 |
总结
现代 Linux 调度是一个分层系统:
- 实时进程使用
SCHED_FIFO或SCHED_RR策略,拥有最高优先级,总是优先于普通进程运行。 - 普通进程由 CFS 管理,其核心思想是通过
vruntime和红黑树来实现按权重(nice值)公平分配 CPU,同时兼顾响应性和吞吐量。
CFS 的成功在于它用简单而优雅的数学模型(公平分配)取代了之前调度器中复杂且容易出错的启发式规则。
相关命令与API
top/htop:- 查看进程的优先级 (
PR/PRI列) 和nice值 (NI列)。 PR: 动态优先级(对于普通进程,值是20 + NI,所以nice=0的进程PR为20)。NI: 就是nice值。
- 查看进程的优先级 (
nice和renice:nice -n -5 ./my_program:以更高的优先级(nice=-5)启动一个程序。renice -n 10 -p 1234:将PID为1234的进程的nice值改为10(降低优先级)。
chrt:chrt -f -p 99 1234:将PID为1234的进程的调度策略改为SCHED_FIFO,实时优先级为99(最高)。- 警告:错误地使用实时优先级可能锁死系统,因为该进程会抢占所有普通进程甚至内核线程。
下面,我使用最生活化的例子来帮助理解
一、进程分类:餐厅的顾客
想象一个餐厅有两类顾客:
-
实时进程 (Real-Time Processes):就像 VIP包间的客人。
- 特点:他们是餐厅最重要的客人,点的都是急菜(比如马上要开会的商务餐)。他们的要求必须立刻响应。厨师必须优先做他们的菜,做完一道马上接着做下一道,直到他们满意为止。
- 子类:
SCHED_FIFO:VIP包间里只有一个客人。他点完菜,厨师就必须一直给他做,直到他说“好了,我歇会儿”或者有更高级别的领导(更高优先级的实时进程)打电话来点菜。SCHED_RR:VIP包间里有一桌客人,大家都是同一个级别的领导。厨师给他们每人炒一个菜(一个时间片),比如先给张总炒,再给李总炒,再给王总炒,循环着来,保证每个人都有得吃,但又不会让某一个人饿着。
-
普通进程 (Normal Processes):就像 大厅散座的客人。
- 特点:他们是来日常吃饭的,不着急。餐厅的目标是让所有散座客人都觉得公平,不会有人等得饿晕,也不会让某个客人独占厨师。这就是我们绝大多数人开的程序,比如浏览器、音乐播放器、Word文档。
调度器就是这个餐厅的总厨,他的任务就是决定下一个给谁炒菜。
二、上下文切换:厨师换锅炒菜
总厨(CPU)只有一个,但他要同时照看几十口锅(进程)。
-
过程:总厨正在给A客人炒宫保鸡丁。突然,总厨需要去给B客人做水煮鱼。
- 保存状态:他先把A客人的宫保鸡丁从火上拿下来,记下现在炒到几分熟、放了哪些调料、火候多大(保存上下文)。
- 加载状态:然后他走到B客人的锅前,看看菜单上要求水煮鱼要怎么做、麻辣程度是多少(加载上下文)。
- 开始烹饪:接着开始为B客人做水煮鱼。
-
开销:总厨跑来跑去、记笔记的时间,就是开销。如果客人太多,总厨大部分时间都在跑来跑去和记笔记,真正炒菜的时间就少了,整个餐厅的出菜效率就低了。所以调度器(总厨)的一个重要目标就是减少不必要的切换。
三、调度算法:总厨的工作策略
早期的总厨们用过几种方法:
-
FIFO (排队法):谁先点菜就先给谁做,做完再做下一个。
- 问题:如果第一个客人点了个“文火慢炖老母鸡”要炖3小时,后面所有点“蛋炒饭”的客人都得饿死。
-
SJF (先做简单的法):总厨看看所有点的菜,哪个最快做好就先做哪个。
- 问题:如果一直在做“蛋炒饭”,那个“文火慢炖老母鸡”的客人永远也吃不上饭。
-
STCF (抢占式先做简单的法):改进版。总厨正在炖老母鸡,这时来了个点“蛋炒饭”的客人,总厨立刻把炖鸡的锅拿下来(保存状态),先花1分钟把蛋炒饭炒好递给客人,再回去继续炖鸡。
- 优点:短任务的客人体验极好。
- 缺点:对长任务客人不友好,而且总厨会非常累(切换开销大)。
-
RR (轮流法):总厨给每个客人分配一个极短的时间,比如30秒。
- 给A客人的菜炒30秒,然后换B客人的菜炒30秒,再换C客人的……如此循环。
- 效果:因为轮换得非常快,每个客人都感觉总厨一直在为自己忙活,响应性非常好。这就是我们电脑能同时运行多个程序的感觉。
四、Linux的CFS总厨:完全公平调度器
Linux现在的总厨叫CFS,他的策略非常聪明:
他的目标不是分“时间”,而是分“CPU比例”。
他有一个虚拟沙漏(vruntime) 给每个客人(进程)。
- 普通客人:沙漏正常流速。客人占用的CPU时间,会正常加到他的沙漏里。
- 重要客人(低nice值):他的沙漏流得慢!比如他实际用了10秒CPU,但沙漏只记5秒。
- 不重要客人(高nice值):他的沙漏流得快!用了10秒CPU,沙漏可能记了20秒。
总厨CFS的工作原则非常简单:永远去服务那个沙漏里沙子最少的客人!
- 为什么? 因为沙漏沙子少,说明他“吃亏”了,实际获得的CPU时间相对于他的重要性来说还不够。
- 结果:重要客人的沙漏流得慢,所以沙子积累得慢,总厨就会更频繁地为他服务(获得更多CPU时间)。不重要客人的沙漏流得快,很快沙子就多了,总厨就会少服务他(获得更少CPU时间)。
这就完美地实现了“按重要性公平分配”,而不是死板地分配时间片。
总结
- VIP客人 -> 实时进程 -> 必须立刻满足。
- 大厅客人 -> 普通进程 -> 要讲公平。
- 厨师换锅 -> 上下文切换 -> 有开销。
- 愚蠢策略 -> FIFO, SJF -> 会导致有人饿死。
- 聪明策略 -> CFS -> 给每人一个沙漏 (
vruntime),永远帮沙子最少的人,实现加权公平。
希望这个比喻能帮你建立一个直观的理解!
658

被折叠的 条评论
为什么被折叠?



