1. 程式人生 > 實用技巧 >23-基礎篇:Linux檔案系統是怎麼工作的

23-基礎篇:Linux檔案系統是怎麼工作的

通過前面CPU和記憶體模組的學習,我相信,你已經掌握了CPU和記憶體的效能分析以及優化思路。從這一節開始,我們將進入下一個重要模組——檔案系統和磁碟的I/O效能。

同CPU、記憶體一樣,磁碟和檔案系統的管理,也是作業系統最核心的功能。

  • 磁碟為系統提供了最基本的持久化儲存。

  • 檔案系統則在磁碟的基礎上,提供了一個用來管理檔案的樹狀結構。

那麼,磁碟和檔案系統是怎麼工作的呢?又有哪些指標可以衡量它們的效能呢?

今天,我就帶你先來看看,Linux檔案系統的工作原理。磁碟的工作原理,我們下一節再來學習。

索引節點和目錄項

檔案系統,本身是對儲存裝置上的檔案,進行組織管理的機制。組織方式不同,就會形成不同的檔案系統。

你要記住最重要的一點,在Linux中一切皆檔案。不僅普通的檔案和目錄,就連塊裝置、套接字、管道等,也都要通過統一的檔案系統來管理。

為了方便管理,Linux檔案系統為每個檔案都分配兩個資料結構,索引節點(index node)和目錄項(directory entry)。它們主要用來記錄檔案的元資訊和目錄結構。

  • 索引節點,簡稱為inode,用來記錄檔案的元資料,比如inode編號、檔案大小、訪問許可權、修改日期、資料的位置等。索引節點和檔案一一對應,它跟檔案內容一樣,都會被持久化儲存到磁碟中。所以記住,索引節點同樣佔用磁碟空間。

  • 目錄項,簡稱為dentry,用來記錄檔案的名字、索引節點指標以及與其他目錄項的關聯關係。多個關聯的目錄項,就構成了檔案系統的目錄結構。不過,不同於索引節點,目錄項是由核心維護的一個記憶體資料結構,所以通常也被叫做目錄項快取。

換句話說,索引節點是每個檔案的唯一標誌,而目錄項維護的正是檔案系統的樹狀結構。目錄項和索引節點的關係是多對一,你可以簡單理解為,一個檔案可以有多個別名。

舉個例子,通過硬連結為檔案建立的別名,就會對應不同的目錄項,不過這些目錄項本質上還是連結同一個檔案,所以,它們的索引節點相同。

索引節點和目錄項紀錄了檔案的元資料,以及檔案間的目錄關係,那麼具體來說,檔案資料到底是怎麼儲存的呢?是不是直接寫到磁碟中就好了呢?

實際上,磁碟讀寫的最小單位是扇區,然而扇區只有512B 大小,如果每次都讀寫這麼小的單位,效率一定很低。所以,檔案系統又把連續的扇區組成了邏輯塊,然後每次都以邏輯塊為最小單元,來管理資料。常見的邏輯塊大小為4KB,也就是由連續的8個扇區組成。

為了幫助你理解目錄項、索引節點以及檔案資料的關係,我畫了一張示意圖。你可以對照著這張圖,來回憶剛剛講過的內容,把知識和細節串聯起來。

不過,這裡有兩點需要你注意。

第一,目錄項本身就是一個記憶體快取,而索引節點則是儲存在磁碟中的資料。在前面的Buffer和Cache原理中,我曾經提到過,為了協調慢速磁碟與快速CPU的效能差異,檔案內容會快取到頁快取Cache中。

那麼,你應該想到,這些索引節點自然也會快取到記憶體中,加速檔案的訪問。

第二,磁碟在執行檔案系統格式化時,會被分成三個儲存區域,超級塊、索引節點區和資料塊區。其中,

  • 超級塊,儲存整個檔案系統的狀態。

  • 索引節點區,用來儲存索引節點。

  • 資料塊區,則用來儲存檔案資料。

虛擬檔案系統

目錄項、索引節點、邏輯塊以及超級塊,構成了Linux檔案系統的四大基本要素。不過,為了支援各種不同的檔案系統,Linux核心在使用者程序和檔案系統的中間,又引入了一個抽象層,也就是虛擬檔案系統VFS(Virtual File System)。

VFS 定義了一組所有檔案系統都支援的資料結構和標準介面。這樣,使用者程序和核心中的其他子系統,只需要跟VFS 提供的統一介面進行互動就可以了,而不需要再關心底層各種檔案系統的實現細節。

這裡,我畫了一張Linux檔案系統的架構圖,幫你更好地理解系統呼叫、VFS、快取、檔案系統以及塊儲存之間的關係。

通過這張圖,你可以看到,在VFS的下方,Linux支援各種各樣的檔案系統,如Ext4、XFS、NFS等等。按照儲存位置的不同,這些檔案系統可以分為三類。

  • 第一類是基於磁碟的檔案系統,也就是把資料直接儲存在計算機本地掛載的磁碟中。常見的Ext4、XFS、OverlayFS等,都是這類檔案系統。

  • 第二類是基於記憶體的檔案系統,也就是我們常說的虛擬檔案系統。這類檔案系統,不需要任何磁碟分配儲存空間,但會佔用記憶體。我們經常用到的 /proc 檔案系統,其實就是一種最常見的虛擬檔案系統。此外,/sys 檔案系統也屬於這一類,主要向用戶空間匯出層次化的核心物件。

  • 第三類是網路檔案系統,也就是用來訪問其他計算機資料的檔案系統,比如NFS、SMB、iSCSI等。

這些檔案系統,要先掛載到 VFS 目錄樹中的某個子目錄(稱為掛載點),然後才能訪問其中的檔案。拿第一類,也就是基於磁碟的檔案系統為例,在安裝系統時,要先掛載一個根目錄(/),在根目錄下再把其他檔案系統(比如其他的磁碟分割槽、/proc檔案系統、/sys檔案系統、NFS等)掛載進來。

檔案系統I/O

把檔案系統掛載到掛載點後,你就能通過掛載點,再去訪問它管理的檔案了。VFS 提供了一組標準的檔案訪問介面。這些介面以系統呼叫的方式,提供給應用程式使用。

就拿cat 命令來說,它首先呼叫 open() ,開啟一個檔案;然後呼叫 read() ,讀取檔案的內容;最後再呼叫 write() ,把檔案內容輸出到控制檯的標準輸出中:

int open(const char *pathname, int flags, mode_t mode); 
ssize_t read(int fd, void *buf, size_t count); 
ssize_t write(int fd, const void *buf, size_t count); 

檔案讀寫方式的各種差異,導致 I/O的分類多種多樣。最常見的有,緩衝與非緩衝I/O、直接與非直接I/O、阻塞與非阻塞I/O、同步與非同步I/O等。 接下來,我們就詳細看這四種分類。

第一種,根據是否利用標準庫快取,可以把檔案I/O分為緩衝I/O與非緩衝I/O。

  • 緩衝I/O,是指利用標準庫快取來加速檔案的訪問,而標準庫內部再通過系統排程訪問檔案。

  • 非緩衝I/O,是指直接通過系統呼叫來訪問檔案,不再經過標準庫快取。

注意,這裡所說的“緩衝”,是指標準庫內部實現的快取。比方說,你可能見到過,很多程式遇到換行時才真正輸出,而換行前的內容,其實就是被標準庫暫時快取了起來。

無論緩衝I/O還是非緩衝I/O,它們最終還是要經過系統呼叫來訪問檔案。而根據上一節內容,我們知道,系統呼叫後,還會通過頁快取,來減少磁碟的I/O操作。

第二,根據是否利用作業系統的頁快取,可以把檔案I/O分為直接I/O與非直接I/O。

  • 直接I/O,是指跳過作業系統的頁快取,直接跟檔案系統互動來訪問檔案。

  • 非直接I/O正好相反,檔案讀寫時,先要經過系統的頁快取,然後再由核心或額外的系統呼叫,真正寫入磁碟。

想要實現直接I/O,需要你在系統呼叫中,指定 O_DIRECT 標誌。如果沒有設定過,預設的是非直接I/O。

不過要注意,直接I/O、非直接I/O,本質上還是和檔案系統互動。如果是在資料庫等場景中,你還會看到,跳過檔案系統讀寫磁碟的情況,也就是我們通常所說的裸I/O。

第三,根據應用程式是否阻塞自身執行,可以把檔案I/O分為阻塞I/O和非阻塞I/O:

  • 所謂阻塞I/O,是指應用程式執行I/O操作後,如果沒有獲得響應,就會阻塞當前執行緒,自然就不能執行其他任務。

  • 所謂非阻塞I/O,是指應用程式執行I/O操作後,不會阻塞當前的執行緒,可以繼續執行其他的任務,隨後再通過輪詢或者事件通知的形式,獲取呼叫的結果。

比方說,訪問管道或者網路套接字時,設定 O_NONBLOCK 標誌,就表示用非阻塞方式訪問;而如果不做任何設定,預設的就是阻塞訪問。

第四,根據是否等待響應結果,可以把檔案I/O分為同步和非同步I/O:

  • 所謂同步I/O,是指應用程式執行I/O操作後,要一直等到整個I/O完成後,才能獲得I/O響應。

  • 所謂非同步I/O,是指應用程式執行I/O操作後,不用等待完成和完成後的響應,而是繼續執行就可以。等到這次 I/O完成後,響應會用事件通知的方式,告訴應用程式。

舉個例子,在操作檔案時,如果你設定了 O_SYNC 或者 O_DSYNC 標誌,就代表同步I/O。如果設定了O_DSYNC,就要等檔案資料寫入磁碟後,才能返回;而O_SYNC,則是在O_DSYNC基礎上,要求檔案元資料也要寫入磁碟後,才能返回。

再比如,在訪問管道或者網路套接字時,設定了O_ASYNC選項後,相應的I/O就是非同步I/O。這樣,核心會再通過SIGIO或者SIGPOLL,來通知程序檔案是否可讀寫。

你可能發現了,這裡的好多概念也經常出現在網路程式設計中。比如非阻塞I/O,通常會跟select/poll配合,用在網路套接字的I/O中。

你也應該可以理解,“Linux 一切皆檔案”的深刻含義。無論是普通檔案和塊裝置、還是網路套接字和管道等,它們都通過統一的VFS 介面來訪問。

效能觀測

學了這麼多檔案系統的原理,你估計也是迫不及待想上手,觀察一下檔案系統的效能情況了。

接下來,開啟一個終端,SSH登入到伺服器上,然後跟我一起來探索,如何觀測檔案系統的效能。

容量

對檔案系統來說,最常見的一個問題就是空間不足。當然,你可能本身就知道,用 df 命令,就能檢視檔案系統的磁碟空間使用情況。比如:

$ df /dev/sda1 
Filesystem     1K-blocks    Used Available Use% Mounted on 
/dev/sda1       30308240 3167020  27124836  11% / 

你可以看到,我的根檔案系統只使用了11%的空間。這裡還要注意,總空間用1K-blocks的數量來表示,你可以給df加上-h選項,以獲得更好的可讀性:

$ df -h /dev/sda1 
Filesystem      Size  Used Avail Use% Mounted on 
/dev/sda1        29G  3.1G   26G  11% / 

不過有時候,明明你碰到了空間不足的問題,可是用df檢視磁碟空間後,卻發現剩餘空間還有很多。這是怎麼回事呢?

不知道你還記不記得,剛才我強調的一個細節。除了檔案資料,索引節點也佔用磁碟空間。你可以給df命令加上 -i 引數,檢視索引節點的使用情況,如下所示:

$ df -i /dev/sda1 
Filesystem      Inodes  IUsed   IFree IUse% Mounted on 
/dev/sda1      3870720 157460 3713260    5% / 

索引節點的容量,(也就是Inode個數)是在格式化磁碟時設定好的,一般由格式化工具自動生成。當你發現索引節點空間不足,但磁碟空間充足時,很可能就是過多小檔案導致的。

所以,一般來說,刪除這些小檔案,或者把它們移動到索引節點充足的其他磁碟中,就可以解決這個問題。

快取

在前面Cache案例中,我已經介紹過,可以用 free 或 vmstat,來觀察頁快取的大小。複習一下,free輸出的Cache,是頁快取和可回收Slab快取的和,你可以從 /proc/meminfo ,直接得到它們的大小:

$ cat /proc/meminfo | grep -E "SReclaimable|Cached" 
Cached:           748316 kB 
SwapCached:            0 kB 
SReclaimable:     179508 kB 

話說回來,檔案系統中的目錄項和索引節點快取,又該如何觀察呢?

實際上,核心使用Slab機制,管理目錄項和索引節點的快取。/proc/meminfo只給出了Slab的整體大小,具體到每一種Slab快取,還要檢視/proc/slabinfo這個檔案。

比如,執行下面的命令,你就可以得到,所有目錄項和各種檔案系統索引節點的快取情況:

$ cat /proc/slabinfo | grep -E '^#|dentry|inode' 
# name            <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail> 
xfs_inode              0      0    960   17    4 : tunables    0    0    0 : slabdata      0      0      0 
... 
ext4_inode_cache   32104  34590   1088   15    4 : tunables    0    0    0 : slabdata   2306   2306      0hugetlbfs_inode_cache     13     13    624   13    2 : tunables    0    0    0 : slabdata      1      1      0 
sock_inode_cache    1190   1242    704   23    4 : tunables    0    0    0 : slabdata     54     54      0 
shmem_inode_cache   1622   2139    712   23    4 : tunables    0    0    0 : slabdata     93     93      0 
proc_inode_cache    3560   4080    680   12    2 : tunables    0    0    0 : slabdata    340    340      0 
inode_cache        25172  25818    608   13    2 : tunables    0    0    0 : slabdata   1986   1986      0 
dentry             76050 121296    192   21    1 : tunables    0    0    0 : slabdata   5776   5776      0 

這個介面中,dentry行表示目錄項快取,inode_cache行,表示VFS索引節點快取,其餘的則是各種檔案系統的索引節點快取。

/proc/slabinfo 的列比較多,具體含義你可以查詢 man slabinfo。在實際效能分析中,我們更常使用 slabtop ,來找到佔用記憶體最多的快取型別。

比如,下面就是我執行slabtop得到的結果:

# 按下c按照快取大小排序,按下a按照活躍物件數排序 
$ slabtop 
Active / Total Objects (% used)    : 277970 / 358914 (77.4%) 
Active / Total Slabs (% used)      : 12414 / 12414 (100.0%) 
Active / Total Caches (% used)     : 83 / 135 (61.5%) 
Active / Total Size (% used)       : 57816.88K / 73307.70K (78.9%) 
Minimum / Average / Maximum Object : 0.01K / 0.20K / 22.88K 

  OBJS ACTIVE  USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE NAME 
69804  23094   0%    0.19K   3324       21     13296K dentry 
16380  15854   0%    0.59K   1260       13     10080K inode_cache 
58260  55397   0%    0.13K   1942       30      7768K kernfs_node_cache 
   485    413   0%    5.69K     97        5      3104K task_struct 
  1472   1397   0%    2.00K     92       16      2944K kmalloc-2048 

從這個結果你可以看到,在我的系統中,目錄項和索引節點佔用了最多的Slab快取。不過它們佔用的記憶體其實並不大,加起來也只有23MB左右。

小結

今天,我帶你梳理了Linux檔案系統的工作原理。

檔案系統,是對儲存裝置上的檔案,進行組織管理的一種機制。為了支援各類不同的檔案系統,Linux在各種檔案系統實現上,抽象了一層虛擬檔案系統(VFS)。

VFS 定義了一組所有檔案系統都支援的資料結構和標準介面。這樣,使用者程序和核心中的其他子系統,就只需要跟 VFS 提供的統一介面進行互動。

為了降低慢速磁碟對效能的影響,檔案系統又通過頁快取、目錄項快取以及索引節點快取,緩和磁碟延遲對應用程式的影響。

在效能觀測方面,今天主要講了容量和快取的指標。下一節,我們將會學習Linux磁碟 I/O的工作原理,並掌握磁碟I/O的效能觀測方法。