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