1. 程式人生 > >ext2檔案系統結構分析

ext2檔案系統結構分析

轉載自:https://blog.csdn.net/yuzhihui_no1/article/details/50256713

ext2檔案系統

總體儲存佈局

我們知道,一個磁碟可以劃分成多個分割槽,每個分割槽必須先用格式化工具(例如某種mkfs命令)格式化成某種格式的檔案系統,然後才能儲存檔案,格式化的過程會在磁碟上寫一些管理儲存佈局的資訊。下圖是一個磁碟分割槽格式化成ext2檔案系統後的儲存佈局。

圖 29.2. ext2檔案系統的總體儲存佈局

ext2檔案系統的總體儲存佈局

 

檔案系統中儲存的最小單位是塊(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)

一個塊組中的塊是這樣利用的:資料塊(Data Block)

儲存所有檔案的資料,比如某個分割槽的塊大小是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。

資料塊

根據不同的檔案型別有以下幾種情況

  • 對於常規檔案,檔案的資料儲存在資料塊中。

  • 對於目錄,該目錄下的所有檔名和目錄名儲存在資料塊中,注意檔名儲存在它所在目錄的資料塊中,除檔名之外,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

hello2hello除了檔名不一樣之外,別的屬性都一模一樣,並且hello的屬性發生了變化,第二欄的數字原本是1,現在變成2了。從根本上說,hellohello2是同一個檔案在檔案系統中的兩個名字,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個名字分別是當前目錄下的aa目錄下的.b目錄下的..。目錄b的硬連結數是2,這兩個名字分別是a目錄下的bb目錄下的.。注意,目錄的硬連結只能這種方式建立,用ln命令可以建立目錄的符號連結,但不能建立目錄的硬連結。

例項剖析

如果要格式化一個分割槽來研究檔案系統格式則必須有一個空閒的磁碟分割槽,為了方便實驗,我們把一個檔案當作分割槽來格式化,然後分析這個檔案中的資料來印證上面所講的要點。首先建立一個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檔案。ifof引數表示輸入檔案和輸出檔案,countbs引數表示拷貝多少次,每次拷多少位元組。

做好之後對檔案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. 超級塊

超級塊

 

超級塊中從0004d0到末尾的204個位元組是填充位元組,保留未用,上圖未畫出。注意,ext2檔案系統中各欄位都是按小端儲存的,如果把位元組在檔案中的位置看作地址,那麼靠近檔案開頭的是低地址,存低位元組。各欄位的位置、長度和含義詳見[ULK]

從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. 塊組描述符

塊組描述符

 

整個檔案系統是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

 

上圖中的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. 根目錄的資料塊

根目錄的資料塊

 

目錄的資料塊由許多不定長的記錄組成,每條記錄描述該目錄下的一個檔案,在上圖中用框表示。第一條記錄描述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也提供了cdls等命令,不需要mount就可以檢視這個檔案系統中的目錄,例如用ls檢視根目錄:

 2  (12) .    2  (12) ..    11  (1000) lost+found

列出了inode號、記錄長度和檔名,這些資訊都是從根目錄的資料塊中讀出來的。

習題

1、請讀者仿照對根目錄的分析,自己分析lost+found目錄的inode和資料塊的格式。

2、mount這個檔案系統,在裡面新增刪除檔案,然後umount下來,再次分析它的格式,和原來的結果比較一下看哪些位元組發生了變化。

資料塊定址

如果一個檔案有多個數據塊,這些資料塊很可能不是連續存放的,應該如何定址到每個塊呢?根據上面的分析,根目錄的資料塊是通過其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. 資料塊的定址

資料塊的定址

 

從上圖可以看出,索引項Blocks[13]指向兩級的間接定址塊,最多可表示(b/4)2+b/4+12個數據塊,對於1K的塊大小最大可表示64.26MB的檔案。索引項Blocks[14]指向三級的間接定址塊,最多可表示(b/4)3+(b/4)2+b/4+12個數據塊,對於1K的塊大小最大可表示16.06GB的檔案。

可見,這種定址方式對於訪問不超過12個數據塊的小檔案是非常快的,訪問檔案中的任意資料只需要兩次讀盤操作,一次讀inode(也就是讀索引項)一次讀資料塊。而訪問大檔案中的資料則需要最多五次讀盤操作:inode、一級間接定址塊、二級間接定址塊、三級間接定址塊、資料塊。實際上,磁碟中的inode和資料塊往往已經被核心快取了,讀大檔案的效率也不會太低。

檔案和目錄操作的系統函式

本節簡要介紹一下檔案和目錄操作常用的系統函式,常用的檔案操作命令如lscpmv等也是基於這些函式實現的。本節的側重點在於講解這些函式的工作原理,而不是如何使用它們,理解了實現原理之後再看這些函式的用法就很簡單了,請讀者自己查閱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/fstatchmod(1)命令是基於chmod函式實現的。

chown(2)/fchown(2)/lchown(2)改變檔案的所有者和組,也就是修改inode中的UserGroup欄位,只有超級使用者才能正確呼叫這幾個函式,這幾個函式之間的區別類似於stat/fstat/lstatchown(1)命令是基於chown函式實現的。

utime(2)函式改變檔案的訪問時間和修改時間,也就是修改inode中的atimemtime欄位。touch(1)命令是基於utime函式實現的。

truncate(2)ftruncate(2)函式把檔案截斷到某個長度,如果新的長度比原來的長度短,則後面的資料被截掉了,如果新的長度比原來的長度長,則後面多出來的部分用0填充,這需要修改inode中的Blocks索引項以及塊點陣圖中相應的bit。這兩個函式的區別類似於stat/fstat

link(2)函式建立硬連結,其原理是在目錄的資料塊中新增一條新記錄,其中的inode號欄位和原檔案相同。symlink(2)函式建立一個符號連結,這需要建立一個新的inode,其中st_mode欄位的檔案型別是符號連結,原檔案的路徑儲存在inode中或者分配一個數據塊來儲存。ln(1)命令是基於linksymlink函式實現的。

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 “根目錄的資料塊”基本一致。這裡的檔名d_name被庫函式處理過,已經在結尾加了'\0',而圖 29.6 “根目錄的資料塊”中的檔名欄位不保證是以'\0'結尾的,需要根據前面的檔名長度欄位確定檔名到哪裡結束。

下面這個例子出自[K&R],作用是遞迴地打印出一個目錄下的所有子目錄和檔案,類似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健壯,它有可能死迴圈,思考一下什麼情況會導致死迴圈。

 

轉載地址:http://docs.linuxtone.org/ebooks/C&CPP/c/ch29s02.html