Linux 檔案系統理解
1.ext2檔案系統整體佈局
一個磁碟可以劃分成多個分割槽,每個分割槽必須先用格式化工具(例如某種mkfs
命令)格式化成某種格式的檔案系統,然後才能儲存檔案,格式化的過程會在磁碟上寫一些管理儲存佈局的資訊。下圖是一個磁碟分割槽格式化成ext2檔案系統後的儲存佈局。
檔案系統中儲存的最小單位是塊(Block),一個塊究竟多大是在格式化時確定的,例如mke2fs
的-b
選項可以設定塊大小為1024、2048或4096位元組,這些 blocks 被聚在一起分成幾個大的 block group。每個 block group 中有多少個 block 是固定的。而上圖中啟動塊(Boot Block)的大小是確定的,就是1KB,啟動塊是由PC標準規定的,用來儲存磁碟分割槽資訊和啟動資訊
1).超級塊(Super Block)
描述整個分割槽的檔案系統資訊,例如塊大小、檔案系統版本號、上次mount
的時間等等。超級塊在每個塊組的開頭都有一份拷貝。
2).塊組描述符表(GDT,Group Descriptor Table)
由很多塊組描述符組成,整個分割槽分成多少個塊組就對應有多少個塊組描述符。每個塊組描述符(Group Descriptor)儲存一個塊組的描述資訊,例如在這個塊組中從哪裡開始是inode表,從哪裡開始是資料塊,空閒的inode和資料塊還有多少個等等。和超級塊類似,塊組描述符表在每個塊組的開頭也都有一份拷貝
e2fsck
檢查檔案系統一致性時,第0個塊組中的超級塊和塊組描述符表就會拷貝到其它塊組,這樣當第0個塊組的開頭意外損壞時就可以用其它拷貝來恢復,從而減少損失。
3).塊點陣圖(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
工具會計算出最優的數值。
4).inode點陣圖(inode Bitmap)
和塊點陣圖類似,本身佔一個塊,其中每個bit表示一個inode是否空閒可用
5).inode表(inode Table)
我們知道,一個檔案除了資料需要儲存之外,一些描述資訊也需要儲存,例如檔案型別(常規、目錄、符號連結等),許可權,檔案大小,建立/修改/訪問時間等,也就是ls -l
命令看到的那些資訊,這些資訊存在inode中而不是資料塊中。每個檔案都有一個inode,一個塊組中的所有inode組成了inode表。由於每個
inode 與 block 都有編號,而每個檔案都會佔用一個 inode ,inode 內則有檔案資料放置的 block 號碼。 因此,我們可以知道的是,如果能夠找到檔案的 inode 的話,那麼自然就會知道這個檔案所放置資料的 block 號碼, 當然也就能夠讀出該檔案的實際資料了。
inode表佔多少個塊在格式化時就要決定並寫入塊組描述符中,mke2fs
格式化工具的預設策略是一個塊組有多少個8KB就分配多少個inode。由於資料塊佔了整個塊組的絕大部分,也可以近似認為資料塊有多少個8KB就分配多少個inode,換句話說,如果平均每個檔案的大小是8KB,當分割槽存滿的時候inode表會得到比較充分的利用,資料塊也不浪費。如果這個分割槽存的都是很大的檔案(比如電影),則資料塊用完的時候inode會有一些浪費,如果這個分割槽存的都是很小的檔案(比如原始碼),則有可能資料塊還沒用完inode就已經用完了,資料塊可能有很大的浪費。如果使用者在格式化時能夠對這個分割槽以後要儲存的檔案大小做一個預測,也可以用mke2fs
的-i
引數手動指定每多少個位元組分配一個inode。
6).資料塊(Data Block)
根據不同的檔案型別有以下幾種情況
-
對於常規檔案,檔案的資料儲存在資料塊中。
-
對於目錄,該目錄下的所有檔名和目錄名儲存在資料塊中,注意檔名儲存在它所在目錄的資料塊中,除檔名之外,
ls -l
命令看到的其它資訊都儲存在該檔案的inode中。注意這個概念:目錄也是一種檔案,是一種特殊型別的檔案。 -
對於符號連結,如果目標路徑名較短則直接儲存在inode中以便更快地查詢,如果目標路徑名較長則分配一個數據塊來儲存。
-
裝置檔案、FIFO和socket等特殊檔案沒有資料塊,裝置檔案的主裝置號和次裝置號儲存在inode中。
2.資料塊定址
如果一個檔案有多個數據塊,這些資料塊很可能不是連續存放的,應該如何定址到每個塊呢?實際上,根目錄的資料塊是通過其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個數據塊,依此類推。
從上圖可以看出,索引項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和資料塊往往已經被核心快取了,讀大檔案的效率也不會太低。
3.檔案和目錄操作的系統函式(這裡面的"(n)"應該表示引數的個數)
1).stat(2)
函式讀取檔案的inode,然後把inode中的各種檔案屬性填入一個struct stat
結構體傳出給呼叫者。stat(1)
命令是基於stat
函式實現的。stat
需要根據傳入的檔案路徑找到inode,假設一個路徑是/opt/file
,則查詢的順序是:
-
讀出inode表中第2項,也就是根目錄的inode,從中找出根目錄資料塊的位置
-
從根目錄的資料塊中找出檔名為
opt
的記錄,從記錄中讀出它的inode號 -
讀出
opt
目錄的inode,從中找出它的資料塊的位置 -
從
opt
目錄的資料塊中找出檔名為file
的記錄,從記錄中讀出它的inode號 -
讀出
file
檔案的inode
還有另外兩個類似stat
的函式:fstat(2)
函式傳入一個已開啟的檔案描述符,傳出inode資訊,lstat(2)
函式也是傳入路徑傳出inode資訊,但是和stat
函式有一點不同,當檔案是一個符號連結時,stat(2)
函式傳出的是它所指向的目標檔案的inode,而lstat
函式傳出的就是符號連結檔案本身的inode。
2).access(2)
函式檢查執行當前程序的使用者是否有許可權訪問某個檔案,傳入檔案路徑和要執行的訪問操作(讀/寫/執行),access
函式取出檔案inode中的st_mode
欄位,比較一下訪問許可權,然後返回0表示允許訪問,返回-1表示錯誤或不允許訪問。
3).chmod(2)
和fchmod(2)
函式改變檔案的訪問許可權,也就是修改inode中的st_mode
欄位。這兩個函式的區別類似於stat
/fstat
。chmod(1)
命令是基於chmod
函式實現的。
4).chown
(
2
)
/fchown(2)
/lchown(2)
改變檔案的所有者和組,也就是修改inode中的User
和Group
欄位,只有超級使用者才能正確呼叫這幾個函式,這幾個函式之間的區別類似於stat
/fstat
/lstat
。chown(1)
命令是基於chown
函式實現的。
5).utime(2)
函式改變檔案的訪問時間和修改時間,也就是修改inode中的atime
和mtime
欄位。touch(1)
命令是基於utime
函式實現的。
6).truncate(2)
和ftruncate(2)
函式把檔案截斷到某個長度,如果新的長度比原來的長度短,則後面的資料被截掉了,如果新的長度比原來的長度長,則後面多出來的部分用0填充,這需要修改inode中的Blocks
索引項以及塊點陣圖中相應的bit。這兩個函式的區別類似於stat
/fstat
。
7).link(2)
函式建立硬連結,其原理是在目錄的資料塊中新增一條新記錄,其中的inode號欄位和原檔案相同。symlink(2)
函式建立一個符號連結,這需要建立一個新的inode,其中st_mode
欄位的檔案型別是符號連結,原檔案的路徑儲存在inode中或者分配一個數據塊來儲存。ln(1)
命令是基於link
和symlink
函式實現的。
8).unlink(2)
函式刪除一個連結。如果是符號連結則釋放這個符號連結的inode和資料塊,清除inode點陣圖和塊點陣圖中相應的位。如果是硬連結則從目錄的資料塊中清除一條檔名記錄,如果當前檔案的硬連結數已經是1了還要刪除它,就同時釋放它的inode和資料塊,清除inode點陣圖和塊點陣圖中相應的位,這樣就真的刪除檔案了。unlink(1)
命令和rm(1)
命令是基於unlink
函式實現的。
9).rename(2)
函式改變檔名,需要修改目錄資料塊中的檔名記錄,如果原檔名和新檔名不在一個目錄下則需要從原目錄資料塊中清除一條記錄然後新增到新目錄的資料塊中。mv(1)
命令是基於rename
函式實現的,因此在同一分割槽的不同目錄中移動檔案並不需要複製和刪除檔案的inode和資料塊,只需要一個改名操作,即使要移動整個目錄,這個目錄下有很多子目錄和檔案也要隨著一起移動,移動操作也只是對頂級目錄的改名操作,很快就能完成。但是,如果在不同的分割槽之間移動檔案就必須複製和刪除inode和資料塊,如果要移動整個目錄,所有子目錄和檔案都要複製刪除,這就很慢了。
10)readlink(2)
函式讀取一個符號連結所指向的目標路徑,其原理是從符號連結的inode或資料塊中讀出儲存的資料,這就是目標路徑。
11).mkdir(2)
函式建立新的目錄,要做的操作是在它的父目錄資料塊中新增一條記錄,然後分配新的inode和資料塊,inode的st_mode
欄位的檔案型別是目錄,在資料塊中填兩個記錄,分別是.
和..
,由於..
表示父目錄,因此父目錄的硬連結數要加1。mkdir(1)
命令是基於mkdir
函式實現的。
12).rmdir(2)
函式刪除一個目錄,這個目錄必須是空的(只包含.
和..
)才能刪除,要做的操作是釋放它的inode和資料塊,清除inode點陣圖和塊點陣圖中相應的位,清除父目錄資料塊中的記錄,父目錄的硬連結數要減1。rmdir(1)
命令是基於rmdir
函式實現的。
13).opendir(3)
/readdir(3)
/closedir(3)
用於遍歷目錄資料塊中的記錄。opendir
開啟一個目錄,返回一個DIR *
指標代表這個目錄,它是一個類似FILE
*
指標的控制代碼,closedir
用於關閉這個控制代碼,把DIR *
指標傳給readdir
讀取目錄資料塊中的記錄,每次返回一個指向struct dirent
的指標,反覆讀就可以遍歷所有記錄,所有記錄遍歷完之後readdir
返回NULL
。結構體struct
dirent
的定義如下: