Minix 文件信息及其数据的定位

来源:互联网 发布:大型网络3d手游2016年 编辑:程序博客网 时间:2024/06/01 07:18

原文:http://hi.baidu.com/heyinjie/item/54cdc7507026209508be17b7


文件信息及其数据的定位

在Minix 1.0文件系统中,一个文件可以有几个不同的文件名,这是由dir_entry决定的。多个dir_entry可以关联同一个文件,但同一个文件只能对应一个索引节点,所以最终系统需要依靠索引节点来描述一个文件。文件最重要的属性是实际数据,通常文件都存放在外部存储器上,要读写指定文件,必须知道文件数据在外存,如磁盘上的分布位置等情况。

一个特定大小的文件通常被分成同样大小的块,连续或不连续的存储在磁盘上,这样的块也叫磁盘逻辑块。Minix 1.0文件系统中,一个块的大小,即block的大小定义为1KB,占用两个连续的磁盘扇区。在同一个设备中,每个块有唯一的逻辑块号,所以只要利用设备号和逻辑块号做一定的转换,便可映射到磁盘的三维坐标中。随后的读写操作就变成基本的写磁盘端口和中断处理了。

       纵观系统,有两个跟索引节点有关的数据结构,列出如下:

struct d_inode {

     unsigned short i_mode;

     unsigned short i_uid;

     unsigned long i_size;

     unsigned long i_time;

     unsigned char i_gid;

     unsigned char i_nlinks;

     unsigned short i_zone[9];

};

 

struct m_inode {

     unsigned short i_mode;

     unsigned short i_uid;

     unsigned long i_size;

     unsigned long i_mtime;

     unsigned char i_gid;

     unsigned char i_nlinks;

     unsigned short i_zone[9];

/* these are in memory also */

     struct task_struct * i_wait;

     unsigned long i_atime;

     unsigned long i_ctime;

     unsigned short i_dev;

     unsigned short i_num;

     unsigned short i_count;

     unsigned char i_lock;

     unsigned char i_dirt;

     unsigned char i_pipe;

     unsigned char i_mount;

     unsigned char i_seek;

     unsigned char i_update;

};

d_inode作为m_inode的前半部分。实际上,d_inode是存储在物理磁盘上的,每个d_inode占用32个字节,故一个block可存放32个d_inode。正是因为磁盘上的d_inode描述着文件的具体信息,如大小,位置,创建时间等,所以一般文件的定位或创建实际上必须读写磁盘上d_inode。前面说了,在指定设备上,我们可以通过逻辑块号来定位文件在磁盘上的实际数据,那么具体是怎么实现的呢?仔细观察不难发现,d_inode中,有i_zone[9]这个域,其中存放的正是存有文件数据的块在磁盘上的逻辑块号。i_zone[0]到i_zone[6]中存放的块号所指向的块,直接用作文件数据存储,我称之为直接块,而i_zone[7]和i_zone[8]分别用作一级间接块和二级间接块寻址。具体得说,i_zone[7]指向的块里存放的不是文件数据,而是512个直接块号。i_zone[8]指向的块中,又含有512个同i_zone[7]一样的一级间接块号,每个间接块号又指向一个含有512个直接块号的块。所以Minix 1.0文件系统理论上最大支持(7+512+512*512)K大小的文件(但实际上由于块号只占两个字节,所以达不到这么大)。

    上面说明了如何根据d_inode定位文件数据,但由于d_inode本身也保存磁盘上,其自身的定位便成了问题。假如在磁盘绝对位置写一个根d_inode,以此获得其他d_inode所在的块,可以想到两种方法:1.保证根inode的每个直接数据块对应一个d_inode,这样的逻辑看似简单,但时空效率都很低,同时也限制了磁盘上的文件数目,带来很多问题,所以该方法不可取;2.保证通过根inode索引的每个直接数据块里存放着连续多个d_inode数据,这样每个块最多存放32个d_inode。此时如果用索引节点号作为i_zone数组(包括直接和间接)索引也能方便得映射到某个文件的d_inode,但是此方法无法描述索引节点的使用情况。当一个文件被用户删除后,该文件的对应的d_inode无法描述这种状况,同时这也带来了磁盘上用过的d_inode无法重用的问题,这样每创建一个新文件,都要在新的位置写入d_inode。也许可以通过在d_inode中添加域来解决这个问题,但这同时带来了新的问题,比如修改后的d_inode必须扩充到64字节才能被块的大小整除,扩充或不扩充都会带来磁盘乃至内存空间的浪费。另一个问题是,即使添加一个标识状态的域,想查找一个没有被使用的d_inode,最坏的可能是遍历一遍磁盘上所有的d_inode,这样的效率显然是不可接受的。Minix 1.0文件系统没有使用根索引节点的算法,而采用了时空效率都很高效的位图搜索,于是,超级块派上了用场。系统中有关超级块的结构定义如下:

struct d_super_block {

     unsigned short s_ninodes;

     unsigned short s_nzones;

     unsigned short s_imap_blocks;

     unsigned short s_zmap_blocks;

     unsigned short s_firstdatazone;

     unsigned short s_log_zone_size;

     unsigned long s_max_size;

     unsigned short s_magic;

};

 

struct super_block {

     unsigned short s_ninodes;

     unsigned short s_nzones;

     unsigned short s_imap_blocks;

     unsigned short s_zmap_blocks;

     unsigned short s_firstdatazone;

     unsigned short s_log_zone_size;

     unsigned long s_max_size;

     unsigned short s_magic;

/* These are only in memory */

     struct buffer_head * s_imap[8];

     struct buffer_head * s_zmap[8];

     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;

};

super block和inode一样,在磁盘上以d_super_block的形式存储。磁盘的0号块存放引导信息,俗称MBR。磁盘的1号块开头处存放着d_super_block,故该块也被称为超级块。当磁盘被挂载时,超级块被读入内存高速缓冲区,再被复制到内核的super_block中。从2号块开始的连续s_imap_blocks个块,存放d_inode位图,从2+s_imap_block号块开始往后的s_zmap_blocks个连续块,存放逻辑块位图。位图数据以块的形式存放,在linux 0.11中,s_imap_block和s_zmap_blocks都等于8,所以两种位图各占8个块,每个块包含8192位,每位代表系统中唯一一个d_inode或磁盘逻辑块。当磁盘被挂载时,所有位图数据被依次读入系统缓冲区,super_block的s_imap和s_zmap维护这些缓冲区指针,位图缓冲区被读入系统后便不再释放。

 

MBR

super block

d_inode

位图

逻辑块

位图

d_inode

数据区

系统可分配的逻辑块区

 

由于定位索引节点的操作依赖于索引节点的创建,因此需要研究inode的创建过程:

struct m_inode * new_inode(int dev)

{

     struct m_inode * inode;

     struct super_block * sb;

     struct buffer_head * bh;

     int i,j;

 

     if (!(inode=get_empty_inode()))

         return NULL;

     if (!(sb = get_super(dev)))

         panic("new_inode with unknown device");

     j = 8192;

     for (i=0 ; i<8 ; i++)

         if ((bh=sb->s_imap[i]))

              if ((j=find_first_zero(bh->b_data))<8192)

                   break;

     if (!bh || j >= 8192 || j+i*8192 > sb->s_ninodes) {

         iput(inode);

         return NULL;

     }

     if (set_bit(j,bh->b_data))

         panic("new_inode: bit already set");

     bh->b_dirt = 1;

     inode->i_count=1;

     inode->i_nlinks=1;

     inode->i_dev=dev;

     inode->i_uid=current->euid;

     inode->i_gid=current->egid;

     inode->i_dirt=1;

     inode->i_num = j + i*8192;

     inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME;

     return inode;

}

该函数的算法比较简单,主要是先获得内核空间中可以使用的m_inode项,然后通过get_super(dev),获得已被读入内核中定义的超级块结构指针。接着遍历inode位图缓冲区,直到找到第一个未被置1的位,得到位号,并置位,其中涉及为操作的过程如下:

#define set_bit(nr,addr) ({\

register int res ; \

__asm__ __volatile__("btsl %2,%3\n\tsetb %%al": \

"=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \

res;})

 

#define find_first_zero(addr) ({ \

int __res; \

__asm__ __volatile__ ("cld\n" \

     "1:\tlodsl\n\t" \

     "notl %%eax\n\t" \

     "bsfl %%eax,%%edx\n\t" \

     "je2f\n\t" \

     "addl %%edx,%%ecx\n\t" \

     "jmp3f\n" \

     "2:\taddl $32,%%ecx\n\t" \

     "cmpl $8192,%%ecx\n\t" \

     "jl 1b\n" \

     "3:" \

     :"=c" (__res):"c" (0),"S" (addr)); \

__res;})

最后最重要的一句是inode->i_num = j + i*8192;

每个人都有身份证,inode->i_num 便是前面提到的索引节点的身份证,也称为索引节点号。该索引节点号等于dir_entry中的inode。至于通过索引节点号在磁盘上定位inode,可以查看以下函数:

static void read_inode(struct m_inode * inode)

{

     struct super_block * sb;

     struct buffer_head * bh;

     int block;

 

     lock_inode(inode);

     if (!(sb=get_super(inode->i_dev)))

         panic("trying to read inode without dev");

     block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks +

         (inode->i_num-1)/INODES_PER_BLOCK;

     if (!(bh=bread(inode->i_dev,block)))

         panic("unable to read i-node block");

     *(struct d_inode *)inode =

         ((struct d_inode *)bh->b_data)

              [(inode->i_num-1)%INODES_PER_BLOCK];

     brelse(bh);

     unlock_inode(inode);

}

其中,block为磁盘上存放该索引节点的逻辑块号,这里跳过磁盘开头的MBR和超级块,以及随后的两种位图区域,并加上当前d_inode在d_inode数据区的偏移块数。由此可见,inode数据区和d_inode位图基本上是一一对应的。由于0号d_inode或block在搜索算法中用来表示未被使用的状态(用法可以参考add_entry函数中的if (!de->inode)语句),所以两种位图区的第0位在被挂载后始终置1,不予使用,算法上也要减去1。

    以上的所有内容叙述了如何定位文件的inode,以及如何通过inode来定位文件在磁盘上的具体数据。但这又引来了另一个问题:系统是采用了何种机制把磁盘上的空闲的逻辑块分配给某个文件的?

    为解决这个问题,首先要明确的是,系统开头的MBR,超级块,以及随后的位图块和位图块后紧接着的d_inode数据区,不属于系统可分配的磁盘数据区范畴,否则,文件系统显然会陷入混乱。那么可分配数据区的起始块位置在哪里?还记得超级块里有一个域叫s_firstdatazone吗,其中记录了可分配数据区的起始块号,另一个域s_nzones则记录了可分配数据区的结束块号。由于系统必须知道数据块的使用情况,所以这里也使用了位图搜索,具体的方式和d_inode类似。同样,我们先看看block是如何被创建的,此过程如下:

#define clear_block(addr) \

__asm__ __volatile__ ("cld\n\t" \

     "rep\n\t" \

     "stosl" \

     ::"a" (0),"c" (BLOCK_SIZE/4),"D" ((long) (addr)))

 

int new_block(int dev)

{

     struct buffer_head * bh;

     struct super_block * sb;

     int i,j;

 

     if (!(sb = get_super(dev)))

         panic("trying to get new block from nonexistant device");

     j = 8192;

     for (i=0 ; i<8 ; i++)

         if ((bh=sb->s_zmap[i]))

              if ((j=find_first_zero(bh->b_data))<8192)

                   break;

     if (i>=8 || !bh || j>=8192)

         return 0;

     if (set_bit(j,bh->b_data))

         panic("new_block: bit already set");

     bh->b_dirt = 1;

     j += i*8192 + sb->s_firstdatazone-1;

     if (j >= sb->s_nzones)

         return 0;

     if (!(bh=getblk(dev,j)))

         panic("new_block: cannot get block");

     if (bh->b_count != 1)

         panic("new block: count is != 1");

     clear_block(bh->b_data);

     bh->b_uptodate = 1;

     bh->b_dirt = 1;

     brelse(bh);

     return j;

}

 

j += i*8192 + sb->s_firstdatazone-1; 这句说明了一切。通过getblk,将该块与某个当前可用的系统缓冲区通过内核中的哈希表绑定。new_block返回的逻辑块号被填入索引节点的i_zone直接或间接寻址块表中。由此,我们回答了刚才提出的问题。


疑问----------------------------------------------------------------------------------

注:这里new_block对应于minix中的alloc_block。find_first_zero对应于alloc_bit。

       而alloc_bit返回1时对应于first_data_zone 。因此,在第i(i从0开始计数)个位示图块内找到第j(j从1开始计数)个可用块。然后 j += i * 8192这样计算出来的块号没有考虑引导块、超级块、i节点位示图块、数据区段位示图块以及存放i节点的块占用的块。而且这个块号是从1开始计数的。

现在规定了first_data_zone之后,计算出来的块号具体到设备上的逻辑块号是 j - 1 + first_data_zone 。

直接写出来就是 j += i*8192 + first_data_zone - 1

这里涉及到了位示图中显示的可用块到实际设备块号之间的转换。很简单,把两个块号序列的开始对齐了,即位示图中的1和实际块号first_data_zone对齐,这样转换就会很容易了。

原创粉丝点击