chapter 12: 块设备 I/O 和缓冲区管理
学习笔记
摘要
本章深入研究了区块设备 I/O 和缓冲管理,重点介绍了原则、I/O 缓冲的优势以及 Unix 缓冲管理算法的不足之处。提出使用信号量设计更高效的缓冲管理算法,介绍了 PV 算法作为示例。还提供了一个编程项目,用于比较 Unix 的缓冲管理算法和 PV 算法,有助于理解文件系统的 I/O 操作。
12.1 区块设备 I/O 缓冲
- 磁盘 I/O 比内存访问慢,促使使用 I/O 缓冲以减少物理 I/O 操作。
- I/O 缓冲涉及使用缓冲器作为块设备的缓存内存。
- 进程通过缓冲器缓存读写磁盘块,避免频繁的磁盘访问。
- I/O 缓冲提高文件 I/O 效率和系统吞吐量。
12.1.1 I/O 缓冲的基本原理
- 读操作:
- 在缓冲缓存中查找 (dev, blk)。
- 如果找到,则从缓冲器读取;否则,分配一个缓冲器,从磁盘读取数据,并将缓冲器保留在缓冲缓存中。
- 写操作:
- 获取缓冲器,写入数据,标记为延迟写入,释放到缓冲缓存。
- 仅当将脏缓冲器重新分配到不同块时,才将脏缓冲器写入磁盘。
12.1.2 缓冲管理术语
- BUFFER 结构: 用于缓冲器的类型,动态从缓冲缓存中分配。
- 函数:
bread(dev, blk):
返回带有有效数据的缓冲器。bwrite(bp):
同步写入;等待完成。dwrite(bp):
延迟写入;标记缓冲器为脏,以供后续使用。awrite(bp):
异步写入;开始 I/O 但不等待完成。
12.1.3 物理块设备 I/O
- 每个设备都有一个包含待处理 I/O 缓冲器的 I/O 队列。
start_io(bp):
将缓冲器输入设备 I/O 队列,并在队列非空时发出 I/O 命令。- 中断处理程序完成当前缓冲器上的 I/O 操作,并在队列非空时启动下一个缓冲器的 I/O。
12.1.4 缓冲器释放
brelse(bp):
释放缓冲器;在同步写入后使用。InterruptHandler():
从 I/O 队列出列,如果是异步的,则释放缓冲器,解除进程阻塞,并在队列非空时发出下一个缓冲器的 I/O 命令。
注意: 延迟写入策略通过仅在必要时将脏缓冲器写入磁盘,减少物理磁盘 I/O 并提高缓冲效率。
12.2 Unix I/O 缓冲管理算法学习笔记
12.2.0 简介
Unix I/O 缓冲管理算法首次出现在 V6 Unix(Ritchie 和 Thompson,1978;Lion,1996)中,详细讨论在 Bach 的第三章中(Bach,1990)。Unix 缓冲管理子系统包括以下组件。
12.2.1 I/O 缓冲
- NBUF 个内核中的缓冲器作为缓冲缓存,每个缓冲器由结构体表示。
typedef struct buf {
struct buf *next_free; // 空闲列表指针
struct buf *next_dev; // 设备列表指针
int dev, blk; // 分配的磁盘块
int opcode; // READ|WRITE
int dirty; // 缓冲器数据已修改
int async; // ASYNC 写标志
int valid; // 缓冲器数据有效
int busy; // 缓冲器正在使用
int wanted; // 有进程需要这个缓冲器
struct semaphore lock = 1; // 缓冲器锁定信号量;值为 1
struct semaphore iodone = 0; // 用于等待 I/O 完成的进程
char buf[BLKSIZE]; // 数据块区域
} BUFFER;
BUFFER buf[NBUF], *freelist; // NBUF 个缓冲器和空闲缓冲器列表
12.2.2 设备表
- 每个块设备由设备表结构表示。
struct devtab {
u16 dev; // 主设备号
BUFFER *dev_list; // 设备缓冲器列表
BUFFER *io_queue; // 设备 I/O 队列
} devtab[NDEV];
- 每个 devtab 包含一个 dev_list,其中包含当前分配给设备的 I/O 缓冲器,以及一个 io_queue,其中包含设备上待处理 I/O 操作的缓冲器。
12.2.3 缓冲初始化
- 系统启动时,所有 I/O 缓冲器在空闲列表中,所有设备列表和 I/O 队列为空。
12.2.4 缓冲列表
- 当缓冲器分配给 (dev, blk) 时,将其插入 devtab 的 dev_list 中。如果缓冲器当前正在使用,则标记为 BUSY 并从空闲列表中移除。BUSY 缓冲器可能也在 devtab 的 I/O 队列中。释放缓冲器时,它会被放回空闲列表,但仍保留在 dev_list 中以供可能的重用。
12.2.5 Unix getblk/brelse 算法
/* getblk: 为独占使用返回一个缓冲器=(dev,blk) */
BUFFER *getblk(dev, blk) {
while (1) {
(1). 在 dev_list 中搜索 bp=(dev, blk);
(2). 如果 (bp 在 dev_list) {
if (bp BUSY) {
设置 bp WANTED 标志;
等待(bp); // 等待 bp 被释放
继续; // 重试算法
}
/* bp 不忙 */
从空闲列表中移出 bp;
标记 bp 为 BUSY;
返回 bp;
}
(3). /* bp 不在缓存中;尝试从空闲列表获取一个空闲缓冲器 */
if (空闲列表为空) {
设置空闲列表 WANTED 标志;
等待(空闲列表); // 等待任何空闲缓冲器
继续; // 重试算法
}
(4). /* 空闲列表非空 */
bp =从空闲列表中取出的第一个 bp;
标记 bp 为 BUSY;
if (bp DIRTY) { // bp 用于延迟写入
awrite(bp); // 异步写入 bp;
继续; // 从 (1) 继续,但不重试
}
(5). 重新分配 bp 到 (dev,blk); // 设置 bp 数据无效,等等
返回 bp;
}
}
/** brelse: 将缓冲器释放为 FREE 到空闲列表 **/
brelse(BUFFER *bp) {
if (bp WANTED)
唤醒(bp); // 唤醒所有等待 bp 的进程
if (空闲列表 WANTED)
唤醒(空闲列表); // 唤醒所有等待空闲列表的进程
清除 bp 和空闲列表 WANTED 标志;
将 bp 插入 (空闲列表的尾部);
}
12.2.7 Unix 算法的特点
-
数据一致性: 为了确保数据一致性,getblk 绝不会分配超过一个缓冲器给相同的 (dev, blk)。这通过在从睡眠中唤醒后重新执行“重试循环”来实现。脏缓冲器在重新分配之前被写出,确保了数据一致性。
-
缓存效果: 通过以下方式实现缓存效果。释放的缓冲器仍保留在设备列表中,以供可能的重用。标记为延迟写的缓冲器不会立即进行 I/O,可供重用。缓冲器被释放到空闲列表的尾部,但从空闲列表的前部分配。这基于 LRU(最近最少使用)原则,有助于延长分配的缓冲器的寿命,从而增加其缓存效果。
-
临界区域: 设备中断处理程序可能操纵缓冲器列表,例如从 devtab 的 I/O 队列中删除
bp,更改其状态并调用 brelse(bp)。因此,在 getblk 和 brelse 中,设备中断在这些关键区域被屏蔽。这是暗示但未在算法中显示的。
12.2.8 Unix 算法的缺点
-
效率低下: 该算法依赖于重试循环。例如,释放一个缓冲器可能唤醒两组进程:那些需要已释放缓冲器的进程,以及那些只需要一个空闲缓冲器的进程。由于只有一个进程可以获取已释放的缓冲器,所有其他被唤醒的进程必须再次进入睡眠状态。从睡眠中唤醒后,每个被唤醒的进程必须从头开始重新执行算法,因为所需的缓冲器可能已经存在。这可能导致过多的进程切换。
-
缓存效果不可预测: 在 Unix 算法中,每个释放的缓冲器都可供使用。如果获取缓冲器的进程需要一个空闲缓冲器,则该缓冲器将被重新分配,即使可能仍然有需要该缓冲器的进程。
-
可能的饥饿: Unix 算法基于“自由经济”的原则,即每个进程都有机会尝试但不能保证成功。因此,可能发生进程饥饿。
-
算法使用 sleep/wakeup,仅适用于单处理器系统。
12.3 新的 I/O 缓冲管理算法
在本节中,我们将展示一种新的 I/O 缓冲管理算法。我们将使用 P/V 操作在信号量上进行进程同步,而不是使用 sleep/wakeup。信号量相对于 sleep/wakeup 的主要优势有:
- 计数信号量可用于表示可用资源的数量,例如空闲缓冲器的数量。
- 当许多进程等待资源时,对信号量的 V 操作仅解除一个等待的进程,因为它已经获得资源,无需重试。
这些信号量属性可用于设计更高效的缓冲管理算法。形式上,我们将问题规定如下。
12.3.1 使用信号量的缓冲管理算法
假设是单处理器内核(一次只运行一个进程)。使用计数信号量上的 P/V 操作设计新的缓冲管理算法,满足以下要求:
- 保证数据一致性。
- 具有良好的缓存效果。
- 高效率:无重试循环,没有不必要的进程“唤醒”。
- 免于死锁和饥饿。
值得注意的是,仅仅将 Unix 算法中的 sleep/wakeup 替换为信号量上的 P/V 操作并不是一个可接受的解决方案,因为这样做会保留所有的重试循环。我们必须重新设计算法以满足所有上述要求,并证明新算法确实优于 Unix 算法。首先,我们定义以下信号量。
BUFFER buf[NBUF]; // NBUF 个 I/O 缓冲器
SEMAPHORE free = NBUF; // 计数信号量,表示 FREE 缓冲器的数量
SEMAPHORE buf[i].sem = 1; // 每个缓冲器都有一个锁定信号量 sem=1
为简化符号,我们将通过缓冲器本身来引用每个缓冲器的信号量。与 Unix 算法一样,最初所有缓冲器都在空闲列表中,所有设备列表和 I/O 队列为空。
12.8 总结
本章涵盖了块设备 I/O 和缓冲管理。主要内容包括块设备 I/O 的原理和 I/O 缓冲的优势。讨论了 Unix 的缓冲管理算法及其缺点。接着使用信号量设计了一种新的缓冲管理算法,以提高 I/O 缓冲缓存的效率和性能。PV 算法的简单实现展示了其易于实施、具有良好的缓存效果,并且不会发生死锁和饥饿的特点。为了帮助读者更好地理解文件系统中的 I/O 操作和中断处理,提出了一个编程项目,要求读者在模拟系统中实现并比较缓冲管理算法的性能。这个项目有助于读者深入了解文件系统中的 I/O 操作和中断处理。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了