Lec 3 磁盘驱动操作
Operating Systems: Three Easy Pieces, CH37-HDD
- 概述了HDD接口、物理结构等
An introduction to disk drive modeling, 1994
- 介绍了磁盘驱动模型
Scheduling Algorithms for Modern Disk Drives
- 主要介绍现代磁盘调度的算法
The RAIDbook, The Digital Large System Mass Storage Handbook, Massiglia
- CH2, page 1-20
- CH2, page 38-52
- CH12, page 1-11
本讲定位
前面两讲讲的SSD是纯电子的随机存取涉笔,读写任意位置代价几乎一致。 第三讲回到HDD。机械磁盘有真实的运动部件,访问代价强烈依赖数据的物理位置,这一点与固态硬盘截然不同,也正是后面文件系统布局、磁盘阵列等所有上层设计要应对的核心约束。理解磁盘怎么工作,是理解为什么文件系统要那样布局的前提。
磁盘接口
现代磁盘对外的接口很简洁:整个磁盘是一个扇区(sector,即 512 字节的块)数组,编号从 0 到 n - 1,这就是设备的地址空间。可以做多扇区操作,很多文件系统一次读写 4 KB 或更多,但厂商唯一保证原子的只有单个 512 字节写,要么完整完成、要么完全不完成;若此时突然断电,一个较大的写可能只完成一部分,这叫撕裂写(torn write)。
接口之外还有一些客户端普遍依赖、却没写进规范的假设,Schlosser 和 Ganger 称之为磁盘的不成文契约(unwritten contract):访问地址空间里彼此靠近的两个块,通常比访问相距很远的两个块快;按连续大块顺序读写,通常是最快的访问方式,远快于随机访问。这条契约和第二讲固态硬盘的契约一脉相承,是后面"尽量顺序访问"这条设计准则的物理根源。
磁盘的物理结构
让我们从理解现代磁盘的一些组成部分开始。
首先是盘片(platter),它是一个圆形的硬质表面,数据通过在其上引入磁性变化来实现持久存储。一块磁盘可能包含一个或多个盘片;每个盘片有两个面,每个面称为一个表面(surface)。这些盘片通常由某种坚硬材料(如铝)制成,然后覆盖一层薄薄的磁性材料,使得即使在断电的情况下,驱动器仍然能够持久地保存比特数据。
所有盘片都围绕着主轴(spindle)固定在一起,主轴连接到一个电机,在通电时以恒定的速度旋转盘片。旋转速度通常以每分钟转数(RPM)来衡量,现代磁盘的典型转速在 7200 RPM 到 15000 RPM 之间。需要注意的是,我们通常更关心单次旋转所需的时间,例如一个 10000 RPM 的磁盘,其单次旋转大约需要 6 毫秒(6 ms)
数据被编码在每个表面的同心圆上,这些同心圆由许多扇区(sectors)*组成;其中每一个同心圆称为一个*磁道(track)。单个表面上包含成千上万条磁道,这些磁道紧密排列,甚至在一根头发的宽度内就可以容纳数百条磁道。
为了在表面上进行读写,需要一种机制来感知(即读取)磁盘上的磁性模式,或者改变(即写入)这些模式。这个读写过程由磁头(disk head)完成;每个表面都有一个对应的磁头。磁头安装在一个统一的磁臂(disk arm)上,磁臂可以在表面上移动,从而将磁头定位到目标磁道的位置。

让我们通过逐步构建模型来理解磁盘是如何工作的,从单个磁道开始。假设我们有一个只有一条磁道的简单磁盘(图 37.1)。这条磁道上只有 12 个扇区,每个扇区大小为 512 字节(这是我们常见的扇区大小),因此它们的编号是 0 到 11。
这个简单系统中只有一个盘片,它围绕连接着电机的主轴旋转。
当然,仅有磁道本身并没有太大意义;我们还希望能够读写这些扇区,因此需要一个磁头,并将其连接到磁臂上,如图 37.2 所示。在图中,磁头位于磁臂末端,当前正位于扇区 6 上方,盘面以逆时针方向旋转。
为了理解在这个简单单磁道磁盘上如何处理请求,假设现在收到一个读取块 0 的请求。磁盘该如何处理这个请求?
在这个简单模型中,磁盘不需要做太多事情。它只需要等待目标扇区旋转到磁头下方。这种等待在现代磁盘中非常常见,也是 I/O 服务时间的重要组成部分,因此有一个专门的名字:旋转延迟(rotational delay)。
在这个例子中,如果完整一圈旋转所需时间为 R,那么从扇区 6 开始等待扇区 0 转到磁头下方,大约需要 R/2 的时间。最坏情况下,如果请求的是扇区 5,则几乎需要等待完整的一圈旋转。
到目前为止,我们的磁盘只有一条磁道,这显然不太现实;现代磁盘通常有数百万条磁道。现在我们来看一个稍微更真实一点的模型:一个具有三条磁道的磁盘(图 37.3 左)。
在图中,磁头当前位于最内侧的磁道(包含扇区 24 到 35);中间磁道包含扇区 12 到 23;最外侧磁道包含扇区 0 到 11。
为了理解磁盘如何访问某个扇区,我们来看一个访问较远扇区的例子,例如读取扇区 11。为了完成这个请求,磁盘首先需要将磁臂移动到正确的磁道(这里是最外侧磁道),这个过程称为寻道(seek)。寻道与旋转一样,是磁盘中最昂贵的操作之一。
寻道过程包含多个阶段:
- 加速阶段:磁臂开始移动并加速
- 匀速阶段:磁臂以全速移动
- 减速阶段:磁臂减速
- 稳定阶段(settling):精确调整磁头,使其对准目标磁道,其中稳定时间通常相当可观,例如 0.5 到 2 毫秒,因为磁盘必须非常精确地定位到正确的磁道(否则数据会读错)。

完成寻道后,磁头已经位于正确的磁道上。图 37.3(右)展示了这个过程。在寻道过程中,盘片仍在旋转;在这个例子中,大约转过了 3 个扇区。因此,此时扇区 9 即将经过磁头,我们只需要再经历一个较短的旋转延迟即可完成访问。当扇区 11 转到磁头下方时,I/O 的最后一个阶段开始,即传输(transfer)阶段,此时数据会从磁盘读取或写入磁盘。
一些其他细节
许多硬盘会采用一种称为磁道偏移(track skew)的技术,以确保在跨越磁道边界时,顺序读取仍然能够顺利进行。在我们之前的简单模型中,这种情况如图 37.4 所示。之所以要对扇区进行这种“错位”安排,是因为当磁头从一个磁道切换到另一个磁道时,即使是相邻磁道,也需要一定时间来重新定位。如果没有这种偏移,当磁头刚移动到下一条磁道时,目标扇区可能已经转过磁头,这样磁盘就不得不等待几乎完整的一圈旋转延迟才能再次读取到该扇区。
另一个现实情况是:外圈磁道通常比内圈磁道拥有更多的扇区,这是由几何结构决定的——外圈的周长更大,因此可以容纳更多数据。这种设计通常被称为多区域磁盘(multi-zoned disk drives)。在这种结构中,磁盘被划分为多个“区域(zone)”,每个区域由一组连续的磁道组成。在同一个区域内,每条磁道的扇区数量相同,而外层区域的每条磁道包含的扇区数会多于内层区域。
最后,现代磁盘驱动器中一个非常重要的组成部分是它的缓存(cache),出于历史原因有时也被称为磁道缓冲区(track buffer)。这个缓存本质上是一小块内存(通常大约为 8MB 或 16MB),用于临时存放从磁盘读取或即将写入磁盘的数据。
例如,在读取某个扇区时,磁盘可能会选择把该磁道上的所有扇区一起读入缓存;这样,如果随后有对同一磁道的访问请求,就可以非常快速地响应。
对于写操作,磁盘有两种策略选择:
- 当数据写入缓存后就立即报告完成,这称为写回缓存(write-back caching)(或“立即返回”)
- 只有当数据真正写入磁盘后才报告完成,这称为直写(write-through)
写回缓存有时会让磁盘看起来“更快”,但也存在风险:如果文件系统或应用程序依赖于数据按照某种顺序写入磁盘以保证正确性,那么写回缓存可能会导致问题(具体可参考文件系统日志(journaling)相关内容)。
磁盘调度
由于 I/O 操作成本很高,操作系统历来都会参与决定磁盘 I/O 请求的执行顺序。更具体地说,当有一组 I/O 请求时,磁盘调度器会检查这些请求,并决定下一个要执行哪一个。不同于作业调度(job scheduling)中通常无法知道每个作业需要多长时间,在磁盘调度中,我们可以较为准确地估计一个“作业”(即一次磁盘请求)需要的时间。通过估计寻道时间(seek)以及可能的旋转延迟(rotational delay),磁盘调度器可以大致知道每个请求的耗时,从而(贪心地)优先选择耗时最短的请求。因此,磁盘调度通常会遵循最短作业优先(SJF, Shortest Job First)的原则。
一种早期的磁盘调度方法称为最短寻道时间优先(SSTF)(也称 SSF)。SSTF 会根据磁道位置对 I/O 请求排序,优先处理距离当前磁头最近的请求。例如,假设当前磁头位于最内侧磁道,而有两个请求:扇区 21(中间磁道)、扇区 2(外侧磁道),那么调度器会先处理扇区 21,然后再处理扇区 2。在这个例子中,SSTF 表现良好:先移动到中间磁道,再到外侧磁道。
不过,SSTF 并不是万能的,原因如下:
- 磁盘几何信息不可见: 操作系统通常看不到真实的磁盘几何结构,它只看到一组逻辑块地址。解决方法是用最近块优先(NBF, Nearest Block First)来替代 SSTF,即选择地址上最接近的块
- 更根本的问题是饥饿。还是上面的例子,如果不断有新的请求到达当前磁头所在的内侧磁道,那么其他磁道上的请求可能永远得不到处理——因为总有“更近”的请求
核心问题: 如何在实现类似 SSTF 的调度策略时,避免饥饿问题?
解决方法早已被提出,最经典的是 SCAN 算法(扫描算法),该算法的思想很简单:磁头在磁盘上来回移动(类似电梯上下移动),按顺序处理沿途的请求。从内向外或从外向内的一次移动称为一次扫描(sweep)。 如果某个请求所在的磁道在当前扫描中已经经过,则不会立即处理,而是等到下一次反方向扫描时再处理
SCAN 有多种变体:
- F-SCAN:在一次扫描开始时“冻结”请求队列,扫描过程中到达的新请求会被延迟到下一轮处理,从而避免远距离请求被饿死
- C-SCAN(循环扫描):只在一个方向扫描(例如从外到内),到达终点后直接跳回起点再开始,这样可以提高公平性, 因为普通 SCAN 会让中间磁道被访问两次,而外侧磁道较少被访问
SCAN 及其变种常被称为电梯算法(elevator algorithm),因为它的行为类似电梯:电梯只会持续向一个方向运行(向上或向下),而不会因为某一层“更近”就突然改变方向。想象一下:如果你从 10 楼下到 1 楼,有人从 3 楼上来按了 4 楼按钮,电梯却因为“4 楼更近”而向上走,那会非常令人抓狂。电梯算法避免了这种情况——在磁盘中,它避免的是饥饿问题
不过,SCAN 及其变种并不是最优方案。问题在于它们(包括 SSTF)并没有很好地遵循 SJF 原则, 原因是它们忽略了旋转延迟
如何设计一种调度算法,使其更接近 SJF,同时同时考虑寻道时间和旋转时间?
解决方法, SPTF,最短定位时间优先(Shortest Positioning Time First),在这之前,我们先更深入地理解问题本身。图 37.8 给出了一个示例。在这个例子中,磁头当前位于内侧磁道的扇区 30 上。调度器需要决定:下一步是处理中间磁道的扇区 16,还是外侧磁道的扇区 8?

答案当然是:“取决于情况(it depends)”。在工程中,“看情况”几乎是一个通用答案,因为工程问题往往涉及权衡取舍。不过,更重要的是理解为什么取决于情况。关键在于:寻道时间和旋转延迟之间的相对大小,如果寻道时间远大于旋转延迟,那么SSTF已经很好了(优先选最近的磁道),如果寻道时间比旋转时间小很多,那么就有应该考虑旋转因素。在当前例子中,如果寻道很快,而旋转较慢,那么与其做一次较短的寻道去中间磁道读取扇区 16(但要等它转一整圈),不如直接寻道更远的外侧磁道,读取扇区 8(因为它可能很快就转到磁头下)
在现代磁盘中,寻道时间和旋转延迟通常是同一个量级(具体仍取决于请求),因此SPTF 能够更好地优化性能,因为它同时考虑了两者。但是在操作系统中很难实现,因为:操作系统通常不知道真实的磁道边界,也不知道磁头当前的精确旋转位置。因此,SPTF 通常是在磁盘内部实现的(由磁盘控制器完成),在现代系统中, 磁盘支持多个未完成请求(outstanding requests),,磁盘内部有自己的高级调度器(可以精确实现 SPTF)
另一个重要优化是 I/O 合并。例如有请求序列:读取块 33、读取块 8、读取块 34。调度器应该把 33 和 34 合并为一个连续请求(两个块一起读)。好处减少请求数量、降低调度和控制开销、提高顺序访问效率。 这种优化在操作系统层面尤其重要。
是否应该“立即执行”请求?
一种直觉策略是只要有请求,就立刻发送给磁盘,这称为工作保持(work-conserving)策略(磁盘不会空闲),但研究发现,有时更好的策略是 稍微等待一下,原因是,等待期间可能会有“更优”的请求到达;可以整体优化调度效率,这种方法称为预测性磁盘调度。关键在于等多久,什么时候该等。这些问题都比较难