kernel源码(二十四)文件系统
1 minix文件系统
1.1 磁盘结构
minix文件系统磁盘结构如下所示

图中,整个磁盘被划分为360个磁盘块(每个磁盘块1Kb)
- 引导块,MBR就在这个磁盘块中。当计算机加电,ROM BIOS将会自动读取该磁盘块到内存并执行其中的代码。
- 分区,一块磁盘,我们可以最多有4个主分区。MBR大小为一个扇区大小,其中446byte存放开机管理程序,64byte存放分区表,每个分区使用16byte,因此可以有4个分区。各个分区可以不同的文件系统,各个分区都有单独的超级块、inode。
- 超级块,用于存放文件系统结构的信息,比如inode/block总量,使用量、剩余量、该分区文件系统格式等。linux中使用super_block[8]来存放超级块。因此最多加载8个文件系统。
- inode位图,用于说明i节点是否被使用,一个inode块共可表示1024*8=8192个i节点的使用情况。最多可有8个inode块,因此共可表示65535个i节点的使用情况,存储在s_imap[8]中
- block位图,逻辑块位图,用于表示该磁盘分区中每个逻辑块的使用情况,存储在s_zmap[8]中,因此共可表示65535个逻辑块的使用情况,也就是64M磁盘空间。
- inode,记录文件的属性,一个文件占用一个inode,上述只画出了4个inode块。
- block盘块,实际记录文件的内容,若文件太大,会占用多个block
下面对各个类型的块对应的结构体做一下说明
1.1.1 super_block
定义在fs.h中
struct super_block { unsigned short s_ninodes; //该文件系统中,inode总数 unsigned short s_nzones; //该文件系统中,block块总数 unsigned short s_imap_blocks; //inode位图所占用的磁盘块数 unsigned short s_zmap_blocks; //znode位图所占用的磁盘块数 unsigned short s_firstdatazone; //数据区开始处的磁盘块号(从0开始计数) unsigned short s_log_zone_size; //log以2为底数表示的逻辑块所包含的磁盘块数 unsigned long s_max_size; //最大文件长度(单位字节) unsigned short s_magic; //文件系统魔数,用于指定是哪种文件系统 /* These are only in memory */ struct buffer_head * s_imap[8]; //inode位图所对应的内存缓冲区的缓冲块,最多有8个缓冲块,也就是8kb。 struct buffer_head * s_zmap[8]; //znode位图所对应的内存缓冲区的缓冲块,最多有8个缓冲块,也就是8kb,一个缓冲块可代表8192个盘块,因此该文件系统最大可表示65536个逻辑块,也就是64M磁盘。 unsigned short s_dev; struct m_inode * s_isup; struct m_inode * s_imount; unsigned long s_time; struct task_struct * s_wait; unsigned char s_lock; unsigned char s_rd_only; unsigned char s_dirt; };
1.1.2 inode结构体
struct m_inode { unsigned short i_mode; //文件的类型和属性(rwx位) unsigned short i_uid; //属主 unsigned long i_size; //文件长度(字节) unsigned long i_mtime; //修改时间 unsigned char i_gid; //属组 unsigned char i_nlinks; //链接数(有多少个文件目录项指向该i节点,比如对于目录,表示该目录下有多少个文件) unsigned short i_zone[9]; //文件所占用的盘上逻辑块号数组。其中0-6是直接块号,zone[7]是一次间接块号,zone[8]是二次间接块号。 /* these are in memory also */ struct task_struct * i_wait; //等待该i节点的进程 unsigned long i_atime; //最后访问时间 unsigned long i_ctime; //i节点自身被修改时间 unsigned short i_dev; //i节点所在设备号 unsigned short i_num; //i节点号 unsigned short i_count; //i节点被引用的次数,0表示空闲 unsigned char i_lock; //锁定标志 unsigned char i_dirt; //脏标志 unsigned char i_pipe; //i节点用作管道标志 unsigned char i_mount; //i节点安装了其他文件系统标志 unsigned char i_seek; unsigned char i_update; //i节点已更新标志 };
1.1.3 inode的直接块和间接块
上面讲的m_inode结构体中,字段i_zone[9]用于存放文件所占用的逻辑块号,其结构如下所示。
当文件小于等于7Kb时,只需要7个直接块号就够了
当文件大于7Kb而小于519Kb(7+512)时,就需要用到一次间接块号,存储在i_zone[7]中,其指向一个逻辑块(因为存储一个short类型的块号需要2字节,因此一个逻辑块能够存储512个块号)
当文件大于519Kb时,就需要用到二次间接块号,存储在i_zone[8]中,可存储512*512个块号
因此minix文件系统支持的最大文件为7kb+512kb+256Mb≈256M,也就是说单文件不能超过256M

1.1.4 znode位图
逻辑块位图用于描述该文件系统中每个数据盘块的使用情况,第一个比特位(位0)不用且置1。需要注意的是,比特位1代表数据区中第一个数据盘块,而非磁盘的第一个盘块(引导块)。
1.2 文件系统目录项结构体
这里介绍目录项的结构体
struct dir_entry { unsigned short inode; char name[14]; };
每一个目录项有一个name字段用于指定目录名字(14字节),还有一个2字节的inode号。
在打开一个文件时(比如/usr/bin/vi),文件系统首先会找到编号为1的inode块(/),根据上面inode结构体中的i_zone[]数组可以找到数据块,该数据块中存放着目录/的目录项结构体dir_entry,因此我们可以找到usr对应的inode节点号。同样的道理,我们根据inode结构体中的i_zone[]数组可以找到数据块,该数据块中存放着目录usr的目录项结构体dir_entry。这样一直找到vi这个文件对应的数据块。

下面演示一个例子,查看目录数据块的内容
使用bochs启动一个模拟的系统(参考https://www.cnblogs.com/zhenjingcool/p/17581470.html 2.3 硬盘启动 小节)
上面我们已经讲过,目录项结构体中,2字节表示inode节点号,14字节表示文件名。下面hexdump命令的输出,我们可以对照ascii码查看。比如0x2e(.),0x2e2e(..),0x69626e(bin)

在linux中,目录也是一个文件,上面我们查看的是/文件的数据块(一个数据块1kb大小)的内容。上述数据块只使用了1024字节中的160字节(10个文件).
1.3 360k软盘文件系统分析
这里,我们在centos下创建一个minix文件系统镜像,并对这个文件系统进行分析
首先创建一个minix.img文件,该文件块大小是1kb,总共有360个块
[root@localhost ~]# dd if=/dev/zero of=minix.img bs=1K count=360 360+0 records in 360+0 records out 368640 bytes (369 kB) copied, 0.000730513 s, 505 MB/s [root@localhost ~]#
然后,使用mkfs.minix命令创建minix文件系统,并写入minix.img
[root@localhost ~]# mkfs.minix minix.img 128 inodes 360 blocks Firstdatazone=8 (8) Zonesize=1024 Maxsize=268966912
从输出,我们也可以看出,该文件系统有128个inode,360个block,第一个数据块是序号8处开始的块
然后我们使用hexdump命令查看这个文件系统文件
[root@localhost ~]# hexdump minix.img 0000000 0000 0000 0000 0000 0000 0000 0000 0000 //引导块都是空的,说明没有引导块,保留 * 0000400 0080 0168 0001 0001 0008 0000 1c00 1008 //对应超级块结构体d_super_block查看,见下面分析 0000410 138f 0001 0000 0000 0000 0000 0000 0000 0000420 0000 0000 0000 0000 0000 0000 0000 0000 * 0000800 0003 0000 0000 0000 0000 0000 0000 0000 //对应inode位图,需要注意的是,这里字节序是反向的,也就是0x0003(0000 0000 0000 0011)要变为(1100 0000 0000 0000)。一个bit位表示一个i节点是否被使用,总共有128个inode,第一个bit位不用且要置1,因此该行共表示127个inode位图节点 0000810 fffe ffff ffff ffff ffff ffff ffff ffff //0xfffe(1111 1111 1111 1110)改为反字节序(0111 1111 1111 1111),上一行表示127个inode节点,还缺一个,也就是这一行中的第一个bit。这里只用了一个inode,其他都未用(0111 1111 ...注:虽然后面bit都是1但是我么不用去管它,super块中已规定只用128个bit表示128个inode的使用情况) 0000820 ffff ffff ffff ffff ffff ffff ffff ffff * 0000c00 0003 0000 0000 0000 0000 0000 0000 0000 //对应znode位图,注意反字节序的问题,对应二进制数为(11 350个0 11...)。从上图中可以看到,前8个block分别存放启动块/super块/inode位图/znode位图/4个inode。因此数据块从第9个(序号从0开始的第8个) 0000c10 0000 0000 0000 0000 0000 0000 0000 0000 //,且第一个bit位不用置1,因此我们可以看到,这里只用了一个数据块(存放根路径/),其他351个数据块未用 0000c20 0000 0000 0000 0000 0000 0000 fffe ffff 0000c30 ffff ffff ffff ffff ffff ffff ffff ffff * 0001000 41ed 0000 0040 0000 1b6c 64c5 0200 0008 //inode块,见下面分析 0001010 0000 0000 0000 0000 0000 0000 0000 0000 * 0002000 0001 002e 0000 0000 0000 0000 0000 0000 //第一块数据,也就是文件/。目前它下面就两个文件:.和.. 。第一个文件信息(inode=1,文件名:ascii码0x2e对应的字符是.,也就是/路径下的.文件) 0002010 0000 0000 0000 0000 0000 0000 0000 0000 //inode=0表示未使用 0002020 0001 2e2e 0000 0000 0000 0000 0000 0000 //第二个文件信息(inode=1,文件名:ascii码0x2e2e对应的字符是..,也就是/路径下的..文件) 0002030 0000 0000 0000 0000 0000 0000 0000 0000 //其他行,inode都为0,表示未使用 0002040 0000 622e 6461 6c62 636f 736b 0000 0000 0002050 0000 0000 0000 0000 0000 0000 0000 0000 * 005a000 [root@localhost ~]#

超级块
我们对应超级块结构体查看
struct d_super_block { unsigned short s_ninodes; //0x0080,十进制128,inode总共128个 unsigned short s_nzones; //0x0168,十进制360,总共360个zone unsigned short s_imap_blocks; //0x0001,十进制1,inode位图占1个块 unsigned short s_zmap_blocks; //0x0001,十进制1,zone位图占1个块 unsigned short s_firstdatazone;//0x0008,十进制8,第一个数据区编号是8 unsigned short s_log_zone_size;//0x0000,log表示的一块数据大小,1kb unsigned long s_max_size; //0x10081c00,十进制268966912,最大文件长度 unsigned short s_magic; //0x138f,minix魔数 };
inode块
inode块占了4个块(因为这个文件系统里面没有文件,只有.和..文件,因此只有4个inode块),但是只有第一个块被使用。一个文件对应一个inode,这里被使用的inode对应的就是根目录"/"。
0001000 41ed 0000 0040 0000 1b6c 64c5 0200 0008 0001010 0000 0000 0000 0000 0000 0000 0000 0000 *
inode的结构体如下所示
struct m_inode { unsigned short i_mode; //0x41ed,040755, 目录文件, rwxr-xr-x unsigned short i_uid; //0x0000, 不属于任何用户,因为我们只是创建了一个minix文件系统,还没有创建用户 unsigned long i_size; // unsigned long i_mtime; // unsigned char i_gid; // unsigned char i_nlinks; // unsigned short i_zone[9]; //0x08,i_zone[0]=8,数据块在第8号区块 };
1.4 高速缓冲区buffer
高速缓冲区是文件系统访问块设备的必经之路。
高速缓冲区位于主内存中,是专门在主存中划出一部分内存用于高速缓冲区。
当需要把数据写到块设备中时,首先会写到buffer中,然后再在合适的时机将buffer中的数据刷到磁盘。
当需要从块设备读取数据时,首先会在buffer中读取,如果buffer中没有才会发送读块设备的命令去硬盘获取数据,然后放入buffer中。
2 buffer.c
buffer.c用于对高速缓冲区进行操作和管理。

高速缓冲区起始位置为内核模块末端end标号处。整个高速缓冲区被划分为1kb大小的数据块,和块设备上的逻辑块大小相同。
缓冲区起始位置处被初始化为buffer_head结构,并组成一个双向链表(头指针为free_list),。

buffer_head结构体的定义如下所示
struct buffer_head { char * b_data; /* pointer to data block (1024 bytes) 指向该缓冲头对应的缓冲块*/ unsigned long b_blocknr; /* block number 块号*/ unsigned short b_dev; /* device (0 = free) 块设备号*/ unsigned char b_uptodate; //更新标志,表示数据是否已更新 unsigned char b_dirt; /* 0-clean,1-dirty 修改标志*/ unsigned char b_count; /* users using this block 使用该块的用户数*/ 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; //空闲表后一缓冲块 };
源码
/* * 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; struct buffer_head * hash_table[NR_HASH]; 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) 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; } 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); if (bh->b_dev == dev && bh->b_dirt) ll_rw_block(WRITE,bh); } 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; } 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)] 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; } static inline void insert_into_queues(struct buffer_head * bh) { /* put at end of free list */ 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 */ 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; 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. */ 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); 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) struct buffer_head * getblk(int dev,int block) { struct buffer_head * tmp, * bh; repeat: if (bh = get_hash_table(dev,block)) return bh; tmp = free_list; do { if (tmp->b_count) 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); 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; } /* 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); 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); 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. */ void bread_page(unsigned long address,int dev,int b[4]) { struct buffer_head * bh[4]; int i; 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; 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. */ struct buffer_head * breada(int dev,int first, ...) { va_list args; struct buffer_head * bh, *tmp; va_start(args,first); if (!(bh=getblk(dev,first))) panic("bread: getblk returned NULL\n"); if (!bh->b_uptodate) ll_rw_block(READ,bh); while ((first=va_arg(args,int))>=0) { tmp=getblk(dev,first); if (tmp) { if (!tmp->b_uptodate) ll_rw_block(READA,bh); tmp->b_count--; } } va_end(args); wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return (NULL); } void buffer_init(long buffer_end) { struct buffer_head * h = start_buffer; void * b; int i; if (buffer_end == 1<<20) b = (void *) (640*1024); else b = (void *) buffer_end; 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; h->b_prev = NULL; h->b_data = (char *) b; h->b_prev_free = h-1; h->b_next_free = h+1; h++; NR_BUFFERS++; if (b == (void *) 0x100000) b = (void *) 0xA0000; } h--; free_list = start_buffer; free_list->b_prev_free = h; h->b_next_free = free_list; for (i=0;i<NR_HASH;i++) hash_table[i]=NULL; }
2.1 缓冲区的初始化
首先,我们看一下缓冲区的初始化,缓冲区的初始话调用函数是buffer_init()函数,这个函数在main.c中被调用
void main(void) { mem_init(main_memory_start,memory_end); trap_init(); blk_dev_init(); chr_dev_init(); tty_init(); time_init(); sched_init(); buffer_init(buffer_memory_end); //参数buffer_memory_end表示缓冲区的末端,对于16M内存,末端被设置成4Mb,对于8Mb内存的系统,缓冲区末端被设置为2Mb。 hd_init(); floppy_init(); sti(); move_to_user_mode(); if (!fork()) { /* we count on this going ok */ init(); } for(;;) pause(); }
缓冲区初始化示意图如下所示,缓冲区初始化过程为:从缓冲区开始start_buffer处和缓冲区末端buffer_end处分别设置缓冲块头结构和对应的数据块,直到缓冲区被分配完成

下面我们看一下buffer_init函数,该函数从缓冲区开始start_buffer处和缓冲区末端buffer_end处分别设置缓冲块头结构和对应的数据块,直到缓冲区被分配完成。其示意图见代码上面
void buffer_init(long buffer_end) //参数buffer_end是缓冲区内存末端。 { struct buffer_head * h = start_buffer; //其中start_buffer在文件开始处定义,即:extern int end; //end为内核代码结尾处 struct buffer_head * start_buffer = (struct buffer_head *) &end; //start_buffer为内核代码结为处的第一个buffer_header结构体 void * b; int i; if (buffer_end == 1<<20) //如果缓冲区高端等于1Mb,因为640kb-1Mb被显示内存和BIOS占用,所以实际可用缓冲区内存高端位置应该是640kb b = (void *) (640*1024); else b = (void *) buffer_end; 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; h->b_prev = NULL; h->b_data = (char *) b; h->b_prev_free = h-1; h->b_next_free = h+1; h++; NR_BUFFERS++; if (b == (void *) 0x100000) b = (void *) 0xA0000; } h--; free_list = start_buffer; free_list->b_prev_free = h; h->b_next_free = free_list; for (i=0;i<NR_HASH;i++) hash_table[i]=NULL; }
2.2 bread()、breada()、bread_page()
这三个函数都是缓冲块读取函数,他们都会进一步调用getblk()函数。
brelse()用于释放缓冲块

其中,我们定义了一个hash_table,共可存储307个buffer_head
struct buffer_head * hash_table[307]; #define _hashfn(dev,block) (((unsigned)(dev^block))%307) //hash函数,设备号和块号异或然后再和307取余。 #define hash(dev,block) hash_table[_hashfn(dev,block)]
并且在remove_from_queues()、insert_into_queues()、find_buffer()等函数中设置这个hash_table以及从hash_table中取值
动态变化的hash_table结构变化如下图所示,我们可以清楚的看到,buffer_head被组织成两个维度的链表:其一是free_list,其二是hash_table,hash_table的作用是能够根据设备号和块号通过hash函数快速的定位到这个buffer_head
图中虚线表示缓冲区中所有缓冲块组成一个双向循环链表。free_list是头指针
双向箭头表示散列在同一hash项中的所有缓冲块组成一个双向链表

下面我们看一下bread()代码。该函数根据设备号和块号获取buffer_head。
该函数的核心是getblk(dev,block)函数,见后面介绍
struct buffer_head * bread(int dev,int block) { struct buffer_head * bh; if (!(bh=getblk(dev,block))) //getblk()函数作用是获取适合的空闲缓冲块 panic("bread: getblk returned NULL\n"); if (bh->b_uptodate) return bh; ll_rw_block(READ,bh); wait_on_buffer(bh); if (bh->b_uptodate) return bh; brelse(bh); return NULL; }
其执行过程也可参考下面的流程图

2.3 getblk()
该函数首先调用get_hash_table函数,在hash表队列中搜索指定设备号和逻辑块号是否已经存在。
如果已经存在,则立即返回该buffer_head;
如果不存在,则从free_list头开始对free_list进行扫描,寻找一个空闲缓冲块。具体过程为:从free_list头开始寻找,并且对各个空闲缓冲块做比较,根据b_dirt和b_lock组合而成的权值,比较哪个空闲块最合适。若找到的空闲块既没有被锁定也没有被修改,则停止寻找
若没有找到空闲块,则让当前进程进入睡眠状态,待继续执行时继续寻找
若该空闲块被锁定,则进程也需进入睡眠,等待其他进程解锁
若在睡眠等待过程中该缓冲块又被其他进程占用,则再重新从头开始搜索缓冲块;否则判断该缓冲块是否被修改过,若是,则将该块写盘,并等待该块解锁。如果此时该块又被其他进程占用,那么再从头开始执行getblk()。
在经历以上过程后,有可能在我们睡眠时,其他进程已经将我们所需要的缓冲块加进了hash队列中,因此,这里需要最后一次搜索一下hash队列。如果真的在hash队列中找到了我们所需要的缓冲块,那么我们又需要对该缓冲块做上面判断处理,因此又一次开始从头执行getblk()函数。
最后,我们找到了一块没有被进程使用、没有上锁、干净的空闲缓冲块。我们将该块引用次数置1,并复位其他几个标志,然后从空闲表中移除该块对应的buffer_head。在设置了该缓冲块所属的设备号和逻辑块号后,再将其插入hash表对应的表项首部,并链接到空闲队列末尾
最后返回这个buffer_head。
以上也就是最近最少使用算法(LRU算法)
上面的过程比较复杂,我们可以简单看一下,重要的是理解如下几点:
- 在缓冲区中,缓冲块头(buffer_head)被组织成两个数据结构,hash_table和free_list。hash_table的hash函数是根据设备号和块号计算的,free_list的初始化是在buffer_init()中进行的
- 某一时刻,某个块可能已经被读到了缓冲区中,也就是说在hash_table中;但是可能还有些块从未被使用过,这些没用过的缓冲块只存在于free_list中,不存在于hash_table中
- 当某个进程需要读某个设备的某个块时,首先从缓冲区的hash_table中找,找到就返回,找不到就在free_list中找,直到找到一个空闲块,然后将这个空闲块从free_list中删除,并且给字段赋值,然后方到hash_table中,同时放到free_list最后面。
#define BADNESS(bh) (((bh)->b_dirt<<1)+(bh)->b_lock) struct buffer_head * getblk(int dev,int block) { struct buffer_head * tmp, * bh; repeat: if (bh = get_hash_table(dev,block)) return bh; tmp = free_list; do { if (tmp->b_count) 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); 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; } /* 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); return bh; }
2.3 其他一些函数
等待指定缓冲块解锁。
如果指定缓冲块b_lock字段置位,说明该缓冲块已被加锁。此时如果进程访问该缓冲块,则把该进程放入该缓冲块的b_wait队列中并且等待该缓冲块解锁。
static inline void wait_on_buffer(struct buffer_head * bh) { cli(); while (bh->b_lock) sleep_on(&bh->b_wait); sti(); }
2.4 数据同步的保证
- 数据结构信息与高速缓冲区中的缓冲块同步问题,由驱动程序独立负责
- 高速缓冲区中数据块与磁盘对应块的同步问题,由缓冲管理程序负责

3 bitmap.c
待补充
浙公网安备 33010602011771号