linux0.12-12-2-buffer

基本上看完赵老师中文解释,都可以自己写这部分的代码。

[622页]

12-2 buffer.c程序

从本节起,我们对fs/目录下的程序逐一进行说明和注释。按照本章第2节中的描述,
本章的程序可以被分成4个部分:
高速缓冲管理;
文件底层操作;
文件数据访问;
文件高层访问控制。
这里首先对第1部分的高速缓冲管理程序进行描述。这部分仅包含一个程序buffer.c。

12-2-1 功能描述

buffer.c程序用于对高速缓冲区(池)进行操作和管理。高速缓冲区位于内核代码块和主内存区之间,
如图12-15所示。高速缓冲区在块设备与内核其他程序之间起着一个桥梁作用。除了块设备
驱动程序以外,内核程序如果需要访问块设备中的数据,就都需要经过高速缓冲区来间接地操作。

图12-15 高速缓冲区在整个物理内存中所处的位置
在这里插入图片描述

图中高速缓冲区的起始位置从内核模块末端end标号开始,end是内核模块链接期间由链接程序ld设置的一个外部变量,内核代码中没有定义整个符号。当在链接生产system模块时,ld程序设置了end的地址,它等于data_start + datasize + bss_size,即bss段结束后的第1个有效地址,即内核模块的末端。另外,链接器还设置了etext何edata两个外部变量。它们分别表示代码
段后第1个地址和数据段后第1个地址。

整个高速缓冲区被划分1024字节大小的缓冲块,正好与块设备上的磁盘逻辑块大小相同。
高速缓冲采用hash表和包含所有缓冲块的链表进行操作管理。在缓冲区初始化过程中,初始化程序从整个缓冲区的两端开始,分别同时设置缓冲块头结构和划分处对应的缓冲块,如果12-16所示。缓冲区的高端被划分成一个1024字节的缓冲块,低端则分别建立起对应各缓冲块的缓冲头结构buffer_head(include/linux/fs.h,68行)。该头结构用于描述对应缓冲块的属性,并且用于把所有缓冲头连接秤链表,直到它们之间已经不能再划分处缓冲块为止。

图12-16 高速缓冲区的初始化
在这里插入图片描述

所有缓冲块的buffer_head被链接秤一个双向链表结构,如图12-17所示。
图中free_list指针是该链表的头指针,指向空闲块链表中第一个"最为空闲的"缓冲块,
即近期最少使用的缓冲块。而该缓冲块的反向指针b_prev_free则指向缓冲块链表中最后一个缓冲块,
即最近刚使用的缓冲块。缓冲块的缓冲头数据结构为:

struct buffer_head {
	char * b_data;						//指向该缓冲块中数据区(1024字节)的指针。
	unsigned long b_blocknr;			//块号。
	unsigned short b_dev;				//数据源的设备号(0=free)。
	unsigned char b_uptodate;			//更新标志:表示数据是否已更新。
	unsigned char b_dirt;				//修改标志:0-未修改(clean),1-已修改(dirty)。
	unsigned char b_count;				//使用该块的用户数。
	unsigned char b_lock;				//缓冲区是否被锁定。0-ok,1-locked
	struct task_struct * b_wait;		//指向等待该缓冲区解锁的任务。
	struct buffer_head * b_prev;		//hash队列上前一块(这四个指针用于缓冲区管理)。
	struct buffer_head * b_next;		//hash队列上下一块。
	struct buffer_head * b_prev_free;	//空闲表上前一块。
	struct buffer_head * b_next_free;	//空闲表上下一块。
};

其中字段b_lock是锁定标志,表示驱动程序正在对该缓冲块内容进行修改,因此该缓冲块处于忙状态而正被锁定。该标志与缓冲块的其他标志无关,主要用于blk_drv/ll_rw_block.c程序中在更新缓冲块中数据信息时锁定缓冲块。因为在更新缓冲块中数据时,当前进程会自愿去睡眠等待,从而别的进程就有机会访问该缓冲块。因此,此时为了禁止其他进程使用其中的数据,一定要在睡眠之前锁定缓冲块。

字段b_count是缓冲管理程序buffer使用的计数值,表示相应缓冲块正被各个进程使用(引用)的次数,因此这个字段用于对缓冲块的程序引用计数管理,也与缓冲块的其他标志无关。当引用计数不为0时,缓冲管理程序就不能释放相应缓冲块。空闲块即是b_count=0的块。当b_count=0时,表示相应缓冲块未被使用(free),否则表示它正被使用。对于程序申请的缓冲块,若缓冲管理程序能够从hash表中得到已存在的指定块时,就会将该块的b_count增1(b_count++)。若缓冲块时重新申请得到的未使用的块,则其头结构中的b_count被设置为等于1。当程序释放其对一个块的引用时,该块的引用此时就会相应地递减(b_count–)。由于标志b_lock表示其他程序正在使用并锁定了指定的缓冲块,因此对于b_lock置位的缓冲块来讲,
其b_count肯定大于0。

字段b_dirt是脏标志,说明缓冲块中内容是否已被修改而与块设备上的对应数据块内容不同(延迟写)。

字段b_uptodate是数据更新(有效)标志,说明缓冲块中数据是否有效。
(a)初始化 或释放块时这两个标志均设置成0,表示该缓冲块此时无效。
(b)当数据被写入缓冲块但还没有被写入设备时则b_dir=1,b_uptodate=0。
©当数据被写入块设备或刚从块设备中读入缓冲块则数据变成有效,即b_uptodate=1。
(d)请注意有一种特殊情况。即在新申请一个设备缓冲块时b_dirt与b_uptodate都为1,
表示缓冲块中数据虽然与块设备上的不同,但是数据仍然是有效的(更新的)。

图中缓冲头结构中"其他字段"包括块设备号、缓冲数据的逻辑块号,这两个字段唯一确定
了缓冲块中数据对应的块设备和数据块。
另外还有几个状态标志:
数据有效(更新)标志、
修改标志、
数据被使用的进程数和本缓冲块是否上锁标志。

内核程序在使用高速缓冲区中的缓冲块时,是指定设备号(dev)和所要访问设备数据的逻辑
块号(block),通过调用缓冲块读取函数bread()、bread_page()或breada()进行操作。
这几个函数都是用缓冲区搜索管理函数getblk(),用于在所有缓冲块中寻找最位空闲的缓冲块。
该函数将在下面重点说明。在系统释放缓冲块时,需要调用brelse()函数。所有这些缓冲块数据
存取和管理函数的调用层次关系可用图12-18来描述。

图12-18 缓冲区管理函数之间的层次关系
在这里插入图片描述

为了能够快速而有效地在缓冲区中寻找判断出请求的数据是否已经被读入到缓冲区中,
buffer.c程序使用了具有307个buffer_head指针项的hash(散列、杂凑)数组表结构。
Hash表所使用的散列函数由设备号和逻辑块号组合而成。程序中使用的具体hash函数是:
(设备号^逻辑块号)Mode307.

图12-17中指针b_prev、b_next就是用于hash表中散列在同一项上多个缓冲块之间的双向链接,
即把hash函数计算出的具有相同散列值的缓冲块链接在散列数组同一项链表上。
有关散列队列上缓冲块的操作方式,可参见《UNIX操作系统设计》一书第3章中的详细描述。
对于动态变化的hash表结构某一时刻的状态可参见图12-19。

其中,双箭头横线表示散列在同一hash表项中缓冲块头结构之间的双向链接指针。虚线表示
缓冲区中所有缓冲块组成的一个双向循环链表(即所谓空闲链表),而free_list是该链表最为
空闲缓冲块处的头指针。实际上这个双向链表是一个最近最少使用LRU(Least Recently Used)
链表。下面我们对缓冲块搜索函数getblk()进行详细说明。

上面提及的三个函数在执行时都调用了getblk(),以获取适合的空闲缓冲块。

该函数首先调用get_hash_table()函数,
在hash表队列中搜索指定设备号和逻辑块号的缓冲块是否已经存在。
如果存在就立刻返回对应缓冲头结构的指针;
如果不存在,则从空闲链表头开始,对空闲链表进行扫描,寻址一个空闲缓冲块。在寻址过程中还要对找到的空闲缓冲块作比较,
根据赋予修改标志和锁定标志组合而成的权值,比较哪个空闲块最合适。
若找到的空闲块即乜有被修改也没有被锁定,就不用继续寻址了。
若没有找到空闲块,则让当前进程进入休睡眠状态,待继续执行时再次寻址。
若空闲块被锁定,则进程也需进入睡眠,等待其他进程解锁。
若在睡眠等待的过程中,该缓冲块又被其他进程占用,那么只要再重头开始搜索缓冲块。
否则判断该缓冲块是否已被修改过,若是,则将该块写盘,并等待该块解锁。
此时如果该缓冲块又被别的进程占用,那么又一次前功尽弃,只好再重头开始执行getblk()。
在经理了以上折腾后,此时有可能出现另外一个意外情况,
也就是在我们睡眠时,可能其他进程已经将我们所需要的缓冲块加进了hash队列中,因此这里需要最后一次搜索一下hash队列。
如果真的在hash队列中找到了我们所需要的缓冲块,那么我们又得对找到的缓冲块进行以上判断处理,
因此,又一次需要从头开始执行getblk()。
最后,我们才算找到了一块没有被进程使用、没有被上锁,而且是干净(修改标志未置位)的空闲缓冲块。
于是我们就将该块的引用次数置1,并复位其他几个标志,然后从空闲表中移出该块的缓冲头结构。
在 设置了该缓冲块所属的设备号和相应的逻辑号后,再将其插入hash表对应表项首部并链接到空闲队列的末尾处。
由于搜索空闲块时从空闲队列头开始的,因此这种先从空闲队列中移出并使用最近不常用的缓冲块,然后
再重新插入到空闲队列尾部的操作也就实现了最近最少使用LRU算法。最终,返回该缓冲块头的指针。
(也可以看看链表的插入)
整个gteblk()处理过程可参见图12-20

从上述分析可知,函数在每次获取新的空闲缓冲块时,就会把它移到free_list头指针所指链表的最后面,即越靠近
链表末端的缓冲块被使用的时间越近。因此如果hash表中没有找到对应缓冲块算法,就会在搜索新空闲缓冲块
时从free_list链表头处开始搜索。可以看出,内核取得缓冲块的算法使用了以下策略:
(a)如果指定的缓冲块存在于hash表中,则说明已经得到可用缓冲块,于是直接返回。
(b)否则就需要在链表中从free_list头指针处开始搜索,即从最近最少使用的缓冲块处开始。

因此最理想的情况是找到一个完全空闲的缓冲块,即b_dirt和b_lock标志均为0的缓冲块;
但是如果不能满足这两个条件,那么就需要根据b_dirt和b_lock标志计算出一个值。因为设备
操作通常很耗时,所以在计算时需加大b_dirt的权重。然后我们在计算结果值最小的缓冲块上
等待(如果缓冲块已经上锁)。最后当标志b_lock为0时,表示所等待的缓冲块原内容已经写到
块设备上。于是getblk()就获得了一块空闲缓冲块。

由以上处理可以看到,getblk()返回的缓冲块可能是一个新的空闲块,也可能正好是含有所需数据的缓冲块,
它已经存在于高速缓冲区中。
因此对于读取数据块操作(bread()),此时就要判断该缓冲块的更新标志,看看所含数据是否有效,
如果有效就可以直接将该数据块返回给申请的程序。否则就需要调用设备的低层块读写函数(ll_rw_block()),
并同时让自己进入睡眠状态,等待数据被读入缓冲块。
在醒来后再判断数据是否有效了。如果有效,就可将次数据返给申请的程序,否则说明对设备的读
操作失败了,没有取到数据。于是,释放该缓冲块,并返回NULL值。
breada()和bread_page()函数与bread()函数类似。
图12-21 bread()函数执行流程框图

当程序不再需要使用一个缓冲块中的数据时,就调用brelse()函数,释放该缓冲块并唤醒
等待该缓冲块而进入睡眠状态的进程。注意,空闲缓冲块链表中的缓冲块,并不是都是空闲的。
只有当被写盘刷新、解锁且没有其他进程引用时(引用计数=0),才能挪作他用。

综上所述,高速缓冲区在提高对块设备的访问效率和增加数据共享方面起着重要的作用。
除驱动程序以外,内核其他上层程序对块设备的读写操作需要经过高速缓冲区管理程序来
间接地实现。它们之间的主要联系是通过高速缓冲区管理程序中的bread()函数和块设备低层接口
函数ll_rw_block()来实现。上层程序若要访问块设备数据就通过bread()向缓冲区管理程序申请。
如果所需的数据已经在高速缓冲区中,管理程序就会将数据直接返回给程序。如果所需的数据暂时还不
在缓冲区中,则管理程序会通过ll_rw_block()向块设备驱动程序申请,同时让程序对应的进程睡眠等待。
等到块设备驱动程序把指定的数据放入高速缓冲区后,管理程序才会返回给上层程序。
见图12-22。

图12-22 内核程序块设备访问操作

对于更新和同步(Synchronization)操作,其主要作用是让内存中的一些缓冲块内容与磁盘等块设备上的信息一直
sync_inodes()的主要作用是把i节点表inode_table中的i节点信息与磁盘上的一致起来。但
需要经过系统需要经过系统高速缓冲区这一中间环境。实际上,任何同步操作都被分成了两个阶段:
(1)数据结构信息与高速缓冲区中的缓冲块同步问题,由驱动程序独立负责。(_应该是指这个 文件底层操作)
(2)高速缓冲区中数据块与磁盘对应块的同步问题,由这里的缓冲管理程序负责。

sync_inodes()函数不会直接与磁盘打交道,它只能前进到缓冲区这一步,即只负责与缓冲区中的
信息同步。剩下的需要缓冲管理程序负责。为了让sync_inodes()知道哪些i节点与磁盘上的不同,
就必须首先让缓冲区汇总内容与磁盘上的内容一致。这样sync_inodes()通过与当前磁盘在
缓冲区中最新数据比较才能知道哪些磁盘Inode需要修改和更新。最后再进行第二次告诉缓冲
去与磁盘设备的同步操作,做到内存中的数据与块设备中的数据真正同步。
_inode.c跨度有点大,有点不理解。

12-2-2 代码注释

/*
 *  linux/fs/buffer.c
 *
 *  (C) 1991  Linus Torvalds
 */

/*
 * 'buffer.c'用于实现缓冲区高速缓存功能。通过不让中断处理过程改变缓冲区,而是让调
 * 用者来执行,避免了竞争条件(当然除改变数据以外)。注意!由于中断可以唤醒一个调
 * 用者,因此就需要开关中断指令(cli-sti)序列来检测由于调用而睡眠。但需要非常地快
 * (我希望是这样)。
 */

/*
 * 注意!有一个程序应不属于这里:检测软盘是否更换。但我想这里是放置
 * 该程序最好的地方了,因为它需要使已更换软盘缓冲失效。
 */

#include <stdarg.h>		// 标准参数头文件。以宏的形式定义变量参数列表。主要说明了-个
						// 类型(va_list)和三个宏(va_start, va_arg和va_end),用于
						// vsprintf、vprintf、vfprintf函数。
#include <linux/config.h>	// 内核配置头文件。定义键盘语言和硬盘类型(HD_TYPE)可选项。
#include <linux/sched.h>	// 调度程序头文件,定义了任务结构task_struct、任务0的数据,
							// 还有一些有关描述符参数设置和获取的嵌入式汇编函数宏语句。
#include <linux/kernel.h>	// 内核头文件。含有一些内核常用函数的原形定义。
#include <asm/system.h>		// 系统头文件。定义了设置或修改描述符/中断门等的嵌入汇编宏。
#include <asm/io.h>			// io头文件。定义硬件端口输入/输出宏汇编语句。

// 变量end是由编译时的连接程序ld生成,用于表明内核代码的末端,即指明内核模块末端
// 位置,参见错误!未找到引用源。。也可以从编译内核时生成的 System.map文件中查出。这里用它来表
// 明高速缓冲区开始于内核代码末端位置。
// 第33行上的buffer_wait变量是等待空闲缓冲块而睡眠的任务队列头指针。它与缓冲块头
// 部结构中b_wait指针的作用不同。当任务申请一个缓冲块而正好遇到系统缺乏可用空闲缓
// 冲块时,当前任务就会被添加到buffer_wait睡眠等待队列中。而b_wait则是专门供等待
// 指定缓冲块(即b_wait对应的缓冲块)的任务使用的等待队列头指针。
extern int end;
struct buffer_head * start_buffer = (struct buffer_head *) &end;
struct buffer_head * hash_table[NR_HASH];			// NR_HASH = 307项。
static struct buffer_head * free_list;				// 空闲缓冲块链表头指针。
static struct task_struct * buffer_wait = NULL;		// 等待空闲缓冲块而睡眠的任务队列。

// 下面定义系统缓冲区中含有的缓冲块个数。这里,NR_BUFFERS是一个定义在linux/fs.h头
// 文件第48行的宏,其值即是变量名nr_buffers,并且在fs.h文件第172行声明为全局变量。
// 大写名称通常都是一个宏名称,Linus这样编写代码是为了利用这个大写名称来隐含地表示
// nr_buffers是一个在内核初始化之后不再改变的“常量”。它将在初始化函数buffer_init()
// 中被设置(第371行)。
int NR_BUFFERS = 0;		// 系统含有缓冲块个数。

 等待指定缓冲块解锁。
// 如果指定的缓冲块bh已经上锁就让进程不可中断地睡眠在该缓冲块的等待队列b_wait中。
// 在缓冲块解锁时,其等待队列上的所有进程将被唤醒。虽然是在关闭中断(cli)之后去睡
// 眠的,但这样做并不会影响在其他进程上下文中响应中断。因为每个进程都在自己的TSS段
// 中保存了标志寄存器EFLAGS的值,所以在进程切换时CPU中当前EFLAGS的值也随之改变。
// 使用sleep_on()进入睡眠状态的进程需要用wake_up()明确地唤醒。
static inline void wait_on_buffer(struct buffer_head * bh)
{
	cli();						// 关中断。
	while (bh->b_lock)			// 如果已被上锁则进程进入睡眠,等待其解锁。
		sleep_on(&bh->b_wait);
	sti();						// 开中断。
}
 设备数据同步。
// 同步设备和内存高速缓冲中数据。其中,sync_inodes()定义在inode.c,59行。
int sys_sync(void)
{
	int i;
	struct buffer_head * bh;
    // 首先调用i节点同步函数,把内存i节点表中所有修改过的i节点写入高速缓冲中。然后
    // 扫描所有高速缓冲区,对已被修改的缓冲块产生写盘请求,将缓冲中数据写入盘中,做到
    // 高速缓冲中的数据与设备中的同步。
	sync_inodes();							/* write out inodes into buffers */
	bh = start_buffer;						// bh指向缓冲区开始处。
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		wait_on_buffer(bh);					// 等待缓冲区解锁(如果已上锁的话)。
		if (bh->b_dirt)
			ll_rw_block(WRITE,bh);			// 产生写设备块请求。
	}
	return 0;
}
 对指定设备进行高速缓冲数据与设备上数据的同步操作。
// 该函数首先搜索高速缓冲区中所有缓冲块。对于指定设备dev的缓冲块,若其数据已被修改
// 过就写入盘中(同步操作)。然后把内存中i节点表数据写入高速缓冲中。之后再对指定设
// 备dev执行一次与上述相同的写盘操作。
int sync_dev(int dev)
{
	int i;
	struct buffer_head * bh;
    // 首先对参数指定的设备执行数据同步操作,让设备上的数据与高速缓冲区中的数据同步。
    // 方法是扫描高速缓冲区中所有缓冲块,对指定设备dev的缓冲块,先检测其是否已被上锁,
    // 若已被上锁就睡眠等待其解锁。然后再判断一次该缓冲块是否还是指定设备的缓冲块并且
    // 已修改过(b_dirt标志置位),若是就对其执行写盘操作。因为在我们睡眠期间该缓冲块
    // 有可能已被释放或者被挪作它用,所以在继续执行前需要再次判断一下该缓冲块是否还是
    // 指定设备的缓冲块,
	bh = start_buffer;						// bh指向缓冲区开始处。
	for (i=0 ; i<NR_BUFFERS ; i++,bh++) {
		if (bh->b_dev != dev)				// 不是设备dev的缓冲块则继续。
			continue;
		wait_on_buffer(bh);					// 等待缓冲区解锁(如果已上锁的话)。
		if (bh->b_dev == dev && bh->b_dirt)
			ll_rw_block(WRITE,bh);
	}
	// 再将i节点数据写入高速缓冲。让i节点表inode_table中的inode与缓冲中的信息同步。
	sync_inodes();
	// 然后在高速缓冲中的数据更新之后,再把它们与设备中的数据同步。这里采用两遍同步操作
    // 是为了提高内核执行效率。第一遍缓冲区同步操作可以让内核中许多“脏块”变干净,使得
    // i节点的同步操作能够高效执行。本次缓冲区同步操作则把那些由于i节点同步操作而又变
    // 脏的缓冲块与设备中数据同步。
	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;
}
 使指定设备在高速缓冲区中的数据无效。
// 扫描高速缓冲区中所有缓冲块。对指定设备的缓冲块复位其有效(更新)标志和已修改标志。
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;
	}
}

/*
 * 该子程序检查一个软盘是否已被更换,如果已经更换就使高速缓冲中与该软驱
 * 对应的所有缓冲区无效。该子程序相对来说较慢,所以我们要尽量少使用它。
 * 所以仅在执行'mount'或'open'时才调用它。我想这是将速度和实用性相结合的
 * 最好方法。若在操作过程中更换软盘,就会导致数据的丢失。这是咎由自取J。
 *
 * 注意!尽管目前该子程序仅用于软盘,以后任何可移动介质的块设备都将使用该
 * 程序,mount/open操作不需要知道是软盘还是其他什么特殊介质。
 */
 检查磁盘是否更换,如果已更换就使对应高速缓冲区无效。
void check_disk_change(int dev)
{
	int i;
    // 首先检测一下是不是软盘设备。因为现在仅支持软盘可移动介质。如果不是则退出。然后
    // 测试软盘是否已更换,如果没有则退出。floppy_change()在blk_drv/floppy.c第139行。
	if (MAJOR(dev) != 2)
		return;
	if (!floppy_change(dev & 0x03))
		return;
	// 软盘已经更换,所以释放对应设备的i节点位图和逻辑块位图所占的高速缓冲区;并使该
    // 设备的i节点和数据块信息所占踞的高速缓冲块无效。	
	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);
}

// 下面两行代码是hash(散列)函数定义和hash表项的计算宏。
// hash表的主要作用是减少查找比较元素所花费的时间。通过在元素的存储位置与关键字之间
// 建立一个对应关系(hash函数),我们就可以直接通过函数计算立刻查询到指定的元素。建
// 立hash函数的指导条件主要是尽量确保散列到任何数组项的概率基本相等。建立函数的方法
// 有多种,这里Linux 0.12主要采用了关键字除留余数法。因为我们寻找的缓冲块有两个条件,
// 即设备号dev和缓冲块号block,因此设计的hash函数肯定需要包含这两个关键值。这里两个
// 关键字的异或操作只是计算关键值的一种方法。再对关键值进行MOD运算就可以保证函数所计
// 算得到的值都处于函数数组项范围内。
#define _hashfn(dev,block) (((unsigned)(dev^block))%NR_HASH)
#define hash(dev,block) hash_table[_hashfn(dev,block)]

 从hash队列和空闲缓冲队列中移走缓冲块。
// hash队列是双向链表结构,空闲缓冲块队列是双向循环链表结构。
static inline void remove_from_queues(struct buffer_head * bh)
{
/* 从hash队列中移除缓冲块 */
	if (bh->b_next)
		bh->b_next->b_prev = bh->b_prev;
	if (bh->b_prev)
		bh->b_prev->b_next = bh->b_next;
// 如果该缓冲区是该队列的头一个块,则让hash表的对应项指向本队列中的下一个缓冲区。		
	if (hash(bh->b_dev,bh->b_blocknr) == bh)
		hash(bh->b_dev,bh->b_blocknr) = bh->b_next;
/* 从空闲缓冲块表中移除缓冲块 */
	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;
}
 将缓冲块插入空闲链表尾部,同时放入hash队列中。
static inline void insert_into_queues(struct buffer_head * bh)
{
/* 放在空闲链表末尾处 */
	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;
    /* 如果该缓冲块对应一个设备,则将其插入新hash队列中 */
    // 请注意当hash表某项第1次插入项时,hash()计算值肯定为NULL,因此此时第161行上
    // 得到的bh->b_next肯定是NULL,所以第163行上应该在bh->b_next不为NULL时才能给
    // b_prev赋bh值。即第163行前应该增加判断“if (bh->b_next)”。该错误到0.96版后
    // 才被纠正。
	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;// 此句前应添加“if (bh->b_next)”判断。
}
 利用hash表在高速缓冲中寻找给定设备和指定块号的缓冲区块。
// 如果找到则返回缓冲区块的指针,否则返回NULL。
static struct buffer_head * find_buffer(int dev, int block)
{		
	struct buffer_head * tmp;
// 搜索hash表,寻找指定设备号和块号的缓冲块。
	for (tmp = hash(dev,block) ; tmp != NULL ; tmp = tmp->b_next)
		if (tmp->b_dev==dev && tmp->b_blocknr==block)
			return tmp;
	return NULL;
}

/*
 * 代码为什么会是这样子的?我听见你问... 原因是竞争条件。由于我们没有对
 * 缓冲块上锁(除非我们正在读取它们中的数据),那么当我们(进程)睡眠时
 * 缓冲块可能会发生一些问题(例如一个读错误将导致该缓冲块出错)。目前
 * 这种情况实际上是不会发生的,但处理的代码已经准备好了。
 */
 利用hash表在高速缓冲区中寻找指定的缓冲块。若找到则对该缓冲块上锁并返回块头指针。
struct buffer_head * get_hash_table(int dev, int block)
{
	struct buffer_head * bh;

	for (;;) {
	// 在高速缓冲中寻找给定设备和指定块的缓冲区块,如果没有找到则返回NULL,退出。
		if (!(bh=find_buffer(dev,block)))
			return NULL;
	// 对该缓冲块增加引用计数,并等待该缓冲块解锁(如果已被上锁)。由于经过了睡眠状态,
	// 因此有必要再验证该缓冲块的正确性,并返回缓冲块头指针。	
		bh->b_count++;
		wait_on_buffer(bh);
		if (bh->b_dev == dev && bh->b_blocknr == block)
			return bh;
	// 如果在睡眠时该缓冲块所属的设备号或块号发生了改变,则撤消对它的引用计数,重新寻找。		
		bh->b_count--;
	}
}

/*
 * OK,下面是getblk函数,该函数的逻辑并不是很清晰,同样也是因为要考虑
 * 竞争条件问题。其中大部分代码很少用到,(例如重复操作语句),因此它应该
 * 比看上去的样子有效得多。
 *
 * 算法已经作了改变:希望能更好,而且一个难以琢磨的错误已经去除。
 */
// 下面宏用于同时判断缓冲区的修改标志和锁定标志,并且定义修改标志的权重要比锁定标志
// 大
#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock)

 取高速缓冲中指定的缓冲块。
// 检查指定(设备号和块号)的缓冲区是否已经在高速缓冲中。如果指定块已经在高速缓冲中,
// 则返回对应缓冲区头指针退出;如果不在,就需要在高速缓冲中设置一个对应设备号和块号的
// 新项。返回相应缓冲区头指针。
struct buffer_head * getblk(int dev,int block)
{
	struct buffer_head * tmp, * bh;

repeat:
 // 搜索hash表,如果指定块已经在高速缓冲中,则返回对应缓冲区头指针,退出。
	if (bh = get_hash_table(dev,block))
		return bh;
// 扫描空闲数据块链表,寻找空闲缓冲区。
// 首先让tmp指向空闲链表的第一个空闲缓冲区头。		
	tmp = free_list;
	do {
// 如果该缓冲区正被使用(引用计数不等于0),则继续扫描下一项。对于b_count=0的块,
// 即高速缓冲中当前没有引用的块不一定就是干净的(b_dirt=0)或没有锁定的(b_lock=0)。
// 因此,我们还是需要继续下面的判断和选择。例如当一个任务改写过一块内容后就释放了,
// 于是该块b_count = 0,但b_lock不等于0;当一个任务执行 breada()预读几个块时,只要
// ll_rw_block()命令发出后,它就会递减b_count;但此时实际上硬盘访问操作可能还在进行,
// 因此此时b_lock=1,但b_count=0。
		if (tmp->b_count)
			continue;
// 如果缓冲头指针bh为空,或者tmp所指缓冲头的标志(修改、锁定)权重小于bh头标志的权
// 重,则让bh指向tmp缓冲块头。 如果该tmp缓冲块头表明缓冲块既没有修改也没有锁定标
// 志置位,则说明已为指定设备上的块取得对应的高速缓冲块,则退出循环。否则我们就继续
// 执行本循环,看看能否找到一个BADNESS()最小的缓冲快。			
		if (!bh || BADNESS(tmp)<BADNESS(bh)) {
			bh = tmp;
			if (!BADNESS(tmp))
				break;
		}
/* 重复操作直到找到适合的缓冲块 */
	} while ((tmp = tmp->b_next_free) != free_list);
	
	// 如果循环检查发现所有缓冲块都正在被使用(所有缓冲块的头部引用计数都>0)中,则睡眠
    // 等待有空闲缓冲块可用。当有空闲缓冲块可用时本进程会被明确地唤醒。然后我们就跳转到
    // 函数开始处重新查找空闲缓冲块。
	if (!bh) {
		sleep_on(&buffer_wait);
		goto repeat;
	}
	
	// 执行到这里,说明我们已经找到了一个比较适合的空闲缓冲块了。于是先等待该缓冲区解锁
    //(如果已被上锁的话)。如果在我们睡眠阶段该缓冲区又被其他任务使用的话,只好重复上述
    // 寻找过程。
	wait_on_buffer(bh);
	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;
	}
    /* 注意!!当进程为了等待该缓冲块而睡眠时,其他进程可能已经将该缓冲块 */
     * 加入进高速缓冲中,所以我们也要对此进行检查。*/
    // 在高速缓冲hash表中检查指定设备和块的缓冲块是否乘我们睡眠之即已经被加入进去。如果
    // 是的话,就再次重复上述寻找过程。
	if (find_buffer(dev,block))
		goto repeat;
    /* OK,最终我们知道该缓冲块是指定参数的唯一一块,而且目前还没有被占用 */
    /* (b_count=0),也未被上锁(b_lock=0),并且是干净的(未被修改的)*/
    // 于是让我们占用此缓冲块。置引用计数为1,复位修改标志和有效(更新)标志。
	bh->b_count=1;
	bh->b_dirt=0;
	bh->b_uptodate=0;
	
	// 从hash队列和空闲块链表中移出该缓冲区头,让该缓冲区用于指定设备和其上的指定块。
    // 然后根据此新的设备号和块号重新插入空闲链表和hash队列新位置处。并最终返回缓冲
    // 头指针。
	remove_from_queues(bh);
	bh->b_dev=dev;
	bh->b_blocknr=block;
	insert_into_queues(bh);
	return bh;
}
 释放指定缓冲块。
// 等待该缓冲块解锁。然后引用计数递减1,并明确地唤醒等待空闲缓冲块的进程。
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);
}

/*
 * 从设备上读取指定的数据块并返回含有数据的缓冲区。如果指定的块不存在
 * 则返回NULL。
 */
 从设备上读取数据块。
// 该函数根据指定的设备号dev和数据块号block,首先在高速缓冲区中申请一块缓冲块。
// 如果该缓冲块中已经包含有有效的数据就直接返回该缓冲块指针,否则就从设备中读取
// 指定的数据块到该缓冲块中并返回缓冲块指针。
struct buffer_head * bread(int dev,int block)
{
	struct buffer_head * bh;
    // 在高速缓冲区中申请一块缓冲块。如果返回值是NULL,则表示内核出错,停机。然后我们
    // 判断其中是否已有可用数据。 如果该缓冲块中数据是有效的(已更新的)可以直接使用,
    // 则返回。
	if (!(bh=getblk(dev,block)))
		panic("bread: getblk returned NULL\n");
	if (bh->b_uptodate)
		return bh;
	// 否则我们就调用底层块设备读写ll_rw_block()函数,产生读设备块请求。然后等待指定
    // 数据块被读入,并等待缓冲区解锁。在睡眠醒来之后,如果该缓冲区已更新,则返回缓冲
    // 区头指针,退出。否则表明读设备操作失败,于是释放该缓冲区,返回NULL,退出。	
	ll_rw_block(READ,bh);
	wait_on_buffer(bh);
	if (bh->b_uptodate)
		return bh;
	brelse(bh);
	return NULL;
}
 复制内存块。
// 从from地址复制一块(1024字节)数据到to位置。
#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一次读四个缓冲块数据读到内存指定的地址处。它是一个完整的函数,
 * 因为同时读取四块可以获得速度上的好处,不用等着读一块,再读一块了。
 */
 读设备上一个页面(4个缓冲块)的内容到指定内存地址处。
// 参数address是保存页面数据的地址;dev是指定的设备号;b[4]是含有4个设备数据块号
// 的数组。该函数仅用于mm/memory.c文件的do_no_page()函数中(第386行)。
void bread_page(unsigned long address,int dev,int b[4])
{
	struct buffer_head * bh[4];
	int i;
    // 该函数循环执行4次,根据放在数组b[]中的4个块号从设备dev中读取一页内容放到指定
    // 内存位置 address处。 对于参数b[i]给出的有效块号,函数首先从高速缓冲中取指定设备
    // 和块号的缓冲块。如果缓冲块中数据无效(未更新)则产生读设备请求从设备上读取相应数
    // 据块。对于b[i]无效的块号则不用去理它了。因此本函数其实可以根据指定的b[]中的块号
    // 随意读取1—4个数据块。
	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个缓冲块上的内容顺序复制到指定地址处。在进行复制(使用)缓冲块之前我们
    // 先要睡眠等待缓冲块解锁(若被上锁的话)。另外,因为可能睡眠过了,所以我们还需要
    // 在复制之前再检查一下缓冲块中的数据是否是有效的。复制完后我们还需要释放缓冲块。		
	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可以象bread一样使用,但会另外预读一些块。该函数参数列表
 * 需要使用一个负数来表明参数列表的结束。
 */
 从指定设备读取指定的一些块。
// 函数参数个数可变,是一系列指定的块号。成功时返回第1块的缓冲块头指针,否则返回
// NULL。
struct buffer_head * breada(int dev,int first, ...)
{
	va_list args;
	struct buffer_head * bh, *tmp;
    // 首先取可变参数表中第1个参数(块号)。接着从高速缓冲区中取指定设备和块号的缓冲
    // 块。如果该缓冲块数据无效(更新标志未置位),则发出读设备数据块请求。
	va_start(args,first);
	if (!(bh=getblk(dev,first)))
		panic("bread: getblk returned NULL\n");
	if (!bh->b_uptodate)
		ll_rw_block(READ,bh);
	// 然后顺序取可变参数表中其他预读块号,并作与上面同样处理,但不引用。注意,336行上
    // 有一个bug。其中的bh应该是tmp。这个bug直到在0.96版的内核代码中才被纠正过来。
    // 另外,因为这里是预读随后的数据块,只需读进高速缓冲区但并不马上就使用,所以第337
    // 行语句需要将其引用计数递减释放掉该块(因为getblk()函数会增加缓冲块引用计数值)。	
	while ((first=va_arg(args,int))>=0) {
		tmp=getblk(dev,first);
		if (tmp) {
			if (!tmp->b_uptodate)
				ll_rw_block(READA,bh);// bh 应该是tmp。
			tmp->b_count--;// 暂时释放掉该预读块。
		}
	}
	// 此时可变参数表中所有参数处理完毕。于是等待第1个缓冲区解锁(如果已被上锁)。在等
    // 待退出之后如果缓冲区中数据仍然有效,则返回缓冲区头指针退出。否则释放该缓冲区返回
    // NULL,退出。
	va_end(args);
	wait_on_buffer(bh);
	if (bh->b_uptodate)
		return bh;
	brelse(bh);
	return (NULL);
}
 缓冲区初始化函数。
// 参数buffer_end是缓冲区内存末端。对于具有16MB内存的系统,缓冲区末端被设置为4MB。
// 对于有8MB内存的系统,缓冲区末端被设置为2MB。该函数从缓冲区开始位置start_buffer
// 处和缓冲区末端buffer_end处分别同时设置(初始化)缓冲块头结构和对应的数据块。直到
// 缓冲区中所有内存被分配完毕。参见程序列表前面的示意图。
void buffer_init(long buffer_end)
{
	struct buffer_head * h = start_buffer;
	void * b;
	int i;
    // 首先根据参数提供的缓冲区高端位置确定实际缓冲区高端位置b。如果缓冲区高端等于1Mb,
    // 则因为从640KB - 1MB被显示内存和 BIOS占用,所以实际可用缓冲区内存高端位置应该是
    // 640KB。否则缓冲区内存高端一定大于1MB。
	if (buffer_end == 1<<20)
		b = (void *) (640*1024);
	else
		b = (void *) buffer_end;
		
    // 这段代码用于初始化缓冲区,建立空闲缓冲块循环链表,并获取系统中缓冲块数目。操作的
    // 过程是从缓冲区高端开始划分1KB大小的缓冲块,与此同时在缓冲区低端建立描述该缓冲块
    // 的结构buffer_head,并将这些buffer_head组成双向链表。
    // h是指向缓冲头结构的指针,而h+1是指向内存地址连续的下一个缓冲头地址,也可以说是
    // 指向h缓冲头的末端外。为了保证有足够长度的内存来存储一个缓冲头结构,需要b所指向
    // 的内存块地址 >= h缓冲头的末端,即要求 >= h+1。		
	while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
		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;		// 指向具有相同hash值的下一个缓冲头。
		h->b_prev = NULL;		// 指向具有相同hash值的前一个缓冲头。
		h->b_data = (char *) b;	// 指向对应缓冲块数据块(1024字节)。
		h->b_prev_free = h-1;	// 指向链表中前一项。
		h->b_next_free = h+1;	// 指向链表中下一项。
		h++;					// h指向下一新缓冲头位置。
		NR_BUFFERS++;			// 缓冲区块数累加。
		if (b == (void *) 0x100000)		// 若b递减到等于1MB,则跳过384KB,
			b = (void *) 0xA0000;		// 让b指向地址0xA0000(640KB)处。
	}
	h--;	// 让h指向最后一个有效缓冲块头。					
	free_list = start_buffer;		// 让空闲链表头指向头一个缓冲块。
	free_list->b_prev_free = h;		// 链表头的b_prev_free指向前一项(即最后一项)。
	h->b_next_free = free_list;		// h的下一项指针指向第一项,形成一个环链。
	// 最后初始化hash表(哈希表、散列表),置表中所有指针为NULL。
	for (i=0;i<NR_HASH;i++)
		hash_table[i]=NULL;
}	

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值