Minix 文件信息及其數據的定位

原文: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對齊,這樣轉換就會很容易了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章