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

小林coding發表於2020-08-13

前言

不多 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 的過程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 檔案系統早期 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 ,來看看非阻塞 I/O,非阻塞的 read 請求在資料未準備好的情況下立即返回,可以繼續往下執行,此時應用程式不斷輪詢核心,直到資料準備好,核心將資料拷貝到應用程式緩衝區,read 呼叫才可以獲取到結果。過程如下圖:

非阻塞 I/O非阻塞 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,還是基於非阻塞 I/O 的多路複用都是同步呼叫。因為它們在 read 呼叫時,核心將資料從核心空間拷貝到應用程式空間,過程都是需要等待的,也就是說這個過程是同步的,如果核心實現的拷貝效率不高,read 呼叫就會在這個同步過程中等待比較長的時間。

而真正的非同步 I/O 是「核心資料準備好」和「資料從核心態拷貝到使用者態」這兩個過程都不用等待。

當我們發起 aio_read 之後,就立即返回,核心自動將資料從核心空間拷貝到應用程式空間,這個拷貝過程同樣是非同步的,核心自動完成的,和前面的同步操作不一樣,應用程式並不需要主動發起拷貝動作。過程如下圖:

非同步 I/O非同步 I/O

下面這張圖,總結了以上幾種 I/O 模型:

在前面我們知道了,I/O 是分為兩個過程的:

  1. 資料準備的過程
  2. 資料從核心空間拷貝到使用者程式緩衝區的過程

阻塞 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 主解說「火影忍者」動漫全集,於是就這麼忘了文章的事情,哈哈哈。

不過,確實很過癮,畢竟偷的了忙中閒,方能人上人嘛。

好了,小林是專為大家圖解的工具人,我們下次見!


好文推薦

涼了!張三同學沒答好「程式間通訊」,被面試官掛了….

萬粉福利,300 頁圖解網路 PDF 打包送你

相關文章