文章目錄
- 一、前言
- 二、認識硬件——磁盤
- 2.1 磁盤的存儲構成
- 2.2 磁盤的邏輯抽象
- 三、操作系統對磁盤的使用
- 3.1 再來理解創建文件
- 3.2 再來理解刪除文件
- 3.3 再來理解目錄
- 四、硬鏈接
- 五、軟鏈接
- 六、結語
一、前言
在之前的【Linux取經路】文件系統之被打開的文件——文件描述符的引入一文中討論了被打開的文件,今天討論的話題則是沒有被打開的文件。文件等于文件內容加文件屬性,沒打開的文件一定是存儲在磁盤上的,并且 Linux 是將文件的屬性和內容分開存儲。文件內容以數據塊的形式進行存儲,文件屬性以 inode 的形式進行存儲。
二、認識硬件——磁盤
我們這里說的磁盤指的是機械磁盤,并非我們現在我們筆記本上使用的 SSD。機械磁盤是計算機上唯一的一個機械設備,也是一個外設。
小Tips:磁頭是一面一個,磁頭與盤面不接觸。磁頭通過向盤面進行充放電來完成數據的寫入。磁盤叫做永久性存儲介質,內存叫做掉電易失性存儲介質。
2.1 磁盤的存儲構成
每一個盤面由多個磁道構成,一個磁道又有多個扇區構成。磁盤被訪問的最基本單元是扇區,一般扇區的大小是 512 字節,有的是 4KB。要修改磁盤中 1 字節的數據,需要把該字節所在的扇區都加載到內存中。可以把磁盤看成是由無數個扇區構成的存儲介質。要把數據存儲到磁盤,第一個需要解決的問題就是如何定位一個扇區,首先需要定位盤面,也就是確定用哪個磁頭,因為一個磁頭對應一個盤面,接下來需要定位磁道,最后定位扇區。所有的磁頭都是同步運動的,在某一時刻,從從上向下看去,以磁頭所在點為半徑的不同盤面上的磁道就會形成一個叫做柱面的結構。磁頭運動主要是去定位磁道,盤面旋轉主要是去定位扇區,磁頭的定位是由硬件電路進行控制。由此可見,磁盤的讀取效率取決于磁頭、盤面的運動速度和運動次數,運動越少,效率越高;運動越多,效率越低。因此,在軟件設計上要求設計者一定要有意識的將相關數據放在一起。
2.2 磁盤的邏輯抽象
最終一個磁盤可以看作是基于扇區的數組,每一個扇區都對應有一個下標來唯一標識。通過這個下標(LBA 邏輯扇區地址),再結合每一面磁道的個數和每一個磁道上扇區的個數就可以定位到該扇區在磁盤上的位置(CHS地址)。
小Tips:不僅 CPU 有寄存器,其它外設也有,磁盤中也有寄存器。比如:控制寄存器,用來存儲 CPU 下發的讀寫指令;數據寄存器,存儲要寫入的磁盤的數據;地址寄存器,存儲 CPU 傳送來的 LBA 地址;狀態寄存器,存儲磁盤的狀態,操作系統通過檢查該狀態寄存器去判斷讀寫是否成功。
三、操作系統對磁盤的使用
上圖為 Linux ext2 磁盤文件系統圖(內核內存映像肯定有所不同),磁盤是典型的塊設備,操作系統首先會對磁盤進行分區,就如我們電腦中的 C 盤和 D 盤。接著,磁盤分區被劃分為若干個塊組(Block group),每個塊組中有許多塊(block),一個 block 的大小是由格式化的時候確定的,并且不可以更改,常見的是 4KB,即 4096字節。
-
Boot Block:通常存儲操作系統啟動的相關信息,比如:操作系統在什么位置、當前磁盤一共被劃分成了多少個分區等。這些信息一般存儲在磁盤的最前面,當然為了防止意外,這些內容在其它地方也會有備份。
-
Block Group:ext2 文件系統會根據分區的大小劃分為數個 Block Group。而每個 Block Group 都有著相同的結構組成。
-
Super Block:存放文件系統本身的信息,這里面記錄了整個分區的信息。例如:整個分區有多大、該分區里面每組的起始位置、每個組的大小、每個組的 inode 數量、每個組的 block 數量、每個組的其實 inode 編號、block 和 inode 的總量,未使用的 block 和 inode 的數量,一個 block 和 inode 的大小、文件系統的類型與名稱、最近一次掛載的時間、最近一次寫入數據的時間、最近一次檢驗磁盤的時間、該文件系統所擁有的字段以及字段的存儲順序和起始位置(也就是規定了一個組的空間劃分)等其它文件系統的相關信息。Super Block 的信息被破壞,可以說整個文件系統結構就被破壞了。該字段并不是在每一個分組中都有,而是在部分組里面有,防止意外發生,操作系統通過“魔數”來判斷是否是 Super Block。
-
Group Descriptor Table:塊組描述符,存儲塊組屬性信息,例如:當前分組的大小、使用情況、下一個文件描述符應該從哪開始。
-
Block Bitmap:塊位圖,將比特位的位置和塊號映射起來,里面記錄著 Data Block 中哪個數據塊已經被占用,哪個數據塊沒有被占用。
-
inode Bitmap:每個 bit 表示一個 inode 是否空閑可用。
-
inode Table:一組 inode。每一個 inode 用來存放單個文件的所有屬性,如:文件大小、所有者、最近修改時間等。每個 inode 的大小一般是 128字節,且每一個 inode 都有唯一的編號。一般而言,一個文件一個 inode。
-
Data block:數據區,存放文件內容。以塊的形式呈現,常見塊的大小是 4KB。每個塊都有自己獨一無二的塊號。
小Tips:操作系統在訪問磁盤的時候,會以塊為基本單位進行訪問。
格式化:每個組的前四個字段存儲的都是一些文件系統的屬性信息或者分組的使用情況信息,這些內容應該在我們使用磁盤之前都準備好。所以,每一個分區在被使用前,都必須將部分文件系統的屬性信息提前設置進對應的分區中,方便后續對分區和分組的使用,這個動作就叫做格式化。
在 Linux 中,文件的屬性里面是不包含文件的名稱。在 Linux 系統里面標識文件用的是 inode 編號。一個 inode 表示一個文件的所有屬性,文件名并不屬于 inode 內的屬性。
一個 inode 與 數據塊的對應關系:
其中直接索引對應的塊中存儲的就是文件內容,二級索引對應的塊中存儲的不是文件內容而是塊號。假設塊的大小是 4KB,塊號用 4字節。那么一個塊就可以存儲 1024 個塊號,這 1024 個塊號對應的塊里面存儲的是文件的內容,三級索引同理。這樣做的目的是在 inode 里面用較少的空間就可以映射出更多的數據塊。
小Tips:inode 編號是以分區為單位進行統一分配的,而且不能跨分區,即每個分區中的 inode 編號都是從 0 開始,且一個分區中的 inode 個數是有上限的,因此可能會存在一下情況:一個分區中的 inode 被用完了,但是數據塊還沒有被用完,這種情況對應的就是創建了非常多的文件,但是每個文件的內容非常小;一個分區中的數據塊被用完了,但是 inode 還沒有被用完,這種情況就是創建的文件并不多,但是每個文件的大小非常大。
3.1 再來理解創建文件
首先創建文件一定是在一個路徑下(目錄)進行創建,這個路徑就會幫我們定位到一個分區,然后去從第一個分組開始查看當前分組的 GDT 字段,看該分組中 inode 的使用情況,若當前分組中的 inode 還有剩余,接著去讀取 inode_Bitmap,獲取最近一個未被使用的 inode 編號,然后拿著 inode 編號去 inode_Table 里面找到對應的 inode,將文件的屬性信息一填。如果有文件內容,先拿著 inode 編號找到對應的分組,根據寫入內容的大小去 Block_Bitmap 中找出對應數量未被使用的塊號,然后將這些塊號寫入到 inode 對應的屬性里面,然后拿著塊號去 Data blocks 中進行寫入。
3.2 再來理解刪除文件
刪除文件只要拿著該文件的 inode 編號,在 inode Table 中找到對應的 indoe,獲取到里面的 blocks,即拿到該文件對應的所有塊號,然后根據這些塊號將 Block Bitmap 中對應的比特位置0(假設 0 表示對應的塊未被使用)。最后再根據 inode 編號到 inode Bitmap 中將該 inode 對應的比特位置為0,至此,一個文件就被刪除啦。可以發現從頭到尾并沒有去修改塊中的內容,這也是為什么拷貝 4G 的文件很慢,刪 4G 的文件很快。所以在理論上,一個被刪除的文件,可以根據 inode 將其恢復出來。
總結:刪文件就是去修改 inode_Bitmap 和 Block_Bitmap 中的字段。在計算機領域的刪除并不等于清空,大部分情況下,刪除都表示可覆蓋。因為清空會導致效率大大下降。
3.3 再來理解目錄
上面說的所有對文件的操作都離不開 inode 編號,但是我們作為普通用戶平時好像也并沒有關注過 inode 編號,我們一般是直接使用文件名,此時就必須再來理解一下目錄了。目錄也是文件,也有自己的 inode,目錄也有屬性。目錄也有(數據塊),目錄的數據塊里面存放的是這個目錄下的所有文件名和該文件名對應的 inode 編號的映射關系,這是一種 key-value 結構,這就是為什么一個目錄里面不允許出現同名文件。與此同時,和目錄有關的一些歷史問題也得到了解決,對于一個目錄,沒有 w,我們無法在該目錄下創建文件,本質就是我們不能向目錄對應的數據塊中寫入文件名和 inode 的對應關系;沒有 r,無法產看該目錄下的文件,本質就是不能讀取目錄文件的數據塊。因此我們在查找一個文件時,首先需要知道該文件所在目錄的 inode,要知道目錄文件的 inode 編號,就需要知道目錄文件所在目錄的 inode 編號,如此遞歸一直到根目錄,這就是為什么我們在操作一個任何一個文件的時候都需要知道它的絕對或者相對路徑。如果每操作一個文件都要去這樣遞歸一層層的查找,那么效率是非常低的。因此在 Linux 操作系統中有一個叫做 dentry 緩存(目錄項緩存),里面記錄了該用戶經常訪問的文件名和 inode 編號之間的映射關系。
四、硬鏈接
// 創建硬鏈接的指令
ln test.txt hard-link
硬連接不是一個獨立的文件,因為它沒有獨立的 inode。所謂建立硬連接,本質其實就是在特定目錄的數據塊中新增文件名和指向的文件的 inode 編號的映射關系。上圖也可以證明文件名不是 inode 中的屬性,因為一個 inode 編號對應一個 inode,如果文件名是 inode 中的屬性,那么上圖中編號 1978740 的文件不可能對應兩個文件名。
小Tips:任意一個文件,無論是目錄,還是普通文件,都有 inode,每一個 inode 內部,都有一個叫做引用計數的計數器,這個計數器記錄了有多少個文件名“指向”該文件(由硬鏈接可以得知,在 Linux 中可以讓多個文件名對應于同一個 inode)。完整的刪除文件過程就是先將特定目錄數據塊中的文件名與 inode 的映射關系刪除,然后根據 inode 的編號,將對應 inode 中的引用計數減減,最終看其是否減到零,減到零再執行 3.2 小結的步驟。此外,創建一個普通文件,它的硬鏈接數默認是1。
創建目錄文件的默認鏈接數為什么是 2 ?
硬鏈接數為2,說明與該文件 inode 編號有關的映射關系有兩個,其中一個映射關系保存在 dir 所在目錄文件的數據塊,即 2023-11-06 目錄文件中的數據塊中,另外一個保存在 dir 目錄自身的數據塊中。
根目錄的硬鏈接數減2就是根目錄下創建的目錄個數。根目錄稍微有一點特殊,根目錄中的 .
和 ..
文件名對應的 inode 編號是一樣的,都是根目錄。除去這倆文件名和 inode 編號的映射關系外,剩下的硬鏈接數就表示根目錄下創建的目錄個數,剩下的 18 個就是根目錄中所有目錄文件中 ..
與根目錄 inode 編號的鏈接關系。可以這樣計算的本質原因就是,根目錄下所有目錄中的 ..
都一定是指向根目錄的,對應根目錄的 inode 編號(也就是上圖中的 2 )。這種計算方法也可以推行到其它任意目錄。
總結:建立硬鏈接就是給一個已存在的文件創建別名,硬鏈接通常用來進行路徑定位,采用硬鏈接,可以進行目錄間切換。
小Tips:Linux 系統不允許用戶對目錄文件建立硬連接。只要是為了避免在文件搜索的時候發生環路問題,系統在搜索文件的時候并不會搜索 .
和 ..
,如果允許用戶為目錄文件創建硬鏈接,那么操作系統在進行文件搜索的時候就無法避免環路問題。
五、軟鏈接
// 創建軟鏈接的指令
ln -s file.txt soft-link
軟鏈接是一個獨立的文件,有獨立的 inode,也有獨立的數據塊,它的數據塊里面存的是指向文件的路徑。軟鏈接非常像 Windows 中的快捷方式。軟鏈接的使用場景:一般發布的可執行程序可能存在一個較深的路徑下面,要執行它的話就需要帶很長一串路徑,顯得十分麻煩,此時我們就可以創建一個軟鏈接指向該可執行文件,之后要想運行該可執行程序就直接去執行軟鏈接即可。
小Tips:可以對任意類型的文件創建軟鏈接。
// 刪除軟硬鏈接都可以用 unlink 指令
unlink soft-link
六、結語
今天的分享到這里就結束啦!如果覺得文章還不錯的話,可以三連支持一下,春人的主頁還有很多有趣的文章,歡迎小伙伴們前去點評,您的支持就是春人前進的動力!