知识点总结
本章讨论了块设备I/O和缓冲区管理;解释了块设备I/O的原理和I/O缓冲的优点;论 述了 Unix的缓冲区管理算法,并指出了其不足之处;还利用信号量设计了新的缓冲区管理 算法,以提高I/O缓冲区的缓存效率和性能;表明了简单的PV算法易于实现,缓存效果好, 不存在死锁和饥饿问题;还提出了一个比较Unix缓冲区管理算法和PV算法性能的编程方 案。编程项目还可以帮助读者更好地理解文件系统中的I/O操作。
在第上一章中,学习了读写普通文件的算法。这些算法依赖于两个关键操作,即 get_block和put_block,这两个操作将磁盘块读写到内存缓冲区中 由于与内存访问相比, 磁盘I/O速度较慢,所以不希望在每次执行读写文件操作时都执行磁盘1/0因此,大多数 文件系统使用I/O缓冲来减少进出存储设备的物理1/0数量一合理设计的I/O缓冲方案可显 著提高文件I/O效率并增加系统吞吐量。
I/O缓冲的基本原理非常简单。文件系统使用一系列I/O缓冲区作为块设备的缓存内存。 当进程试图读取(dev, blk)标识的磁盘块时,它首先在缓冲区缓存中搜索分配给磁盘块的缓 冲区。如果该缓冲区存在并且包含有效数据,那么它只需从缓冲区中读取数据,而无须再次 从磁盘中读取数据块=如果该缓冲区不存在,它会为磁盘块分配一个缓冲区,将数据从磁盘 读人缓冲区,然后从缓冲区读取数据,当某个块被读入时,该缓冲区将被保存在缓冲区缓存 中,以供任意进程对同一个块的下一次读/写请求使用:同样.当进程写入磁盘块时,它首 先会获取-个分配给该块的缓冲区然后,它将数据写入缓冲区,将缓冲区标记为脏,以延 退写入,并将其释放到缓冲区缓存中 由于脏缓冲区包含有效的数据,因此可以使用它来满 足对同一块的后续读/写清求,而不会引起实际磁盘I/O,脏缓冲区只有在被重新分配到不 同的块时才会写入磁盘。
在讨论緩冲区管理算法之前.我们先来介绍以F术语。在read flle/write file中,我们 假设它们从内存中的一个专用缓冲区进行读/写“对于1/0缓冲’将从缓冲区缓存中动态分 配缓冲区。假设BUFFER是缓冲区(见卜文定义)的结构类型,而且getblk(dev, blk)从缓冲 区缓存中分配一个指定给(dev, blk)的缓冲区」定义一个breadCdev, blk)函数,它会返回一 个包含有效数据的缓冲区(指针)。
同步写入操作等待写操作完成。它用于顺序块或可移动块设备,如USB驱动器。对于 随机访问设备,例如硬盘,所有的写操作都是延迟写操作。在延迟写操作中,dwrite(bp)将 缓冲区标记为脏,并将其释放到缓冲区缓存中。由于脏缓冲区包含有效数据,因此可用来满 足同一个块的后续读/写请求。这不仅减少了物理磁盘I/O的数量,而且提高了缓冲区缓存 的效果。脏缓冲区只有在被重新分配到不同的磁盘块时才会被写入磁盘,此时缓冲区将被以 下代码写入:
bp->opcode = ASYNC; start_io(bp);
awrite()会调用start_io()在缓冲区开始I/O操作,但是不会等待操作完成。当异步(ASYNC) 写操作完成后,磁盘中断处理程序将释放缓冲区。
物理块设备I/O :每个设备都有一个I/O队列,其中包含等待I/O操作的缓冲区。缓冲 区上的start_io()操作如下:
enter bp into device I/O queue; if (bp is first buffer in I/O queue) issue I/O command for bp to device;
当I/O操作完成后,设备中断处理程序会完成当前缓冲区上的I/O操作,并启动I/O队 列中的下一个缓冲区的I/O (如果队列不为空)。设备中断处理程序的算法如下:
IntermiptHandler () ( bp = dequeue(device I/O queue); // bp = remove head of I/O queue (bp->opcode == ASYNC)? brelse(bp) : unblock process on bp; if (1 empty(device I/O queue)) issue I/O command for first bp in I/O queue;
Unix I/0缓冲区管理算法
Unix I/O缓冲区管理算法最早出现在第6版Unix中(Ritchie和Thompson 1978 ; Lion 1996 )o Bach著作的第3章对该算法进行了详细论述(Bach 1990 )o Unix缓冲区管理子系 统由以下几部分组成。
(1) 1/0缓冲区:内核中的一系列NBUF缓冲区用作缓冲区缓存。每个缓冲区用一个结 构体表示。 缓冲区结构体由两部分组成:用于缓冲区管理的缓冲头部分和用于数据块的数据部分。 为了保护内核内存,状态字段可以定义为一个位向量,其中每个位表示一个唯一的状态条 件。为了便于讨论,这里将它们定义为int。 (2) 设备表:每个块设备用一个设备表结构表示。 每个设备表都有一个devjist,包含当前分配给该设备的I/O缓冲区,还有一个io_ queue,包含设备上等待I/O操作的缓冲区。1/。队列的组织方式应确保最佳I/O操作。例如. 它可以实现各种磁盘调度算法,如电梯算法或线性扫描算法等。为了简单起见,Unix使用 FIFO I/O 队列。 (3) 缓冲区初始化:当系统启动时,所有I/O缓冲区都在空闲列表中,所有设备列表和 I/O队列均为空。 (4) 缓冲区列表:当缓冲区分配给(dev, blk)时,它会被插入设备表的devjist中。如 果缓冲区当前正在使用,则会将其标记为BUSY (繁忙)并从空闲列表中删除。繁忙缓冲区
加执行时间 此外,研究(Wang 2002 )表明.散列对缓冲区缓存性能几乎没有影响,实际 上,我们可以通过简单的散列函数hash(dev, blk) = dev将设备列表视为散列队列一这样,将 设备列表用作散列队列不会损失通用性:Unix算法非常简单,易于理解.也许是因为它太 过简单.大多数人第•眼看•到它时并没有产生深刻的印象。由于不断的重试循环,有些人甚 至认为它很不成熟。但是,你越研究它.就会发现它越有意义 这一异常简单而有效的算法 证明了 Unix最初设计者的独创性。下面是关于Unix算法的一些具体说明,
(1 )数据一致性:为「确保数据一致性,getblk一定不能给同一个(dev, blk)分配多个 缓冲区 这可以通过让进程从休眠状态唤醒后再次执行“重试循环”来实现。读者可以验证 分配的每个缓冲区都是唯一的一其次,脏缓冲区在重新分配之前被写出来,这保证了数据的 一致性。 (2) 缓存效果:缓存效果可通过以下方法实现 释放的緩冲区保留在设备列表中,以便 可能重用,标记为延迟写入的緩冲区不会立即产生I/O,并且可以重用。缓冲区会被释放到 空闲列表的末尾,但分配是从空闲列表的前面开始的,这是基于LRU (最近最少使用)原则, 它有助于延长所分配缓冲区的使用期,从而提高它们的缓存效果, (3) 临界区:设备中断处理程序可操作缓冲区列表,例如从设备表的I/O队列中删除 bp,更改其状态并调用brelse(bp)。所以,在getb汰和brelse中,设备中断在这些临界区中 会被屏蔽。这些都是隐含的,但没有在算法中表现出来
Unix算法的缺点
虽然Unix算法非常简单和简洁.但它也有以下缺点。
(1 )效率低下:该算法依赖于重试循环,例如,释放缓冲区可能会唤醒两组进程:需要 释放的缓冲区的进程,以及只需要空闲缓冲区的进程。由于只有一个进程可以获取释放的缓 冲区,所以,其他所有被唤醒的进程必须重新进入休眠状态。从休眠状态唤醒后,每个被唤 醒的进程必须从头开始重新执行算法,因为所需的缓冲区可能已经存在。这会导致过多的进 程切换。
(2) 缓存效果不可预知:在Unix算法中,每个释放的缓冲区都可被获取'如果缓冲区 由需要空闲缓冲区的进程获取,那么将会重新分配缓冲区,即使有些进程仍然需要当前的缓 冲区。
(3) 可能会出现饥饿:Unix算法基于“*经济”原则,即每个进程都有尝试的机会, 但不能保证成功,因此,可能会出现进程饥饿
(4) 该算法使用只适用丁单处理器系统的休眠/唤醒操作
12.3新的I/O缓冲区管理算法
在本节中,我们将展示一种用于1/0缓冲区管理的新算法。我们将在信号址上使用p/v 来实现进程同步,而不是使用休眠/唤醒,与休眠/唤醒相比,信号量的主要优点是:
(1) 计数信号量可用来表示可用资源的数量,例如:空闲缓冲区的数量。
(2) 当多个进程等待一个资源时,信号量上的V操作只会释放一个等待进程,该进程 不必重试,因为它保证拥有资源。
这些信号量属性可用于设计更有效的缓冲区管理算法我们正式对这个问题做以下详细 说明。
使用信号量的缓冲区管理算法
假设有一个单处理器内核(一次运行一个进程)。使用计数信号量上的P/V来设计满足 以下要求的新的缓冲区管理算法:
(1) 保证数据一致性。
(2) 良好的缓存效果。
(3) 高效率:没有重试循环,没有不必要的进程“唤醒”。
(4) 无死锁和饥饿。
注意,仅通过信号量上的P/V来替换Unix算法中的休眠/唤醒并不可取,因为这样会 保留所有的重试循环。我们必须重新设计算法来满足所有上述要求,并证明新算法的确优于 Unix算法。首先,我们定义以下信号量。
BUFFER buf[NBUF]; // NBUF I/O buffers
SEMAPHORE free = NBUF; // counting semaphore for FREE buffers
SEMAPHORE buf[i].sem = 1; // each buffer has a lock sem=l;
为了简化符号,我们将用缓冲区本身来表示每个缓冲区的信号量。与Unix算法一样, 最开始,所有缓冲区都在空闲列表中,所有设备列表和I/O队列均为空。下面展示一个使用 信号量的简单缓冲区管理算法。
PV算法
(1) 缓冲区唯一性:在getblk()中,如果有空闲缓冲区,则进程不会在(1 )处等待, 而是会搜索dev_listc如果所需的缓冲区已经存在,则进程不会重新创建同一个缓冲区*如 果所需的缓冲区不存在,则进程会使用一个空闲缓冲区来创建所需的缓冲区.而这个空闲缓 冲区保证是存在的,如果没有空闲缓冲区,则需要同一个缓冲区的几个进程可能在(1 )处 阻塞。当在(10)处释放出一个空闲缓冲区时,它仅释放一个进程来创建所需的缓冲区C 一 旦创建了缓冲区,它就会存在于dev_list中,这将防止其他进程再次创建同一个缓冲区。因 此,分配的每个缓冲区都是唯一的。
(2) 无重试循环:进程重新执行while(l)循环的唯一位置是在(6)处,但这不是重试, 因为进程正在不断地执行。
(3) 无不必要唤醒:在getblk ()中,进程可以在(1 )处等待空闲缓冲区,也可以在(4) 处等待所需的缓冲区,在任意一种情况下,在有缓冲区之前,都不会唤醒进程重新运行。此 外,当在(9)处有一个脏缓冲区即将被释放并且在(1)处有多个进程等待空闲缓冲区时, 该缓冲区不会被释放而是直接被写入。这样可以避免不必要的进程唤醒“
(4) 缓存效果:在Unix算法中,每个释放的缓冲区都可被获取。而在新的算法中,始 终保留含等待程序的缓冲区以供重用一只有缓冲区不含等待程序时,才会被释放为空闲。这 样可以提高缓冲区的缓存效果,
(5) 无死锁和饥饿:在getblk ()中、信号量锁定顺序始终是单向的,即P(free),然后是 P(bp),但决不会反过来,因此不会发生死锁:如果没有空闲缓冲区,所有清求进程都将在 (1)处阻塞,这意味着,虽然有进程在等待空闲缓冲区,但所有正在使用的缓冲区都不能接 纳任何新用户::这保证了繁忙缓冲区最终将被释放为空闲缓冲区■因此,不会发生空闲缓冲 区饥饿的情况:
虽然我们已经证明了新算法是正确的.但是它是否能比Unix算法表现得更好仍是一个 有待验证的问题,只能通过真实的定量数据来回答。因此,我们设计了 个编程项目来比较 缓冲区管理算法的性能。编程项目还可以帮助读者更好地理解文件系统中的I/O操作
编程项目:I/o缓冲区管理算法比较
该编程项目将会实现-个模拟系统.以比较Unix I/O缓冲区管理算法和使用信号量的 PV算法的性能:虽然当前的目标是I/。缓冲区性能,但真iE的目标是让读者理解文件系统 的I/O操作。下面介绍该项目,
系统组织
Box#1 :用户界面这是模拟系统的用户界面部分它会提示输入命令、显示命令执 行、显示系统状态和执行结果等在开发过程中,读者可以手动输入命令来执行任务:在最 后测试.过程中.任务应该有自己的输入命令序列.例虬 各任务可以使取包含命令的输入文
件。完成任务切换之后,将从另一个文件读取任务输入。
多任务处理系统
Box#2这是多任务处理系统的CPU端,模拟单处理器(单CPU)文件系统的内核模 式。实际上,它与第4章中所述的用于用户级线程的多任务系统相同,只是以下修改除外。 当系统启动时,它会创建并运行一个优先级最低的主任务,但它会创建ntask匸作任务,所 有任务的优先级都是1,并将它们输入readyQueue。然后,主任务执行以下代码,该代码将 任务切换为从readyQueue运行工作任务。由于主任务的优先级最低,所以如果没有可运行的任务或所有任务都已结束,它将再次 运行。在后一种情况下,主任务执行end_task(),在其中收集并显示模拟结果,然后终止, 从而结束模拟运行。
所有工作任务都执行同一个body()函数,其中每个任务从输入文件中读取命令来执行 读或写磁盘块操作,直到命令文件结束。
#define CMDLEN 10 int cmdfile[NTASK]; // opened command file descriptors int task = ntask; // number of active tasks int body() { int dev, blk; char opcode, cmd[CMDLEN]; whiled) ( if (read(cmdfile[running->pid], cmd, CMDLEN)==0)( running->status = DEAD; // task ends task--; // dec task count by 1 tswtich(); } sscanf (cmd,、'%c%4d%5d", &opcode, &dev, &blk); if (opcode=«zrz) // read (dev, blk) readBlk(dev, blk); if (opcode== *wx) writeBlk(dev, blk); // write (dev, blk)各任务的命令由rand()函数取模极限值来随机生成。每个命令都是三重形式
[r XXX yyyy] or [w xxx yyyy] , where xxx=dev, yyyy=blkno;
对于[r dev blk]命令,任务调用
BUFFER *bread(dev, blk)
来获取包含有效数据的缓冲区(指针)。从缓冲区读取数据后,它会将缓冲区释放回缓冲区 缓存以便重用。在bread()中,任务可以等待getblk()中的缓冲区,或者等待缓冲区数据生 效。如果是这样,它会休眠(在Unix算法中)或者被阻塞(在PV算法中),将任务切换到 运行readyQueue中的下一个任务。
对于[w dev blk]命令,任务试图获取用于(dev, blk)的缓冲区。如果必须要等待 getblk()中的缓冲区,它将会休眠或被阻塞,并切换任务以运行下一个任务。在将数据写入 缓冲区之后,它会将延迟写入的缓冲区标记为脏,并将该缓冲区释放到缓冲区缓存中。然 后,它会试图执行下一个命令等。
缓冲区管理器
缓冲区管理器实现各缓冲区管理函数,包括:
BUFFER *bread(dev, blk) dwrite(BUFFER *bp) awrite(BUFFER *bp) BUFFER *getblk(dev, blk) brelse(BUFFER *bp)这些函数可能会调用磁盘驱动程序来发出物理磁盘I/Oo
磁盘驱动程序
磁盘驱动程序由两部分组成。
(1) start_io():维护设备I/O队列,并对I/O队列中的缓冲区执行I/O操作。
(2) 中断处理程序:在每次I/O操作结束时,磁盘控制器会中断CPU”当接收到中断 后,中断处理程序首先从IntStatus中读取中断状态,IntStatus包含:
[dev | R|W I StatusCode] |< e.g. 10 chars >|
其中dev会标识设备。中断处理程序从设备I/O队列的头部删除缓冲区,并为缓冲区处理 中断。对于磁盘读取中断.中断处理程序会先将数据块从DataJn移动到缓冲区的数据区域. 然后,它将缓冲区数据标记为有效,并唤醒或解除对正在缓冲区上等待有效数据的任务的 阻塞。被唤醒的进程将会从缓冲区读取数据并将缓冲区释放回缓冲区缓存。对于磁盘写入 中断,没有进程在缓冲区中等待I/O完成。此类异步写入缓冲区由中断处理程序处理,它会 关闭缓冲区的异步写入标记并将缓冲区释放到缓冲区缓存中一然后,它会检査设备I/O队 列°如果I/O队列为非空,它会对I/O队列中的第一个缓冲区发岀I/O命令。在发出磁盘写 入操作之前,它会将缓冲区的数据复制到DataOut以便磁盘控制器获取。最后,它会写一个 ACK到IntAck来确认当前中断,允许磁盘控制器继续控制。当中断处理程序处理完当前中 断后,会从最后一个中断点开始恢复正常任务执行。
12.5.5磁盘控制器
(3) Box#3:这是磁盘控制器,它是主进程的一个子进程。因此,它与CPU端独立运行, 除了它们之间的通信通道,通信通道是CPU和磁盘控制器之间的接口。通信通道由主进程 和子进程之间的管道实现。
•命令:从CPU到磁盘控制器的I/O命令。
DataOut:在写操作中从CPU到磁盘控制器的数据输出。 Dataln:在读操作中从磁盘控制器到CPU的数据。 IntStatus:从磁盘控制器到CPU的中断状态。 IntAck:从CPU到磁盘控制器的中断确认。
磁盘中断
从磁盘控制器到CPU的中断由SIGUSR1 (#10)信号实现。在每次I/O操作结束时, 磁盘控制器会发出kill(ppid, SIGUSR1)系统调用,向父进程发送SIGUSR1信号,充当虚拟 CPU中断。通常,虚拟CPU会在临界区屏蔽出/入磁盘中断(信号)。为防止竞态条件,磁 盘控制器必须要从CPU接收一个中断确认,才能再次中断。
虚拟磁盘
(4) Box#4 :这些是Linux文件模拟的虚拟磁盘。使用Linux系统调用Iseek()、read() 和write。,我们可以支持虚拟磁盘上的任何块I/O操作。为了简单起见,将磁盘块大小设置 为16字节。由于数据内容无关紧要,所以可以将它们设置为16个字符的固定序列。
项目要求
实现两个版本的缓冲区管理算法:
•使用休眠/唤醒的Unix算法。 •使用信号量的新算法。
比较两种算法在不同任务/缓冲区比率下的缓冲区缓存命中率、实际I/O操作数量、任 务切换、重试和总运行时间等方面的性能。
12.5.9基本代码示例
为了简单起见,我们只列出了 CPU端的main.c文件和磁盘控制器的controller.c文件。 仅对其他所含文件的函数做了简要解释。而且,并未说明用于收集性能统计信息的代码段, 但是可以把它们轻松添加到基本代码中。
/**********•* CPU side: main.c file ********** #include #include #include #include #include #include <stdio.h> <stdlib.h> <sys/time.h> 〈signal.h> <string.h> "main.h" struct itimerval itimer; struct timeval tvO, tvl, sigset_t sigmask; int stack[NTASK][SSIZE]; int pd[5][2]; int int int int int int int int int cmdfile[NTASK]; record[NTASK+2][13]; task; ntask; nbuffer; ndevice; nblock; sample; timer_switch; tv2; char cmd[CMLEN]; char buffer[BLOCK]; char dev_stat[CMLEN]; PROC proc[NTASK], MainProc; PROC *running; PROC *readyQ, *freeQ, *waitQ; FILE *fp; int body(); void catcher(); // n // // // // // // // // // // // // // // // // // // // // constants, PROC stiruct pipes for timer interrupt count running time signal mask for critical regions task stacks 5 pipes for communication task command file descriptors record each task use buff status count alive task number number number number number of of of of of tasks buffers devices blocks per device per task inputs count time switch coiranand=[r|w dev blkno] save command to commuicate with device device status NTASK tasks and main task pointer to current running task process link lists record simulation results file task body function signal catcher (interrupt handler) #include #include ♦include #include nproc.cn "buf-c" "utility.c" "manager.cn // // // // PROC init, queue functions link lists buffer struct, generate cmds, end_task() getblk,brelse, bread,dwrite,awrite int main(int argc, char *argv[]) if(argc != 6)( printf("usage: a.out ntask nbuffer ndevice nblock nsample\n"); exit(0);
实践操作
运行代码:
#include <stdio.h> #include <errno.h> #include <stdlib.h> int main (){ FILE* fd; fd=fopen("/src/hello","r"); if(NULL==fd){ perror("cannot open20191212 file"); return -1; } return 0; }
结果如下:
遇到的问题:
问题:在3级文件系统中,挂载是什么
解决:通过查找博客园、csdn,知乎等平台,总结出以下内容:
挂载,指的就是将设备文件中的*目录连接到 Linux 根目录下的某一目录(最好是空目录),访问此目录就等同于访问设备文件。
并不是根目录下任何一个目录都可以作为挂载点,由于挂载操作会使得原有目录中文件被隐藏,因此根目录以及系统原有目录都不要作为挂载点,会造成系统异常甚至崩溃,挂载点最好是新建的空目录。