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,
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; //空闲表后一缓冲块
};

源码

/*
 *  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;
}    
buffer.c

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

待补充

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值