块设备I/O和缓冲区管理
I/O缓冲的基本原理
- 读取磁盘数据时,首先在缓冲区的缓存中搜索,若缓冲区存在且包含有效数据则直接从缓冲区读取。若缓冲区不存在则为该磁盘块分配一个缓冲区,将数据读入缓冲区,再从缓冲区读取数据。同时会将分配的缓冲区保存在缓冲区缓存中。
- 磁盘块被写入时,首先为该磁盘块分配缓冲区,并将数据写入该缓冲区,将缓冲区标记为脏。脏缓冲区可以满足对同一块的后续读/写请求,因此不会引起实际磁盘I/O。当脏缓冲区重新分配到不同块时才会被写入磁盘。
相关伪代码:
BUFFER *bread(dev,blk)
{
BUFFER *bp = getblk(dev,blk); //为磁盘块分配缓冲区
if (bp data valid) //如果该缓冲区包含有效数据
return bp; //则返回该缓冲区
bp->opcode = READ; //否则将数据读入该缓冲区
strat_io(bp);
wait for I/O completion;
return bp;
}
write_block(dev, blk, data) //对磁盘块进行写入
{
BUFFER *bp = bread(dev,blk);
write data to bp;
(synchronous write)? bwrite(bp) : dwrite(bp);
}
bwrite (BUFFER *bp) //同步写入
{
bp->opcode = WRITE;
start_io(bp);
wait for I/O completion;
brelse(bp);
}
dwrite(BUFFER *bp) //延迟写入
{
mark bp dirty for delay_write;
brelse(bp);
}
同步写入等待写操作完成,用于顺序快或者可移动块设备。
延迟写入即上文提到的脏缓冲区,只有脏缓冲区重新分配到不同的磁盘块才会被写入磁盘。
I/O队列,包含等待I/O操作的缓冲区。伪代码:
start_io(BUFFER *bp)
{
enter bp into device I/O queue;
if (bp is first buffer in I/O queue)
issue I/O command for bp to device;
}
Unix I/O缓冲区管理算法
Unix缓冲区管理子系统
-
I/O缓冲区:缓冲区结构体由用于缓冲区管理的缓冲头部分和用于数据块的数据部分组成。
typdef struct buf{
struct buf *next_free;
struct buf *next_dev;
int dev,blk;
int opcode;
int dirty;
int async;
int valid;
int busy;
int wanted;
struct semaphore lock=1;
struct semaphore iodone=0;
char buf[BLKSIZE];
}BUFFER;
BUFFER buf[NBUF], *freelist;
-
设备表:其中dev_list包含当前分配给该设备的I/O缓冲区。io_queue包含设备上等待I/O操作的缓冲区。
struct devtab{
u16 dev;
BUFFER *dev_list;
BUFFER *io_queue;
}devtab[NDEV];
-
缓冲区初始化:系统启动时,所有I/O缓冲区都在空闲列表中,所有设备列表和I/O队列均为空。
-
缓冲区列表:缓冲区分配给磁盘时,将被插入设备表的dev_list中。此时若缓冲区正在使用,处于繁忙,则将其从空闲列表删除,而繁忙的缓冲区也可能在设备表的io_queue中。当其不在繁忙时,会将其释放回空闲列表,仍保留在dev_list中。像前文中一样,当其重新分配时才可能从一个dev_list更改到另一个dev_list。
-
Unix getblk/brelse算法:
BUFFER *getblk(dev,blk){
while(1){
(1).search dev_list for a bp=(dev,blk); //为标识的磁盘块分配缓冲区
(2).if (bp in dev_list){ //若缓冲区在设备表的dev_list中
if (bp BUSY){ //若该缓冲区处于繁忙状态
set bp WANTED flag;
sleep(bp); //等待该缓冲区释放
continue; //重试该算法
}
take bp out of freelist; //若该缓冲区处于空闲状态,将缓冲区从空闲列表中删除
mark bp BUSY; //将该缓冲区标记为繁忙
reurn bp;
}
(3).
if (freelist empty){ //若没有缓冲区处于空闲状态
set bp WANTED flag;
sleep(freelist); //等待一个空闲状态的缓冲区
continue; //重试该算法
}
/*若存在空闲状态的缓冲区*/
(4).
bp =first bp taken out of freelist; //分配空闲列表最前面的缓冲区
mark bp BUSY; //将其标记为繁忙
if (bp DIRTY){ //若为延迟写入
awrite(bp);
continue;
}
(5).
reassign bp to (dev,blk); //重新分配时,将缓冲区数据写入磁盘
return bp;
}
}
brelse (BUFFER *bp){
if (bp WANTED){
wakeup(bp);
if (freelist WANTED)
wakeup(freelist);
clear bp and freelist WANTED flags;
insert bp to (tail of) freelist;
}
Unix算法说明
- 数据一致性:getblk()不能给一个同一个表示的磁盘块分配多个缓冲区,同时脏缓冲区在重新分配之前被写出来。
- 缓冲效果:缓冲区释放回空闲列表,仍保留在dev_list中;延迟写入的缓冲区不会立即引起实际磁盘I/O;释放的缓冲区在空闲列表末尾,分配是从空闲列表前面开始。
- 临界区:在getblk和brelse中,设备中断在临界区中会背屏蔽。
Unix算法缺点
- 效率低下:依赖于重试循环。
- 缓存效果不可预知
- 饥饿现象:每个进程都有尝试的机会,但不能保证成功。
- 只适用于单处理系统的休眠/唤醒操作
在信号量上使用PV来实现进程同步,将用缓冲区本身来表示每个缓冲区的信号量。
PV算法
BUFFER *getblk(dev,blk)
{
while(1){
(1). p(free); //首先获取一个空闲缓冲区
(2). if (bp in dev_list){ //若该缓冲区在设备表的dev_list中
(3). if (bp not BUSY){ //且处于空闲状态
remove from freelist; //将其从空闲列表中删除
P(bp); //lock bp not wait
return bp;
}
//若缓冲区存在缓存内且繁忙
V(free); //放弃空闲缓冲区
(4). P(bp); //在缓冲队列中等待
return bp;
}
//缓冲区不在缓存中,为磁盘创建一个缓冲区
(5). bp = first buffer taken out of freelist;
P(bp); //lock bp no wait
(6). if (bp dirty){ //若为脏缓冲区
awrite(bp); //缓冲区写入磁盘
continue;
}
(7). reassign bp to (dev,blk); //重新分配
return bp;
}
}
brelse (BUFFER *bp)
{
(8).if (bp queue has waiter) {V(bp); return; }
(9).if (bp dirty && freee queue has waiter){ awrite(bp); return;}
(10).enter bp into (tail of) freelist; V(bp); V(free);
}
- 缓冲区唯一性
- 无重试循环
- 无不必要唤醒
- 缓存效果
- 无死锁和饥饿
PV算法缺点
- 1.一旦没有空闲状态缓冲区,所有请求进程都将被阻塞在getblk()中的(1)。
- 2.当进程从空闲列表信号量队列中唤醒时,可能发现所需缓冲区已经存在,但处于繁忙,此时将在(4)处被阻塞。