<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                企業??AI智能體構建引擎,智能編排和調試,一鍵部署,支持知識庫和私有化部署方案 廣告
                # 第?29?章?文件系統 **目錄** + [1\. 引言](ch29s01.html) + [2\. ext2文件系統](ch29s02.html) + [2.1\. 總體存儲布局](ch29s02.html#id2857323) + [2.2\. 實例剖析](ch29s02.html#id2858019) + [2.3\. 數據塊尋址](ch29s02.html#id2859212) + [2.4\. 文件和目錄操作的系統函數](ch29s02.html#id2859394) + [3\. VFS](ch29s03.html) + [3.1\. 內核數據結構](ch29s03.html#id2860264) + [3.2\. dup和dup2函數](ch29s03.html#id2860911) ## 1.?引言 本章主要解答以下問題: 1. 文件系統在內核中是如何實現的?如何呈現給用戶一個樹狀的目錄結構?如何處理用戶的文件和目錄操作請求? 2. 磁盤是一種順序的存儲介質,一個樹狀的目錄結構如何扯成一條線存到磁盤上?怎樣設計文件系統的存儲格式使訪問磁盤的效率最高?各種文件和目錄操作在磁盤上的實際效果是什么? **圖?29.1.?文件系統的表示和存儲** ![文件系統的表示和存儲](https://box.kancloud.cn/2016-04-02_56ff80d808045.png) 我們首先介紹一種文件系統的存儲格式-早期Linux廣泛使用的ext2文件系統。現在Linux最常用的ext3文件系統也是與ext2兼容的,基本格式是一致的,只是多了一些擴展。然后再介紹文件系統在內核中是如何實現的。 ## 2.?ext2文件系統 ### 2.1.?總體存儲布局 我們知道,一個磁盤可以劃分成多個分區,每個分區必須先用格式化工具(例如某種`mkfs`命令)格式化成某種格式的文件系統,然后才能存儲文件,格式化的過程會在磁盤上寫一些管理存儲布局的信息。下圖是一個磁盤分區格式化成ext2文件系統后的存儲布局。 **圖?29.2.?ext2文件系統的總體存儲布局** ![ext2文件系統的總體存儲布局](https://box.kancloud.cn/2016-04-02_56ff80d81781d.png) 文件系統中存儲的最小單位是塊(Block),一個塊究竟多大是在格式化時確定的,例如`mke2fs`的`-b`選項可以設定塊大小為1024、2048或4096字節。而上圖中啟動塊(Boot Block)的大小是確定的,就是1KB,啟動塊是由PC標準規定的,用來存儲磁盤分區信息和啟動信息,任何文件系統都不能使用啟動塊。啟動塊之后才是ext2文件系統的開始,ext2文件系統將整個分區劃成若干個同樣大小的塊組(Block Group),每個塊組都由以下部分組成。 超級塊(Super Block) 描述整個分區的文件系統信息,例如塊大小、文件系統版本號、上次`mount`的時間等等。超級塊在每個塊組的開頭都有一份拷貝。 塊組描述符表(GDT,Group Descriptor Table) 由很多塊組描述符組成,整個分區分成多少個塊組就對應有多少個塊組描述符。每個塊組描述符(Group Descriptor)存儲一個塊組的描述信息,例如在這個塊組中從哪里開始是inode表,從哪里開始是數據塊,空閑的inode和數據塊還有多少個等等。和超級塊類似,塊組描述符表在每個塊組的開頭也都有一份拷貝,這些信息是非常重要的,一旦超級塊意外損壞就會丟失整個分區的數據,一旦塊組描述符意外損壞就會丟失整個塊組的數據,因此它們都有多份拷貝。通常內核只用到第0個塊組中的拷貝,當執行`e2fsck`檢查文件系統一致性時,第0個塊組中的超級塊和塊組描述符表就會拷貝到其它塊組,這樣當第0個塊組的開頭意外損壞時就可以用其它拷貝來恢復,從而減少損失。 塊位圖(Block Bitmap) 一個塊組中的塊是這樣利用的:數據塊存儲所有文件的數據,比如某個分區的塊大小是1024字節,某個文件是2049字節,那么就需要三個數據塊來存,即使第三個塊只存了一個字節也需要占用一個整塊;超級塊、塊組描述符表、塊位圖、inode位圖、inode表這幾部分存儲該塊組的描述信息。那么如何知道哪些塊已經用來存儲文件數據或其它描述信息,哪些塊仍然空閑可用呢?塊位圖就是用來描述整個塊組中哪些塊已用哪些塊空閑的,它本身占一個塊,其中的每個bit代表本塊組中的一個塊,這個bit為1表示該塊已用,這個bit為0表示該塊空閑可用。 為什么用`df`命令統計整個磁盤的已用空間非常快呢?因為只需要查看每個塊組的塊位圖即可,而不需要搜遍整個分區。相反,用`du`命令查看一個較大目錄的已用空間就非常慢,因為不可避免地要搜遍整個目錄的所有文件。 與此相聯系的另一個問題是:在格式化一個分區時究竟會劃出多少個塊組呢?主要的限制在于塊位圖本身必須只占一個塊。用`mke2fs`格式化時默認塊大小是1024字節,可以用`-b`參數指定塊大小,現在設塊大小指定為b字節,那么一個塊可以有8b個bit,這樣大小的一個塊位圖就可以表示8b個塊的占用情況,因此一個塊組最多可以有8b個塊,如果整個分區有s個塊,那么就可以有s/(8b)個塊組。格式化時可以用`-g`參數指定一個塊組有多少個塊,但是通常不需要手動指定,`mke2fs`工具會計算出最優的數值。 inode位圖(inode Bitmap) 和塊位圖類似,本身占一個塊,其中每個bit表示一個inode是否空閑可用。 inode表(inode Table) 我們知道,一個文件除了數據需要存儲之外,一些描述信息也需要存儲,例如文件類型(常規、目錄、符號鏈接等),權限,文件大小,創建/修改/訪問時間等,也就是`ls -l`命令看到的那些信息,這些信息存在inode中而不是數據塊中。每個文件都有一個inode,一個塊組中的所有inode組成了inode表。 inode表占多少個塊在格式化時就要決定并寫入塊組描述符中,`mke2fs`格式化工具的默認策略是一個塊組有多少個8KB就分配多少個inode。由于數據塊占了整個塊組的絕大部分,也可以近似認為數據塊有多少個8KB就分配多少個inode,換句話說,如果平均每個文件的大小是8KB,當分區存滿的時候inode表會得到比較充分的利用,數據塊也不浪費。如果這個分區存的都是很大的文件(比如電影),則數據塊用完的時候inode會有一些浪費,如果這個分區存的都是很小的文件(比如源代碼),則有可能數據塊還沒用完inode就已經用完了,數據塊可能有很大的浪費。如果用戶在格式化時能夠對這個分區以后要存儲的文件大小做一個預測,也可以用`mke2fs`的`-i`參數手動指定每多少個字節分配一個inode。 數據塊(Data Block) 根據不同的文件類型有以下幾種情況 * 對于常規文件,文件的數據存儲在數據塊中。 * 對于目錄,該目錄下的所有文件名和目錄名存儲在數據塊中,注意文件名保存在它所在目錄的數據塊中,除文件名之外,`ls -l`命令看到的其它信息都保存在該文件的inode中。注意這個概念:目錄也是一種文件,是一種特殊類型的文件。 * 對于符號鏈接,如果目標路徑名較短則直接保存在inode中以便更快地查找,如果目標路徑名較長則分配一個數據塊來保存。 * 設備文件、FIFO和socket等特殊文件沒有數據塊,設備文件的主設備號和次設備號保存在inode中。 現在做幾個小實驗來理解這些概念。例如在`home`目錄下`ls -l`: ``` $ ls -l total 32 drwxr-xr-x 114 akaedu akaedu 12288 2008-10-25 11:33 akaedu drwxr-xr-x 114 ftp ftp 4096 2008-10-25 10:30 ftp drwx------ 2 root root 16384 2008-07-04 05:58 lost+found ``` 為什么各目錄的大小都是4096的整數倍?因為這個分區的塊大小是4096,目錄的大小總是數據塊的整數倍。為什么有的目錄大有的目錄小?因為目錄的數據塊保存著它下邊所有文件和目錄的名字,如果一個目錄中的文件很多,一個塊裝不下這么多文件名,就可能分配更多的數據塊給這個目錄。再比如: ``` $ ls -l /dev ... prw-r----- 1 syslog adm 0 2008-10-25 11:39 xconsole crw-rw-rw- 1 root root 1, 5 2008-10-24 16:44 zero ``` `xconsole`文件的類型是`p`(表示pipe),是一個FIFO文件,后面會講到它其實是一塊內核緩沖區的標識,不在磁盤上保存數據,因此沒有數據塊,文件大小是0。`zero`文件的類型是`c`,表示字符設備文件,它代表內核中的一個設備驅動程序,也沒有數據塊,原本應該寫文件大小的地方寫了`1, 5`這兩個數字,表示主設備號和次設備號,訪問該文件時,內核根據設備號找到相應的驅動程序。再比如: ``` $ touch hello $ ln -s ./hello halo $ ls -l total 0 lrwxrwxrwx 1 akaedu akaedu 7 2008-10-25 15:04 halo -> ./hello -rw-r--r-- 1 akaedu akaedu 0 2008-10-25 15:04 hello ``` 文件`hello`是剛創建的,字節數為0,符號鏈接文件`halo`指向`hello`,字節數卻是7,為什么呢?其實7就是“./hello”這7個字符,符號鏈接文件就保存著這樣一個路徑名。再試試硬鏈接: ``` $ ln ./hello hello2 $ ls -l total 0 lrwxrwxrwx 1 akaedu akaedu 7 2008-10-25 15:08 halo -> ./hello -rw-r--r-- 2 akaedu akaedu 0 2008-10-25 15:04 hello -rw-r--r-- 2 akaedu akaedu 0 2008-10-25 15:04 hello2 ``` `hello2`和`hello`除了文件名不一樣之外,別的屬性都一模一樣,并且`hello`的屬性發生了變化,第二欄的數字原本是1,現在變成2了。從根本上說,`hello`和`hello2`是同一個文件在文件系統中的兩個名字,`ls -l`第二欄的數字是硬鏈接數,表示一個文件在文件系統中有幾個名字(這些名字可以保存在不同目錄的數據塊中,或者說可以位于不同的路徑下),硬鏈接數也保存在inode中。既然是同一個文件,inode當然只有一個,所以用`ls -l`看它們的屬性是一模一樣的,因為都是從這個inode里讀出來的。再研究一下目錄的硬鏈接數: ``` $ mkdir a $ mkdir a/b $ ls -ld a drwxr-xr-x 3 akaedu akaedu 4096 2008-10-25 16:15 a $ ls -la a total 20 drwxr-xr-x 3 akaedu akaedu 4096 2008-10-25 16:15 . drwxr-xr-x 115 akaedu akaedu 12288 2008-10-25 16:14 .. drwxr-xr-x 2 akaedu akaedu 4096 2008-10-25 16:15 b $ ls -la a/b total 8 drwxr-xr-x 2 akaedu akaedu 4096 2008-10-25 16:15 . drwxr-xr-x 3 akaedu akaedu 4096 2008-10-25 16:15 .. ``` 首先創建目錄`a`,然后在它下面創建子目錄`a/b`。目錄`a`的硬鏈接數是3,這3個名字分別是當前目錄下的`a`,`a`目錄下的`.`和`b`目錄下的`..`。目錄`b`的硬鏈接數是2,這兩個名字分別是`a`目錄下的`b`和`b`目錄下的`.`。注意,_目錄的硬鏈接只能這種方式創建,用`ln`命令可以創建目錄的符號鏈接,但不能創建目錄的硬鏈接_。 ### 2.2.?實例剖析 如果要格式化一個分區來研究文件系統格式則必須有一個空閑的磁盤分區,為了方便實驗,我們把一個文件當作分區來格式化,然后分析這個文件中的數據來印證上面所講的要點。首先創建一個1MB的文件并清零: ``` $ dd if=/dev/zero of=fs count=256 bs=4K ``` 我們知道`cp`命令可以把一個文件拷貝成另一個文件,而`dd`命令可以把一個文件的一部分拷貝成另一個文件。這個命令的作用是把`/dev/zero`文件開頭的1M(256×4K)字節拷貝成文件名為`fs`的文件。剛才我們看到`/dev/zero`是一個特殊的設備文件,它沒有磁盤數據塊,對它進行讀操作傳給設備號為`1, 5`的驅動程序。`/dev/zero`這個文件可以看作是無窮大的,不管從哪里開始讀,讀出來的都是字節0x00。因此這個命令拷貝了1M個0x00到`fs`文件。`if`和`of`參數表示輸入文件和輸出文件,`count`和`bs`參數表示拷貝多少次,每次拷多少字節。 做好之后對文件`fs`進行格式化,也就是_把這個文件的數據塊合起來看成一個1MB的磁盤分區,在這個分區上再劃分出塊組_。 ``` $ mke2fs fs mke2fs 1.40.2 (12-Jul-2007) fs is not a block special device. Proceed anyway? (y,n) (輸入y回車) Filesystem label= OS type: Linux Block size=1024 (log=0) Fragment size=1024 (log=0) 128 inodes, 1024 blocks 51 blocks (4.98%) reserved for the super user First data block=1 Maximum filesystem blocks=1048576 1 block group 8192 blocks per group, 8192 fragments per group 128 inodes per group Writing inode tables: done Writing superblocks and filesystem accounting information: done This filesystem will be automatically checked every 27 mounts or 180 days, whichever comes first. Use tune2fs -c or -i to override. ``` 格式化一個真正的分區應該指定塊設備文件名,例如`/dev/sda1`,而這個`fs`是常規文件而不是塊設備文件,`mke2fs`認為用戶有可能是誤操作了,所以給出提示,要求確認是否真的要格式化,輸入`y`回車完成格式化。 現在`fs`的大小仍然是1MB,但不再是全0了,其中已經有了塊組和描述信息。用`dumpe2fs`工具可以查看這個分區的超級塊和塊組描述符表中的信息: ``` $ dumpe2fs fs dumpe2fs 1.40.2 (12-Jul-2007) Filesystem volume name: <none> Last mounted on: <not available> Filesystem UUID: 8e1f3b7a-4d1f-41dc-8928-526e43b2fd74 Filesystem magic number: 0xEF53 Filesystem revision #: 1 (dynamic) Filesystem features: resize_inode dir_index filetype sparse_super Filesystem flags: signed directory hash Default mount options: (none) Filesystem state: clean Errors behavior: Continue Filesystem OS type: Linux Inode count: 128 Block count: 1024 Reserved block count: 51 Free blocks: 986 Free inodes: 117 First block: 1 Block size: 1024 Fragment size: 1024 Reserved GDT blocks: 3 Blocks per group: 8192 Fragments per group: 8192 Inodes per group: 128 Inode blocks per group: 16 Filesystem created: Sun Dec 16 14:56:59 2007 Last mount time: n/a Last write time: Sun Dec 16 14:56:59 2007 Mount count: 0 Maximum mount count: 30 Last checked: Sun Dec 16 14:56:59 2007 Check interval: 15552000 (6 months) Next check after: Fri Jun 13 14:56:59 2008 Reserved blocks uid: 0 (user root) Reserved blocks gid: 0 (group root) First inode: 11 Inode size: 128 Default directory hash: tea Directory Hash Seed: 6d0e58bd-b9db-41ae-92b3-4563a02a5981 Group 0: (Blocks 1-1023) Primary superblock at 1, Group descriptors at 2-2 Reserved GDT blocks at 3-5 Block bitmap at 6 (+5), Inode bitmap at 7 (+6) Inode table at 8-23 (+7) 986 free blocks, 117 free inodes, 2 directories Free blocks: 38-1023 Free inodes: 12-128 128 inodes per group, 8 inodes per block, so: 16 blocks for inode table ``` 根據上面講過的知識簡單計算一下,塊大小是1024字節,1MB的分區共有1024個塊,第0個塊是啟動塊,啟動塊之后才算ext2文件系統的開始,因此Group 0占據第1個到第1023個塊,共1023個塊。塊位圖占一個塊,共有1024×8=8192個bit,足夠表示這1023個塊了,因此只要一個塊組就夠了。默認是每8KB分配一個inode,因此1MB的分區對應128個inode,這些數據都和`dumpe2fs`的輸出吻合。 用常規文件制作而成的文件系統也可以像磁盤分區一樣`mount`到某個目錄,例如: ``` $ sudo mount -o loop fs /mnt $ cd /mnt/ $ ls -la total 17 drwxr-xr-x 3 akaedu akaedu 1024 2008-10-25 12:20 . drwxr-xr-x 21 root root 4096 2008-08-18 08:54 .. drwx------ 2 root root 12288 2008-10-25 12:20 lost+found ``` `-o loop`選項告訴`mount`這是一個常規文件而不是一個塊設備文件。`mount`會把它的數據塊中的數據當作分區格式來解釋。文件系統格式化之后在根目錄下自動生成三個子目錄:`.`,`..`和`lost+found`。其它子目錄下的`.`表示當前目錄,`..`表示上一級目錄,而根目錄的`.`和`..`都表示根目錄本身。`lost+found`目錄由`e2fsck`工具使用,如果在檢查磁盤時發現錯誤,就把有錯誤的塊掛在這個目錄下,因為這些塊不知道是誰的,找不到主,就放在這里“失物招領”了。 現在可以在`/mnt`目錄下添加刪除文件,這些操作會自動保存到文件`fs`中。然后把這個分區`umount`下來,以確保所有的改動都保存到文件中了。 ``` $ sudo umount /mnt ``` 注意,下面的實驗步驟是對新創建的文件系統做的,如果你在文件系統中添加刪除過文件,跟著做下面的步驟時結果可能和我寫的不太一樣,不過也不影響理解。 現在我們用二進制查看工具查看這個文件系統的所有字節,并且同`dumpe2fs`工具的輸出信息相比較,就可以很好地理解文件系統的存儲布局了。 ``` $ od -tx1 -Ax fs 000000 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 * 000400 80 00 00 00 00 04 00 00 33 00 00 00 da 03 00 00 000410 75 00 00 00 01 00 00 00 00 00 00 00 00 00 00 00 ... ``` 其中以*開頭的行表示這一段數據全是零因此省略了。下面詳細分析`od`輸出的信息。 從000000開始的1KB是啟動塊,由于這不是一個真正的磁盤分區,啟動塊的內容全部為零。從000400到0007ff的1KB是超級塊,對照著`dumpe2fs`的輸出信息,詳細分析如下: **圖?29.3.?超級塊** ![超級塊](https://box.kancloud.cn/2016-04-02_56ff80d829bbe.png) 超級塊中從0004d0到末尾的204個字節是填充字節,保留未用,上圖未畫出。注意,ext2文件系統中各字段都是按小端存儲的,如果把字節在文件中的位置看作地址,那么靠近文件開頭的是低地址,存低字節。各字段的位置、長度和含義詳見[[ULK]](bi01.html#bibli.ulk "Understanding the Linux Kernel")。 從000800開始是塊組描述符表,這個文件系統較小,只有一個塊組描述符,對照著`dumpe2fs`的輸出信息分析如下: ``` ... Group 0: (Blocks 1-1023) Primary superblock at 1, Group descriptors at 2-2 Reserved GDT blocks at 3-5 Block bitmap at 6 (+5), Inode bitmap at 7 (+6) Inode table at 8-23 (+7) 986 free blocks, 117 free inodes, 2 directories Free blocks: 38-1023 Free inodes: 12-128 ... ``` **圖?29.4.?塊組描述符** ![塊組描述符](https://box.kancloud.cn/2016-04-02_56ff80d86a793.png) 整個文件系統是1MB,每個塊是1KB,應該有1024個塊,除去啟動塊還有1023個塊,分別編號為1-1023,它們全都屬于Group 0。其中,Block 1是超級塊,接下來的塊組描述符指出,塊位圖是Block 6,因此中間的Block 2-5是塊組描述符表,其中Block 3-5保留未用。塊組描述符還指出,inode位圖是Block 7,inode表是從Block 8開始的,那么inode表到哪個塊結束呢?由于超級塊中指出每個塊組有128個inode,每個inode的大小是128字節,因此共占16個塊,inode表的范圍是Block 8-23。 從Block 24開始就是數據塊了。塊組描述符中指出,空閑的數據塊有986個,由于文件系統是新創建的,空閑塊是連續的Block 38-1023,用掉了前面的Block 24-37。從塊位圖中可以看出,前37位(前4個字節加最后一個字節的低5位)都是1,就表示Block 1-37已用: ``` 001800 ff ff ff ff 1f 00 00 00 00 00 00 00 00 00 00 00 001810 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 * 001870 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 80 001880 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff * ``` 在塊位圖中,Block 38-1023對應的位都是0(一直到001870那一行最后一個字節的低7位),接下來的位已經超出了文件系統的空間,不管是0還是1都沒有意義。可見,塊位圖每個字節中的位應該按從低位到高位的順序來看。以后隨著文件系統的使用和添加刪除文件,塊位圖中的1就變得不連續了。 塊組描述符指出,空閑的inode有117個,由于文件系統是新創建的,空閑的inode也是連續的,inode編號從1到128,空閑的inode編號從12到128。從inode位圖可以看出,前11位都是1,表示前11個inode已用: ``` 001c00 ff 07 00 00 00 00 00 00 00 00 00 00 00 00 00 00 001c10 ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff ff * ``` 以后隨著文件系統的使用和添加刪除文件,inode位圖中的1就變得不連續了。 001c00這一行的128位就表示了所有inode,因此下面的行不管是0還是1都沒有意義。已用的11個inode中,前10個inode是被ext2文件系統保留的,其中第2個inode是根目錄,第11個inode是`lost+found`目錄,塊組描述符也指出該組有兩個目錄,就是根目錄和`lost+found`。 探索文件系統還有一個很有用的工具`debugfs`,它提供一個命令行界面,可以對文件系統做各種操作,例如查看信息、恢復數據、修正文件系統中的錯誤。下面用`debugfs`打開`fs`文件,然后在提示符下輸入`help`看看它都能做哪些事情: ``` $ debugfs fs debugfs 1.40.2 (12-Jul-2007) debugfs: help ``` 在`debugfs`的提示符下輸入`stat /`命令,這時在新的一屏中顯示根目錄的inode信息: ``` Inode: 2 Type: directory Mode: 0755 Flags: 0x0 Generation: 0 User: 1000 Group: 1000 Size: 1024 File ACL: 0 Directory ACL: 0 Links: 3 Blockcount: 2 Fragment: Address: 0 Number: 0 Size: 0 ctime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007 atime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007 mtime: 0x4764cc3b -- Sun Dec 16 14:56:59 2007 BLOCKS: (0):24 TOTAL: 1 ``` 按q退出這一屏,然后用`quit`命令退出`debugfs`: ``` debugfs: quit ``` 把以上信息和`od`命令的輸出對照起來分析: **圖?29.5.?根目錄的inode** ![根目錄的inode](https://box.kancloud.cn/2016-04-02_56ff80d87eb5e.png) 上圖中的`st_mode`以八進制表示,包含了文件類型和文件權限,最高位的4表示文件類型為目錄(各種文件類型的編碼詳見stat(2)),低位的755表示權限。Size是1024,說明根目錄現在只有一個數據塊。Links為3表示根目錄有三個硬鏈接,分別是根目錄下的`.`和`..`,以及`lost+found`子目錄下的`..`。注意,雖然我們通常用`/`表示根目錄,但是并沒有名為`/`的硬鏈接,事實上,`/`是路徑分隔符,不能在文件名中出現。這里的`Blockcount`是以512字節為一個塊來數的,并非格式化文件系統時所指定的塊大小,磁盤的最小讀寫單位稱為扇區(Sector),通常是512字節,所以`Blockcount`是磁盤的物理塊數量,而非分區的邏輯塊數量。根目錄數據塊的位置由上圖中的`Blocks[0]`指出,也就是第24個塊,它在文件系統中的位置是24×0x400=0x6000,從`od`命令的輸出中找到006000地址,它的格式是這樣: **圖?29.6.?根目錄的數據塊** ![根目錄的數據塊](https://box.kancloud.cn/2016-04-02_56ff80d89b676.png) 目錄的數據塊由許多不定長的記錄組成,每條記錄描述該目錄下的一個文件,在上圖中用框表示。第一條記錄描述inode號為2的文件,也就是根目錄本身,該記錄的總長度為12字節,其中文件名的長度為1字節,文件類型為2(見下表,注意此處的文件類型編碼和`st_mode`不一致),文件名是`.`。 **表?29.1.?目錄中的文件類型編碼** | 編碼 | 文件類型 | | --- | --- | | 0 | Unknown | | 1 | Regular file | | 2 | Directory | | 3 | Character device | | 4 | Block device | | 5 | Named pipe | | 6 | Socket | | 7 | Symbolic link | 第二條記錄也是描述inode號為2的文件(根目錄),該記錄總長度為12字節,其中文件名的長度為2字節,文件類型為2,文件名字符串是`..`。第三條記錄一直延續到該數據塊的末尾,描述inode號為11的文件(`lost+found`目錄),該記錄的總長度為1000字節(和前面兩條記錄加起來是1024字節),文件類型為2,文件名字符串是`lost+found`,后面全是0字節。如果要在根目錄下創建新的文件,可以把第三條記錄截短,在原來的0字節處創建新的記錄。如果該目錄下的文件名太多,一個數據塊不夠用,則會分配新的數據塊,塊編號會填充到inode的`Blocks[1]`字段。 `debugfs`也提供了`cd`、`ls`等命令,不需要`mount`就可以查看這個文件系統中的目錄,例如用`ls`查看根目錄: ``` 2 (12) . 2 (12) .. 11 (1000) lost+found ``` 列出了inode號、記錄長度和文件名,這些信息都是從根目錄的數據塊中讀出來的。 #### 習題 1、請讀者仿照對根目錄的分析,自己分析`lost+found`目錄的inode和數據塊的格式。 2、`mount`這個文件系統,在里面添加刪除文件,然后`umount`下來,再次分析它的格式,和原來的結果比較一下看哪些字節發生了變化。 ### 2.3.?數據塊尋址 如果一個文件有多個數據塊,這些數據塊很可能不是連續存放的,應該如何尋址到每個塊呢?根據上面的分析,根目錄的數據塊是通過其inode中的索引項`Blocks[0]`找到的,事實上,這樣的索引項一共有15個,從`Blocks[0]`到`Blocks[14]`,每個索引項占4字節。前12個索引項都表示塊編號,例如上面的例子中`Blocks[0]`字段保存著24,就表示第24個塊是該文件的數據塊,如果塊大小是1KB,這樣可以表示從0字節到12KB的文件。如果剩下的三個索引項`Blocks[12]`到`Blocks[14]`也是這么用的,就只能表示最大15KB的文件了,這是遠遠不夠的,事實上,剩下的三個索引項都是間接索引。 索引項`Blocks[12]`所指向的塊并非數據塊,而是稱為間接尋址塊(Indirect Block),其中存放的都是類似`Blocks[0]`這種索引項,再由索引項指向數據塊。設塊大小是b,那么一個間接尋址塊中可以存放b/4個索引項,指向b/4個數據塊。所以如果把`Blocks[0]`到`Blocks[12]`都用上,最多可以表示b/4+12個數據塊,對于塊大小是1K的情況,最大可表示268K的文件。如下圖所示,注意文件的數據塊編號是從0開始的,`Blocks[0]`指向第0個數據塊,`Blocks[11]`指向第11個數據塊,`Blocks[12]`所指向的間接尋址塊的第一個索引項指向第12個數據塊,依此類推。 **圖?29.7.?數據塊的尋址** ![數據塊的尋址](https://box.kancloud.cn/2016-04-02_56ff80d8b8a00.png) 從上圖可以看出,索引項`Blocks[13]`指向兩級的間接尋址塊,最多可表示(b/4)<sup>2</sup>+b/4+12個數據塊,對于1K的塊大小最大可表示64.26MB的文件。索引項`Blocks[14]`指向三級的間接尋址塊,最多可表示(b/4)<sup>3</sup>+(b/4)<sup>2</sup>+b/4+12個數據塊,對于1K的塊大小最大可表示16.06GB的文件。 可見,這種尋址方式對于訪問不超過12個數據塊的小文件是非常快的,訪問文件中的任意數據只需要兩次讀盤操作,一次讀inode(也就是讀索引項)一次讀數據塊。而訪問大文件中的數據則需要最多五次讀盤操作:inode、一級間接尋址塊、二級間接尋址塊、三級間接尋址塊、數據塊。實際上,磁盤中的inode和數據塊往往已經被內核緩存了,讀大文件的效率也不會太低。 ### 2.4.?文件和目錄操作的系統函數 本節簡要介紹一下文件和目錄操作常用的系統函數,常用的文件操作命令如`ls`、`cp`、`mv`等也是基于這些函數實現的。本節的側重點在于講解這些函數的工作原理,而不是如何使用它們,理解了實現原理之后再看這些函數的用法就很簡單了,請讀者自己查閱Man Page了解其用法。 `stat(2)`函數讀取文件的inode,然后把inode中的各種文件屬性填入一個`struct stat`結構體傳出給調用者。`stat(1)`命令是基于`stat`函數實現的。`stat`需要根據傳入的文件路徑找到inode,假設一個路徑是`/opt/file`,則查找的順序是: 1. 讀出inode表中第2項,也就是根目錄的inode,從中找出根目錄數據塊的位置 2. 從根目錄的數據塊中找出文件名為`opt`的記錄,從記錄中讀出它的inode號 3. 讀出`opt`目錄的inode,從中找出它的數據塊的位置 4. 從`opt`目錄的數據塊中找出文件名為`file`的記錄,從記錄中讀出它的inode號 5. 讀出`file`文件的inode 還有另外兩個類似`stat`的函數:`fstat(2)`函數傳入一個已打開的文件描述符,傳出inode信息,`lstat(2)`函數也是傳入路徑傳出inode信息,但是和`stat`函數有一點不同,當文件是一個符號鏈接時,`stat(2)`函數傳出的是它所指向的目標文件的inode,而`lstat`函數傳出的就是符號鏈接文件本身的inode。 `access(2)`函數檢查執行當前進程的用戶是否有權限訪問某個文件,傳入文件路徑和要執行的訪問操作(讀/寫/執行),`access`函數取出文件inode中的`st_mode`字段,比較一下訪問權限,然后返回0表示允許訪問,返回-1表示錯誤或不允許訪問。 `chmod(2)`和`fchmod(2)`函數改變文件的訪問權限,也就是修改inode中的`st_mode`字段。這兩個函數的區別類似于`stat`/`fstat`。`chmod(1)`命令是基于`chmod`函數實現的。 `chown(2)`/`fchown(2)`/`lchown(2)`改變文件的所有者和組,也就是修改inode中的`User`和`Group`字段,只有超級用戶才能正確調用這幾個函數,這幾個函數之間的區別類似于`stat`/`fstat`/`lstat`。`chown(1)`命令是基于`chown`函數實現的。 `utime(2)`函數改變文件的訪問時間和修改時間,也就是修改inode中的`atime`和`mtime`字段。`touch(1)`命令是基于`utime`函數實現的。 `truncate(2)`和`ftruncate(2)`函數把文件截斷到某個長度,如果新的長度比原來的長度短,則后面的數據被截掉了,如果新的長度比原來的長度長,則后面多出來的部分用0填充,這需要修改inode中的`Blocks`索引項以及塊位圖中相應的bit。這兩個函數的區別類似于`stat`/`fstat`。 `link(2)`函數創建硬鏈接,其原理是在目錄的數據塊中添加一條新記錄,其中的inode號字段和原文件相同。`symlink(2)`函數創建一個符號鏈接,這需要創建一個新的inode,其中`st_mode`字段的文件類型是符號鏈接,原文件的路徑保存在inode中或者分配一個數據塊來保存。`ln(1)`命令是基于`link`和`symlink`函數實現的。 `unlink(2)`函數刪除一個鏈接。如果是符號鏈接則釋放這個符號鏈接的inode和數據塊,清除inode位圖和塊位圖中相應的位。如果是硬鏈接則從目錄的數據塊中清除一條文件名記錄,如果當前文件的硬鏈接數已經是1了還要刪除它,就同時釋放它的inode和數據塊,清除inode位圖和塊位圖中相應的位,這樣就真的刪除文件了。`unlink(1)`命令和`rm(1)`命令是基于`unlink`函數實現的。 `rename(2)`函數改變文件名,需要修改目錄數據塊中的文件名記錄,如果原文件名和新文件名不在一個目錄下則需要從原目錄數據塊中清除一條記錄然后添加到新目錄的數據塊中。`mv(1)`命令是基于`rename`函數實現的,因此在同一分區的不同目錄中移動文件并不需要復制和刪除文件的inode和數據塊,只需要一個改名操作,即使要移動整個目錄,這個目錄下有很多子目錄和文件也要隨著一起移動,移動操作也只是對頂級目錄的改名操作,很快就能完成。但是,如果在不同的分區之間移動文件就必須復制和刪除inode和數據塊,如果要移動整個目錄,所有子目錄和文件都要復制刪除,這就很慢了。 `readlink(2)`函數讀取一個符號鏈接所指向的目標路徑,其原理是從符號鏈接的inode或數據塊中讀出保存的數據,這就是目標路徑。 `mkdir(2)`函數創建新的目錄,要做的操作是在它的父目錄數據塊中添加一條記錄,然后分配新的inode和數據塊,inode的`st_mode`字段的文件類型是目錄,在數據塊中填兩個記錄,分別是`.`和`..`,由于`..`表示父目錄,因此父目錄的硬鏈接數要加1。`mkdir(1)`命令是基于`mkdir`函數實現的。 `rmdir(2)`函數刪除一個目錄,這個目錄必須是空的(只包含`.`和`..`)才能刪除,要做的操作是釋放它的inode和數據塊,清除inode位圖和塊位圖中相應的位,清除父目錄數據塊中的記錄,父目錄的硬鏈接數要減1。`rmdir(1)`命令是基于`rmdir`函數實現的。 `opendir(3)`/`readdir(3)`/`closedir(3)`用于遍歷目錄數據塊中的記錄。`opendir`打開一個目錄,返回一個`DIR *`指針代表這個目錄,它是一個類似`FILE *`指針的句柄,`closedir`用于關閉這個句柄,把`DIR *`指針傳給`readdir`讀取目錄數據塊中的記錄,每次返回一個指向`struct dirent`的指針,反復讀就可以遍歷所有記錄,所有記錄遍歷完之后`readdir`返回`NULL`。結構體`struct dirent`的定義如下: ``` struct dirent { ino_t d_ino; /* inode number */ off_t d_off; /* offset to the next dirent */ unsigned short d_reclen; /* length of this record */ unsigned char d_type; /* type of file */ char d_name[256]; /* filename */ }; ``` 這些字段和[圖?29.6 “根目錄的數據塊”](ch29s02.html#fs.datablock)基本一致。這里的文件名`d_name`被庫函數處理過,已經在結尾加了'\0',而[圖?29.6 “根目錄的數據塊”](ch29s02.html#fs.datablock)中的文件名字段不保證是以'\0'結尾的,需要根據前面的文件名長度字段確定文件名到哪里結束。 下面這個例子出自[[K&R]](bi01.html#bibli.kr "The C Programming Language"),作用是遞歸地打印出一個目錄下的所有子目錄和文件,類似`ls -R`。 **例?29.1.?遞歸列出目錄中的文件列表** ``` #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <dirent.h> #include <stdio.h> #include <string.h> #define MAX_PATH 1024 /* dirwalk: apply fcn to all files in dir */ void dirwalk(char *dir, void (*fcn)(char *)) { char name[MAX_PATH]; struct dirent *dp; DIR *dfd; if ((dfd = opendir(dir)) == NULL) { fprintf(stderr, "dirwalk: can't open %s\n", dir); return; } while ((dp = readdir(dfd)) != NULL) { if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) continue; /* skip self and parent */ if (strlen(dir)+strlen(dp->d_name)+2 > sizeof(name)) fprintf(stderr, "dirwalk: name %s %s too long\n", dir, dp->d_name); else { sprintf(name, "%s/%s", dir, dp->d_name); (*fcn)(name); } } closedir(dfd); } /* fsize: print the size and name of file "name" */ void fsize(char *name) { struct stat stbuf; if (stat(name, &stbuf) == -1) { fprintf(stderr, "fsize: can't access %s\n", name); return; } if ((stbuf.st_mode & S_IFMT) == S_IFDIR) dirwalk(name, fsize); printf("%8ld %s\n", stbuf.st_size, name); } int main(int argc, char **argv) { if (argc == 1) /* default: current directory */ fsize("."); else while (--argc > 0) fsize(*++argv); return 0; } ``` 然而這個程序還是不如`ls -R`健壯,它有可能死循環,思考一下什么情況會導致死循環。 ## 3.?VFS Linux支持各種各樣的文件系統格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁盤分區、光盤或其它存儲設備都有不同的文件系統格式,然而這些文件系統都可以`mount`到某個目錄下,使我們看到一個統一的目錄樹,各種文件系統上的目錄和文件我們用`ls`命令看起來是一樣的,讀寫操作用起來也都是一樣的,這是怎么做到的呢?Linux內核在各種不同的文件系統格式之上做了一個抽象層,使得文件、目錄、讀寫訪問等概念成為抽象層的概念,因此各種文件系統看起來用起來都一樣,這個抽象層稱為虛擬文件系統(VFS,Virtual Filesystem)。上一節我們介紹了一種典型的文件系統在磁盤上的存儲布局,這一節我們介紹運行時文件系統在內核中的表示。 ### 3.1.?內核數據結構 Linux內核的VFS子系統可以圖示如下: **圖?29.8.?VFS** ![VFS](https://box.kancloud.cn/2016-04-02_56ff80d8d4669.png) 在[第?28?章 _文件與I/O_](ch28.html#io)中講過,每個進程在PCB(Process Control Block)中都保存著一份文件描述符表,文件描述符就是這個表的索引,每個表項都有一個指向已打開文件的指針,現在我們明確一下:已打開的文件在內核中用`file`結構體表示,文件描述符表中的指針指向`file`結構體。 在`file`結構體中維護File Status Flag(`file`結構體的成員`f_flags`)和當前讀寫位置(`file`結構體的成員`f_pos`)。在上圖中,進程1和進程2都打開同一文件,但是對應不同的`file`結構體,因此可以有不同的File Status Flag和讀寫位置。`file`結構體中比較重要的成員還有`f_count`,表示引用計數(Reference Count),后面我們會講到,`dup`、`fork`等系統調用會導致多個文件描述符指向同一個`file`結構體,例如有`fd1`和`fd2`都引用同一個`file`結構體,那么它的引用計數就是2,當`close(fd1)`時并不會釋放`file`結構體,而只是把引用計數減到1,如果再`close(fd2)`,引用計數就會減到0同時釋放`file`結構體,這才真的關閉了文件。 每個`file`結構體都指向一個`file_operations`結構體,這個結構體的成員都是函數指針,指向實現各種文件操作的內核函數。比如在用戶程序中`read`一個文件描述符,`read`通過系統調用進入內核,然后找到這個文件描述符所指向的`file`結構體,找到`file`結構體所指向的`file_operations`結構體,調用它的`read`成員所指向的內核函數以完成用戶請求。在用戶程序中調用`lseek`、`read`、`write`、`ioctl`、`open`等函數,最終都由內核調用`file_operations`的各成員所指向的內核函數完成用戶請求。`file_operations`結構體中的`release`成員用于完成用戶程序的`close`請求,之所以叫`release`而不叫`close`是因為它不一定真的關閉文件,而是減少引用計數,只有引用計數減到0才關閉文件。對于同一個文件系統上打開的常規文件來說,`read`、`write`等文件操作的步驟和方法應該是一樣的,調用的函數應該是相同的,所以圖中的三個打開文件的`file`結構體指向同一個`file_operations`結構體。如果打開一個字符設備文件,那么它的`read`、`write`操作肯定和常規文件不一樣,不是讀寫磁盤的數據塊而是讀寫硬件設備,所以`file`結構體應該指向不同的`file_operations`結構體,其中的各種文件操作函數由該設備的驅動程序實現。 每個`file`結構體都有一個指向`dentry`結構體的指針,“dentry”是directory entry(目錄項)的縮寫。我們傳給`open`、`stat`等函數的參數的是一個路徑,例如`/home/akaedu/a`,需要根據路徑找到文件的inode。為了減少讀盤次數,內核緩存了目錄的樹狀結構,稱為dentry cache,其中每個節點是一個`dentry`結構體,只要沿著路徑各部分的dentry搜索即可,從根目錄`/`找到`home`目錄,然后找到`akaedu`目錄,然后找到文件`a`。dentry cache只保存最近訪問過的目錄項,如果要找的目錄項在cache中沒有,就要從磁盤讀到內存中。 每個`dentry`結構體都有一個指針指向`inode`結構體。`inode`結構體保存著從磁盤inode讀上來的信息。在上圖的例子中,有兩個dentry,分別表示`/home/akaedu/a`和`/home/akaedu/b`,它們都指向同一個inode,說明這兩個文件互為硬鏈接。`inode`結構體中保存著從磁盤分區的inode讀上來信息,例如所有者、文件大小、文件類型和權限位等。每個`inode`結構體都有一個指向`inode_operations`結構體的指針,后者也是一組函數指針指向一些完成文件目錄操作的內核函數。和`file_operations`不同,`inode_operations`所指向的不是針對某一個文件進行操作的函數,而是影響文件和目錄布局的函數,例如添加刪除文件和目錄、跟蹤符號鏈接等等,屬于同一文件系統的各`inode`結構體可以指向同一個`inode_operations`結構體。 `inode`結構體有一個指向`super_block`結構體的指針。`super_block`結構體保存著從磁盤分區的超級塊讀上來的信息,例如文件系統類型、塊大小等。`super_block`結構體的`s_root`成員是一個指向`dentry`的指針,表示這個文件系統的根目錄被`mount`到哪里,在上圖的例子中這個分區被`mount`到`/home`目錄下。 `file`、`dentry`、`inode`、`super_block`這幾個結構體組成了VFS的核心概念。對于ext2文件系統來說,在磁盤存儲布局上也有inode和超級塊的概念,所以很容易和VFS中的概念建立對應關系。而另外一些文件系統格式來自非UNIX系統(例如Windows的FAT32、NTFS),可能沒有inode或超級塊這樣的概念,但為了能`mount`到Linux系統,也只好在驅動程序中硬湊一下,在Linux下看FAT32和NTFS分區會發現權限位是錯的,所有文件都是`rwxrwxrwx`,因為它們本來就沒有inode和權限位的概念,這是硬湊出來的。 ### 3.2.?dup和dup2函數 `dup`和`dup2`都可用來復制一個現存的文件描述符,使兩個文件描述符指向同一個`file`結構體。如果兩個文件描述符指向同一個`file`結構體,File Status Flag和讀寫位置只保存一份在`file`結構體中,并且`file`結構體的引用計數是2。如果兩次`open`同一文件得到兩個文件描述符,則每個描述符對應一個不同的`file`結構體,可以有不同的File Status Flag和讀寫位置。請注意區分這兩種情況。 ``` #include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd); ``` 如果調用成功,這兩個函數都返回新分配或指定的文件描述符,如果出錯則返回-1。`dup`返回的新文件描述符一定該進程未使用的最小文件描述符,這一點和`open`類似。`dup2`可以用`newfd`參數指定新描述符的數值。如果`newfd`當前已經打開,則先將其關閉再做`dup2`操作,如果`oldfd`等于`newfd`,則`dup2`直接返回`newfd`而不用先關閉`newfd`再復制。 下面這個例子演示了`dup`和`dup2`函數的用法,請結合后面的連環畫理解程序的執行過程。 **例?29.2.?dup和dup2示例程序** ``` #include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int fd, save_fd; char msg[] = "This is a test\n"; fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); if(fd<0) { perror("open"); exit(1); } save_fd = dup(STDOUT_FILENO); dup2(fd, STDOUT_FILENO); close(fd); write(STDOUT_FILENO, msg, strlen(msg)); dup2(save_fd, STDOUT_FILENO); write(STDOUT_FILENO, msg, strlen(msg)); close(save_fd); return 0; } ``` **圖?29.9.?dup/dup2示例程序** ![dup/dup2示例程序](https://box.kancloud.cn/2016-04-02_56ff80d8f3dc2.png) 重點解釋兩個地方: * 第3幅圖,要執行`dup2(fd, 1);`,文件描述符1原本指向`tty`,現在要指向新的文件`somefile`,就把原來的關閉了,但是`tty`這個文件原本有兩個引用計數,還有文件描述符`save_fd`也指向它,所以只是將引用計數減1,并不真的關閉文件。 * 第5幅圖,要執行`dup2(save_fd, 1);`,文件描述符1原本指向`somefile`,現在要指向新的文件`tty`,就把原來的關閉了,`somefile`原本只有一個引用計數,所以這次減到0,是真的關閉了。
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看