看了之前的關於Linux記憶體管理和程式排程的文章,相比讀者們應該對Linux有了大致的瞭解,本文的主題是Linux虛擬檔案系統。閒話少說,開始!
1.軟連結和硬連結的區別
我們知道檔案都有檔名與資料,資料分兩部分:使用者資料 (user data) 與後設資料 (metadata)。使用者資料,即檔案資料塊 (data block),資料塊是記錄檔案真實內容的地方;而後設資料則是檔案的附加屬性,如檔案大小、建立時間、所有者等資訊。在 Linux 中,後設資料中的 inode 號(inode 是檔案後設資料的一部分但其並不包含檔名,inode 號即索引節點號)才是檔案的唯一標識而非檔名。檔名僅是為了方便人們的記憶和使用,系統或程式通過 inode 號尋找正確的檔案資料塊
為解決檔案的共享使用,Linux 系統引入了兩種連結:硬連結 (hard link) 與軟連結(又稱符號連結,即 soft link 或 symbolic link)。連結為 Linux 系統解決了檔案的共享使用,還帶來了隱藏檔案路徑、增加許可權安全及節省儲存等好處。若一個 inode 號對應多個檔名,則稱這些檔案為硬連結。硬連結就是同一個檔案使用了多個別名
由於硬連結是有著相同 inode 號僅檔名不同的檔案,因此硬連結存在以下幾點特性:
- 檔案有相同的 inode 及 data block;
- 只能對已存在的檔案進行建立;
- 不能交叉檔案系統進行硬連結的建立;
- 不能對目錄進行建立,只可對檔案建立;
- 刪除一個硬連結檔案並不影響其他有相同 inode 號的檔案。
inode 號僅在各檔案系統下是唯一的,當 Linux 掛載多個檔案系統後將出現 inode 號重複的現象,因此硬連結建立時不可跨檔案系統
軟連結與硬連結不同,若檔案使用者資料塊中存放的內容是另一檔案的路徑名的指向,則該檔案就是軟連線。軟連結就是一個普通檔案,只是資料塊內容有點特殊。軟連結有著自己的 inode 號以及使用者資料塊。因此軟連結的建立與使用沒有類似硬連結的諸多限制:
- 軟連結有自己的檔案屬性及許可權等;
- 可對不存在的檔案或目錄建立軟連結;
- 軟連結可交叉檔案系統;
- 軟連結可對檔案或目錄建立;
- 建立軟連結時,連結計數 i_nlink 不會增加;
- 刪除軟連結並不影響被指向的檔案,但若被指向的原檔案被刪除,則相關軟連線被稱為死連結(即 dangling link,若被指向路徑檔案被重新建立,死連結可恢復為正常的軟連結)。
- 一般情況下,檔名和inode號碼是"一一對應"關係,每個inode號碼對應一個檔名。但是,Unix/Linux系統允許,多個檔名指向同一個inode號碼。這意味著,可以用不同的檔名訪問同樣的內容;對檔案內容進行修改,會影響到所有檔名;但是,刪除一個檔名,不影響另一個檔名的訪問。這種情況就被稱為"硬連結"(hard link)。
2.Linux VFS
Linux 有著極其豐富的檔案系統,大體上可分如下幾類:
- 網路檔案系統,如 nfs、cifs 等;
- 磁碟檔案系統,如 ext4、ext3 等;
- 特殊檔案系統,如 proc、sysfs、ramfs、tmpfs 等。
實現以上這些檔案系統並在 Linux 下共存的基礎就是 Linux VFS(Virtual File System 又稱 Virtual Filesystem Switch),即虛擬檔案系統。VFS 作為一個通用的檔案系統,抽象了檔案系統的四個基本概念:檔案、目錄項 (dentry)、索引節點 (inode) 及掛載點,其在核心中為使用者空間層的檔案系統提供了相關的介面。VFS 實現了 open()、read() 等系統調並使得 cp 等使用者空間程式可跨檔案系統。VFS 真正實現了上述內容中:在 Linux 中除程式之外一切皆是檔案。
Linux VFS 存在四個基本物件:超級塊物件 (superblock object)、索引節點物件 (inode object)、目錄項物件 (dentry object) 及檔案物件 (file object)。超級塊物件代表一個已安裝的檔案系統;索引節點物件代表一個檔案;目錄項物件代表一個目錄項,如裝置檔案 event5 在路徑 /dev/input/event5 中,其存在四個目錄項物件:/ 、dev/ 、input/ 及 event5。檔案物件代表由程式開啟的檔案。為檔案路徑的快速解析,Linux VFS 設計了目錄項快取(Directory Entry Cache,即 dcache)。
3.檔案的開啟過程
open()系統呼叫的過程如下:
1.檢視system-wide open-file table(系統開啟檔案表)中是否有該檔案,即檢視該檔案是否已經被其他程式開啟了
2.如果存在,那麼該程式會在自己的per-process open-file table(程式開啟檔案表)中,建立一個專案,指向system-wide open-file table中的該檔案
3.如果不存在,則需要根據file name在directory中查詢該file,通常directory中的部分內容在cache中,這樣可以加快搜尋速度。
4.一旦檔案被找到,那麼FCB(file control block)檔案控制塊會被複制到system-wide open-file table中,該表不僅僅儲存FCB,而且記錄每個檔案被多少個程式開啟
5.接下來,在per-process open-file table(程式開啟檔案表)中,簡直一個entry,指向程式開啟檔案表中該專案
當程式close()一個檔案時:
1.該程式的per-process open-flle table中的對應項會被刪除,系統開啟表中的該檔案計數器會減1
2.如果系統開啟表中的計算為0,那麼刪除該檔案項
4.inode的理解
作業系統讀取硬碟的時候,不會一個個扇區地讀取,這樣效率太低,而是一次性連續讀取多個扇區,即一次性讀取一個"塊"(block)。這種由多個扇區組成的"塊",是檔案存取的最小單位。"塊"的大小,最常見的是4KB,即連續八個 sector組成一個 block。
檔案資料都儲存在"塊"中,那麼很顯然,我們還必須找到一個地方儲存檔案的元資訊,比如檔案的建立者、檔案的建立日期、檔案的大小等等。這種儲存檔案元資訊的區域就叫做inode,中文譯名為"索引節點"。
inode包含檔案的元資訊,具體來說有以下內容:
* 檔案的位元組數
* 檔案擁有者的User ID
* 檔案的Group ID
* 檔案的讀、寫、執行許可權
* 檔案的時間戳,共有三個:ctime指inode上一次變動的時間,mtime指檔案內容上一次變動的時間,atime指檔案上一次開啟的時間。
* 連結數,即有多少檔名指向這個inode
* 檔案資料block的位置
除了檔名以外的所有檔案資訊,都存在inode之中
每個inode都有一個號碼,作業系統用inode號碼來識別不同的檔案。
表面上,使用者通過檔名,開啟檔案。實際上,系統內部這個過程分成三步:首先,系統找到這個檔名對應的inode號;其次,通過inode號,獲取inode資訊;最後,根據inode資訊,找到檔案資料所在的block,讀出資料。
目錄(directory)也是一種檔案,目錄檔案的結構非常簡單,就是一系列目錄項(dirent)的列表。每個目錄項,由兩部分組成:所包含檔案的檔名,以及該檔名對應的inode號碼。
資料塊定址
inode中記錄了檔案資料塊的位置,有三種定址方式:direct blocks直接指向資料塊;single indirect指向一個block,該block中為資料塊的指標;double indirect,兩級block
Linux系統篇-檔案系統&虛擬檔案系統(非常重要!)
5.檔案描述符
在Linux系統中一切皆可以看成是檔案,檔案又可分為:普通檔案、目錄檔案、連結檔案和裝置檔案。檔案描述符(file descriptor)是核心為了高效管理已被開啟的檔案所建立的索引,其是一個非負整數(通常是小整數),用於指代被開啟的檔案,所有執行I/O操作的系統呼叫都通過檔案描述符。程式剛剛啟動的時候,0是標準輸入,1是標準輸出,2是標準錯誤。如果此時去開啟一個新的檔案,它的檔案描述符會是3。POSIX標準要求每次開啟檔案時(含socket)必須使用當前程式中最小可用的檔案描述符號碼
檔案描述符是系統的一個重要資源,雖然說系統記憶體有多少就可以開啟多少的檔案描述符,但是在實際實現過程中核心是會做相應的處理的,一般最大開啟檔案數會是系統記憶體的10%(以KB來計算)(稱之為系統級限制)
6.檔案描述符和開啟檔案之間的關係
每一個檔案描述符會與一個開啟檔案相對應,同時,不同的檔案描述符也會指向同一個檔案。相同的檔案可以被不同的程式開啟也可以在同一個程式中被多次開啟。系統為每一個程式維護了一個檔案描述符表,該表的值都是從0開始的,所以在不同的程式中你會看到相同的檔案描述符,這種情況下相同檔案描述符有可能指向同一個檔案,也有可能指向不同的檔案。具體情況要具體分析,要理解具體其概況如何,需要檢視由核心維護的3個資料結構。
1. 程式級的檔案描述符表
2. 系統級的開啟檔案描述符表
3. 檔案系統的i-node表
程式級的描述符表的每一條目記錄了單個檔案描述符的相關資訊。
1. 控制檔案描述符操作的一組標誌。(目前,此類標誌僅定義了一個,即close-on-exec標誌)
2. 對開啟檔案控制程式碼的引用
核心對所有開啟的檔案的檔案維護有一個系統級的描述符表格(open file description table)。有時,也稱之為開啟檔案表(open file table),並將表格中各條目稱為開啟檔案控制程式碼(open file handle)。一個開啟檔案控制程式碼儲存了與一個開啟檔案相關的全部資訊,如下所示:
1. 當前檔案偏移量(呼叫read()和write()時更新,或使用lseek()直接修改)
2. 開啟檔案時所使用的狀態標識(即,open()的flags引數)
3. 檔案訪問模式(如呼叫open()時所設定的只讀模式、只寫模式或讀寫模式)
4. 與訊號驅動相關的設定
5. 對該檔案i-node物件的引用
6. 檔案型別(例如:常規檔案、套接字或FIFO)和訪問許可權
7. 一個指標,指向該檔案所持有的鎖列表
8. 檔案的各種屬性,包括檔案大小以及與不同型別操作相關的時間戳
在程式A中,檔案描述符1和30都指向了同一個開啟的檔案控制程式碼(標號23)。這可能是通過呼叫dup()、dup2()、fcntl()或者對同一個檔案多次呼叫了open()函式而形成的。
程式A的檔案描述符2和程式B的檔案描述符2都指向了同一個開啟的檔案控制程式碼(標號73)。這種情形可能是在呼叫fork()後出現的(即,程式A、B是父子程式關係),或者當某程式通過UNIX域套接字將一個開啟的檔案描述符傳遞給另一個程式時,也會發生。再者是不同的程式獨自去呼叫open函式開啟了同一個檔案,此時程式內部的描述符正好分配到與其他程式開啟該檔案的描述符一樣。
此外,程式A的描述符0和程式B的描述符3分別指向不同的開啟檔案控制程式碼,但這些控制程式碼均指向i-node表的相同條目(1976),換言之,指向同一個檔案。發生這種情況是因為每個程式各自對同一個檔案發起了open()呼叫。同一個程式兩次開啟同一個檔案,也會發生類似情況。
7. 總結
1. 由於程式級檔案描述符表的存在,不同的程式中會出現相同的檔案描述符,它們可能指向同一個檔案,也可能指向不同的檔案
2. 兩個不同的檔案描述符,若指向同一個開啟檔案控制程式碼,將共享同一檔案偏移量。因此,如果通過其中一個檔案描述符來修改檔案偏移量(由呼叫read()、write()或lseek()所致),那麼從另一個描述符中也會觀察到變化,無論這兩個檔案描述符是否屬於不同程式,還是同一個程式,情況都是如此。
3. 要獲取和修改開啟的檔案標誌(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可執行fcntl()的F_GETFL和F_SETFL操作,其對作用域的約束與上一條頗為類似。
4. 檔案描述符標誌(即,close-on-exec)為程式和檔案描述符所私有。對這一標誌的修改將不會影響同一程式或不同程式中的其他檔案描述符