
本文主要以一張圖為基礎,向大家介紹 Linux在 I/O 上做了哪些事情,即 Linux 中直接 I/O 原理,希望本文的經驗和思路能為讀者提供一些幫助和思考。
我們先看一張圖:
【資料圖】
這張圖大體上描述了 Linux 系統上,應用程序對磁盤上的文件進行讀寫時,從上到下經歷了哪些事情。
這篇文章就以這張圖為基礎,介紹 Linux 在 I/O 上做了哪些事情。
文件系統,本身是對存儲設備上的文件,進行組織管理的機制。組織方式不同,就會形成不同的文件系統。比如常見的 Ext4、XFS、ZFS 以及網絡文件系統 NFS 等等。
但是不同類型的文件系統標準和接口可能各有差異,我們在做應用開發的時候卻很少關心系統調用以下的具體實現,大部分時候都是直接系統調用 open, read, write, close 來實現應用程序的功能,不會再去關注我們具體用了什么文件系統(UFS、XFS、Ext4、ZFS),磁盤是什么接口(IDE、SCSI,SAS,SATA 等),磁盤是什么存儲介質(HDD、SSD)。
應用開發者之所以這么爽,各種復雜細節都不用管直接調接口,是因為內核為我們做了大量的有技術含量的臟活累活。開始的那張圖看到 Linux 在各種不同的文件系統之上,虛擬了一個 VFS,目的就是統一各種不同文件系統的標準和接口,讓開發者可以使用相同的系統調用來使用不同的文件系統。
在 Linux 中一切皆文件。不僅普通的文件和目錄,就連塊設備、套接字、管道等,也都要通過統一的文件系統來管理。
用 ls -l 命令看最前面的字符可以看到這個文件是什么類型brw-r--r-- 1 root root 1, 2 4月 25 11:03 bnod // 塊設備文件crw-r--r-- 1 root root 1, 2 4月 25 11:04 cnod // 符號設備文件drwxr-xr-x 2 wrn3552 wrn3552 6 4月 25 11:01 dir // 目錄-rw-r--r-- 1 wrn3552 wrn3552 0 4月 25 11:01 file // 普通文件prw-r--r-- 1 root root 0 4月 25 11:04 pipeline // 有名管道srwxr-xr-x 1 root root 0 4月 25 11:06 socket.sock // socket文件lrwxrwxrwx 1 root root 4 4月 25 11:04 softlink -> file // 軟連接-rw-r--r-- 2 wrn3552 wrn3552 0 4月 25 11:07 hardlink // 硬鏈接(本質也是普通文件)
Linux 文件系統設計了兩個數據結構來管理這些不同種類的文件:
inode(index node):索引節點dentry(directory entry):目錄項inode
inode 是用來記錄文件的 metadata,所謂 metadata 在 Wikipedia 上的描述是 data of data,其實指的就是文件的各種屬性,比如 inode 編號、文件大小、訪問權限、修改日期、數據的位置等。
wrn3552@novadev:~/playground$ stat file 文件:file 大小:0 塊:0 IO 塊:4096 普通空文件設備:fe21h/65057d Inode:32828 硬鏈接:2權限:(0644/-rw-r--r--) Uid:( 3041/ wrn3552) Gid:( 3041/ wrn3552)最近訪問:2021-04-25 11:07:59.603745534 +0800最近更改:2021-04-25 11:07:59.603745534 +0800最近改動:2021-04-25 11:08:04.739848692 +0800創建時間:-
inode 和文件一一對應,它跟文件內容一樣,都會被持久化存儲到磁盤中。所以,inode 同樣占用磁盤空間,只不過相對于文件來說它大小固定且大小不算大。
dentry
dentry 用來記錄文件的名字、inode 指針以及與其他 dentry 的關聯關系。
wrn3552@novadev:~/playground$ tree.├── dir│ └── file_in_dir├── file└── hardlink文件的名字:像 dir、file、hardlink、file_in_dir 這些名字是記錄在 dentry 里的inode 指針:就是指向這個文件的 inode與其他 dentry 的關聯關系:其實就是每個文件的層級關系,哪個文件在哪個文件下面,構成了文件系統的目錄結構
不同于 inode,dentry 是由內核維護的一個內存數據結構,所以通常也被叫做 dentry cache。
這里有張圖解釋了文件是如何存儲在磁盤上的,首先,磁盤再進行文件系統格式化的時候,會分出來 3 個區:
Superblockinode blocksdata blocks(其實還有 boot block,可能會包含一些 bootstrap 代碼,在機器啟動的時候被讀到,這里忽略)其中 inode blocks 放的都是每個文件的 inode,data blocks 里放的是每個文件的內容數據。這里關注一下 superblock,它包含了整個文件系統的 metadata,具體有:
inode/data block 總量、使用量、剩余量文件系統的格式,屬主等等各種屬性superblock 對于文件系統來說非常重要,如果 superblock 損壞了,文件系統就掛載不了了,相應的文件也沒辦法讀寫。既然 superblock 這么重要,那肯定不能只有一份,壞了就沒了,它在系統中是有很多副本的,在 superblock 損壞的時候,可以使用 fsck(File System Check and repair)來恢復。回到上面的那張圖,可以很清晰地看到文件的各種屬性和文件的數據是如何存儲在磁盤上的:
dentry 里包含了文件的名字、目錄結構、inode 指針inode 指針指向文件特定的 inode(存在 inode blocks 里)每個 inode 又指向 data blocks 里具體的 logical block,這里的 logical block 存的就是文件具體的數據這里解釋一下什么是 logical block:
對于不同存儲介質的磁盤,都有最小的讀寫單元??/sys/block/sda/queue/physical_block_size?
?HDD 叫做 sector(扇區),SSD 叫做 page(頁面)對于 hdd 來說,每個 sector 大小 512Bytes對于 SSD 來說每個 page 大小不等(和 cell 類型有關),經典的大小是 4KB但是 Linux 覺得按照存儲介質的最小讀寫單元來進行讀寫可能會有效率問題,所以支持在文件系統格式化的時候指定 block size 的大小,一般是把幾個 physical_block 拼起來就成了一個 logical block??/sys/block/sda/queue/logical_block_size?
?理論上應該是 logical_block_size >= physical_block_size,但是有時候我們會看到 physical_block_size = 4K,logical_block_size = 512B 情況,其實這是因為磁盤上做了一層 512B 的仿真(emulation)(詳情可參考 512e 和 4Kn)這里簡單介紹一個廣泛應用的文件系統 ZFS,一些數據庫應用也會用到 ZFS,先看一張 zfs 的層級結構圖:
這是一張從底向上的圖:
將若干物理設備 disk 組成一個虛擬設備 vdev(同時,disk 也是一種 vdev)再將若干個虛擬設備 vdev 加到一個 zpool 里在 zpool 的基礎上創建 zfs 并掛載(zvol 可以先不看,我們沒有用到)root@:~ # zpool create tank raidz /dev/ada1 /dev/ada2 /dev/ada3 raidz /dev/ada4 /dev/ada5 /dev/ada6root@:~ # zpool list tankNAME SIZE ALLOC FREE CKPOINT EXPANDSZ FRAG CAP DEDUP HEALTH ALTROOTtank 11G 824K 11.0G - - 0% 0% 1.00x ONLINE -root@:~ # zpool status tank pool: tank state: ONLINE scan: none requestedconfig: NAME STATE READ WRITE CKSUM tank ONLINE 0 0 0 raidz1-0 ONLINE 0 0 0 ada1 ONLINE 0 0 0 ada2 ONLINE 0 0 0 ada3 ONLINE 0 0 0 raidz1-1 ONLINE 0 0 0 ada4 ONLINE 0 0 0 ada5 ONLINE 0 0 0 ada6 ONLINE 0 0 0創建了一個名為 tank 的 zpool這里的 raidz 同 RAID5
除了 raidz 還支持其他方案:
創建 zfs
root@:~ # zfs create -o mountpoint=/mnt/srev tank/srevroot@:~ # df -h tank/srevFilesystem Size Used Avail Capacity Mounted ontank/srev 7.1G 117K 7.1G 0% /mnt/srev創建了一個 zfs,掛載到了 /mnt/srev這里沒有指定 zfs 的 quota,創建的 zfs 大小即 zpool 大小
對 zfs 設置 quota
root@:~ # zfs set quota=1G tank/srevroot@:~ # df -h tank/srevFilesystem Size Used Avail Capacity Mounted ontank/srev 1.0G 118K 1.0G 0% /mnt/srev
上面的層級圖和操作步驟可以看到 zfs 是基于 zpool 創建的,zpool 可以動態擴容意味著存儲空間也可以動態擴容,而且可以創建多個文件系統,文件系統共享完整的 zpool 空間無需預分配。
事務文件系統zfs 的寫操作是事務的,意味著要么就沒寫,要么就寫成功了,不會像其他文件系統那樣,應用打開了文件,寫入還沒保存的時候斷電,導致文件為空。zfs 保證寫操作事務采用的是 copy on write 的方式:
ARC 緩存ZFS 中的 ARC(Adjustable Replacement Cache) 讀緩存淘汰算法,是基于 IBM 的 ARP(Adaptive Replacement Cache) 演化而來。在一些文件系統中實現的標準 LRU 算法其實是有缺陷的:比如復制大文件之類的線性大量 I/O 操作,導致緩存失效率猛增(大量文件只讀一次,放到內存不會被再讀,坐等淘汰)。
另外,緩存可以根據時間來進行優化(LRU,最近最多使用),也可以根據頻率進行優化(LFU,最近最常使用),這兩種方法各有優劣,但是沒辦法適應所有場景。
ARC 的設計就是嘗試在 LRU 和 LFU 之間找到一個平衡,根據當前的 I/O workload 來調整用 LRU 多一點還是 LFU 多一點。
ARC 定義了 4 個鏈表:
LRU list:最近最多使用的頁面,存具體數據LFU list:最近最常使用的頁面,存具體數據Ghost list for LRU:最近從 LRU 表淘汰下來的頁面信息,不存具體數據,只存頁面信息Ghost list for LFU:最近從 LFU 表淘汰下來的頁面信息,不存具體數據,只存頁面信息ARC 工作流程大致如下:
LRU list 和 LFU list 填充和淘汰過程和標準算法一樣當一個頁面從 LRU list 淘汰下來時,這個頁面的信息會放到 LRU ghost 表中如果這個頁面一直沒被再次引用到,那么這個頁面的信息最終也會在 LRU ghost 表中被淘汰掉如果這個頁面在 LRU ghost 表中未被淘汰的時候,被再一次訪問了,這時候會引起一次幽靈(phantom)命中phantom 命中的時候,事實上還是要把數據從磁盤第一次放緩存但是這時候系統知道剛剛被 LRU 表淘汰的頁面又被訪問到了,說明 LRU list 太小了,這時它會把 LRU list 長度加一,LFU 長度減一對于 LFU 的過程也與上述過程類似關于 ZFS 詳細介紹可以參考:
這篇文章
磁盤根據不同的分類方式,有各種不一樣的類型。
根據磁盤的存儲介質可以分兩類(大家都很熟悉):
HDD(機械硬盤)SSD(固態硬盤)根據磁盤接口分類:
IDE (Integrated Drive Electronics)SCSI (Small Computer System Interface)SAS (Serial Attached SCSI)SATA (Serial ATA)…不同的接口,往往分配不同的設備名稱。比如, IDE 設備會分配一個 hd 前綴的設備名,SCSI 和 SATA 設備會分配一個 sd 前綴的設備名。如果是多塊同類型的磁盤,就會按照 a、b、c 等的字母順序來編號。
其實在 Linux 中,磁盤實際上是作為一個塊設備來管理的,也就是以塊為單位讀寫數據,并且支持隨機讀寫。每個塊設備都會被賦予兩個設備號,分別是主、次設備號。主設備號用在驅動程序中,用來區分設備類型;而次設備號則是用來給多個同類設備編號。
g18-"299" on ~# ls -l /dev/sda*brw-rw---- 1 root disk 8, 0 Apr 25 15:53 /dev/sdabrw-rw---- 1 root disk 8, 1 Apr 25 15:53 /dev/sda1brw-rw---- 1 root disk 8, 10 Apr 25 15:53 /dev/sda10brw-rw---- 1 root disk 8, 2 Apr 25 15:53 /dev/sda2brw-rw---- 1 root disk 8, 5 Apr 25 15:53 /dev/sda5brw-rw---- 1 root disk 8, 6 Apr 25 15:53 /dev/sda6brw-rw---- 1 root disk 8, 7 Apr 25 15:53 /dev/sda7brw-rw---- 1 root disk 8, 8 Apr 25 15:53 /dev/sda8brw-rw---- 1 root disk 8, 9 Apr 25 15:53 /dev/sda9這些 sda 磁盤主設備號都是 8,表示它是一個 sd 類型的塊設備次設備號 0-10 表示這些不同 sd 塊設備的編號
和 VFS 類似,為了對上層屏蔽不同塊設備的差異,內核在文件系統和塊設備之前抽象了一個 Generic Block Layer(通用塊層),有時候一些人也會把下面的 I/O 調度層并到通用塊層里表述。
這兩層主要做兩件事:
跟 VFS 的功能類似。向上,為文件系統和應用程序,提供訪問塊設備的標準接口;向下,把各種異構的磁盤設備抽象為統一的塊設備,并提供統一框架來管理這些設備的驅動程序對 I/O 請求進行調度,通過重新排序、合并等方式,提高磁盤讀寫效率下圖是一個完整的 I/O 棧全景圖:
可以看到中間的 Block Layer 其實就是 Generic Block Layer,在圖中可以看到 Block Layer 的 I/O 調度分為兩類,分別表示單隊列和多隊列的調度:
I/O schedulerblkmq老版本的內核里只支持單隊列的 I/O scheduler,在 3.16 版本的內核開始支持多隊列 blkmq,這里介紹幾種經典的 I/O 調度策略。
單隊列 I/O scheduler:
NOOP:事實上是個 FIFO 的隊列,只做基本的請求合并CFQ:Completely Fair Queueing,完全公平調度器,給每個進程維護一個 I/O 調度隊列,按照時間片來均勻分布每個進程 I/O 請求,DeadLine:為讀和寫請求創建不同的 I/O 隊列,確保達到 deadline 的請求被優先處理多隊列 blkmq:
bfq:Budget Fair Queueing,也是公平調度器,不過不是按時間片來分配,而是按請求的扇區數量(帶寬)kyber:維護兩個隊列(同步/讀、異步/寫),同時嚴格限制發到這兩個隊列的請求數以保證相應時間mq-deadline:多隊列版本的 deadline具體各種 I/O 調度策略可以參考 IOSchedulers關于 blkmq 可以參考 Linux Multi-Queue Block IO Queueing Mechanism (blk-mq) Details多隊列調度可以參考 Block layer introduction part 2: the request layer一般來說 I/O 性能指標有這幾個:
使用率:ioutil,指的是磁盤處理 I/O 的時間百分比,ioutil 只看有沒有 I/O 請求,不看 I/O 請求的大小。ioutil 越高表示一直都有 I/O 請求,不代表磁盤無法響應新的 I/O 請求IOPS:每秒的 I/O 請求數吞吐量/帶寬:每秒的 I/O 請求大小,通常是 MB/s 或者 GB/s 為單位響應時間:I/O 請求發出到收到響應的時間飽和度:指的是磁盤處理 I/O 的繁忙程度。這個指標比較玄學,沒有直接的數據可以表示,一般是根據平均隊列請求長度或者響應時間跟基準測試的結果進行對比來估算(在做基準測試時,還會分順序/隨機、讀/寫進行排列組合分別去測 IOPS 和帶寬)
上面的指標除了飽和度外,其他都可以在監控系統中看到。Linux 也提供了一些命令來輸出不同維度的 I/O 狀態:
iostat -d -x:看各個設備的 I/O 狀態,數據來源 /proc/diskstatspidstat -d:看近處的 I/Oiotop:類似 top,按 I/O 大小對進程排序來源:騰訊云開發者,??點擊查看原文??。
騰訊云開發者
騰訊云官方社區公眾號,匯聚技術開發者群體,分享技術干貨,打造技術影響力交流社區。
664篇原創內容
公眾號