眾所周知,計算機系統在掉電后也能存儲數據的就是磁盤了,所以大量數據大部分時間是存放在磁盤的;現在新買的PC,磁盤從數百G到1TB不等;服務器的磁盤從數十TB到上百TB,這么大的存儲空間,該怎么高效地管理和使用了?站在硬件角度,cpu的分頁機制把虛擬內存切割成大量4KB大小的塊,所以4KB也成了硬件層面最小的內存分配單元;對比內存,磁盤的管理方式也類似,只不過磁盤最小的存儲或讀寫單元是512byte,稱之為扇區(用戶哪怕只想讀1格byte,驅動每次也要讀512byte的數據);不過現在的文件一般都遠超512byte,所以存儲單個文件肯定需要超過1個扇區的空間,這就導致了磁盤的磁頭要挨個讀不同的扇區,花費大量時間在磁盤上尋址,導致IO效率低下,形成了瓶頸!為了提升讀取效率,磁盤一般都是一次性連續讀取多個扇區,即一次性讀取一個"塊"(block)。這種由多個扇區組成的"塊",是文件存取的最小單位。"塊"的大小,最常見的是4KB(和內存頁的大小保持一致,便於從磁盤讀寫數據???),即連續八個 sector組成一個 block;
1、上一篇文章介紹了高速緩存區,為了方便管理這么一大塊緩存區,linux采用了buffer_head結構體來描述緩存區的各種屬性;同理:磁盤上也是被人為划分成了很多“塊”,為了方便管理這些塊,也需要相應的結構體,linux采用結構體叫m_inode(或則這樣理解:文件數據都存放在block中,那么很顯然,我們還必須找到一個地方儲存文件的元信息,比如文件的創建者、文件的創建日期、文件的大小等等。這種儲存文件元信息的區域就叫做inode,中文譯名為“索引節點”;每一個文件都有對應的inode,里面包含了與該文件有關的一些信息),如下:
注意:
- 一個文件只需要一個inode節點來存儲文件的元信息就夠了,所以文件和inode節點是一一對應的(注意這里是文件,不是文件名);
- 如果說文件很大,占用了很多的磁盤block,怎么才能找全文件的占用的所有磁盤block了?此刻就要用到inode結構體的i_zone[9]字段了,文件中的數據存放在哪個硬盤上的邏輯塊上就是由這個數組來映射的:前面7個是直接存儲文件數據塊,第8個是間接塊,第9個是二級間接塊!所有直接+間接+二級間接塊加起來,一共64M,這個在0.11版本所在的1991年已經非常大了!
struct m_inode { unsigned short i_mode;/*文件類型和屬性,ls查看的結果,比如drwx------*/ unsigned short i_uid;/*文件宿主id*/ unsigned long i_size; unsigned long i_mtime;/*文件內容上一次變動的時間*/ unsigned char i_gid;/*groupid:宿主所在的組id*/ unsigned char i_nlinks; /*鏈接數:有多少個其他的文件夾鏈接到這里*/ unsigned short i_zone[9];/*文件映射的邏輯塊號*/ /* these are in memory also */ struct task_struct * i_wait;/*等待該inode節點的進程隊列*/ unsigned long i_atime;/*文件上一次打開的時間*/ unsigned long i_ctime;/*文件的inode上一次變動的時間*/ unsigned short i_dev;/*設備號*/ unsigned short i_num; /* 多少個進程在使用這個inode*/ unsigned short i_count; unsigned char i_lock;/*互斥鎖*/ unsigned char i_dirt; unsigned char i_pipe; unsigned char i_mount; unsigned char i_seek; /* 數據是否是最新的,或者說有效的, update代表數據的有效性,dirt代表文件是否需要回寫, 比如寫入文件的時候,a進程寫入的時候,dirt是1,因為需要回寫到硬盤, 但是數據是最新的,update是1,這時候b進程讀取這個文件的時候,可以從 緩存里直接讀取。 */ unsigned char i_update; };
為了把內存文件塊的數據映射到磁盤的block,linux專門寫了_bmap函數:
i_zone映射關系圖示:
//// 文件數據塊映射到盤塊的處理操作。(block位圖處理函數,bmap - block map) // 參數:inode - 文件的i節點指針;block - 文件中的數據塊號;create - 創建塊標志。 // 該函數把指定的文件數據塊block對應到設備上邏輯塊上,並返回邏輯塊號。如果創建標志 // 置位,則在設備上對應邏輯塊不存在時就申請新磁盤塊,返回文件數據塊block對應在設備 // 上的邏輯塊號(盤塊號)。 static int _bmap(struct m_inode * inode,int block,int create) { struct buffer_head * bh; int i; // 首先判斷參數文件數據塊號block的有效性。如果塊號小於0,則停機。如果塊號大於 // 直接塊數7+間接塊數(相當於二級指針)512+二次間接塊數(相當於三級指針)512*512,超出文件系統表示范圍,則停機。 // 這種間接塊、二次間接塊類似內存分頁的機制 if (block<0) panic("_bmap: block<0"); if (block >= 7+512+512*512) panic("_bmap: block>big"); // 然后根據文件塊號的大小值和是否設置了創建標志分別進行處理。如果該塊號小於7, // 則使用直接塊表示。如果創建標志置位,並且i節點中對應塊的邏輯塊(區段)字段為0, // 則相應設備申請一磁盤塊(邏輯塊),並且將磁盤上邏輯塊號(盤塊號)填入邏輯塊 // 字段中。然后設置i節點改變時間,置i節點已修改標志。然后返回邏輯塊號。 if (block<7) { if (create && !inode->i_zone[block]) if ((inode->i_zone[block]=new_block(inode->i_dev))) { inode->i_ctime=CURRENT_TIME; inode->i_dirt=1; } return inode->i_zone[block]; } // 如果該塊號>=7,且小於7+512,則說明使用的是一次間接塊。下面對一次間接塊進行處理。 // 如果是創建,並且該i節點中對應間接塊字段i_zone[7]是0,表明文件是首次使用間接塊, // 則需申請一磁盤塊用於存放間接塊信息,並將此實際磁盤塊號填入間接塊字段中。然后 // 設置i節點修改標志和修改時間。如果創建時申請磁盤塊失敗,則此時i節點間接塊字段 // i_zone[7] = 0,則返回0.或者不創建,但i_zone[7]原來就為0,表明i節點中沒有間接塊, // 於是映射磁盤是吧,則返回0退出。 block -= 7; if (block<512) { if (create && !inode->i_zone[7]) if ((inode->i_zone[7]=new_block(inode->i_dev))) { inode->i_dirt=1; inode->i_ctime=CURRENT_TIME; } if (!inode->i_zone[7]) return 0; // 現在讀取設備上該i節點的一次間接塊。並取該間接塊上第block項中的邏輯塊號(盤塊 // 號)i。每一項占2個字節。如果是創建並且間接塊的第block項中的邏輯塊號為0的話, // 則申請一磁盤塊,並讓間接塊中的第block項等於該新邏輯塊塊號。然后置位間接塊的 // 已修改標志。如果不是創建,則i就是需要映射(尋找)的邏輯塊號。 if (!(bh = bread(inode->i_dev,inode->i_zone[7]))) return 0; i = ((unsigned short *) (bh->b_data))[block]; if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block]=i; bh->b_dirt=1; } // 最后釋放該間接塊占用的緩沖塊,並返回磁盤上新申請或原有的對應block的邏輯塊號。 brelse(bh); return i; } // 若程序運行到此,則表明數據塊屬於二次間接塊。其處理過程與一次間接塊類似。下面是對 // 二次間接塊的處理。首先將block再減去間接塊所容納的塊數(512),然后根據是否設置了 // 創建標志進行創建或尋找處理。如果是新創建並且i節點的二次間接塊字段為0,則序申請一 // 磁盤塊用於存放二次間接塊的一級信息,並將此實際磁盤塊號填入二次間接塊字段中。之后, // 置i節點已修改標志和修改時間。同樣地,如果創建時申請磁盤塊失敗,則此時i節點二次 // 間接塊字段i_zone[8]為0,則返回0.或者不是創建,但i_zone[8]原來為0,表明i節點中沒有 // 間接塊,於是映射磁盤塊失敗,返回0退出。 block -= 512; if (create && !inode->i_zone[8]) if ((inode->i_zone[8]=new_block(inode->i_dev))) { inode->i_dirt=1; inode->i_ctime=CURRENT_TIME; } if (!inode->i_zone[8]) return 0; // 現在讀取設備上該i節點的二次間接塊。並取該二次間接塊的一級塊上第 block/512 項中 // 的邏輯塊號i。如果是創建並且二次間接塊的一級塊上第 block/512 項中的邏輯塊號為0的 // 話,則需申請一磁盤塊(邏輯塊)作為二次間接塊的二級快i,並讓二次間接塊的一級塊中 // 第block/512 項等於二級塊的塊號i。然后置位二次間接塊的一級塊已修改標志。並釋放 // 二次間接塊的一級塊。如果不是創建,則i就是需要映射的邏輯塊號。 if (!(bh=bread(inode->i_dev,inode->i_zone[8]))) return 0; i = ((unsigned short *)bh->b_data)[block>>9]; if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block>>9]=i; bh->b_dirt=1; } brelse(bh); // 如果二次間接塊的二級塊塊號為0,表示申請磁盤塊失敗或者原來對應塊號就為0,則返回 // 0退出。否則就從設備上讀取二次間接塊的二級塊,並取該二級塊上第block項中的邏輯塊號。 if (!i) return 0; if (!(bh=bread(inode->i_dev,i))) return 0; i = ((unsigned short *)bh->b_data)[block&511]; // 如果是創建並且二級塊的第block項中邏輯塊號為0的話,則申請一磁盤塊(邏輯塊),作為 // 最終存放數據信息的塊。並讓二級塊中的第block項等於該新邏輯塊塊號(i)。然后置位二級塊 // 的已修改標志。 if (create && !i) if ((i=new_block(inode->i_dev))) { ((unsigned short *) (bh->b_data))[block&511]=i; bh->b_dirt=1; } // 最后釋放該二次間接塊的二級塊,返回磁盤上新申請的或原有的對應block的邏輯塊號。 brelse(bh); return i; }
通過上述的結構體,inode是管理起來了,但還是不夠,還缺了一些屬性,比如inode又多少了?那些被使用了?哪些還空着?塊被鎖定了么等等,為了繼續管理這些屬性,linux又創建了一個叫做super_block的結構體:
struct super_block { unsigned short s_ninodes;/*i節點數量*/ unsigned short s_nzones;/*文件系統總長度:block < sb->s_firstdatazone || block >= sb->s_nzones*/ unsigned short s_imap_blocks;/*i節點位圖數量*/ unsigned short s_zmap_blocks;/*數據塊位圖數量*/ unsigned short s_firstdatazone;/*第一個塊的位置:block < sb->s_firstdatazone || block >= sb->s_nzones*/ 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];/*i node位圖在高速緩存區的指針數組*/ struct buffer_head * s_zmap[8];/*邏輯塊位圖在高速緩存區的指針數組*/ unsigned short s_dev;/*設備號,可以通過該號找到超級塊*/ struct m_inode * s_isup;/*根目錄的i node*/ struct m_inode * s_imount; /*文件系統filesystem安裝的i node*/ unsigned long s_time;/*修改時間*/ struct task_struct * s_wait;/*等待該塊的進程*/ unsigned char s_lock;/*是否被鎖定*/ unsigned char s_rd_only;/*是否只讀*/ unsigned char s_dirt;/*是否被修改*/ };
Linux文件系統格式化時候,格式化上面三個區域:supper block, inode 與 block 的區塊,假設某一個數據的屬性與權限數據是放置到 inode 5 號,而這個 inode 記錄了檔案數據的實際放置點為 3,4,10 這四個 block 號碼,此時我們的操作系統就能夠據此來尋找數據了,稱為索引式文件系統;上述的文字描述看起來可能有點抽象,這些屬性之間的關系如下圖所示:通過超級塊檢索數據塊位圖和inode塊位圖;再通過數據塊位圖檢索數據塊,inode塊位圖檢索inode節點塊!所以說抓住了超級塊,就等於檢索了整個文件系統!
- 下面圖示中每個塊的大小統一都是1024byte=1KB,所以一個數據塊位圖能表示1024*8=8192個數據塊!每個數據塊是1KB,單個超級塊一共能管理8MB的磁盤空間!0.11這個版本一共用了8個超級塊,能管理8*8MB=64MB的磁盤空間!
- block號是線性增加的,所以block號的計算方法(inode.c/read_node方法):block = 2 + sb->s_imap_blocks + sb->s_zmap_blocks + (inode->i_num-1)/INODES_PER_BLOCK;
- inode也是存放在快里面的,每塊能存放inode節點數量計算公式:#define INODES_PER_BLOCK ((BLOCK_SIZE)/(sizeof (struct d_inode)))
和task數組類似,linux仍然采用數組的形式統一集中管理所有超級塊,這個版本一共設置了8個超級塊:
// 超級塊結構表數組(NR_SUPER = 8) struct super_block super_block[NR_SUPER];
通過遍歷超級塊數組、比對設備號找到超級塊結構體;這里注意:linux常見的mount命令,本質就是把super_block的dev字段設置成對應的設備,讓super_block關聯上設備;然后把super_block讀到高速緩存區,后續操作系統或應用程序直接讀寫該緩存區;最后把super_block的實例加入超級塊數組,便於統一管理!
//// 取指定設備的超級塊 // 在超級塊表(數組)中搜索指定設備dev的超級塊結構信息。若找到剛返回超級塊的指針, // 否則返回空指針 struct super_block * get_super(int dev) { struct super_block * s; // 首先判斷參數給出設備的有效性。若設備號為0則返回NULL,然后讓s指向超級塊數組 // 起始處,開始搜索整個超級塊數組,以尋找指定設備dev的超級塊。 if (!dev) return NULL; s = 0+super_block; while (s < NR_SUPER+super_block) // 如果當前搜索項是指定設備的超級塊,即該超級塊的設備號字段值與函數參數指定的 // 相同,則先等待該超級塊解鎖。在等待期間,該超級塊項有可能被其他設備使用,因此 // 等待返回之后需要再判斷一次是否是指定設備的超級塊,如果是則返回該超級塊的指針。 // 否則就重新對超級塊數組再搜索一遍,因此此時s需重又指向超級塊數組開始處。 if (s->s_dev == dev) { wait_on_super(s); if (s->s_dev == dev) return s; s = 0+super_block; // 如果當前搜索項不是,則檢查下一項,如果沒有找到指定的超級塊,則返回空指針。 } else s++; return NULL; }
2、上面的各種框架搭建好后,在正式填充和使用這些結構體之前,還需要完善位圖工具,畢竟數據塊和inode都涉及到位圖塊的使用了嘛!linux有個bitmap.c文件提供了大量的位圖操作,比如:
(1)clear_block:清空1024byte的內存,作用了memset完全一樣!
//// 將指定地址(addr)處的一塊1024字節內存清零 // 輸入:eax = 0; ecx = 以字節為單位的數據塊長度(BLOCK_SIZE/4);edi = 指定 // 起始地址addr。 #define clear_block(addr) \ __asm__ __volatile__ ("cld\n\t" \ // 清方向位 "rep\n\t" \ // 重復執行存儲數據(0). "stosl" \ ::"a" (0),"c" (BLOCK_SIZE/4),"D" ((long) (addr)))
(2) 指定bit位置1,並返回原bit值;
//// 把指定地址開始的第nr個位偏移處的bit位置位(nr可大於321).返回原bit位值。 // 輸入:%0-eax(返回值):%1 -eax(0);%2-nr,位偏移值;%3-(addr),addr的內容。 // res是一個局部寄存器變量。該變量將被保存在指定的eax寄存器中,以便於高效 // 訪問和操作。這種定義變量的方法主要用於內嵌匯編程序中。詳細說明可以參考 // gcc手冊”在指定寄存器中的變量“。整個宏是一個語句表達式(即圓括號括住的組合句), // 其值是組合語句中最后一條表達式語句res的值。 // btsl指令用於測試並設置bit位。把基地址(%3)和bit位偏移值(%2)所指定的bit位值 // 先保存到進位標志CF中,然后設置該bit位為1.指令setb用於根據進位標志CF設置 // 操作數(%al)。如果CF=1則%al = 1,否則%al = 0。 #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;})
相應的,也有對指定bit清0的方法:
//// 復位指定地址開始的第nr位偏移處的bit位。返回原bit位值的反碼。 // 輸入:%0-eax(返回值);%1-eax(0);%2-nr,位偏移值;%3-(addr),addr的內容。 // btrl指令用於測試並復位bit位。其作用與上面的btsl類似,但是復位指定bit位。 // 指令setnb用於根據進位標志CF設置操作數(%al).如果CF=1則%al=0,否則%al=1. #define clear_bit(nr,addr) ({\ register int res ; \ __asm__ __volatile__("btrl %2,%3\n\tsetnb %%al": \ "=a" (res):"0" (0),"r" (nr),"m" (*(addr))); \ res;})
(3)從指定地址開始尋找第一個bit為0的位,目的就是找第一個沒被用的塊;
//// 從addr開始尋找第1個0值bit位。 // 輸入:%0-ecx(返回值);%1-ecx(0); %2-esi(addr). // 在addr指定地址開始的位圖中尋找第1個是0的bit位,並將其距離addr的bit位偏移 // 值返回。addr是緩沖塊數據區的地址,掃描尋找的范圍是1024字節(8192bit位)。 #define find_first_zero(addr) ({ \ int __res; \ __asm__ __volatile__ ("cld\n" \ // 清方向位 "1:\tlodsl\n\t" \ // 取[esi]→eax. "notl %%eax\n\t" \ // eax中每位取反。 "bsfl %%eax,%%edx\n\t" \ // 從位0掃描eax中是1的第1個位,其偏移值→edx "je 2f\n\t" \ // 如果eax中全是0,則向前跳轉到標號2處。 "addl %%edx,%%ecx\n\t" \ // 偏移值加入ecx(ecx是位圖首個0值位的偏移值) "jmp 3f\n" \ // 向前跳轉到標號3處 "2:\taddl $32,%%ecx\n\t" \ // 未找到0值位,則將ecx加1個字長的位偏移量32 "cmpl $8192,%%ecx\n\t" \ // 已經掃描了8192bit位(1024字節) "jl 1b\n" \ // 若還沒有掃描完1塊數據,則向前跳轉到標號1處 "3:" \ // 結束。此時ecx中是位偏移量。 :"=c" (__res):"c" (0),"S" (addr)); \ __res;})
3、光有工具還不夠,要先生成超級塊、inode位圖和數據塊才能運營整個文件系統,不是么?所以還要先建inode:
//// 為設備dev建立一個新i節點。初始化並返回該新i節點的指針。 // 在內存i節點表中獲取一個空閑i節點表項,並從i節點位圖中找一個空閑i節點。 struct m_inode * new_inode(int dev) { struct m_inode * inode; struct super_block * sb; struct buffer_head * bh; int i,j; // 首先從內存i節點表(inode_table)中獲取一個空閑i節點項,並讀取指定設備的 // 超級塊結構。然后掃描超級塊中8塊i節點位圖,尋找首個0bit位,尋找空閑節點, // 獲取放置該i節點的節點號。如果全部掃描完還沒找到,或者位圖所在的緩沖塊無效 // (bh=NULL),則放回先前申請的i節點表中的i節點,並返回NULL退出(沒有空閑的i節點)。 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; } // 現在我們已經找到了還未使用的i節點號j。於是置位i節點j對應的i節點位圖相應bit位。 // 然后置i節點位圖所在緩沖塊已修改標志。最后初始化該i節點結構(i_ctime是i節點內容改變時間)。 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; // i節點所在的設備號 inode->i_uid=current->euid; // i節點所屬用戶ID inode->i_gid=current->egid; // 組id inode->i_dirt=1; // 已修改標志置位 inode->i_num = j + i*8192; // 對應設備中的i節點號 inode->i_mtime = inode->i_atime = inode->i_ctime = CURRENT_TIME; return inode; }
上述方法調用了get_empty_inode,核心思想是從inode_table中找空閑的inode,主要依靠判斷i_count、i_dirt、i_lock這3個字段:
//// 從i節點表(inode_table)中獲取一個空閑i節點項。 // 尋找引用計數count為0的i節點,並將其寫盤后清零,返回指針。引用計數被置1. struct m_inode * get_empty_inode(void) { struct m_inode * inode; static struct m_inode * last_inode = inode_table; int i; do { // 在初始化last_inode指針指向i節點表頭一項后循環掃描整個i節點表。如果last_inode // 已經指向i節點表的最后一項之后,則讓其重新指向i節點表開始處,以繼續循環尋找空閑 // i節點項。如果last_inode所指向的i節點的計數值為0,則說明可能找到空閑i節點項。 // 讓inode指向該i節點。如果該i節點的已修改標志和鎖定標志均為0,則我們可以使用該i // 節點,於是退出for循環。 inode = NULL; for (i = NR_INODE; i ; i--) { if (++last_inode >= inode_table + NR_INODE) last_inode = inode_table; if (!last_inode->i_count) { inode = last_inode; if (!inode->i_dirt && !inode->i_lock) break; } } // 如果沒有找到空閑i節點(inode=NULL),則將i節點表打印出來供調試使用,並停機。 if (!inode) { for (i=0 ; i<NR_INODE ; i++) printk("%04x: %6d\t",inode_table[i].i_dev, inode_table[i].i_num); panic("No free inodes in mem"); } // 等待該i節點解鎖,如果該i節點已修改標志被置位的話,則將該i節點刷新,因為刷新時 // 可能會睡眠,因此需要再次循環等待該i節點解鎖。 wait_on_inode(inode); while (inode->i_dirt) { write_inode(inode); wait_on_inode(inode); } // 如果i節點又被其他占用的話(i節點的計數值不為0了),則重新尋找空閑i節點。否則 // 說明已找到符合要求的空閑i節點項。則將該i節點項內容清零,並置引用計數為1, // 返回該i節點指針。 } while (inode->i_count); memset(inode,0,sizeof(*inode)); inode->i_count = 1; return inode; }
用完后可以釋放:注意看最后一行調用了memset,直接把整個inode節點存的數據全部清零!(這里可以對比后續的iput方法,只是執行了inode->i_count--,把引用計數減一,並未清空inode的任何數據!)
//// 釋放指定的i節點 // 該函數首先判斷參數給出的i節點號的有效性和課釋放性。若i節點仍然在使用中則不能 // 被釋放。然后利用超級塊信息對i節點位圖進行操作,復位i節點號對應的i節點位圖中 // bit位,並清空i節點結構。 void free_inode(struct m_inode * inode) { struct super_block * sb; struct buffer_head * bh; // 首先判斷參數給出的需要釋放的i節點有效性或合法性。如果i節點指針=NULL,則 // 退出。如果i節點上的設備號字段為0,則說明該節點沒有使用。於是用0清空對應i // 節點所占內存區並返回。memset()定義在include/string.h中,這里表示用0填寫 // inode指針指定處、長度是sizeof(*inode)的內存快。 if (!inode) return; if (!inode->i_dev) { memset(inode,0,sizeof(*inode)); return; } // 如果此i節點還有其他程序引用,則不能釋放,說明內核有問題,停機。如果文件 // 連接數不為0,則表示還有其他文件目錄項在使用該節點,因此也不應釋放,而應該放回等。 if (inode->i_count>1) { printk("trying to free inode with count=%d\n",inode->i_count); panic("free_inode"); } if (inode->i_nlinks) panic("trying to free inode with links"); // 在判斷完i節點的合理性之后,我們開始利用超級塊信息對其中的i節點位圖進行 // 操作。首先取i節點所在設備的超級塊,測試設備是否存在。然后判斷i節點號的 // 范圍是否正確,如果i節點號等於0或大於該設備上i節點總數,則出錯(0號i節點 // 保留沒有使用)。如果該i節點對應的節點位圖不存在,則出錯。因為一個緩沖塊 // 的i節點位圖有8192 bit。因此i_num>>13(即i_num/8192)可以得到當前i節點所在 // 的s_imap[]項,即所在盤塊。 if (!(sb = get_super(inode->i_dev))) panic("trying to free inode on nonexistent device"); if (inode->i_num < 1 || inode->i_num > sb->s_ninodes) panic("trying to free inode 0 or nonexistant inode"); if (!(bh=sb->s_imap[inode->i_num>>13])) panic("nonexistent imap in superblock"); // 現在我們復位i節點對應的節點位圖中的bit位。如果該bit位已經等於0,則顯示 // 出錯警告信息。最后置i節點位圖所在緩沖區已修改標志,並清空該i節點結構 // 所占內存區。 if (clear_bit(inode->i_num&8191,bh->b_data)) printk("free_inode: bit already cleared.\n\r"); bh->b_dirt = 1; memset(inode,0,sizeof(*inode)); }
4、由於數據都是先存在高速緩存區,不會直接讀寫磁盤,所以此時要先在高速緩存區新建塊,這里面就涉及到了上面的位圖操作!
//// 向設備申請一個邏輯塊。 // 函數首先取得設備的超級塊,並在超級塊中的邏輯塊位圖中尋找第一個0值bit位(代表一個 // 空閑邏輯塊)。然后位置對應邏輯塊在邏輯塊位圖中的bit位。接着為該邏輯塊在緩沖區中取得 // 一塊對應緩沖塊。最后將該緩沖塊清零,並設置其已更新標志和已修改標志。並返回邏輯塊 // 號。函數執行成功則返回邏輯塊號,否則返回0. int new_block(int dev) { struct buffer_head * bh; struct super_block * sb; int i,j; // 首先獲取設備dev的超級塊。如果指定設備的超級塊不存在,則出錯當機。然后掃描 // 文件系統的8塊邏輯位圖,尋找首個0值bit位,以尋找空閑邏輯塊,獲取放置該邏輯塊的 // 塊號。如果全部掃描完8塊邏輯塊位圖的所有bit位(i >= 8 或 j >= 8192)還沒找到0值 // bit位或者位圖所在的緩沖塊指針無效(bh=NULL)則返回0退出(沒有空閑邏輯塊)。 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; // 接着設置找到的新邏輯塊j對應邏輯塊位圖中的bit位。若對應bit位已經置位,則出錯 // 停機。否則置存放位圖的對應緩沖區塊已修改標志。因為邏輯塊位圖僅表示盤上數據區 // 中邏輯塊的占用情況,則邏輯塊位圖中bit位偏移值表示從數據區開始處算起的塊號, // 因此這里需要加上數據區第1個邏輯塊的塊號,把j轉換成邏輯塊號。此時如果新邏輯塊 // 大於該設備上的總邏輯塊數,則說明指定邏輯塊在對應設備上不存在。申請失敗,返回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; // 然后在高速緩沖區中為該設備上指定的邏輯塊號取得一個緩沖塊,並返回緩沖塊頭指針。 // 因為剛取得的邏輯塊其引用次數一定為1(getblk()中會設置),因此若不為1則停機。 // 最后將新邏輯塊清零,並設置其已更新標志和已修改標志。然后釋放對應緩沖塊,返回 // 邏輯塊號。 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; }
塊用完后也需要釋放,如下:先把位圖對應的位置清0,再把高速緩存區對應的塊釋放;
//// 釋放設備dev上數據區中的邏輯塊block. // 復位指定邏輯塊block對應的邏輯塊位圖bit位 // 參數:dev是設備號,block是邏輯塊號(盤塊號) void free_block(int dev, int block) { struct super_block * sb; struct buffer_head * bh; // 首先取設備dev上文件系統的超級塊信息,根據其中數據區開始邏輯塊號和文件系統中邏輯 // 塊總數信息判斷參數block的有效性。如果指定設備超級塊不存在,則出錯當機。若邏輯塊 // 號小於盤上面數據區第一個邏輯塊的塊號或者大於設備上總邏輯塊數,也出錯當機。 if (!(sb = get_super(dev))) panic("trying to free block on nonexistent device"); if (block < sb->s_firstdatazone || block >= sb->s_nzones) panic("trying to free block not in datazone"); // 然后從hash表中尋找該塊數據。若找到了則判斷其有效性,並清已修改和更新標志,釋放 // 該數據塊。該段代碼的主要用途是如果該邏輯塊目前存在於高速緩沖區中,就釋放對應 // 的緩沖塊。 bh = get_hash_table(dev,block); // 下面的代碼會造成數據塊不能釋放。因為當b_count > 1時,這段代碼會僅打印一段信息而 // 沒有執行釋放操作。 if (bh) { if (bh->b_count != 1) { printk("trying to free block (%04x:%d), count=%d\n", dev,block,bh->b_count); return; } bh->b_dirt=0; bh->b_uptodate=0; brelse(bh); } // 接着我們復位block在邏輯塊位圖中的bit(置0),先計算block在數據區開始算起的數據 // 邏輯塊號(從1開始計數)。然后對邏輯塊(區塊)位圖進行操作,復位對應的bit位。如果對應 // bit位原來就是0,則出錯停機。由於1個緩沖塊有1024字節,即8192比特位,因此block/8192 // 即可計算出指定塊block在邏輯位圖中的哪個塊上。而block&8192可以得到block在邏輯塊位圖 // 當前塊中的bit偏移位置。,不用擔心偏移超出8191的范圍。 block -= sb->s_firstdatazone - 1 ; if (clear_bit(block&8191,sb->s_zmap[block/8192]->b_data)) { printk("block (%04x:%d) ",dev,block+sb->s_firstdatazone-1); panic("free_block: bit already cleared"); } // 最后置相應邏輯塊位圖所在緩沖區已修改標志。 sb->s_zmap[block/8192]->b_dirt = 1; }
注意:上述的各種塊操作,都是針對內存中的高速緩存區,並未直接操作磁盤!
5、(1)前面建好了block和inode,至此終於可以開始讀寫數據了,比如這里的write_inode函數:
//// 將i節點信息寫入緩沖區中。 // 該函數把參數指定的i節點寫入緩沖區相應的緩沖塊中,待緩沖區刷新時會寫入盤中。為了確定i節點 // 所在的設備邏輯塊號(或緩沖塊),必須首先讀取相應設備上的超級塊,以獲取用於計算邏輯塊號的 // 每塊i節點數信息INODES_PER_BLOCK。在計算出i節點所在的邏輯塊號后,就把該邏輯塊讀入一緩沖塊 // 中。然后把i節點內容復制到緩沖塊的相應位置處。 static void write_inode(struct m_inode * inode) { struct super_block * sb; struct buffer_head * bh; int block; // 首先鎖定該i節點,如果該i節點沒有被修改或者該i節點的設備號等於零,則解鎖該i節點,並退出。 // 對於沒有被修改過的i節點,其內容與緩沖區中或設備中的相同。然后獲取該i節點的超級塊。 lock_inode(inode); if (!inode->i_dirt || !inode->i_dev) { unlock_inode(inode); return; } if (!(sb=get_super(inode->i_dev))) panic("trying to write inode without device"); // 該i節點所在的設備邏輯塊號=(啟動塊+超級塊)+i節點位圖占用的塊數+邏輯塊位圖占用的塊數 // +(i節點號-1)/每塊含有的i節點數。我們從設備上讀取i節點所在的邏輯塊,並將該i節點信息復制 // 到邏輯塊對應i節點的項位置處。 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 *)bh->b_data) [(inode->i_num-1)%INODES_PER_BLOCK] = *(struct d_inode *)inode; // 然后置緩沖區已修改標志,而i節點內容已經與緩沖區中的一致,因此修改標志置零。然后釋放該 // 含有i節點的緩沖區,並解鎖該i節點。 bh->b_dirt=1; inode->i_dirt=0; brelse(bh); unlock_inode(inode); }
注意:上面的write函數並未直接把數據寫入磁盤,而是先寫入了緩存區,所以linux又提供了專門同步的接口,如下:這兩個函數最終都調用了ll_rw_block方法向磁盤寫數據!其中sys_sync還是個系統調用了!
//// 設備數據同步,這是個系統調用; // 同步設備和內存高速緩沖中數據,其中sync_inode()定義在inode.c中。 // 把內存中高速緩存區的數寫回到磁盤,需要調用磁盤的驅動代碼 int sys_sync(void) { int i; struct buffer_head * bh; // 首先調用i節點同步函數,把內存i節點表中所有修改過的i節點寫入高速緩沖中。 // 然后掃描所有高速緩沖區,對已被修改的緩沖塊產生寫盤請求,將緩沖中數據寫入 // 盤中,做到高速緩沖中的數據與設備中的同步。 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; } //// 對指定設備進行高速緩沖數據與設備上數據的同步操作 // 該函數首先搜索高速緩沖區所有緩沖塊。對於指定設備dev的緩沖塊,若其數據已經 // 被修改過就寫入盤中(同步操作)。然后把內存中i節點表數據寫入 高速緩沖中。之后 // 再對指定設備dev執行一次與上述相同的寫盤操作。 int sync_dev(int dev) { int i; struct buffer_head * bh; // 首先對參數指定的設備執行數據同步操作,讓設備上的數據與高速緩沖區中的數據 // 同步。方法是掃描高速緩沖區中所有緩沖塊,對指定設備dev的緩沖塊,先檢測其 // 是否已被上鎖,若已被上鎖就睡眠等待其解鎖。然后再判斷一次該緩沖塊是否還是 // 指定設備的緩沖塊並且已修改過(b_dirt標志置位),若是就對其執行寫盤操作。 // 因為在我們睡眠期間該緩沖塊有可能已被釋放或者被挪作他用,所以在繼續執行前 // 需要再次判斷一下該緩沖塊是否還是指定設備的緩沖塊。 bh = start_buffer; for (i=0 ; i<NR_BUFFERS ; i++,bh++) { if (bh->b_dev != dev) // 不是設備dev的緩沖塊則繼續 continue; wait_on_buffer(bh); // 等待緩沖區解鎖 if (bh->b_dev == dev && bh->b_dirt) ll_rw_block(WRITE,bh); //lowlevel } // 再將i節點數據吸入高速緩沖。讓i節點表inode_table中的inode與緩沖中的信息同步。 sync_inodes(); // 然后在高速緩沖中的數據更新之后,再把他們與設備中的數據同步。這里采用兩遍同步 // 操作是為了提高內核執行效率。第一遍緩沖區同步操作可以讓內核中許多"臟快"變干凈, // 使得i節點的同步操作能夠高效執行。本次緩沖區同步操作則把那些由於i節點同步操作 // 而又變臟的緩沖塊與設備中數據同步。 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; }
(2)同理,也有讀read_inode的函數:
//// 讀取指定i節點信息。 // 從設備上讀取含有指定i節點信息的i節點盤塊,然后復制到指定的i節點結構中。為了確定i節點 // 所在的設備邏輯塊號(或緩沖塊),必須首先讀取相應設備上的超級塊,以獲取用於計算邏輯 // 塊號的每塊i節點數信息INODES_PER_BLOCK.在計算出i節點所在的邏輯塊號后,就把該邏輯塊讀入 // 一緩沖塊中。然后把緩沖塊中相應位置處的i節點內容復制到參數指定的位置處。 static void read_inode(struct m_inode * inode) { struct super_block * sb; struct buffer_head * bh; int block; // 首先鎖定該i節點,並取該節點所在設備的超級塊。 lock_inode(inode); if (!(sb=get_super(inode->i_dev))) panic("trying to read inode without dev"); // 該i節點所在的設備邏輯塊號=(啟動塊+超級塊)+i節點位圖占用的塊數+邏輯塊位圖占用的塊數 // +(i節點號-1)/每塊含有的i節點數。雖然i節點號從0開始編號,但第i個0號i節點不用,並且 // 磁盤上也不保存對應的0號i節點結構。因此存放i節點的盤塊的第i塊上保存的是i節點號是1--16 // 的i節點結構而不是0--15的。因此在上面計算i節點號對應的i節點結構所在盤塊時需要減1,即: // B=(i節點號-1)/每塊含有i節點結構數。例如,節點號16的i節點結構應該在B=(16-1)/16 = 0的 // 塊上。這里我們從設備上讀取該i節點所在的邏輯塊,並復制指定i節點內容到inode指針所指位置處。 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]; // 最后釋放讀入的緩沖塊,並解鎖該i節點。 brelse(bh); unlock_inode(inode); }
(3)inode用完后,並不是直接調用free_inode去清零inode節點的數據,而是先把i_count計數減一,如果計數是0了,再清零inode節點的數據,如下:
//// 放回(放置)一個i節點引用計數值遞減1,並且若是管道i節點,則喚醒等待的進程。 // 若是塊設備文件i節點則刷新設備。並且若i節點的鏈接計數為0,則釋放該i節點占用 // 的所有磁盤邏輯塊,並釋放該i節點。 void iput(struct m_inode * inode) { // 首先判斷參數給出的i節點的有效性,並等待inode節點解鎖,如果i節點的引用計數 // 為0,表示該i節點已經是空閑的。內核再要求對其進行放回操作,說明內核中其他 // 代碼有問題。於是顯示錯誤信息並停機。 if (!inode) return; wait_on_inode(inode); if (!inode->i_count) panic("iput: trying to free free inode"); // 如果是管道i節點,則喚醒等待該管道的進程,引用次數減1,如果還有引用則返回。 // 否則釋放管道占用的內存頁面,並復位該節點的引用計數值、已修改標志和管道標志, // 並返回。對於管道節點,inode->i_size存放這內存也地址。 if (inode->i_pipe) { wake_up(&inode->i_wait); if (--inode->i_count) return; free_page(inode->i_size); inode->i_count=0; inode->i_dirt=0; inode->i_pipe=0; return; } // 如果i節點對應的設備號 = 0,則將此節點的引用計數遞減1,返回。例如用於管道操作 // 的i節點,其i節點的設備號為0. if (!inode->i_dev) { inode->i_count--; return; } // 如果是塊設備文件的i節點,此時邏輯塊字段0(i_zone[0])中是設備號,則刷新該設備。 // 並等待i節點解鎖。 if (S_ISBLK(inode->i_mode)) { sync_dev(inode->i_zone[0]); wait_on_inode(inode); } // 如果i節點的引用計數大於1,則計數遞減1后就直接返回(因為該i節點還有人在用,不能 // 釋放),否則就說明i節點的引用計數值為1。如果i節點的鏈接數為0,則說明i節點對應文件 // 被刪除。於是釋放該i節點的所有邏輯塊,並釋放該i節點。函數free_inode()用於實際釋 // 放i節點操作,即復位i節點對應的i節點位圖bit位,清空i節點結構內容。 repeat: if (inode->i_count>1) { inode->i_count--; return; } if (!inode->i_nlinks) { truncate(inode); free_inode(inode); return; } // 如果該i節點已做過修改,則回寫更新該i節點,並等待該i節點解鎖。由於這里在寫i節點 // 時需要等待睡眠,此時其他進程有可能修改i節點,因此在進程被喚醒后需要再次重復進行 // 上述判斷過程(repeat)。 if (inode->i_dirt) { write_inode(inode); /* we can sleep - so do again */ wait_on_inode(inode); goto repeat; } // 程序若能執行到此,則說明該i節點的引用計數值i_count是1、鏈接數不為零,並且內容 // 沒有被修改過。因此此時只要把i節點引用計數遞減1,返回。此時該i節點的i_count=0, // 表示已釋放。 inode->i_count--; return; }
參考:
1、https://www.jianshu.com/p/9ef6542ced92 Linux文件系統和inode
2、https://blog.csdn.net/YuZhiHui_No1/article/details/43951153 Linux內核源碼分析--文件系統