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,
0000810 fffe ffff ffff ffff ffff ffff ffff ffff //0xfffe(1111 1111 1111 1110)改为反字节序(0111 1111 1111 1111),因此总共128个inode节点,对应这里的128个比特位,这里只用了一个inode,其他都未用(1000 0000 ...)
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; //空闲表后一缓冲块
};
源码
![](https://img-blog.csdnimg.cn/img_convert/8ba7d61204f55c4a5d4f1998a7897f9c.gif)
![](https://img-blog.csdnimg.cn/img_convert/29f82bb5226abdd4b2ad59403706a03b.gif)
/*
* 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
待补充