Linux 文件系統是怎麼工作的?

和 CPU、內存一樣,磁盤和文件系統的管理,也是操作系統最核心的功能。

那麼,磁盤和文件系統是怎麼工作的呢? 又有哪些指標可以衡量它們的性能呢?

索引節點和目錄項

文件系統,本身是對存儲設備上的文件,進行組織管理的機制。組織方式不同,就會形成不同的文件系統。

我們要記住最重要的一點,在 Linux 中一切皆文件。不僅普通的文件和目錄,就連塊設備、 套接字、管道等,也都要通過統一的文件系統來管理。

爲了方便管理,Linux 文件系統爲每個文件都分配兩個數據結構,索引節點 (index node) 和目錄項(directory entry)。它們主要用來記錄文件的元信息和目錄結構。

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

舉個例子,通過硬鏈接爲文件創建的別名,就會對應不同的目錄項,不過這些目錄項本質上還是鏈接同一個文件,所以,它們的索引節點相同。

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

實際上,磁盤讀寫的最小單位是扇區,然而扇區只有 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/vda1
文件系統           1K-塊         已用         可用        已用%    掛載點
/dev/vda1      104846316     28228044     76618272      27%     /

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

df -h /dev/vda1
 文件系統         容量     已用     可用   已用%   掛載點
/dev/vda1       100G     27G      74G   27%     /

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

其實除了文件數據,索引節點也佔用磁盤空間。可以給 df 命令加上 -i 參數,查看索引節點的使用情況,如下所示:

df -h -i /dev/vda1
文件系統         Inode    已用(I)   可用(I)   已用(I)%  掛載點
/dev/vda1        50M     162K     50M        1%       /

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

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

緩存

可以用 free 或 vmstat,來觀察頁緩存的大小。free 輸出的 Cache,是頁緩存和可回收 Slab 緩存的和,你可以從 /proc/meminfo ,直接得到它們的大小:

cat /proc/meminfo | grep -E "SReclaimable|Cached"
Cached:          2014100 kB
SwapCached:         5316 kB
SReclaimable:     216128 kB

話說回來,文件系統中的目錄項和索引節點緩存,又該如何觀察呢?

實際上,內核使用 Slab 機制,管理目錄項和索引節點的緩存。/proc/meminfo 只給出了 Slab 的整體大小,具體到每一種 Slab 緩存,還要查看 /proc/slabinfo 這個文件。

比如,運行下面的命令,你就可以得到,所有目錄項和各種文件系統索引節點的緩存情況:

# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunab
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>
ovl_inode             66     66    736   22    4 : tunables    0    0    0 : slabdata      3      3      0
fuse_inode             0      0    832   19    4 : tunables    0    0    0 : slabdata      0      0      0
xfs_inode         100470 110736   1024   16    4 : tunables    0    0    0 : slabdata   6921   6921      0
mqueue_inode_cache     64     64   1024   16    4 : tunables    0    0    0 : slabdata      4      4      0
hugetlbfs_inode_cache     48     48    680   24    4 : tunables    0    0    0 : slabdata      2      2      0
sock_inode_cache    4581   4807    704   23    4 : tunables    0    0    0 : slabdata    209    209      0
shmem_inode_cache   1816   2541    760   21    4 : tunables    0    0    0 : slabdata    121    121      0
proc_inode_cache   10210  13024    728   22    4 : tunables    0    0    0 : slabdata    592    592      0
inode_cache        36498  38832    656   24    4 : tunables    0    0    0 : slabdata   1618   1618      0
dentry            150086 183204    192   21    1 : tunables    0    0    0 : slabdata   8724   8724      0

這個界面中,dentry 行表示目錄項緩存,inode_cache 行,表示 VFS 索引節點緩存,其 餘的則是各種文件系統的索引節點緩存。

/proc/slabinfo 的列比較多,具體含義可以查詢 man slabinfo。在實際性能分析中,我們更常使用 slabtop ,來找到佔用內存最多的緩存類型。

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

# 按下 c 按照緩存大小排序,按下 a 按照活躍對象數排序 
slabtop
Active / Total Objects (% used)    : 991123 / 1087653 (91.1%)
 Active / Total Slabs (% used)      : 40627 / 40627 (100.0%)
 Active / Total Caches (% used)     : 103 / 138 (74.6%)
 Active / Total Size (% used)       : 329426.37K / 371563.66K (88.7%)
 Minimum / Average / Maximum Object : 0.01K / 0.34K / 8.00K

 OBJS  ACTIVE   USE OBJ SIZE  SLABS OBJ/SLAB CACHE SIZE    NAME                   
183204 150026   81%    0.19K   8724	  21       34896K    dentry
 38832  36498   93%    0.64K   1618	  24       25888K    inode_cache
....
  6174   6009   97%    0.19K   294	  21	   1176K    cred_jar
  6066   5878   96%    0.44K   337	  18	   2696K    xfrm_dst_cache
  5950   5950   100%   0.02K   35	  170	   140K     avtab_node
  4807   4581   95%    0.69K   209	  23	   3344K    sock_inode_cache
  3948   3812   96%    1.12K   141	  28	   4512K    signal_cache
  3744   3680   98%    0.25K   234	  16	   936K     skbuff_head_cache
  3634   3634   100%   0.09K   79	  46	   316K     trace_event_file

從這個結果你可以看到,在我的系統中,目錄項(dentry)和索引節點(inode_cache)佔用了最多的 Slab 緩存。不 過它們佔用的內存其實並不大,加起來也只有 60MB 左右。

總結

文件系統,是對存儲設備上的文件,進行組織管理的一種機制。爲了支持各類不同的文件系統,Linux 在各種文件系統實現上,抽象了一層虛擬文件系統 (VFS)。

VFS 定義了一組所有文件系統都支持的數據結構和標準接口。這樣,用戶進程和內核中的其他子系統,就只需要跟 VFS 提供的統一接口進行交互。

爲了降低慢速磁盤對性能的影響,文件系統又通過頁緩存、目錄項緩存以及索引節點緩存,緩和磁盤延遲對應用程序的影響。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FoOP107BqxV4dC9mVKBSOw