一口氣搞懂「文件系統」,就靠這 25 張圖了

前言
不多 BB,直接上「硬菜」。
正文
文件系統的基本組成
文件系統是作業系統中負責管理持久數據的子系統,說簡單點,就是負責把用戶的文件存到磁碟硬體中,因為即使電腦斷電了,磁碟里的數據並不會丟失,所以可以持久化的保存文件。
文件系統的基本數據單位是文件,它的目的是對磁碟上的文件進行組織管理,那組織的方式不同,就會形成不同的文件系統。
Linux 最經典的一句話是:「一切皆文件」,不僅普通的文件和目錄,就連塊設備、管道、socket 等,也都是統一交給文件系統管理的。
Linux 文件系統會為每個文件分配兩個數據結構:索引節點(index node)和目錄項(directory entry),它們主要用來記錄文件的元資訊和目錄層次結構。
- 索引節點,也就是 inode,用來記錄文件的元資訊,比如 inode 編號、文件大小、訪問許可權、創建時間、修改時間、數據在磁碟的位置等等。索引節點是文件的唯一標識,它們之間一一對應,也同樣都會被存儲在硬碟中,所以索引節點同樣佔用磁碟空間。
- 目錄項,也就是 dentry,用來記錄文件的名字、索引節點指針以及與其他目錄項的層級關聯關係。多個目錄項關聯起來,就會形成目錄結構,但它與索引節點不同的是,目錄項是由內核維護的一個數據結構,不存放於磁碟,而是快取在記憶體。
由於索引節點唯一標識一個文件,而目錄項記錄著文件的名,所以目錄項和索引節點的關係是多對一,也就是說,一個文件可以有多個別字。比如,硬鏈接的實現就是多個目錄項中的索引節點指向同一個文件。
注意,目錄也是文件,也是用索引節點唯一標識,和普通文件不同的是,普通文件在磁碟裡面保存的是文件數據,而目錄文件在磁碟裡面保存子目錄或文件。
目錄項和目錄是一個東西嗎?
雖然名字很相近,但是它們不是一個東西,目錄是個文件,持久化存儲在磁碟,而目錄項是內核一個數據結構,快取在記憶體。
如果查詢目錄頻繁從磁碟讀,效率會很低,所以內核會把已經讀過的目錄用目錄項這個數據結構快取在記憶體,下次再次讀到相同的目錄時,只需從記憶體讀就可以,大大提高了文件系統的效率。
注意,目錄項這個數據結構不只是表示目錄,也是可以表示文件的。
那文件數據是如何存儲在磁碟的呢?
磁碟讀寫的最小單位是扇區,扇區的大小只有 512B
大小,很明顯,如果每次讀寫都以這麼小為單位,那這讀寫的效率會非常低。
所以,文件系統把多個扇區組成了一個邏輯塊,每次讀寫的最小單位就是邏輯塊(數據塊),Linux 中的邏輯塊大小為 4KB
,也就是一次性讀寫 8 個扇區,這將大大提高了磁碟的讀寫的效率。
以上就是索引節點、目錄項以及文件數據的關係,下面這個圖就很好的展示了它們之間的關係:
索引節點是存儲在硬碟上的數據,那麼為了加速文件的訪問,通常會把索引節點載入到記憶體中。
另外,磁碟進行格式化的時候,會被分成三個存儲區域,分別是超級塊、索引節點區和數據塊區。
- 超級塊,用來存儲文件系統的詳細資訊,比如塊個數、塊大小、空閑塊等等。
- 索引節點區,用來存儲索引節點;
- 數據塊區,用來存儲文件或目錄數據;
我們不可能把超級塊和索引節點區全部載入到記憶體,這樣記憶體肯定撐不住,所以只有當需要使用的時候,才將其載入進記憶體,它們載入進記憶體的時機是不同的:
- 超級塊:當文件系統掛載時進入記憶體;
- 索引節點區:當文件被訪問時進入記憶體;
虛擬文件系統
文件系統的種類眾多,而作業系統希望對用戶提供一個統一的介面,於是在用戶層與文件系統層引入了中間層,這個中間層就稱為虛擬文件系統(Virtual File System,VFS)。
VFS 定義了一組所有文件系統都支援的數據結構和標準介面,這樣程式設計師不需要了解文件系統的工作原理,只需要了解 VFS 提供的統一介面即可。
在 Linux 文件系統中,用戶空間、系統調用、虛擬機文件系統、快取、文件系統以及存儲之間的關係如下圖:
Linux 支援的文件系統也不少,根據存儲位置的不同,可以把文件系統分為三類:
- 磁碟的文件系統,它是直接把數據存儲在磁碟中,比如 Ext 2/3/4、XFS 等都是這類文件系統。
- 記憶體的文件系統,這類文件系統的數據不是存儲在硬碟的,而是佔用記憶體空間,我們經常用到的
/proc
和/sys
文件系統都屬於這一類,讀寫這類文件,實際上是讀寫內核中相關的數據數據。 - 網路的文件系統,用來訪問其他電腦主機數據的文件系統,比如 NFS、SMB 等等。
文件系統首先要先掛載到某個目錄才可以正常使用,比如 Linux 系統在啟動時,會把文件系統掛載到根目錄。
文件的使用
我們從用戶角度來看文件的話,就是我們要怎麼使用文件?首先,我們得通過系統調用來打開一個文件。
write 的過程
fd = open(name, flag); # 打開文件
...
write(fd,...); # 寫數據
...
close(fd); # 關閉文件
上面簡單的程式碼是讀取一個文件的過程:
- 首先用
open
系統調用打開文件,open
的參數中包含文件的路徑名和文件名。 - 使用
write
寫數據,其中write
使用open
所返回的文件描述符,並不使用文件名作為參數。 - 使用完文件後,要用
close
系統調用關閉文件,避免資源的泄露。
我們打開了一個文件後,作業系統會跟蹤進程打開的所有文件,所謂的跟蹤呢,就是作業系統為每個進程維護一個打開文件表,文件表裡的每一項代表「文件描述符」,所以說文件描述符是打開文件的標識。
打開文件表
作業系統在打開文件表中維護著打開文件的狀態和資訊:
- 文件指針:系統跟蹤上次讀寫位置作為當前文件位置指針,這種指針對打開文件的某個進程來說是唯一的;
- 文件打開計數器:文件關閉時,作業系統必須重用其打開文件表條目,否則表內空間不夠用。因為多個進程可能打開同一個文件,所以系統在刪除打開文件條目之前,必須等待最後一個進程關閉文件,該計數器跟蹤打開和關閉的數量,當該計數為 0 時,系統關閉文件,刪除該條目;
- 文件磁碟位置:絕大多數文件操作都要求系統修改文件數據,該資訊保存在記憶體中,以免每個操作都從磁碟中讀取;
- 訪問許可權:每個進程打開文件都需要有一個訪問模式(創建、只讀、讀寫、添加等),該資訊保存在進程的打開文件表中,以便作業系統能允許或拒絕之後的 I/O 請求;
在用戶視角里,文件就是一個持久化的數據結構,但作業系統並不會關心你想存在磁碟上的任何的數據結構,作業系統的視角是如何把文件數據和磁碟塊對應起來。
所以,用戶和作業系統對文件的讀寫操作是有差異的,用戶習慣以位元組的方式讀寫文件,而作業系統則是以數據塊來讀寫文件,那屏蔽掉這種差異的工作就是文件系統了。
我們來分別看一下,讀文件和寫文件的過程:
- 當用戶進程從文件讀取 1 個位元組大小的數據時,文件系統則需要獲取位元組所在的數據塊,再返回數據塊對應的用戶進程所需的數據部分。
- 當用戶進程把 1 個位元組大小的數據寫進文件時,文件系統則找到需要寫入數據的數據塊的位置,然後修改數據塊中對應的部分,最後再把數據塊寫回磁碟。
所以說,文件系統的基本操作單位是數據塊。
文件的存儲
文件的數據是要存儲在硬碟上面的,數據在磁碟上的存放方式,就像程式在記憶體中存放的方式那樣,有以下兩種:
- 連續空間存放方式
- 非連續空間存放方式
其中,非連續空間存放方式又可以分為「鏈表方式」和「索引方式」。
不同的存儲方式,有各自的特點,重點是要分析它們的存儲效率和讀寫性能,接下來分別對每種存儲方式說一下。
連續空間存放方式
連續空間存放方式顧名思義,文件存放在磁碟「連續的」物理空間中。這種模式下,文件的數據都是緊密相連,讀寫效率很高,因為一次磁碟尋道就可以讀出整個文件。
使用連續存放的方式有一個前提,必須先知道一個文件的大小,這樣文件系統才會根據文件的大小在磁碟上找到一塊連續的空間分配給文件。
所以,文件頭裡需要指定「起始塊的位置」和「長度」,有了這兩個資訊就可以很好的表示文件存放方式是一塊連續的磁碟空間。
注意,此處說的文件頭,就類似於 Linux 的 inode。
連續空間存放方式
連續空間存放的方式雖然讀寫效率高,但是有「磁碟空間碎片」和「文件長度不易擴展」的缺陷。
如下圖,如果文件 B 被刪除,磁碟上就留下一塊空缺,這時,如果新來的文件小於其中的一個空缺,我們就可以將其放在相應空缺里。但如果該文件的大小大於所有的空缺,但卻小於空缺大小之和,則雖然磁碟上有足夠的空缺,但該文件還是不能存放。當然了,我們可以通過將現有文件進行挪動來騰出空間以容納新的文件,但是這個在磁碟挪動文件是非常耗時,所以這種方式不太現實。
磁碟碎片
另外一個缺陷是文件長度擴展不方便,例如上圖中的文件 A 要想擴大一下,需要更多的磁碟空間,唯一的辦法就只能是挪動的方式,前面也說了,這種方式效率是非常低的。
那麼有沒有更好的方式來解決上面的問題呢?答案當然有,既然連續空間存放的方式不太行,那麼我們就改變存放的方式,使用非連續空間存放方式來解決這些缺陷。
非連續空間存放方式
非連續空間存放方式分為「鏈表方式」和「索引方式」。
我們先來看看鏈表的方式。
鏈表的方式存放是離散的,不用連續的,於是就可以消除磁碟碎片,可大大提高磁碟空間的利用率,同時文件的長度可以動態擴展。根據實現的方式的不同,鏈表可分為「隱式鏈表」和「顯式鏈接」兩種形式。
文件要以「隱式鏈表」的方式存放的話,實現的方式是文件頭要包含「第一塊」和「最後一塊」的位置,並且每個數據塊裡面留出一個指針空間,用來存放下一個數據塊的位置,這樣一個數據塊連著一個數據塊,從鏈頭開是就可以順著指針找到所有的數據塊,所以存放的方式可以是不連續的。
隱式鏈表
隱式鏈表的存放方式的缺點在於無法直接訪問數據塊,只能通過指針順序訪問文件,以及數據塊指針消耗了一定的存儲空間。隱式鏈接分配的穩定性較差,系統在運行過程中由於軟體或者硬體錯誤導致鏈表中的指針丟失或損壞,會導致文件數據的丟失。
如果取出每個磁碟塊的指針,把它放在記憶體的一個表中,就可以解決上述隱式鏈表的兩個不足。那麼,這種實現方式是「顯式鏈接」,它指把用於鏈接文件各數據塊的指針,顯式地存放在記憶體的一張鏈接表中,該表在整個磁碟僅設置一張,每個表項中存放鏈接指針,指向下一個數據塊號。
對於顯式鏈接的工作方式,我們舉個例子,文件 A 依次使用了磁碟塊 4、7、2、10 和 12 ,文件 B 依次使用了磁碟塊 6、3、11 和 14 。利用下圖中的表,可以從第 4 塊開始,順著鏈走到最後,找到文件 A 的全部磁碟塊。同樣,從第 6 塊開始,順著鏈走到最後,也能夠找出文件 B 的全部磁碟塊。最後,這兩個鏈都以一個不屬於有效磁碟編號的特殊標記(如 -1 )結束。記憶體中的這樣一個表格稱為文件分配表(File Allocation Table,FAT)。
顯式鏈接
由於查找記錄的過程是在記憶體中進行的,因而不僅顯著地提高了檢索速度,而且大大減少了訪問磁碟的次數。但也正是整個表都存放在記憶體中的關係,它的主要的缺點是不適用於大磁碟。
比如,對於 200GB 的磁碟和 1KB 大小的塊,這張表需要有 2 億項,每一項對應於這 2 億個磁碟塊中的一個塊,每項如果需要 4 個位元組,那這張表要佔用 800MB 記憶體,很顯然 FAT 方案對於大磁碟而言不太合適。
接下來,我們來看看索引的方式。
鏈表的方式解決了連續分配的磁碟碎片和文件動態擴展的問題,但是不能有效支援直接訪問(FAT除外),索引的方式可以解決這個問題。
索引的實現是為每個文件創建一個「索引數據塊」,裡面存放的是指向文件數據塊的指針列表,說白了就像書的目錄一樣,要找哪個章節的內容,看目錄查就可以。
另外,文件頭需要包含指向「索引數據塊」的指針,這樣就可以通過文件頭知道索引數據塊的位置,再通過索引數據塊里的索引資訊找到對應的數據塊。
創建文件時,索引塊的所有指針都設為空。當首次寫入第 i 塊時,先從空閑空間中取得一個塊,再將其地址寫到索引塊的第 i 個條目。
索引的方式
索引的方式優點在於:
- 文件的創建、增大、縮小很方便;
- 不會有碎片的問題;
- 支援順序讀寫和隨機讀寫;
由於索引數據也是存放在磁碟塊的,如果文件很小,明明只需一塊就可以存放的下,但還是需要額外分配一塊來存放索引數據,所以缺陷之一就是存儲索引帶來的開銷。
如果文件很大,大到一個索引數據塊放不下索引資訊,這時又要如何處理大文件的存放呢?我們可以通過組合的方式,來處理大文件的存。
先來看看鏈表 + 索引的組合,這種組合稱為「鏈式索引塊」,它的實現方式是在索引數據塊留出一個存放下一個索引數據塊的指針,於是當一個索引數據塊的索引資訊用完了,就可以通過指針的方式,找到下一個索引數據塊的資訊。那這種方式也會出現前面提到的鏈表方式的問題,萬一某個指針損壞了,後面的數據也就會無法讀取了。
鏈式索引塊
還有另外一種組合方式是索引 + 索引的方式,這種組合稱為「多級索引塊」,實現方式是通過一個索引塊來存放多個索引數據塊,一層套一層索引,像極了俄羅斯套娃是吧。
多級索引塊
Unix 文件的實現方式
我們先把前面提到的文件實現方式,做個比較:
那早期 Unix 文件系統是組合了前面的文件存放方式的優點,如下圖:
早期 Unix 文件系統
它是根據文件的大小,存放的方式會有所變化:
- 如果存放文件所需的數據塊小於 10 塊,則採用直接查找的方式;
- 如果存放文件所需的數據塊超過 10 塊,則採用一級間接索引方式;
- 如果前面兩種方式都不夠存放大文件,則採用二級間接索引方式;
- 如果二級間接索引也不夠存放大文件,這採用三級間接索引方式;
那麼,文件頭(Inode)就需要包含 13 個指針:
- 10 個指向數據塊的指針;
- 第 11 個指向索引塊的指針;
- 第 12 個指向二級索引塊的指針;
- 第 13 個指向三級索引塊的指針;
所以,這種方式能很靈活地支援小文件和大文件的存放:
- 對於小文件使用直接查找的方式可減少索引數據塊的開銷;
- 對於大文件則以多級索引的方式來支援,所以大文件在訪問數據塊時需要大量查詢;
這個方案就用在了 Linux Ext 2/3 文件系統里,雖然解決大文件的存儲,但是對於大文件的訪問,需要大量的查詢,效率比較低。
為了解決這個問題,Ext 4 做了一定的改變,具體怎麼解決的,本文就不展開了。
空閑空間管理
前面說到的文件的存儲是針對已經被佔用的數據塊組織和管理,接下來的問題是,如果我要保存一個數據塊,我應該放在硬碟上的哪個位置呢?難道需要將所有的塊掃描一遍,找個空的地方隨便放嗎?
那這種方式效率就太低了,所以針對磁碟的空閑空間也是要引入管理的機制,接下來介紹幾種常見的方法:
- 空閑表法
- 空閑鏈表法
- 點陣圖法
空閑表法
空閑表法就是為所有空閑空間建立一張表,表內容包括空閑區的第一個塊號和該空閑區的塊個數,注意,這個方式是連續分配的。如下圖:
空閑表法
當請求分配磁碟空間時,系統依次掃描空閑表裡的內容,直到找到一個合適的空閑區域為止。當用戶撤銷一個文件時,系統回收文件空間。這時,也需順序掃描空閑表,尋找一個空閑表條目並將釋放空間的第一個物理塊號及它佔用的塊數填到這個條目中。
這種方法僅當有少量的空閑區時才有較好的效果。因為,如果存儲空間中有著大量的小的空閑區,則空閑表變得很大,這樣查詢效率會很低。另外,這種分配技術適用於建立連續文件。
空閑鏈表法
我們也可以使用「鏈表」的方式來管理空閑空間,每一個空閑塊里有一個指針指向下一個空閑塊,這樣也能很方便的找到空閑塊並管理起來。如下圖:
空閑鏈表法
當創建文件需要一塊或幾塊時,就從鏈頭上依次取下一塊或幾塊。反之,當回收空間時,把這些空閑塊依次接到鏈頭上。
這種技術只要在主存中保存一個指針,令它指向第一個空閑塊。其特點是簡單,但不能隨機訪問,工作效率低,因為每當在鏈上增加或移動空閑塊時需要做很多 I/O 操作,同時數據塊的指針消耗了一定的存儲空間。
空閑表法和空閑鏈表法都不適合用於大型文件系統,因為這會使空閑表或空閑鏈表太大。
點陣圖法
點陣圖是利用二進位的一位來表示磁碟中一個盤塊的使用情況,磁碟上所有的盤塊都有一個二進位位與之對應。
當值為 0 時,表示對應的盤塊空閑,值為 1 時,表示對應的盤塊已分配。它形式如下:
1111110011111110001110110111111100111 ...
在 Linux 文件系統就採用了點陣圖的方式來管理空閑空間,不僅用於數據空閑塊的管理,還用於 inode 空閑塊的管理,因為 inode 也是存儲在磁碟的,自然也要有對其管理。
文件系統的結構
前面提到 Linux 是用點陣圖的方式管理空閑空間,用戶在創建一個新文件時,Linux 內核會通過 inode 的點陣圖找到空閑可用的 inode,並進行分配。要存儲數據時,會通過塊的點陣圖找到空閑的塊,並分配,但仔細計算一下還是有問題的。
數據塊的點陣圖是放在磁碟塊里的,假設是放在一個塊里,一個塊 4K,每位表示一個數據塊,共可以表示 4 * 1024 * 8 = 2^15
個空閑塊,由於 1 個數據塊是 4K 大小,那麼最大可以表示的空間為 2^15 * 4 * 1024 = 2^27
個 byte,也就是 128M。
也就是說按照上面的結構,如果採用「一個塊的點陣圖 + 一系列的塊」,外加「一個塊的 inode 的點陣圖 + 一系列的 inode 的結構」能表示的最大空間也就 128M,這太少了,現在很多文件都比這個大。
在 Linux 文件系統,把這個結構稱為一個塊組,那麼有 N 多的塊組,就能夠表示 N 大的文件。
下圖給出了 Linux Ext2 整個文件系統的結構和塊組的內容,文件系統都由大量塊組組成,在硬碟上相繼排布:
最前面的第一個塊是引導塊,在系統啟動時用於啟用引導,接著後面就是一個一個連續的塊組了,塊組的內容如下:
- 超級塊,包含的是文件系統的重要資訊,比如 inode 總個數、塊總個數、每個塊組的 inode 個數、每個塊組的塊個數等等。
- 塊組描述符,包含文件系統中各個塊組的狀態,比如塊組中空閑塊和 inode 的數目等,每個塊組都包含了文件系統中「所有塊組的組描述符資訊」。
- 數據點陣圖和 inode 點陣圖, 用於表示對應的數據塊或 inode 是空閑的,還是被使用中。
- inode 列表,包含了塊組中所有的 inode,inode 用於保存文件系統中與各個文件和目錄相關的所有元數據。
- 數據塊,包含文件的有用數據。
你可以會發現每個塊組裡有很多重複的資訊,比如超級塊和塊組描述符表,這兩個都是全局資訊,而且非常的重要,這麼做是有兩個原因:
- 如果系統崩潰破壞了超級塊或塊組描述符,有關文件系統結構和內容的所有資訊都會丟失。如果有冗餘的副本,該資訊是可能恢復的。
- 通過使文件和管理數據儘可能接近,減少了磁頭尋道和旋轉,這可以提高文件系統的性能。
不過,Ext2 的後續版本採用了稀疏技術。該做法是,超級塊和塊組描述符表不再存儲到文件系統的每個塊組中,而是只寫入到塊組 0、塊組 1 和其他 ID 可以表示為 3、 5、7 的冪的塊組中。
目錄的存儲
在前面,我們知道了一個普通文件是如何存儲的,但還有一個特殊的文件,經常用到的目錄,它是如何保存的呢?
基於 Linux 一切皆文件的設計思想,目錄其實也是個文件,你甚至可以通過 vim
打開它,它也有 inode,inode 裡面也是指向一些塊。
和普通文件不同的是,普通文件的塊裡面保存的是文件數據,而目錄文件的塊裡面保存的是目錄裡面一項一項的文件資訊。
在目錄文件的塊中,最簡單的保存格式就是列表,就是一項一項地將目錄下的文件資訊(如文件名、文件 inode、文件類型等)列在表裡。
列表中每一項就代表該目錄下的文件的文件名和對應的 inode,通過這個 inode,就可以找到真正的文件。
目錄格式哈希表
通常,第一項是「.
」,表示當前目錄,第二項是「..
」,表示上一級目錄,接下來就是一項一項的文件名和 inode。
如果一個目錄有超級多的文件,我們要想在這個目錄下找文件,按照列表一項一項的找,效率就不高了。
於是,保存目錄的格式改成哈希表,對文件名進行哈希計算,把哈希值保存起來,如果我們要查找一個目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個文件的資訊在相應的塊裡面。
Linux 系統的 ext 文件系統就是採用了哈希表,來保存目錄的內容,這種方法的優點是查找非常迅速,插入和刪除也較簡單,不過需要一些預備措施來避免哈希衝突。
目錄查詢是通過在磁碟上反覆搜索完成,需要不斷地進行 I/O 操作,開銷較大。所以,為了減少 I/O 操作,把當前使用的文件目錄快取在記憶體,以後要使用該文件時只要在記憶體中操作,從而降低了磁碟操作次數,提高了文件系統的訪問速度。
軟鏈接和硬鏈接
有時候我們希望給某個文件取個別名,那麼在 Linux 中可以通過硬鏈接(Hard Link) 和軟鏈接(Symbolic Link) 的方式來實現,它們都是比較特殊的文件,但是實現方式也是不相同的。
硬鏈接是多個目錄項中的「索引節點」指向一個文件,也就是指向同一個 inode,但是 inode 是不可能跨越文件系統的,每個文件系統都有各自的 inode 數據結構和列表,所以硬鏈接是不可用於跨文件系統的。由於多個目錄項都是指向一個 inode,那麼只有刪除文件的所有硬鏈接以及源文件時,系統才會徹底刪除該文件。
硬鏈接
軟鏈接相當於重新創建一個文件,這個文件有獨立的 inode,但是這個文件的內容是另外一個文件的路徑,所以訪問軟鏈接的時候,實際上相當於訪問到了另外一個文件,所以軟鏈接是可以跨文件系統的,甚至目標文件被刪除了,鏈接文件還是在的,只不過指向的文件找不到了而已。
軟鏈接
文件 I/O
文件的讀寫方式各有千秋,對於文件的 I/O 分類也非常多,常見的有
- 緩衝與非緩衝 I/O
- 直接與非直接 I/O
- 阻塞與非阻塞 I/O VS 同步與非同步 I/O
接下來,分別對這些分類討論討論。
緩衝與非緩衝 I/O
文件操作的標準庫是可以實現數據的快取,那麼根據「是否利用標準庫緩衝」,可以把文件 I/O 分為緩衝 I/O 和非緩衝 I/O:
- 緩衝 I/O,利用的是標準庫的快取實現文件的加速訪問,而標準庫再通過系統調用訪問文件。
- 非緩衝 I/O,直接通過系統調用訪問文件,不經過標準庫快取。
這裡所說的「緩衝」特指標準庫內部實現的緩衝。
比方說,很多程式遇到換行時才真正輸出,而換行前的內容,其實就是被標準庫暫時快取了起來,這樣做的目的是,減少系統調用的次數,畢竟系統調用是有 CPU 上下文切換的開銷的。
直接與非直接 I/O
我們都知道磁碟 I/O 是非常慢的,所以 Linux 內核為了減少磁碟 I/O 次數,在系統調用後,會把用戶數據拷貝到內核中快取起來,這個內核快取空間也就是「頁快取」,只有當快取滿足某些條件的時候,才發起磁碟 I/O 的請求。
那麼,根據是「否利用作業系統的快取」,可以把文件 I/O 分為直接 I/O 與非直接 I/O:
- 直接 I/O,不會發生內核快取和用戶程式之間數據複製,而是直接經過文件系統訪問磁碟。
- 非直接 I/O,讀操作時,數據從內核快取中拷貝給用戶程式,寫操作時,數據從用戶程式拷貝給內核快取,再由內核決定什麼時候寫入數據到磁碟。
如果你在使用文件操作類的系統調用函數時,指定了 O_DIRECT
標誌,則表示使用直接 I/O。如果沒有設置過,默認使用的是非直接 I/O。
如果用了非直接 I/O 進行寫數據操作,內核什麼情況下才會把快取數據寫入到磁碟?
以下幾種場景會觸發內核快取的數據寫入磁碟:
- 在調用
write
的最後,當發現內核快取的數據太多的時候,內核會把數據寫到磁碟上; - 用戶主動調用
sync
,內核快取會刷到磁碟上; - 當記憶體十分緊張,無法再分配頁面時,也會把內核快取的數據刷到磁碟上;
- 內核快取的數據的快取時間超過某個時間時,也會把數據刷到磁碟上;
阻塞與非阻塞 I/O VS 同步與非同步 I/O
為什麼把阻塞 / 非阻塞與同步與非同步放一起說的呢?因為它們確實非常相似,也非常容易混淆,不過它們之間的關係還是有點微妙的。
先來看看阻塞 I/O,當用戶程式執行 read
,執行緒會被阻塞,一直等到內核數據準備好,並把數據從內核緩衝區拷貝到應用程式的緩衝區中,當拷貝過程完成,read
才會返回。
注意,阻塞等待的是「內核數據準備好」和「數據從內核態拷貝到用戶態」這兩個過程。過程如下圖:
阻塞 I/O
知道了阻塞 I/O ,來看看非阻塞 I/O,非阻塞的 read 請求在數據未準備好的情況下立即返回,可以繼續往下執行,此時應用程式不斷輪詢內核,直到數據準備好,內核將數據拷貝到應用程式緩衝區,read
調用才可以獲取到結果。過程如下圖:
非阻塞 I/O
注意,這裡最後一次 read 調用,獲取數據的過程,是一個同步的過程,是需要等待的過程。這裡的同步指的是內核態的數據拷貝到用戶程式的快取區這個過程。
舉個例子,訪問管道或 socket 時,如果設置了 O_NONBLOCK
標誌,那麼就表示使用的是非阻塞 I/O 的方式訪問,而不做任何設置的話,默認是阻塞 I/O。
應用程式每次輪詢內核的 I/O 是否準備好,感覺有點傻乎乎,因為輪詢的過程中,應用程式啥也做不了,只是在循環。
為了解決這種傻乎乎輪詢方式,於是 I/O 多路復用技術就出來了,如 select、poll,它是通過 I/O 事件分發,當內核數據準備好時,再以事件通知應用程式進行操作。
這個做法大大改善了應用進程對 CPU 的利用率,在沒有被通知的情況下,應用進程可以使用 CPU 做其他的事情。
下圖是使用 select I/O 多路復用過程。注意,read
獲取數據的過程(數據從內核態拷貝到用戶態的過程),也是一個同步的過程,需要等待:
I/O 多路復用
實際上,無論是阻塞 I/O、非阻塞 I/O,還是基於非阻塞 I/O 的多路復用都是同步調用。因為它們在 read 調用時,內核將數據從內核空間拷貝到應用程式空間,過程都是需要等待的,也就是說這個過程是同步的,如果內核實現的拷貝效率不高,read 調用就會在這個同步過程中等待比較長的時間。
而真正的非同步 I/O 是「內核數據準備好」和「數據從內核態拷貝到用戶態」這兩個過程都不用等待。
當我們發起 aio_read
之後,就立即返回,內核自動將數據從內核空間拷貝到應用程式空間,這個拷貝過程同樣是非同步的,內核自動完成的,和前面的同步操作不一樣,應用程式並不需要主動發起拷貝動作。過程如下圖:
非同步 I/O
下面這張圖,總結了以上幾種 I/O 模型:
在前面我們知道了,I/O 是分為兩個過程的:
- 數據準備的過程
- 數據從內核空間拷貝到用戶進程緩衝區的過程
阻塞 I/O 會阻塞在「過程 1 」和「過程 2」,而非阻塞 I/O 和基於非阻塞 I/O 的多路復用只會阻塞在「過程 2」,所以這三個都可以認為是同步 I/O。
非同步 I/O 則不同,「過程 1 」和「過程 2 」都不會阻塞。
用故事去理解這幾種 I/O 模型
舉個你去飯堂吃飯的例子,你好比用戶程式,飯堂好比作業系統。
阻塞 I/O 好比,你去飯堂吃飯,但是飯堂的菜還沒做好,然後你就一直在那裡等啊等,等了好長一段時間終於等到飯堂阿姨把菜端了出來(數據準備的過程),但是你還得繼續等阿姨把菜(內核空間)打到你的飯盒裡(用戶空間),經歷完這兩個過程,你才可以離開。
非阻塞 I/O 好比,你去了飯堂,問阿姨菜做好了沒有,阿姨告訴你沒,你就離開了,過幾十分鐘,你又來飯堂問阿姨,阿姨說做好了,於是阿姨幫你把菜打到你的飯盒裡,這個過程你是得等待的。
基於非阻塞的 I/O 多路復用好比,你去飯堂吃飯,發現有一排窗口,飯堂阿姨告訴你這些窗口都還沒做好菜,等做好了再通知你,於是等啊等(select
調用中),過了一會阿姨通知你菜做好了,但是不知道哪個窗口的菜做好了,你自己看吧。於是你只能一個一個窗口去確認,後面發現 5 號窗口菜做好了,於是你讓 5 號窗口的阿姨幫你打菜到飯盒裡,這個打菜的過程你是要等待的,雖然時間不長。打完菜後,你自然就可以離開了。
非同步 I/O 好比,你讓飯堂阿姨將菜做好並把菜打到飯盒裡後,把飯盒送到你面前,整個過程你都不需要任何等待。
遲到理由
是的,小林依然遲到了,因為最近發生了一件非常倒霉的事情,我之前使用的圖床掛掉了……
這就導致我所有文章的圖片都掛了,好在大部分部落格平台都會轉存圖片,所以微信公眾號、CSDN、知乎等平台都正常,但我的本地文章筆記和部落格園平台的圖片都掛掉了,在部落格園還有個讀者私信提醒我的文章圖片掛了,他很喜歡小林文章,希望早點恢圖片,太感動了。
這就是白嫖免費圖床的下場,本打算換阿里雲圖床,但阿里雲圖床是按訪問流量收費的,如果有人搞你,那直接刷爆你的錢包,想想都可怕,小林窮搞不起搞不起。
後來,詢問了一位朋友 guide 哥,他說可以使用 GitHub 作為圖床,用開源工具 Picgo 關聯 GitHub 上傳圖片,再通過 jsdelivr CDN 加速訪問,這一套組合很完美,於是我就採用了此方案搭建了自己的圖床,依舊繼續白嫖,我就不信 GitHub 也掛!
圖床雖然搞定了,最糟糕的事情才開始,我要把以前近 500
張的圖片重新保存(以前有的圖片丟了)和分類,並一個一個上傳到 Github,接著還得把圖片的新地址改到本地文章,這工作量簡直要命,到現在我也才搞定了作業系統篇的圖片,網路篇的圖片還有 2/3 沒弄完,瞬間後悔自己畫那麼多圖。
唉,發完這篇文章,小林還得繼續恢復圖片……
最近,我都在 B 站學習作業系統,但有時候是想看作業系統,但奈何 B 站首頁推送太豐富,看著看著半天就過去了,甚至還花了一天時間專門看一個 UP 主解說「火影忍者」動漫全集,於是就這麼忘了文章的事情,哈哈哈。
不過,確實很過癮,畢竟偷的了忙中閑,方能人上人嘛。
好了,小林是專為大家圖解的工具人,我們下次見!