原文转自:http://blog.chinaunix.net/uid-7471615-id-83762.html
“
UNIX内核(2):磁盘缓冲原理,缓冲分配、回收及用OO观点建模”对UNIX磁盘缓冲的分配回收做了大致的说明,并给出了一些代码范例。本文将对磁盘的读写以及使用磁盘缓冲的利弊进行一下简要说明。
读取磁盘块
为了读取一个磁盘块,进程需要调用getblk来获取缓冲。如果能在hash Q上找到该缓冲,那么内核就能够立刻获得数据。否则,内核将向磁盘驱动提交一个读请求并且休眠,直到数据就绪。
为了读取一个磁盘块,进程需要调用getblk来获取缓冲。如果能在hash Q上找到该缓冲,那么内核就能够立刻获得数据。否则,内核将向磁盘驱动提交一个读请求并且休眠,直到数据就绪。
读取数据块的伪代码如下:
BufferHeader * bread (int fs_blk_no)
{
getblk(fs_blk_no);
if (buffer的数据有效)
返回该buffer;
发起读磁盘请求;
sleep (读磁盘完成事件);
返回该buffer;
getblk(fs_blk_no);
if (buffer的数据有效)
返回该buffer;
发起读磁盘请求;
sleep (读磁盘完成事件);
返回该buffer;
}
Linux 0.99.15的实现如下:
struct buffer_head * bread(dev_t dev, int block, int size)
{
struct buffer_head * bh;
if (!(bh = getblk(dev, block, size))) { // 几乎不可能发生
printk("VFS: bread: READ error on device %d/%d\n",
MAJOR(dev), MINOR(dev));
return NULL;
}
if (bh->b_uptodate)
return bh;
ll_rw_block(READ, 1, &bh); // 发起一个读请求(该函数可以发起多个读请求)
wait_on_buffer(bh); // sleep
if (bh->b_uptodate)
return bh;
brelse(bh); // 找到不合适的buffer
return NULL;
}
Linux 0.99.15的实现如下:
struct buffer_head * bread(dev_t dev, int block, int size)
{
struct buffer_head * bh;
if (!(bh = getblk(dev, block, size))) { // 几乎不可能发生
printk("VFS: bread: READ error on device %d/%d\n",
MAJOR(dev), MINOR(dev));
return NULL;
}
if (bh->b_uptodate)
return bh;
ll_rw_block(READ, 1, &bh); // 发起一个读请求(该函数可以发起多个读请求)
wait_on_buffer(bh); // sleep
if (bh->b_uptodate)
return bh;
brelse(bh); // 找到不合适的buffer
return NULL;
}
磁盘驱动将向磁盘控制器发出读请求,控制器接受请求并开始与驱动传递数据。当数据传输完成,控制器发出一个中断来通知CPU数据就绪。该中断的处理例程将唤醒等待的进程。此时数据也处于可用状态。需要使用数据的进程便能够访问该缓冲中的数据。一旦使用完,进程便释放缓冲。
预读数据
为了提高性能,根据临近原则,可以预读下一个数据块。此时采用异步读,完成之后磁盘控制器发出一个中断,在处理中断过程中需要将包含有预读数据的buffer释放以供后续使用(如果不释放该buffer,那么就需要由发起异步读的进程来释放,而异步读的策略就是,发出读取请求后就不管了,因为请求者不需要该数据)。在异步读处理中,首先读取第一块数据(与bread()一样),然后读取第二块buffer。第一块数据的读取需要同步,而第二块数据(预读数据)为异步读取,由中断处理例程根据该读取请求为异步而直接释放该buffer。其伪代码如下:
BufferHeader * breada(int blk_no_immediate, int blk_no_asyn)
{
if (blk_no_immediate不在缓冲中)
{
getblk(blk_no_immediate);
if (buffer的数据无效)
发起读磁盘请求;
}
if (blk_no_asyn不在缓冲中)
{
getblk(blk_no_asyn);
if (buffer的数据无效)
发起读磁盘请求;
else
brelse(getblk()返回的buffer;
}
sleep (读blk_no_immediate磁盘块完成事件);
返回该buffer;
}
Linux 0.99.15的实现(省略一些“次要”代码):
struct buffer_head * breada(dev_t dev,int first, ...)
{
va_list args;
unsigned int blocksize;
struct buffer_head * bh, *tmp;
va_start(args,first);
……
if (!(bh = getblk(dev, first, blocksize))) { // 几乎不可能发生
printk("VFS: breada: READ error on device %d/%d\n",
MAJOR(dev), MINOR(dev));
return NULL;
}
if (!bh->b_uptodate)
ll_rw_block(READ, 1, &bh); // 发起一个读请求(该函数可以发起多个读请求)
while ((first=va_arg(args,int))>=0) { // 预读一个或多个块(视参数个数而定)
tmp = getblk(dev, first, blocksize);
if (tmp) {
if (!tmp->b_uptodate)
ll_rw_block(READA, 1, &tmp);
tmp->b_count--;
}
}
va_end(args);
wait_on_buffer(bh); // 等待读blk_no_immediate磁盘块完成事件
if (bh->b_uptodate)
return bh;
brelse(bh);
return (NULL);
}
由于预读操作完成后会由中断处理例程来释放buffer,这样,将导致freelist的不完整性。因此,在brelse()中,freelist不仅需要加锁保护,还需要屏蔽中断(此处仅仅需要屏蔽磁盘中断)。
写磁盘块
类似地,内核通知磁盘驱动要写一个buffer到磁盘上,磁盘驱动将安排一个I/O。如果是同步写,那么发出写请求的进程将进入睡眠状态直到操作完成。如果是异步写,那么发出请求的进程将不会等待。同样,该buffer将在中断处理例程中被释放。
需要注意的是delay-write和异步写的区别。delay-write表示该buffer被标识为延迟写,然后释放该buffer。等待该buffer被重新分配时才会请求磁盘驱动调度一个写操作。这样的话,如果另一个进程对该buffer又作了修改,就只有一个写操作,而不是同/异步写中的两个操作,这也达到了节约I/O资源的目的。
由于异步写操作同样需要由中断处理例程来释放缓冲,磁盘中断必须屏蔽掉。
写磁盘操作的伪代码如下:
void bwrite(BufferHeader * input)
{
发起写磁盘请求;
if (同步I/O)
{
sleep (I/O完成事件);
brelse (input);
}
else if (delay-write)
标记该buffer并将其放在freelist的首部;
}
在实际的实现中(如Linux0.99.15),需要考虑到定期同步buffer与磁盘等,因此实现都比较复杂,此处就不加以讨论。大家要是有兴趣可以自己研究下。
使用磁盘缓冲的利与弊
利:
预读数据
为了提高性能,根据临近原则,可以预读下一个数据块。此时采用异步读,完成之后磁盘控制器发出一个中断,在处理中断过程中需要将包含有预读数据的buffer释放以供后续使用(如果不释放该buffer,那么就需要由发起异步读的进程来释放,而异步读的策略就是,发出读取请求后就不管了,因为请求者不需要该数据)。在异步读处理中,首先读取第一块数据(与bread()一样),然后读取第二块buffer。第一块数据的读取需要同步,而第二块数据(预读数据)为异步读取,由中断处理例程根据该读取请求为异步而直接释放该buffer。其伪代码如下:
BufferHeader * breada(int blk_no_immediate, int blk_no_asyn)
{
if (blk_no_immediate不在缓冲中)
{
getblk(blk_no_immediate);
if (buffer的数据无效)
发起读磁盘请求;
}
if (blk_no_asyn不在缓冲中)
{
getblk(blk_no_asyn);
if (buffer的数据无效)
发起读磁盘请求;
else
brelse(getblk()返回的buffer;
}
sleep (读blk_no_immediate磁盘块完成事件);
返回该buffer;
}
Linux 0.99.15的实现(省略一些“次要”代码):
struct buffer_head * breada(dev_t dev,int first, ...)
{
va_list args;
unsigned int blocksize;
struct buffer_head * bh, *tmp;
va_start(args,first);
……
if (!(bh = getblk(dev, first, blocksize))) { // 几乎不可能发生
printk("VFS: breada: READ error on device %d/%d\n",
MAJOR(dev), MINOR(dev));
return NULL;
}
if (!bh->b_uptodate)
ll_rw_block(READ, 1, &bh); // 发起一个读请求(该函数可以发起多个读请求)
while ((first=va_arg(args,int))>=0) { // 预读一个或多个块(视参数个数而定)
tmp = getblk(dev, first, blocksize);
if (tmp) {
if (!tmp->b_uptodate)
ll_rw_block(READA, 1, &tmp);
tmp->b_count--;
}
}
va_end(args);
wait_on_buffer(bh); // 等待读blk_no_immediate磁盘块完成事件
if (bh->b_uptodate)
return bh;
brelse(bh);
return (NULL);
}
由于预读操作完成后会由中断处理例程来释放buffer,这样,将导致freelist的不完整性。因此,在brelse()中,freelist不仅需要加锁保护,还需要屏蔽中断(此处仅仅需要屏蔽磁盘中断)。
写磁盘块
类似地,内核通知磁盘驱动要写一个buffer到磁盘上,磁盘驱动将安排一个I/O。如果是同步写,那么发出写请求的进程将进入睡眠状态直到操作完成。如果是异步写,那么发出请求的进程将不会等待。同样,该buffer将在中断处理例程中被释放。
需要注意的是delay-write和异步写的区别。delay-write表示该buffer被标识为延迟写,然后释放该buffer。等待该buffer被重新分配时才会请求磁盘驱动调度一个写操作。这样的话,如果另一个进程对该buffer又作了修改,就只有一个写操作,而不是同/异步写中的两个操作,这也达到了节约I/O资源的目的。
由于异步写操作同样需要由中断处理例程来释放缓冲,磁盘中断必须屏蔽掉。
写磁盘操作的伪代码如下:
void bwrite(BufferHeader * input)
{
发起写磁盘请求;
if (同步I/O)
{
sleep (I/O完成事件);
brelse (input);
}
else if (delay-write)
标记该buffer并将其放在freelist的首部;
}
在实际的实现中(如Linux0.99.15),需要考虑到定期同步buffer与磁盘等,因此实现都比较复杂,此处就不加以讨论。大家要是有兴趣可以自己研究下。
使用磁盘缓冲的利与弊
利:
- 统一磁盘访问接口,使系统设计变得简单。
- 程序员无需考虑数据对齐。
- 减少磁盘访问量,从而减少拥堵,增加了系统的吞吐量并且减少了访问时间。(想象一下北京车少了,但是每辆车装的人多了会怎样,呵呵)
- 保证磁盘数据的完整性。
- 延迟写机制在系统崩溃时将导致数据错误。
- 无法确定数据在何时会真正写到磁盘上(甚至fflush()都无法保证)。
- 额外的数据拷贝(用户进程<-->内核<-->磁盘)将导致大数据量时性能下降。
参考:
The Design of The UNIX Operation System, by Maurice J. Bach
Linux Kernel Source Code v0.99.15, by Linus Torvalds
Copyleft (C) raof01.
本文可以用于除商业外的所有用途。此处“用途”包括(但不限于)拷贝/翻译(部分或全部),不包括根据本文描述来产生代码及思想。若用于非商业,请保留此权利声明,并标明文章原始地址和作者信息;若要用于商业,请与作者联系(raof01@gmail.com),否则作者将使用法律来保证权利。
本文可以用于除商业外的所有用途。此处“用途”包括(但不限于)拷贝/翻译(部分或全部),不包括根据本文描述来产生代码及思想。若用于非商业,请保留此权利声明,并标明文章原始地址和作者信息;若要用于商业,请与作者联系(raof01@gmail.com),否则作者将使用法律来保证权利。