1.

磁盘调度:为什么你的电脑有时会“卡顿”?
你有没有遇到过这种情况?电脑明明配置不低,但在打开一个大型软件,或者同时拷贝好几个大文件时,整个系统就变得一卡一卡的,鼠标移动都像在“慢动作回放”。
很多人第一反应是CPU不够快,或者内存不够大,但其实,很多时候“罪魁祸首”是硬盘,更具体地说,是硬盘的“调度”出了问题。
想象一下,硬盘的磁头就像图书馆里唯一的图书管理员。
现在,有十个人同时递上纸条,要求他去书架上取不同的书。
如果这个管理员拿到纸条后,完全按照递交的顺序,从第一张纸条开始,跑到书架最左边取一本,再跑到最右边取一本,接着又跑回中间……他大部分时间都花在来回奔跑的路上,真正拿书的时间反而很少。
结果就是,所有人都在焦急地等待,整个图书馆的效率低得令人发指。
磁盘调度算法,就是这位“图书管理员”的工作策略。
它决定了磁头(管理员)按照什么样的顺序去响应来自各个进程(读者)的I/O请求(取书请求)。
一个好的策略,能让磁头移动的总距离最短,从而用最少的时间完成最多的请求,系统自然就流畅了。
一个糟糕的策略,就会让磁头像无头苍蝇一样乱撞,导致I/O队列堆积,反映到用户层面,就是程序无响应、系统卡顿。
在深入算法之前,我们必须先搞懂一次磁盘读/写到底需要多长时间。
这可不是简单的“读数据”时间。
它主要分为三部分:
- 寻道时间:磁头移动到目标磁道所花的时间。
这是机械硬盘上最耗时的部分,通常以毫秒计,也是所有调度算法主要优化的目标。
- 旋转延迟时间:磁头到达正确磁道后,等待目标扇区旋转到磁头下方的时间。
对于常见的7200转/分钟的硬盘,平均旋转延迟大约是4.17毫秒。
- 传输时间:从磁盘读出或向磁盘写入数据所经历的时间。
这个时间通常很短,与数据量大小和转速有关。
所以,总时间
=
传输时间。
而磁盘调度算法的核心使命,就是千方百计地减少寻道时间。
接下来,我们就从最“老实”的算法开始,看看这位“图书管理员”都有哪些工作方法,以及我在实际系统调优中踩过的那些坑。
2.
先来先服务:最公平,但也最“笨”的策略
先来先服务,英文叫FCFS,这可能是最容易理解的算法了。
它的规则简单粗暴:谁先来,我就先服务谁。
完全按照I/O请求到达的先后顺序排队处理。
我举个实际的例子。
假设磁头当前停在100号磁道,现在等待队列里有三个请求,按到达顺序分别是:55,
180,
最后移动到40。
我们来算算它跑了多远:|100-55|
+
310个磁道距离。
这个距离可不短。
FCFS的优点很明显:绝对公平,每个请求都不会“饿死”(即永远得不到响应);实现起来也极其简单,几乎不需要额外的逻辑。
在一些请求非常稀疏,或者对公平性要求极高的特殊场景下(比如某些实时系统),它可能被采用。
但它的缺点更致命:平均寻道时间极长,效率低下。
就像我们例子中看到的,磁头毫无规划地来回“折返跑”,做了大量无用功。
我早年管理过一个老旧的文件服务器,初期没做任何优化,用的就是类似FCFS的策略。
当多个用户同时请求服务器上不同位置的文件时(比如一个在磁盘开头,一个在末尾),磁盘响应速度急剧下降,用户体验非常差。
这让我第一次深刻认识到,公平和效率,在磁盘I/O的世界里常常是鱼与熊掌。
所以,在现代操作系统中,纯粹的FCFS很少作为主要的磁盘调度算法。
它更像是一个基准线,用来衬托其他更聪明算法的优越性。
理解FCFS,你就理解了磁盘调度需要解决的核心矛盾:如何在不失公平的前提下,大幅提升效率?
3.
最短寻道时间优先:效率优先的“功利主义者”
为了解决FCFS效率低下的问题,人们很自然地想到了一个“聪明”的办法:最短寻道时间优先,也就是SSTF。
这个算法的思想直白而有效:磁头总是优先服务距离自己当前位置最近的请求。
还是用刚才的例子:磁头在100,队列请求是55,
180,
40。
SSTF会怎么选?它会先看看谁离100最近。
55距离45,180距离80,40距离60。
最近的是55。
所以先服务55。
到了55之后,再看剩下的请求(180和40),谁离55最近?40距离15,180距离125。
最近的是40。
服务完40,最后再服务180。
移动轨迹:100
->
200个磁道。
比FCFS的310少了足足110个磁道!效率提升立竿见影。
SSTF的优点非常突出:它能显著减少平均寻道时间,提高磁盘的吞吐量。
在实际测试中,对于一般的I/O负载,SSTF的性能通常比FCFS好得多。
它一度是非常流行的算法。
但是,SSTF有一个致命的缺陷:饥饿现象。
想象一下,磁头正在中间区域工作,此时不断有新的请求到达磁头当前位置的附近。
那么磁头就会一直忙于服务这些“近水楼台”的请求,而远处磁道的请求(比如最内圈或最外圈的)可能永远也得不到服务。
这就好比在热门商圈,出租车都抢着接附近的短途单,而住在郊区的乘客永远打不到车。
我在一个数据库服务器上就遇到过类似问题。
当时系统日志写在磁盘外圈,而业务数据频繁读写在内圈。
采用类SSTF策略时,在业务高峰期,日志写入请求被持续“插队”,导致日志同步延迟,甚至一度影响了事务的持久性。
这就是饥饿现象带来的实际风险。
因此,SSTF虽然高效,但并不是一个“健壮”的算法,它缺乏公平性保障。
4.
扫描算法与LOOK算法:有纪律的“巡逻兵”
为了兼顾效率和公平,避免饥饿,我们需要给磁头的移动加上一些“纪律”。
这就引出了扫描算法,也叫电梯算法,或者SCAN。
你可以把磁头想象成一部电梯,磁道就像从1楼到100楼的楼层。
电梯(磁头)的运动方式是:先选择一个方向(比如向上),然后沿着这个方向一路服务沿途的所有请求,直到到达这个方向的“最顶层”(磁盘的物理末端)。
然后,调转方向,向下运行,同样服务沿途的请求,直到到达“最底层”(磁盘的物理开端)。
如此往复。
假设磁头起始在100,初始方向是向磁道号增大的方向(向外)。
请求队列为:55,
180,
20。
- 磁头从100开始向上(外)扫描。
沿途会服务120和180。
- 到达最外圈(假设是199)后,掉头向下(内)扫描。
沿途会服务55,
40,
20。
移动轨迹:100
->
20。
这个算法保证了任何一个磁道上的请求,在磁头两次扫描之内必然会被服务到,彻底解决了饥饿问题。
它的平均寻道性能也优于FCFS,虽然可能不如SSTF在特定负载下那么极致。
但是SCAN也有缺点:它有点“死板”。
比如,当磁头移动到最外圈时,即使最外圈没有任何请求,它也必须走到头才能掉头。
同样,在向内移动时也必须走到最内圈。
这会产生一些不必要的空跑。
于是,LOOK算法对SCAN做了一个非常实用的优化。
LOOK算法去掉了“必须走到物理尽头”这个硬性规定。
它的策略是:磁头朝一个方向移动,并服务该方向上的所有请求。
但当这个方向上没有更远的请求时,就立即掉头,而不是走到磁盘尽头。
还是上面的例子,用LOOK算法:
- 磁头从100向上扫描,服务120和180。
- 到达180后,发现向上已经没有等待的请求了(40,55,20都在它下面),于是立即掉头向下。
- 向下扫描,依次服务55,
40,
20。
移动轨迹:100
->
20。
可以看到,它省去了从180跑到199再跑回来的那段无用行程。
LOOK算法在实际应用中比SCAN更常见,因为它更“聪明”,在绝大多数情况下都能提供与SCAN相同的公平性保证,同时寻道性能更好。
现在很多操作系统的默认磁盘调度器,其核心思想就来源于LOOK。
5.
循环扫描与C-LOOK:追求极致的公平
SCAN和LOOK算法虽然公平,但仔细想想,它们对磁道两端的请求还是有点“不公平”。
比如,当磁头从内向外扫描时,刚被服务过的内圈请求,如果很快又来了,它必须等磁头走到最外圈、掉头、再扫回来才能被再次服务。
而位于中间区域的请求,被服务的频率可能会更高。
为了让所有磁道位置获得更均衡的响应时间,循环扫描算法,即C-SCAN被提了出来。
它的规则是:磁头只朝一个方向移动(比如从内到外),服务沿途的请求。
当到达这一端的尽头时,它不是立即掉头服务反方向的请求,而是直接快速移动到另一端(不服务任何请求),然后重新开始原方向的扫描。
这就像摩天轮,总是朝一个方向转圈,乘客只在一边上下一—边下。
用之前的例子(请求:55,
180,
向外移动):
- 从100向外,服务120,
180。
- 到达最外圈(199)后,直接快速移动到最内圈(0),这个移动过程不服务任何请求。
- 从最内圈(0)开始继续向外扫描,服务沿途的20,
40,
55。
这个算法的好处是,为每个磁道上的请求提供了非常一致的等待时间,方差比SCAN更小。
特别适合那些对响应时间一致性要求高的应用。
同样,C-SCAN也有它的“死板”之处:必须走到物理尽头。
因此,它的优化版本C-LOOK应运而生。
C-LOOK算法的行为是:
- 磁头朝一个方向移动,服务该方向上的所有请求。
- 当该方向没有更多请求时,磁头直接跳到另一个方向最远的那个请求的位置(而不是磁盘物理端点),然后继续原方向扫描。
这相当于LOOK算法的“循环版”。
我们用C-LOOK再算一次:
- 磁头从100向外,服务120,
180。
- 到达180后,向外已无请求。
此时,它不走到199,而是直接“跳”到当前另一个方向(向内)最远的那个请求的位置,也就是20。
- 从20开始继续向外扫描,服务20,
40,
55。
移动轨迹:100
->
55。
C-LOOK避免了磁头在磁盘两端无请求区域的空跑,是综合性能(寻道效率)和公平性(等待时间一致性)都非常优秀的算法,也是现代Linux等操作系统中常用的算法之一。
6.
实战:在Linux中查看与更改磁盘调度算法
理论说了这么多,不如动手玩一下。
在Linux系统中,我们可以很方便地查看和修改磁盘使用的调度算法。
这对于性能调优和问题排查非常有用。
首先,我们需要找到磁盘的设备名。
通常,系统磁盘是sda,第二个是sdb,以此类推。
我们可以用lsblk命令查看。
假设我们要查看和修改sda的调度算法。
相关的信息在/sys/block/[设备名]/queue/scheduler这个虚拟文件中。
查看当前调度器:
cat/sys/block/sda/queue/scheduler
你可能会看到类似这样的输出:[mq-deadline]
kyber
none。
方括号[]括起来的mq-deadline,就表示当前正在使用的调度算法。
可用的调度器有哪些?不同的内核版本和磁盘类型(如NVMe
SSD)支持的调度器可能不同。
常见的有:
- mq-deadline:
适用于多队列块设备(现代硬盘)的限期调度器,是SSD和高速硬盘的默认推荐,可以看作是C-LOOK思想的一种高效实现。
- kyber:
SSD)设计的调度器,它尝试直接控制请求的延迟。
- bfq:
完全公平队列调度器,它更注重为每个进程提供公平的带宽份额,适合桌面交互式环境或虚拟机,保证前台操作流畅。
- none:
不使用任何I/O调度器,通常用于由硬件自身或用户空间程序(如某些虚拟化层)直接管理I/O的场景。
临时更改调度器(重启后失效):
echo'bfq'
/sys/block/sda/queue/scheduler
再次用cat命令查看,你会发现当前调度器变成了bfq。
永久更改调度器需要通过内核引导参数或修改系统配置文件(如/etc/default/grub),这里不展开,因为涉及系统稳定性,操作需谨慎。
那么,如何选择?根据我的经验:
- 对于传统的机械硬盘,
bfq或mq-deadline都是不错的选择。bfq能让你在后台编译代码时,前台视频播放不卡顿;mq-deadline则能提供更稳定的吞吐量。 - 对于SATA/SAS接口的固态硬盘,
mq-deadline通常是默认且安全的选择。 - 对于NVMe固态硬盘,由于其极高的并行度和低延迟,使用
none调度器或者kyber可能获得最佳性能,因为调度器本身的开销可能成为瓶颈。我自己的NVMe系统盘就使用了
none调度器。
重要提示:在修改生产环境的调度器之前,一定要在测试环境进行充分的基准测试和压力测试。
一个不合适的调度器可能导致性能下降甚至系统不稳定。
我曾经因为将一台数据库服务器的调度器从deadline误改为cfq(一个更老的公平调度器),导致在高并发写入时出现间歇性延迟飙升,排查了好久才找到这个原因。
7.
算法对比与场景选择指南
纸上谈兵终觉浅,我们把这些算法放到一个表格里直观对比一下,再结合场景聊聊怎么选。
style="text-align:left">算法 | style="text-align:left">核心思想 | style="text-align:left">优点 | style="text-align:left">缺点 | style="text-align:left">适用场景 |
|---|---|---|---|---|
style="text-align:left">FCFS | style="text-align:left">绝对公平,先来先到 | style="text-align:left">实现简单,无饥饿 | style="text-align:left">平均寻道时间长,效率低 | style="text-align:left">教学示例,对公平性有极端要求的特殊实时系统 |
style="text-align:left">SSTF | style="text-align:left">贪心,总是找最近的 | style="text-align:left">平均寻道时间较短,吞吐量高 | style="text-align:left">可能导致饥饿,响应时间方差大 | style="text-align:left">负载较轻且请求分布均匀的旧式系统,现已较少单独使用 |
style="text-align:left">SCAN | style="text-align:left">电梯算法,双向扫描到尽头 | style="text-align:left">兼顾效率与公平,无饥饿 | style="text-align:left">两端请求等待时间可能较长,有无效扫描 | style="text-align:left">早期Unix系统,负载较重的机械硬盘系统 |
style="text-align:left">LOOK | style="text-align:left">SCAN的优化,无请求即回头 | style="text-align:left">比SCAN寻道时间更短,无饥饿 | style="text-align:left">两端请求响应时间仍不均 | style="text-align:left">现代机械硬盘的通用选择,Linux的 |
style="text-align:left">C-SCAN | style="text-align:left">单向扫描,循环服务 | style="text-align:left">等待时间更均匀,无饥饿 | style="text-align:left">必须扫描到端点,存在空跑 | style="text-align:left">对响应时间一致性要求高的系统(如流媒体服务器) |
style="text-align:left">C-LOOK | style="text-align:left">C-SCAN的优化,跳至最远请求 | style="text-align:left">等待时间均匀,且寻道效率高 | style="text-align:left">实现稍复杂 | style="text-align:left">综合性能最佳的通用算法,现代I/O调度器的设计蓝本 |
如何根据场景选择?
个人桌面电脑:你希望边下载文件边看视频不卡顿。
那么,像BFQ这样基于完全公平思想的调度器就非常适合。
它能智能地为每个进程(下载软件、视频播放器)分配I/O带宽,保证前台应用的流畅体验。
这背后的思想,其实融合了LOOK的扫描方式和进程权重的考量。
数据库服务器/文件服务器:这类应用追求高吞吐量和稳定的延迟。
mq-deadline(基于C-LOOK思想)通常是默认的推荐选择。
它能有效减少寻道时间,同时通过“期限”机制防止任何请求等待过久,在效率和公平间取得了很好的平衡。
我负责的多数线上MySQL服务器,都会确认其调度器为
mq-deadline。高性能计算/缓存服务器:极致追求低延迟和高IOPS(每秒I/O操作数)。
如果使用的是NVMe
SSD,由于其内部并行性极高,操作系统层面的调度可能反而成为开销。
这时,使用“none”调度器,让硬件自己管理队列,或者使用专为低延迟设计的kyber,往往能压榨出硬盘的最后一分性能。
虚拟化/云主机环境:宿主机需要公平地将I/O资源分配给多个虚拟机。
BFQ在这里也大有用武之地,它可以确保某个虚拟机的疯狂读写不会饿死其他虚拟机。
而虚拟机内部,则可以根据其自身的负载类型,再选择
mq-deadline或none。
说到底,没有一种算法是“银弹”。
现代操作系统的I/O调度器(如Linux的mq-deadline,bfq)都是非常复杂的模块,它们不仅仅是简单的扫描或查找,还融合了队列管理、优先级、期限预测、合并相邻请求等高级特性。
但万变不离其宗,其底层优化逻辑,依然是我们今天讨论的这些经典磁盘调度算法的延伸与融合。
理解它们,就是理解了磁盘I/O性能优化的基石。
下次当你再遇到系统存储性能瓶颈时,不妨先看看你的磁盘调度策略是否合适,这或许就是解决问题的第一把钥匙。


