少于 1 分钟阅读 次阅读

在计算机系统中,硬盘(尤其是传统机械硬盘)的寻道操作是其中最慢的物理操作之一。想象一下,磁头在盘片上快速移动寻找数据的位置,这比 CPU 和内存的速度慢了数个数量级。如果操作系统内核只是简单地将应用程序发出的 I/O 请求,按照接收的顺序直接发送给硬盘,那么系统的整体性能将会惨不忍睹。I/O 调度程序 正是为了解决这个核心矛盾而诞生的:它作为内核中管理块设备请求队列的“交通指挥官”,核心目标是通过智能地重组请求顺序,大幅减少耗时的磁盘寻址时间,从而提升系统整体吞吐量和响应速度

一、核心思想:合并与排序

I/O 调度程序提升性能主要依靠两大“法宝”:合并排序

  1. 合并:当一个新的 I/O 请求到来时,如果请求访问的磁盘扇区与队列中某个已有请求的扇区正好是连续的,那么这两个请求就可以合并为一个更大的、连续的请求。这样做的好处是显而易见的:将多次 I/O 操作的开销(包括命令下发、中断处理等)压缩为一次,更重要的是,只需一次磁头寻道就能读取或写入一大片连续数据,效率成倍提升。

  2. 排序:这是 I/O 调度程序的精髓所在。它的灵感来源于现实中的电梯调度算法。电梯不会在楼层间随意地跳来跳去,而是会先朝着一个方向运行,服务完该方向的所有请求后,再掉头服务另一个方向的请求。类似地,I/O 调度程序会尽量按照磁盘扇区的物理顺序(从小到大或从大到小)来排列请求队列。这样,磁盘磁头的移动就变得有序而高效,像电梯一样沿“直线”扫描,避免了在盘片上来回“折返跑”所浪费的大量寻道时间。

通过这两种操作,I/O 调度程序虚拟化了块设备,让多个并发 I/O 请求能够高效、合理地共享磁盘带宽。需要注意的是,它的目标是系统全局吞吐量最大化,有时甚至会为了整体利益而“不公平”地对待某些个别请求,这也为后续的发展埋下了伏笔。


二、演进之路:从简单到复杂

Linux 内核中的 I/O 调度程序经历了多次迭代,每一种新方案都是为了解决前代方案在特定场景下暴露出的问题。

1. Linus 电梯:经典的起点

这是 Linux 早期(2.4 内核)默认的调度程序,得名于 Linus Torvalds。它忠实地执行了合并与排序的基本思想:

  • 新请求到来时,先尝试与队列中的相邻请求合并。
  • 若无法合并,则寻找合适的位置插入,以保持队列的扇区有序性。
  • 作为一种简单的预防措施,如果发现队列中有“驻留时间过长”的请求,则新请求会被放到队尾,以防旧请求被无限期推迟。

存在的问题:这种“年龄”检测机制是粗糙且被动的。在持续高强度访问磁盘某一区域的场景下(例如持续的数据库写入),访问其他区域的请求(比如用户的交互式读操作)可能因为永远找不到“合适”的插入位置而被长时间“饿死”(Starvation)。特别是,异步的写操作可能会持续地占据磁盘,阻塞了同步的、对用户体验至关重要的读操作,导致系统响应变卡。

2. 最终期限调度程序:引入公平的时限

为了解决“饥饿”问题,2.6 内核引入了最终期限调度程序。它在 Linus 电梯排序队列的基础上,增加了两个先进先出队列:

  • 排序队列:与 Linus 电梯一样,按扇区顺序排列,以最大化吞吐量。
  • 读 FIFO 队列写 FIFO 队列:分别按照请求到达的时间顺序存放读、写请求。

每个请求都被赋予一个“最后期限”。默认情况下,读请求必须在 500ms 内得到服务,写请求为 5s。调度器平时仍从排序队列头部取请求,以保证效率。但一旦发现某个读或写 FIFO 队列头部的请求超过了其期限,就会立即暂停排序队列,优先处理这个超时的请求

这种方法巧妙地在效率与公平间取得了平衡。它确保了即使在高磁盘负载下,任何请求的等待时间也有一个上限,特别是保护了同步的读操作,能获得更快的响应。

3. 预测调度程序:主动的优化

最终期限调度器解决了公平性问题,但可能损害了吞吐量。设想一个场景:系统正在大量写入,此时一个读请求到来,调度器中断写入去处理这个读请求,完成后磁头又得摆回去继续写,造成了额外的寻道。

预测调度程序在最终期限的基础上,增加了“预测启发”能力。当它处理完一个读请求后,不会立即返回去处理之前的写请求,而是有意地等待非常短的时间(默认为 6ms)。在这短暂的窗口期内,如果应用程序紧接着又发来了一个相邻磁盘位置的读请求(这在顺序读文件中很常见),调度程序就可以立刻处理它,从而避免了磁头两次无谓的摆动。

如果预测准确,这短暂的等待能避免大量的寻道操作,同时提升吞吐量和响应时间。当然,如果等待期间没有新请求到来,则会损失几毫秒的性能。它通过统计进程的 I/O 行为模式来做出更聪明的决策。在 2.6 内核的相当长一段时间里,它是默认的调度程序。

4. 完全公平排队调度程序:进程级的公平

之前的调度程序关注的是单个请求的公平。而 CFQ 调度程序则将公平性提升到了进程级别。它为每个发起 I/O 的进程(或线程)单独维护一个请求队列,并在这些队列之间采用时间片轮转的方式进行调度,确保每个进程都能公平地分配到一定的磁盘带宽。

这种设计特别适合需要保证交互式进程响应速度的场景,如桌面多媒体应用(保证音频播放不卡顿)。它保证了系统的“公平性”,但可能在一定程度上牺牲了整体的吞吐量。

5. 空操作调度程序:为特殊设备而生

顾名思义,这是最简单的调度程序。它几乎不做什么:仅进行最基本的请求合并,然后大致按照先来先服务的顺序将请求提交给驱动。

它的存在意义在于,对于没有机械寻道开销的设备(如 SSD、闪存卡、RAMDISK),传统的基于寻道优化的排序逻辑变得多余,甚至可能带来不必要的 CPU 开销。对于这些“真正的随机访问设备”,NOOP 这种极简的设计反而可能是最高效的。


三、如何选择?

Linux 内核允许在启动时通过内核命令行参数 elevator= 来指定默认的 I/O 调度程序:

  • elevator=deadline:最终期限调度程序,在数据库、高负载服务器等场景下表现稳定。
  • elevator=cfq:完全公平排队调度程序,曾是许多桌面发行版的默认选择,注重公平性。
  • elevator=noop:空操作调度程序,适用于 SSD、虚拟机等环境。
  • elevator=as:预测调度程序(在较新内核中已被其他算法替代或整合)。

留下评论