转载自:
Linux 文件系统之高速缓冲区 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/438163516
高速缓冲区是文件系统访问块设备中数据的必经要道。
高速缓冲区在块设备与内核其他程序之间起着一个桥梁作用。除了块设备驱动程序 以外,内核程序如果需要访问块设备中的数据,就都需要经过高速缓冲区来间接地操作。
为了访问块设备上文件系统中的数据,内核可以每次都访问块设备,进行读或写操作。但是每次 I/O 操作的时间与内存和 CPU 的处理速度相比是非常慢的。为了提高系统的性能,内核就在内存中开辟了一个高速数据缓冲区(池)(buffer cache),并将其划分成一个个与磁盘数据块大小相等的缓冲块来使用和管理,以期减少访问块设备的次数。
在 linux内核中,高速缓冲区位于内核代码和主内存区之间,如图所示
高速缓冲中存放着最近被使用过的各个块设备中的数据块。
当需要从块设备中读取数据时,缓冲区管理程序首先会在高速缓冲中寻找。如果相应数据已经在缓冲中,就无需再从块设备上读。如果数据不在高速缓冲中,就发出读块设备的命令,将数据读到高速缓冲中。
当需要把数据写到块设备中时,系统就会在高速缓冲区中申请一块空闲的缓冲块来临时存放这些数据。至于什么时候把数据真正地写到设备中去,则是通过设备数据同步实现的。
Linux 内核实现高速缓冲区的程序是 buffer.c。文件系统中其他程序通过指定需要访问的设备号和数据逻辑块号来调用它的块读写函数。这些接口函数有:块读取函数 bread()、块提前预读函数 breada()和页块读取函数 bread_page()。页块读取函数一次读取一页内存所能容纳的缓冲块数(4 块)。
高速缓冲区初始化
整个高速缓冲区被划分成 1024 字节大小的缓冲块,这正好与块设备上的磁盘逻辑块大小相同。
高速缓冲采用 hash 表和包含所有缓冲块的链表进行操作管理。在缓冲区初始化过程中,初始化程序从整个缓冲区的两端开始,分别同时设置缓冲块头结构和划分出对应的缓冲块,见图所示。
缓冲区的高端被划分成一个个 1024 字节的缓冲块,低端则分别建立起对应各缓冲块的缓冲头结构 buffer_head。该头结构用于描述对应缓冲块的属性,并且用于把所有缓冲头连接成链表。划分操作一直持续到缓冲区中没有足够的内存再划分出缓冲块为止。
高速缓冲区结构和链表
所有缓冲块的 buffer_head 被链接成一个双向链表结构,称为空闲链表,如图所示。
图中 free_list指针是该链表的头指针,指向空闲块链表中第一个“最为空闲的”缓冲块,即近期最少使用的缓冲块。而该缓冲块的反向指针 b_prev_free 则指向缓冲块链表中最后一个缓冲块,即最近刚使用的缓冲块。
图中缓冲头结构中“其他字段”包括块设备号、缓冲数据的逻辑块号,这两个字段唯一确定了缓冲
块中数据对应的块设备和数据块。另外还有几个状态标志:数据有效(更新)标志、修改标志、数据被使用的进程数和本缓冲块是否上锁标志。
内核程序在使用高速缓冲区中的缓冲块时,是指定设备号(dev)和所要访问设备数据的逻辑块号
(block),通过调用缓冲块读取函数 bread()、bread_page()或 breada()进行操作。这几个函数都使用缓冲区搜索管理函数 getblk(),用于在所有缓冲块中寻找匹配或最为空闲的缓冲块。该函数将在下面重点说明。在系统释放缓冲块时,需要调用 brelse()函数。所有这些缓冲块数据存取和管理函数的调用层次关系可用下图来描述。
高速缓冲区的 Hash 表
为了能够快速而有效地在缓冲区中寻找判断出请求的数据块是否已经被读入到缓冲区中,buffer.c 程序使用了具有 307 个 buffer_head 指针项的 hash(散列、杂凑)数组表结构。Hash 表所使用的散列函数由设备号和逻辑块号组合而成。程序中使用的具体 hash 函数是:(设备号^逻辑块号) Mod 307。图中指针 b_prev、b_next 就是用于 hash 表中散列在同一项上多个缓冲块之间的双向链接,即把 hash 函数计算出的具有相同散列值的缓冲块链接在散列数组同一项链表上。对于动态变化的 hash 表结构某一时刻的状态可参见下图所示。
其中,双箭头横线表示散列在同一 hash 表项中缓冲块头结构之间的双向链接指针。虚线表示缓冲区中所有缓冲块组成的一个双向循环链表(即所谓空闲链表),而 free_list 是该链表最为空闲缓冲块处的头指针。实际上这个双向链表是一个最近最少使用 LRU(Least Recently Used)链表。
缓冲块搜索函数
上面提及的三个函数在执行时都调用了 getblk(),以获取适合的空闲缓冲块。该函数会首先调用
get_hash_table()函数,在 hash 表队列中搜索指定设备号和逻辑块号的缓冲块是否已经存在。如果指定的缓冲块存在就立刻返回对应缓冲头结构的指针;如果不存在,则从空闲链表头开始,对空闲链表进行扫描,寻找一个空闲缓冲块。在寻找过程中还要对找到的空闲缓冲块作比较,根据赋予修改标志和锁定标志组合而成的权值,比较哪个空闲块最适合。若找到的空闲块既没有被修改也没有被锁定,就不用继续寻找了。若此时没有找到空闲块,则让当前进程进入睡眠状态,待继续执行时再次寻找。若该空闲块被锁定,则进程也需进入睡眠,等待驱动程序对其解锁。若在睡眠等待的过程中,该缓冲块又被其他进程占用,那么只要再重头开始搜索缓冲块。否则判断该缓冲块是否已被修改过,若是,则将该块写盘,并等待该块解锁。此时如果该缓冲块又被别的进程占用,那么又一次全功尽弃,只好再重头开始执行 getblk()。
在经历了以上折腾后,此时有可能出现另外一个意外情况,也就是在我们睡眠时,可能其他进程已
经将我们所需要的缓冲块加进了 hash 队列中,因此这里需要最后一次搜索一下 hash 队列。如果真的在hash 队列中找到了我们所需要的缓冲块,那么我们又得对找到的缓冲块进行以上判断处理,因此,又一次地我们需要重头开始执行 getblk()。
最后,我们才算找到了一块没有被进程使用、没有被上锁,而且是干净(修改标志未置位)的空闲缓冲块。于是我们就将该块的引用次数置 1,并复位其他几个标志,然后从空闲表中移出该块的缓冲头结构。在设置了该缓冲块所属的设备号和相应的逻辑号后,再将其插入 hash 表对应表项首部并链接到空闲队列的末尾处。由于搜索空闲块是从空闲队列头开始的,因此这种先从空闲队列中移出并使用最近不常用的缓冲块,然后再重新插入到空闲队列尾部的操作也就实现了最近最少使用 LRU 算法。最终,返回该缓冲块头的指针。
从上述分析可以可知,函数在每次获取新的空闲缓冲块时,就会把它移到 free_list 头指针所指链表
的最后面,即越靠近链表末端的缓冲块被使用的时间就越近。因此如果 hash 表中没有找到对应缓冲块,就会在搜索新空闲缓冲块时从 free_list 链表头处开始搜索。可以看出,内核取得缓冲块的算法使用了以下策略:
- 如果指定的缓冲块存在于 hash 表中,则说明已经得到可用缓冲块,于是直接返回;
- 否则就需要在链表中从 free_list 头指针处开始搜索,即从最近最少使用的缓冲块处开始。
因此最理想的情况是找到一个完全空闲的缓冲块,即 b_dirt 和 b_lock 标志均为 0 的缓冲块;但是如果不能满足这两个条件,那么就需要根据 b_dirt 和 b_lock 标志计算出一个值。因为设备操作通常很耗时,所以在计算时需加大 b_dirt 的权重。然后我们在计算结果值最小的缓冲块上等待(如果缓冲块已经上锁)。最后当标志 b_lock 为 0 时,表示所等待的缓冲块原内容已经写到块设备上。于是 getblk()就获得了一块空闲缓冲块。
缓冲块读取函数
由以上处理我们可以看到,getblk()返回的缓冲块可能是一个新的空闲块,也可能正好是含有我们需要数据的缓冲块,它已经存在于高速缓冲区中。因此对于读取数据块操作(bread()),此时就要判断该缓冲块的更新标志,看看所含数据是否有效,如果有效就可以直接将该数据块返回给申请的程序。否则就需要调用设备的低层块读写函数(ll_rw_block()),并同时让自己进入睡眠状态,等待数据被读入缓冲块。在醒来后再判断数据是否有效了。如果有效,就可将此数据返给申请的程序,否则说明对设备的读操作失败了,没有取到数据。于是,释放该缓冲块,并返回 NULL 值。
当程序不再需要使用一个缓冲块中的数据时,就可调用 brelse()函数,以释放该缓冲块并唤醒因等待该缓冲块而进入睡眠状态的进程。注意,空闲缓冲块链表中的缓冲块,并不是都是空闲的。因此只有当被写盘刷新、解锁且没有其他进程引用时(引用计数=0),才能挪作它用。
高速缓冲区访问过程和同步操作
综上所述,高速缓冲区在提高对块设备的访问效率和增加数据共享方面起着重要的作用。除驱动程
序以外,内核其他上层程序对块设备的读写操作都需要经过高速缓冲区管理程序来间接地实现。它们之间的主要联系是通过高速缓冲区管理程序中的 bread()函数和块设备底层接口函数 ll_rw_block()来实现。
上层程序若要访问块设备数据就通过 bread()向缓冲区管理程序申请。如果所需的数据已经在高速缓冲区中,管理程序就会将数据直接返回给程序。如果所需的数据暂时还不在缓冲区中,则管理程序会通过ll_rw_block()向块设备驱动程序申请,同时让程序对应的进程睡眠等待。等到块设备驱动程序把指定的数据放入高速缓冲区后,管理程序才会返回给上层程序。见下图所示。
对于更新和同步(Synchronization)操作,其主要作用是让内存中的一些缓冲块内容与磁盘等块设
备上的信息一致。sync_inodes()的主要作用是把 i 节点表 inode_table 中的 i 节点信息与磁盘上的一致起来。但需要经过系统高速缓冲区这一中间环节。
实际上,任何同步操作都被分成了两个阶段:
- 数据结构信息与高速缓冲区中的缓冲块同步问题,由驱动程序独立负责;
- 高速缓冲区中数据块与磁盘对应块的同步问题,由这里的缓冲管理程序负责。
sync_inodes()函数不会直接与磁盘打交道,它只能前进到缓冲区这一步,即只负责与缓冲区中的信息同步。剩下的操作需要缓冲管理程序负责。为了让 sync_inodes()知道哪些 i 节点与磁盘上的不同,就必须首先让缓冲区中内容与磁盘上的内容一致。这样 sync_inodes()通过与当前磁盘在缓冲区中的最新数据比较才能知道哪些磁盘inode需要修改和更新。最后再进行高速缓冲区与磁盘设备的第二次同步操作,做到内存中的数据与块设备中的数据真正的同步。
代码注释:
/*
* linux/fs/buffer.c
*
* (C) 1991 Linus Torvalds
*/
/*
* 'buffer.c' implements the buffer-cache functions. Race-conditions have
* been avoided by NEVER letting a interrupt change a buffer (except for the
* data, of course), but instead letting the caller do it. NOTE! As interrupts
* can wake up a caller, some cli-sti sequences are needed to check for
* sleep-on-calls. These should be extremely quick, though (I hope).
*/
/*
* NOTE! There is one discordant note here: checking floppies for
* disk change. This is where it fits best, I think, as it should
* invalidate changed floppy-disk-caches.
*/
#include <stdarg.h>
#include <linux/config.h>
#include <linux/sched.h>
#include <linux/kernel.h>
#include <asm/system.h>
#include <asm/io.h>
extern int end;
struct buffer_head * start_buffer = (struct buffer_head *) &end; //end表示内核代码结束地址————编译内核时连接器生成
struct buffer_head * hash_table[NR_HASH]; //NR_HASH为什么是307个? 307*4 = 1228字节
static struct buffer_head * free_list;
static struct task_struct * buffer_wait = NULL;
int NR_BUFFERS = 0;
static inline void wait_on_buffer(struct buffer_head * bh)
{
cli(); //关中断
while (bh->b_lock) //如果buffer head处于lock状态
sleep_on(&bh->b_wait); //当前内核线程休眠
sti(); //开中断
}
int sys_sync(void)
{
int i;
struct buffer_head * bh;
sync_inodes(); /* write out inodes into buffers */
bh = start_buffer;
for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
wait_on_buffer(bh);
if (bh->b_dirt)
ll_rw_block(WRITE,bh);
}
return 0;
}
//将buffer中的数据写回磁盘
int sync_dev(int dev)
{
int i;
struct buffer_head * bh;
bh = start_buffer;
for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
if (bh->b_dev != dev)
continue;
wait_on_buffer(bh); //等待buffer空闲
if (bh->b_dev == dev && bh->b_dirt) //如果缓冲区被修改了
ll_rw_block(WRITE,bh); //写磁盘
}
//再将i节点数据写入高速缓冲,让i节点表inode_table中的inode与缓冲中的信息同步。
//然后在高速缓冲中的数据更新之后,再把它们与设备中的数据同步。这里采用两遍同步操作
//是为了提高内核执行效率。第一遍缓冲区同步操作可以让内核中许多“脏块”变干净,使得
//i节点的同步操作能够高效执行。本次缓冲区同步操作则把那些由于i节点同步操作而又变
//脏的缓冲块与设备中数据同步。
sync_inodes();
bh = start_buffer;
for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
if (bh->b_dev != dev)
continue;
wait_on_buffer(bh);
if (bh->b_dev == dev && bh->b_dirt)
ll_rw_block(WRITE,bh);
}
return 0;
}
/*
* 所有buffer head结构清0
*/
void inline invalidate_buffers(int dev)
{
int i;
struct buffer_head * bh;
bh = start_buffer;
for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
if (bh->b_dev != dev)
continue;
wait_on_buffer(bh);
if (bh->b_dev == dev)
bh->b_uptodate = bh->b_dirt = 0;
}
}
/*
* This routine checks whether a floppy has been changed, and
* invalidates all buffer-cache-entries in that case. This
* is a relatively slow routine, so we have to try to minimize using
* it. Thus it is called only upon a 'mount' or 'open'. This
* is the best way of combining speed and utility, I think.
* People changing diskettes in the middle of an operation deserve
* to loose :-)
*
* NOTE! Although currently this is only for floppies, the idea is
* that any additional removable block-device will use this routine,
* and that mount/open needn't know that floppies/whatever are
* special.
*/
void check_disk_change(int dev)
{
int i;
if (MAJOR(dev) != 2)
return;
if (!floppy_change(dev & 0x03))
return;
for (i=0 ; i<NR_SUPER ; i++)
if (super_block[i].s_dev == dev)
put_super(super_block[i].s_dev);
invalidate_inodes(dev);
invalidate_buffers(dev);
}
#define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
#define hash(dev,block) hash_table[_hashfn(dev,block)]
/*
* buffer head结构从LRU链表和hash表中移除
*/
static inline void remove_from_queues(struct buffer_head * bh)
{
/* remove from hash-queue */
if (bh->b_next)
bh->b_next->b_prev = bh->b_prev;
if (bh->b_prev)
bh->b_prev->b_next = bh->b_next;
if (hash(bh->b_dev,bh->b_blocknr) == bh)
hash(bh->b_dev,bh->b_blocknr) = bh->b_next;
/* remove from free list */
if (!(bh->b_prev_free) || !(bh->b_next_free))
panic("Free block list corrupted");
bh->b_prev_free->b_next_free = bh->b_next_free;
bh->b_next_free->b_prev_free = bh->b_prev_free;
if (free_list == bh)
free_list = bh->b_next_free;
}
/*
* buffer head结构插入LRU链表和hash表
*/
static inline void insert_into_queues(struct buffer_head * bh)
{
/*
* put at end of free list
* buffer head结构插入LRU链表
*/
bh->b_next_free = free_list;
bh->b_prev_free = free_list->b_prev_free;
free_list->b_prev_free->b_next_free = bh;
free_list->b_prev_free = bh;
/*
* put the buffer in new hash-queue if it has a device
* buffer head结构插入hash链表
*/
bh->b_prev = NULL;
bh->b_next = NULL;
if (!bh->b_dev)
return;
bh->b_next = hash(bh->b_dev,bh->b_blocknr);
hash(bh->b_dev,bh->b_blocknr) = bh;
bh->b_next->b_prev = bh;
}
static struct buffer_head * find_buffer(int dev, int block)
{
struct buffer_head * tmp;
//遍历hash表上所有node, dev和b_blocknr同时匹配为有效
for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp->b_next)
if (tmp->b_dev==dev && tmp->b_blocknr==block)
return tmp;
return NULL;
}
/*
* Why like this, I hear you say... The reason is race-conditions.
* As we don't lock buffers (unless we are readint them, that is),
* something might happen to it while we sleep (ie a read-error
* will force it bad). This shouldn't really happen currently, but
* the code is ready.
* 函数作用:从某个hash表中获取特定的buffer head结构
* Hash 表所使用的散列函数由设备号和逻辑块号组合而成。程序中使用的具体
* hash 函数是:(设备号^逻辑块号) Mod 307
*/
struct buffer_head * get_hash_table(int dev, int block)
{
struct buffer_head * bh;
for (;;) {
if (!(bh=find_buffer(dev,block)))
return NULL;
bh->b_count++;
wait_on_buffer(bh); //等待buffer head解除锁定
if (bh->b_dev == dev && bh->b_blocknr == block)
return bh;
bh->b_count--;
}
}
/*
* Ok, this is getblk, and it isn't very clear, again to hinder(阻碍)
* race-conditions. Most of the code is seldom used, (ie repeating),
* so it should be much more efficient than it looks.
*
* The algoritm is changed: hopefully better, and an elusive(不可捉摸) bug removed.
*/
#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) //同时得到buffer head的 b_dirt+b_lock状态
struct buffer_head * getblk(int dev,int block)
{
struct buffer_head * tmp, * bh;
repeat:
//如果该buffer head在hash表中,那么直接返回
if (bh = get_hash_table(dev,block))
return bh;
//不在hash表中
tmp = free_list;
do {
if (tmp->b_count) //引用计数需要为0
continue;
if (!bh || BADNESS(tmp)<BADNESS(bh)) {
bh = tmp;
if (!BADNESS(tmp))
break;
}
/* and repeat until we find something good */
} while ((tmp = tmp->b_next_free) != free_list); //遍历free_list
//如果循环检查发现所有缓冲块都正在被使用(所有缓冲块的头部引用计数都>0)中,则睡眠
//等待有空闲缓冲块可用。当有空闲缓冲块可用时本进程会被明确地唤醒。然后我们就跳转到
//函数开始处重新查找空闲缓冲块。
if (!bh) {
sleep_on(&buffer_wait); //等待有空闲buffer head的进程链表
goto repeat;
}
//执行到这里,说明我们已经找到了一个比较适合的空闲缓冲块了。于是先等待该缓冲区解锁
//(如果已被上锁的话)。如果在我们睡眠阶段该缓冲块又被其他任务使用的话,只好又重复上
//述寻找过程。
wait_on_buffer(bh); //等待某个特定的buffer head解锁,这里要注意与sleep_on(&buffer_wait);的区别
if (bh->b_count)
goto repeat;
//如果该缓冲区已被修改,则将数据写盘,并再次等待缓冲区解锁。同样地,若该缓冲区又被
//其他任务使用的话,只好再重复上述寻找过程。
while (bh->b_dirt) { //缓冲区被修改了
sync_dev(bh->b_dev); //将数据写盘
wait_on_buffer(bh);
if (bh->b_count) //表明缓冲区被其他内核线程使用了
goto repeat;
}
/* NOTE!! While we slept waiting for this block, somebody else might */
/* already have added "this" block to the cache. check it */
if (find_buffer(dev,block))
goto repeat;
/* OK, FINALLY we know that this buffer is the only one of it's kind, */
/* and that it's unused (b_count=0), unlocked (b_lock=0), and clean */
bh->b_count=1;
bh->b_dirt=0;
bh->b_uptodate=0;
remove_from_queues(bh);
bh->b_dev=dev;
bh->b_blocknr=block;
insert_into_queues(bh); //一个删除,一个插入,实现了LRU链表
return bh;
}
void brelse(struct buffer_head * buf)
{
if (!buf)
return;
wait_on_buffer(buf);
if (!(buf->b_count--))
panic("Trying to free free buffer");
wake_up(&buffer_wait);
}
/*
* bread() reads a specified block and returns the buffer that contains
* it. It returns NULL if the block was unreadable.
*/
struct buffer_head * bread(int dev,int block)
{
struct buffer_head * bh;
if (!(bh=getblk(dev,block)))
panic("bread: getblk returned NULL\n");
if (bh->b_uptodate)
return bh;
ll_rw_block(READ,bh); //读写硬盘block到高速buffer
wait_on_buffer(bh);
if (bh->b_uptodate)
return bh;
brelse(bh);
return NULL;
}
#define COPYBLK(from,to) \
__asm__("cld\n\t" \
"rep\n\t" \
"movsl\n\t" \
::"c" (BLOCK_SIZE/4),"S" (from),"D" (to) \
:"cx","di","si")
/*
* bread_page reads four buffers into memory at the desired address. It's
* a function of its own, as there is some speed to be got by reading them
* all at the same time, not waiting for one to be read, and then another
* etc.
* 每个buffer 1KB, 一个page 4KB, 所以 bread_page一次读4个buffer到一个page
*/
void bread_page(unsigned long address,int dev,int b[4])
{
struct buffer_head * bh[4];
int i;
//读4个buffer
for (i=0 ; i<4 ; i++)
if (b[i]) {
if (bh[i] = getblk(dev,b[i]))
if (!bh[i]->b_uptodate)
ll_rw_block(READ,bh[i]);
} else
bh[i] = NULL;
//将4个buffer的内容拷贝到一个page
for (i=0 ; i<4 ; i++,address += BLOCK_SIZE)
if (bh[i]) {
wait_on_buffer(bh[i]);
if (bh[i]->b_uptodate)
COPYBLK((unsigned long) bh[i]->b_data,address);
brelse(bh[i]);
}
}
/*
* Ok, breada can be used as bread, but additionally to mark other
* blocks for reading as well. End the argument list with a negative
* number.
* OK,breada可以象bread一样使用,但会另外预读一些块。该函数参数列表
* 需要使用一个负数来表明参数列表的结束
*/
struct buffer_head * breada(int dev,int first, ...)
{
va_list args;
struct buffer_head * bh, *tmp;
va_start(args,first);
//first指定的block
if (!(bh=getblk(dev,first)))
panic("bread: getblk returned NULL\n");
if (!bh->b_uptodate)
ll_rw_block(READ,bh);
//后续参数指定的block
while ((first=va_arg(args,int))>=0) {
tmp=getblk(dev,first);
if (tmp) {
if (!tmp->b_uptodate)
ll_rw_block(READA,bh); //READA ———— read ahead
tmp->b_count--;
}
}
va_end(args);
wait_on_buffer(bh); //等待第一个buffer head解锁
if (bh->b_uptodate)
return bh; //在等待退出之后如果缓冲块中数据仍然有效,则返回缓冲块头指针
brelse(bh);
return (NULL);
}
/*
* 高速缓冲区初始化函数
*/
void buffer_init(long buffer_end)
{
//1.计算buffer head区域的起始地址 ———— start_buffer指向end,buffer_head区域从end开始
struct buffer_head * h = start_buffer;
void * b;
int i;
//2.计算得到buffer区域的起始地址
if (buffer_end == 1<<20) //1MB
b = (void *) (640*1024);
else
b = (void *) buffer_end;
// BLOCK_SIZE: 逻辑块大小(1KB)
/*
* |----buffer head区域增长方向----> ..|.. <-------------buffer区域增长方向---------------|
* 缓冲区低端 判断地址空间是否重叠 缓冲区高端
*/
while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) { //判断下一个buffer的起始地址是否与下一个buffer head的起始地址重复
//1.填写buffer head结构
h->b_dev = 0;
h->b_dirt = 0;
h->b_count = 0;
h->b_lock = 0;
h->b_uptodate = 0;
h->b_wait = NULL;
h->b_next = NULL;
h->b_prev = NULL;
h->b_data = (char *) b;
h->b_prev_free = h-1;
h->b_next_free = h+1;
//2.h指针指向下一个buffer head结构
h++;
//3.可用buffer个数加一
NR_BUFFERS++;
//跳过显存和BIOS ROM区域(640KB ~ 1MB)
if (b == (void *) 0x100000)
b = (void *) 0xA0000; //640KB
}
//3.将所有buffer head构建成一个双向循环链表 ———— 实际上这个双向链表是一个最近最少使用 LRU(Least Recently Used)链表
h--; //最后一个buffer head结构
free_list = start_buffer; //free_list指向第一个buffer head
free_list->b_prev_free = h; //第一个buffer head的前一个,那就是最后一个buffer head
h->b_next_free = free_list; //最后一个buffer head的下一个,那就是第一个buffer head
//4.初始化307个hash表头
for (i=0;i<NR_HASH;i++)
hash_table[i]=NULL;
}