by cszhao1980
理解了上述内容,下面的这些程序就不难理解了。
首先是函数brelse(buf bp),该函数将传入的缓存归还到AV队列中,函数采用尾插法,
即缓存会插到AV队列的队尾——这样做显然有助于提高“延迟写”技术的效率。莱昂
特别指出,brelse没有清理B_DONE标志,这一点非常重要,读完本章后大家就会明白。
接下来是binit()函数,完成初始化:
(1) 每个缓存都被放入到空闲设备队列(bfreelist b队列)中;(有趣的是,使用头插法)
(2) 每个缓存都通过调用brelse放入到AV队列中;
(3) 初始化每个设备的b list——都为空(仅有头结点)。
还有notavail()函数,其作用是将传入的缓存从AV队列中取下来,并为缓存设置B_BUSY标
志,表示该缓存已经被某设备占用,not available了。
clrbuf()函数就比较简单了,它将缓存区清0。
incore()有两个参数:设备号(adev)和块号(blkno),它会Loop该设备的任务队列(b队列),
看是否已经为此块分配了缓存(b_blkno == blkno)。
getblk(dev, block)相对比较复杂,它的作用是为指定设备的指定块分配一个缓存。
(1) 它首先检查该设备的b队列,看是否已经为此block分配过缓存,如果有,则检查B_BUSY标记;
i. 如置位,则设置B_WANTED标记后睡眠;醒来后,跳回程序开头;
ii. 否则,调用notavail占用此缓存(注意,如果B_BUSY未置位,则此缓存已经被归还到AV队列了);
(2) 如果没有这样的缓存,则需要直接从(bfreelist的)AV队列中分配。
如果该队列为空,则B_WANTED置位后睡眠;
(3) 否则,调用notavail(bfreelist->av_forw)占用AV队列的第一个缓存;
(4) 如该缓存的B_DELWRI置位(“延迟写”),则调用bwrite进行实际io,然后跳回程序开头;
【思考题】:该缓存被从AV队列中摘下来,谁负责归还呢?
(5) 如否,则将该缓存从原设备b队列中摘下来,放入新设备的b队列中。
getblk还有个简单的用法,即getblk(NODEV),NODEV定义为-1。getblk发现dev为负数时,就直接
从AV队列里取一个缓存下来(经过一摘一放后,它仍在bfreelist的b队列中)。由于此种情况下没有
使用block参数,故调用时,可省略第二个参数的输入。
下面看一下读写相关的函数,首先是bread(dev, block),它将指定设备的指定块读入缓存。
首先,它调用getblk来获取一个缓存;
然后,检查该缓存,如果B_DONE标记已经设置,则表示走了大运,无需再读磁盘了,直接return即可;
如果没这么走运,则需要启动设备进行实际的读操作,然后调用iowait等待操作完成。
【注】:记得末,brelse没有清理B_DONE标志。
【思考题】:这样的盘块从何而来?
令人疑惑的是,b_wcount的值是一个负数:-256,Why?因为这个值会被直接设置给这与RK设备的rkcs寄
存器,而该寄存器的内容是要传输word数的补码。系统的这种写法其实破坏了其一直努力实现的device
independent性,rkcs寄存器的特殊要求应该由更底层的函数来实现。
bwrite()进行真实io,将指定缓存的内容写入设备,对同步写,它会等待io结束,然后调用brelse()
将缓存归还AV队列。
bflush()函数大家都比较熟悉了,它会Loop指定设备的任务(b)队列,调用bwrite()函数将“延迟写”
的缓存写入物理设备。
现在,让我们看一下正常的读过程。根据前面的描述,其过程大致如下:
(1) 当进程要读取磁盘盘块时,会首先获取一个buffer,启动调用bread启动设备操作,
bread会调用iowait睡眠(以此buffer地址为原因);
(2) 设备操作完成后,会启动rkintr中断处理程序,该程序会唤醒等待的进程;
(3) 进程读取buffer的内容,然后调用brelse释放该buffer。
对于写操作,也有类似的过程,其根本特点是进程会等待io完成,然后再进行下面的工作。
因此,它们也称为同步io操作。
除了同步io之外,在读码过程中,我们还遇到了有关异步(B_ASYNC)的代码,它们是做什么的呢?
对于写操作,比较好理解。异步写操作用于“延迟写”技术——在真正写时,也会调用bwrite(),
该函数启动设备操作后,直接return即可,无需调用iowait等待。
读操作的情形稍微复杂一些,它用于“预读”。预读用以提高效率,如果我们能预测下一次要读取的盘块
的话,我们可以预先读取它,以提高效率。
breada()函数提供了预读的功能,相比bread它多指定了一个参数:read ahead块号,即要“提前/额外”读
入的块的块号。它首先会以同步方式启动“正选”块的读入,然后会以异步方式去启动设备读“提前”
块——这很容易理解,因为在启动读操作时,还没有进程等待此盘块,因此,无法使用同步方式。
rkintr在处理异步读入的盘块时,会直接调用brelse将其释放到AV队列(B_DONE仍设置),但在其被再
次分配之前,如果遇到相同盘块的读取请求,会直接返回该buffer,无需再次读入——参见对bread函数的描述。
在这里顺便谈一下unix v6的预读算法。在inode结构中有一个变量i_lastr,记录的是上一次读的盘块号,如
果本次读的盘块号正好为i_lastr+1,我们有理由相信,下次读操作有很大机会是读取i_lastr+2号盘块,因此,
就可以调用breada进行预读。
我们没有讨论所谓的原始(raw)输入输出函数physio——它的解析就留给读者吧。
博客地址:http://blog.youkuaiyun.com/cszhao1980
博客专栏地址:http://blog.youkuaiyun.com/column/details/lions-unix.html