吐血整理 | 肝翻Linux文件系統所有知識點
在 Linux 操作系統的廣袤天地里,文件系統宛如一座錯綜復雜而又井然有序的知識寶庫,蘊藏著無數的奧秘與智慧。從我們日常創建、讀取和保存文件,到整個系統的高效存儲管理與數據組織,Linux 文件系統都在背后默默地發揮著關鍵作用。Linux系統一般有4個主要部分:內核、shell、文件系統和應用程序。內核、shell和文件系統一起形成了基本的操作系統結構,它們使得用戶可以運行程序、管理文件并使用系統。
它就像是一位無聲的管理者,精心安排著數據在磁盤上的存放位置,巧妙地構建起目錄與文件之間的層級關系,確保我們能夠迅速而準確地找到所需的信息。無論是對于初涉 Linux 領域的新手,渴望了解其基礎的文件操作原理;還是經驗豐富的系統管理員,需要深入探究文件系統的優化與故障排查技巧;又或是開發者,試圖在應用程序中高效地與文件系統交互,掌握 Linux 文件系統的知識都至關重要。
一、Linux內核
內核是操作系統的核心,具有很多最基本功能,它負責管理系統的進程、內存、設備驅動程序、文件和網絡系統,決定著系統的性能和穩定性。Linux 內核由如下幾部分組成:內存管理、進程管理、設備驅動程序、文件系統和網絡管理等。如圖:
圖片
系統調用接口:SCI 層提供了某些機制執行從用戶空間到內核的函數調用。這個接口依賴于體系結構,甚至在相同的處理器家族內也是如此。SCI 實際上是一個非常有用的函數調用多路復用和多路分解服務。在 ./linux/kernel 中您可以找到 SCI 的實現,并在 ./linux/arch 中找到依賴于體系結構的部分。
1.1內存管理
對任何一臺計算機而言,其內存以及其它資源都是有限的。為了讓有限的物理內存滿足應用程序對內存的大需求量,Linux 采用了稱為“虛擬內存”的內存管理方式。Linux 將內存劃分為容易處理的“內存頁”(對于大部分體系結構來說都是 4KB)。Linux 包括了管理可用內存的方式,以及物理和虛擬映射所使用的硬件機制。
不過內存管理要管理的可不止 4KB 緩沖區。Linux 提供了對 4KB 緩沖區的抽象,例如 slab 分配器。這種內存管理模式使用 4KB 緩沖區為基數,然后從中分配結構,并跟蹤內存頁使用情況,比如哪些內存頁是滿的,哪些頁面沒有完全使用,哪些頁面為空。這樣就允許該模式根據系統需要來動態調整內存使用。為了支持多個用戶使用內存,有時會出現可用內存被消耗光的情況。
由于這個原因,頁面可以移出內存并放入磁盤中。這個過程稱為交換,因為頁面會被從內存交換到硬盤上。內存管理的源代碼可以在 ./linux/mm 中找到。
1.2進程管理
進程實際是某特定應用程序的一個運行實體。在 Linux 系統中,能夠同時運行多個進程,Linux 通過在短的時間間隔內輪流運行這些進程而實現“多任務”。這一短的時間間隔稱為“時間片”,讓進程輪流運行的方法稱為“進程調度” ,完成調度的程序稱為調度程序。進程調度控制進程對CPU的訪問。
當需要選擇下一個進程運行時,由調度程序選擇最值得運行的進程。可運行進程實際上是僅等待CPU資源的進程,如果某個進程在等待其它資源,則該進程是不可運行進程。Linux使用了比較簡單的基于優先級的進程調度算法選擇新的進程。通過多任務機制,每個進程可認為只有自己獨占計算機,從而簡化程序的編寫。每個進程有自己單獨的地址空間,并且只能由這一進程訪問,這樣,操作系統避免了進程之間的互相干擾以及“壞”程序對系統可能造成的危害。
為了完成某特定任務,有時需要綜合兩個程序的功能,例如一個程序輸出文本,而另一個程序對文本進行排序。為此,操作系統還提供進程間的通訊機制來幫助完成這樣的任務。Linux 中常見的進程間通訊機制有信號、管道、共享內存、信號量和套接字等。
內核通過 SCI 提供了一個應用程序編程接口(API)來創建一個新進程(fork、exec 或 Portable Operating System Interface [POSⅨ] 函數),停止進程(kill、exit),并在它們之間進行通信和同步(signal 或者 POSⅨ 機制)。
1.3文件系統
和 DOS 等操作系統不同,Linux 操作系統中單獨的文件系統并不是由驅動器號或驅動器名稱(如 A: 或 C: 等)來標識的。相反,和 UNIX 操作系統一樣,Linux 操作系統將獨立的文件系統組合成了一個層次化的樹形結構,并且由一個單獨的實體代表這一文件系統。
Linux 將新的文件系統通過一個稱為“掛裝”或“掛上”的操作將其掛裝到某個目錄上,從而讓不同的文件系統結合成為一個整體。Linux 操作系統的一個重要特點是它支持許多不同類型的文件系統。
Linux 中最普遍使用的文件系統是 Ext2,它也是 Linux 土生土長的文件系統。但 Linux 也能夠支持 FAT、VFAT、FAT32、MINIX 等不同類型的文件系統,從而可以方便地和其它操作系統交換數據。
由于 Linux 支持許多不同的文件系統,并且將它們組織成了一個統一的虛擬文件系統.虛擬文件系統(VirtualFileSystem,VFS):隱藏了各種硬件的具體細節,把文件系統操作和不同文件系統的具體實現細節分離了開來,為所有的設備提供了統一的接口,VFS提供了多達數十種不同的文件系統。
虛擬文件系統可以分為邏輯文件系統和設備驅動程序。邏輯文件系統指Linux所支持的文件系統,如ext2,fat等,設備驅動程序指為每一種硬件控制器所編寫的設備驅動程序模塊。虛擬文件系統(VFS)是 Linux 內核中非常有用的一個方面,因為它為文件系統提供了一個通用的接口抽象。
VFS 在 SCI 和內核所支持的文件系統之間提供了一個交換層。即VFS 在用戶和文件系統之間提供了一個交換層。
在 VFS 上面,是對諸如 open、close、read 和 write 之類的函數的一個通用 API 抽象。在 VFS 下面是文件系統抽象,它定義了上層函數的實現方式。它們是給定文件系統(超過 50 個)的插件。
文件系統的源代碼可以在 ./linux/fs 中找到。文件系統層之下是緩沖區緩存,它為文件系統層提供了一個通用函數集(與具體文件系統無關)。這個緩存層通過將數據保留一段時間(或者隨即預先讀取數據以便在需要是就可用)優化了對物理設備的訪問。
緩沖區緩存之下是設備驅動程序,它實現了特定物理設備的接口。因此,用戶和進程不需要知道文件所在的文件系統類型,而只需要象使用 Ext2 文件系統中的文件一樣使用它們。
1.4設備驅動程序
設備驅動程序是 Linux 內核的主要部分。和操作系統的其它部分類似,設備驅動程序運行在高特權級的處理器環境中,從而可以直接對硬件進行操作,但正因為如此,任何一個設備驅動程序的錯誤都可能導致操作系統的崩潰。
設備驅動程序實際控制操作系統和硬件設備之間的交互。設備驅動程序提供一組操作系統可理解的抽象接口完成和操作系統之間的交互,而與硬件相關的具體操作細節由設備驅動程序完成。一般而言,設備驅動程序和設備的控制芯片有關,例如,如果計算機硬盤是 SCSI 硬盤,則需要使用 SCSI 驅動程序,而不是 IDE 驅動程序。
1.5網絡接口(NET)
提供了對各種網絡標準的存取和各種網絡硬件的支持。網絡接口可分為網絡協議和網絡驅動程序。網絡協議部分負責實現每一種可能的網絡傳輸協議。眾所周知,TCP/IP 協議是 Internet 的標準協議,同時也是事實上的工業標準。Linux 的網絡實現支持 BSD 套接字,支持全部的TCP/IP協議。
Linux內核的網絡部分由BSD套接字、網絡協議層和網絡設備驅動程序組成。網絡設備驅動程序負責與硬件設備通訊,每一種可能的硬件設備都有相應的設備驅動程序。
二、Linux shell
2.1什么是 shell
Shell 是系統的用戶界面,提供了用戶和內核進行交互操作的一種接口。同時,Shell 也是一個命令解釋器,它解釋由用戶輸入的命令并且把它們送到內核。不僅如此,Shell 有自己的編程語言用于對命令的編輯,它允許用戶編寫由 shell 命令組成的程序。
通常在圖形界面中對實際體驗帶來差異的不是不同發行版的各種終端模擬器,而是這個 Shell(殼)。有殼就有核,這里的核就是指 UNIX/Linux 內核,Shell 是指“提供給使用者使用界面”的軟件(命令解析器),類似于 DOS 下的 command(命令行)和后來的 cmd.exe 。
UNIX/Linux 操作系統下的 Shell 既是用戶交互的界面,也是控制系統的腳本語言。當然這一點有別于 Windows 下的命令行,雖然該命令行也提供很簡單的控制語句。在 Windows 操作系統下,有些用戶從來都不會直接使用 Shell。然而在 UNIX 系列操作系統下,Shell 仍然是控制系統啟動和其它很多實用工具的腳本解釋程序。
2.2shell 類別
在UNIX/Linux 中比較常見的 Shell:
- Bourne Again Shell (簡稱 bash)
- Bourne Shell(簡稱 sh)
- C-Shell(簡稱 csh)
- Korn Shell(簡稱 ksh)
- Z shell(簡稱 zsh)
Ubuntu 終端默認使用的是 bash,默認的桌面環境是GNOME 或者 Unity(基于 GNOME),我們的環境中使用的分別是 zsh 和 xfce;還可以通過 cat /etc/shells 來查看我們主機上的 shell 類型。
目前主要有下列版本的shell:
- Bourne Shell:是貝爾實驗室開發的。
- BASH:是GNU的Bourne Again Shell,是GNU操作系統上默認的shell,大部分linux的發行套件使用的都是這種shell。
- Korn Shell:是對Bourne SHell的發展,在大部分內容上與Bourne Shell兼容。
- C Shell:是SUN公司Shell的BSD版本。
三、Linux系統文件
3.1文件系統的概念
首先我們來想一個問題,磁盤等存儲設備上存儲的不過是01100111這樣的字符數據,但是為什么我們在電腦上打開某盤訪問時看到的卻是整齊的目錄結構呢,一塊磁盤上的數據是怎樣被操作系統識別為一棵目錄樹然后被我們讀寫訪問的?這其中就離不開文件系統。
文件系統是用于在存儲設備或分區上組織管理文件的方法和數據結構,每一個系統可讀寫文件的設備上都包含了一個完整的系統可識別的文件系統用來組織設備上的文件,比如我們常用的U盤,里面就有一個FAT32、NTFS或者exFAT等其它類型的文件系統,再比如我們打開Linux系統中的設備目錄/dev,其中每一個可讀取的塊設備上都有一個文件系統,正因如此我們才可以把這樣的塊設備掛載到系統的某個目錄然后訪問其中的文件。另外我們也可以注意到,一個可讀取設備即使沒有存儲任何文件,它的存儲空間也已經被使用了一部分,這部分就是文件系統所占用的空間。
我們常常對一個設備進行格式化操作,這里的格式化就是指采用指定文件系統類型對設備空間進行登記索引并建立相應的管理表格的一個過程;在Linux中,正因為有文件系統,我們可以把任何設備一視同仁當作一個文件看待,這也是Linux的設計哲學之一:一切皆文件。
文件類型
Linux下面的文件類型主要有:
- 普通文件:C語言元代碼、SHELL腳本、二進制的可執行文件等。分為純文本和二進制。
- 目錄文件:目錄,存儲文件的唯一地方。
- 鏈接文件:指向同一個文件或目錄的的文件。
- 設備文件:與系統外設相關的,通常在/dev下面。分為塊設備和字符設備。
- 管道(FIFO)文件 : 提供進程建通信的一種方式
- 套接字(socket) 文件:該文件類型與網絡通信有關可以通過ls –l, file, stat幾個命令來查看文件的類型等相關信息。
Linux目錄
文件結構是文件存放在磁盤等存貯設備上的組織方法。主要體現在對文件和目錄的組織上。目錄提供了管理文件的一個方便而有效的途徑。Linux使用標準的目錄結構,在安裝的時候,安裝程序就已經為用戶創建了文件系統和完整而固定的目錄組成形式,并指定了每個目錄的作用和其中的文件類型。
完整的目錄樹可劃分為小的部分,這些小部分又可以單獨存放在自己的磁盤或分區上。這樣,相對穩定的部分和經常變化的部分可單獨存放在不同的分區中,從而方便備份或系統管理。目錄樹的主要部分有 root、/usr、/var、/home 等(圖2) 。這樣的布局可方便在 Linux 計算機之間共享文件系統的某些部分。
Linux采用的是樹型結構。最上層是根目錄,其他的所有目錄都是從根目錄出發而生成的。微軟的DOS和windows也是采用樹型結構,但是在DOS和 windows中這樣的樹型結構的根是磁盤分區的盤符,有幾個分區就有幾個樹型結構,他們之間的關系是并列的。
最頂部的是不同的磁盤(分區),如:C,D,E,F等。但是在linux中,無論操作系統管理幾個磁盤分區,這樣的目錄樹只有一個。從結構上講,各個磁盤分區上的樹型目錄不一定是并列的。
3.2虛擬文件系統(VFS)的概念
在Linux中支持多達數十套文件系統,不同設備上的文件系統可能都不一樣,那么為什么我們在實際操作時可以不用理會設備上的文件系統類型,就能用統一的命令對不同設備上的文件進行讀寫呢?這就涉及到在文件系統上層的抽象層,虛擬文件系統。
學過面向對象就很好理解,虛擬文件系統作為抽象層規定了一個文件系統要實現哪些接口和數據結構,文件系統研發人員就依據這些標準去研發,因此即使不同文件系統內部實現各有不同,但是都實現了可供上層虛擬文件系統進行統一調用的接口,用戶不需要關心各個文件系統內部的實現細節,只要通過統一的系統調用(比如open()、read()、write())就可以讀取不同文件系統上的文件數據,從用戶的角度看就好像只有一個文件系統一樣。
3.3Unix文件系統
VFS提供了一個通用的文件系統模型,該模型囊括了任何文件系統的常用功能集和行為,該模型偏重于Unix風格的文件系統。Unix文件系統主要有四個抽象概念:文件、目錄項、索引節點和安裝點。
(1)安裝點:當我們運行一個Linux操作系統時,我們可以發現整個操作系統看起來就是一棵目錄樹,最頂層的目錄為根目錄"/",所有文件都組織在這個目錄樹中,這個最頂層的文件系統就稱為根文件系統,所有已安裝的文件系統都作為根文件系統樹的枝葉出現在系統中,當我們新增一個設備(文件系統)時,需要將其掛載到目錄樹中的某個目錄中才能進行訪問,這個目錄就稱為安裝點。
(2)文件:文件就是一個有序字節串,字節串中第一個字節就是文件的頭,最后一個字節就是文件的尾。
(3)目錄項:目錄也是文件,也是用索引節點唯一標識,和普通文件不同的是,普通文件在磁盤里面保存的是文件數據,而目錄文件在磁盤里面保存子目錄或文件。
目錄項和目錄是一個東西嗎?
雖然名字很相近,但是它們不是一個東西,目錄是個文件,持久化存儲在磁盤,而目錄項是內核一個數據結構,緩存在內存。
如果查詢目錄頻繁從磁盤讀,效率會很低,所以內核會把已經讀過的目錄用目錄項這個數據結構緩存在內存,下次再次讀到相同的目錄時,只需從內存讀就可以,大大提高了文件系統的效率。
(4)索引節點(inode):Unix文件系統將文件的相關信息和文件本身這兩個概念加以區分,例如訪問控制權限、文件大小、擁有者和創建時間等就屬于文件相關信息,存儲在一個單獨的數據結構當中,該結構稱為索引節點
3.4文件系統特點
- 文件系統要有嚴格的組織形式,使得文件能夠以塊為單位進行存儲。
- 文件系統中也要有索引區,用來方便查找一個文件分成的多個塊都存放在了什么位置。
- 如果文件系統中有的文件是熱點文件,近期經常被讀取和寫入,文件系統應該有緩存層。
- 文件應該用文件夾的形式組織起來,方便管理和查詢。
- Linux內核要在自己的內存里面維護一套數據結構,來保存哪些文件被哪些進程打開和使用。
總體來說,文件系統的主要功能梳理如下:
圖片
3.5EXT系列的文件系統的格式
inode與塊的存儲
硬盤分成相同大小的單元,我們稱為塊(Block)。一塊的大小是扇區大小的整數倍,默認是4K。在格式化的時候,這個值是可以設定的。
一大塊硬盤被分成了一個個小的塊,用來存放文件的數據部分。這樣一來,如果我們像存放一個文件,就不用給他分配一塊連續的空間了。我們可以分散成一個個小塊進行存放。這樣就靈活得多,也比較容易添加、刪除和插入數據。
inode就是文件索引的意思,我們每個文件都會對應一個inode;一個文件夾就是一個文件,也對應一個inode。
inode數據結構如下:
struct ext4_inode {
__le16 i_mode; /* File mode */
__le16 i_uid; /* Low 16 bits of Owner Uid */
__le32 i_size_lo; /* Size in bytes */
__le32 i_atime; /* Access time */
__le32 i_ctime; /* Inode Change time */
__le32 i_mtime; /* Modification time */
__le32 i_dtime; /* Deletion Time */
__le16 i_gid; /* Low 16 bits of Group Id */
__le16 i_links_count; /* Links count */
__le32 i_blocks_lo; /* Blocks count */
__le32 i_flags; /* File flags */
......
__le32 i_block[EXT4_N_BLOCKS];/* Pointers to blocks */
__le32 i_generation; /* File version (for NFS) */
__le32 i_file_acl_lo; /* File ACL */
__le32 i_size_high;
......
};
inode里面有文件的讀寫權限i_mode,屬于哪個用戶i_uid,哪個組i_gid,大小是多少i_size_io,占用多少個塊i_blocks_io,i_atime是access time,是最近一次訪問文件的時間;i_ctime是change time,是最近一次更改inode的時間;i_mtime是modify time,是最近一次更改文件的時間等。
所有的文件都是保存在i_block里面。具體保存規則由EXT4_N_BLOCKS決定,EXT4_N_BLOCKS有如下的定義:
#define EXT4_NDIR_BLOCKS 12
#define EXT4_IND_BLOCK EXT4_NDIR_BLOCKS
#define EXT4_DIND_BLOCK (EXT4_IND_BLOCK + 1)
#define EXT4_TIND_BLOCK (EXT4_DIND_BLOCK + 1)
#define EXT4_N_BLOCKS (EXT4_TIND_BLOCK + 1)
在ext2和ext3中,其中前12項直接保存了塊的位置,也就是說,我們可以通過i_block[0-11],直接得到保存文件內容的塊。
圖片
但是,如果一個文件比較大,12塊放不下。當我們用到i_block[12]的時候,就不能直接放數據塊的位置了,要不然i_block很快就會用完了。
那么可以讓i_block[12]指向一個塊,這個塊里面不放數據塊,而是放數據塊的位置,這個塊我們稱為間接塊。如果文件再大一些,i_block[13]會指向一個塊,我們可以用二次間接塊。二次間接塊里面存放了間接塊的位置,間接塊里面存放了數據塊的位置,數據塊里面存放的是真正的數據。如果文件再大點,那么i_block[14]同理。這里面有一個非常顯著的問題,對于大文件來講,我們要多次讀取硬盤才能找到相應的塊,這樣訪問速度就會比較慢。
為了解決這個問題,ext4做了一定的改變。它引入了一個新的概念,叫作Extents。比方說,一個文件大小為128M,如果使用4k大小的塊進行存儲,需要32k個塊。如果按照ext2或者ext3那樣散著放,數量太大了。但是Extents可以用于存放連續的塊,也就是說,我們可以把128M放在一個Extents里面。這樣的話,對大文件的讀寫性能提高了,文件碎片也減少了。
Exents是一個樹狀結構:
圖片
每個節點都有一個頭,ext4_extent_header可以用來描述某個節點:
struct ext4_extent_header {
__le16 eh_magic; /* probably will support different formats */
__le16 eh_entries; /* number of valid entries */
__le16 eh_max; /* capacity of store in entries */
__le16 eh_depth; /* has tree real underlying blocks? */
__le32 eh_generation; /* generation of the tree */
};
eh_entries表示這個節點里面有多少項。這里的項分兩種,如果是葉子節點,這一項會直接指向硬盤上的連續塊的地址,我們稱為數據節點ext4_extent;如果是分支節點,這一項會指向下一層的分支節點或者葉子節點,我們稱為索引節點ext4_extent_idx。這兩種類型的項的大小都是12個byte。
/*
* This is the extent on-disk structure.
* It's used at the bottom of the tree.
*/
struct ext4_extent {
__le32 ee_block; /* first logical block extent covers */
__le16 ee_len; /* number of blocks covered by extent */
__le16 ee_start_hi; /* high 16 bits of physical block */
__le32 ee_start_lo; /* low 32 bits of physical block */
};
/*
* This is index on-disk structure.
* It's used at all the levels except the bottom.
*/
struct ext4_extent_idx {
__le32 ei_block; /* index covers logical blocks from 'block' */
__le32 ei_leaf_lo; /* pointer to the physical block of the next *
* level. leaf or next index could be there */
__le16 ei_leaf_hi; /* high 16 bits of physical block */
__u16 ei_unused;
};
如果文件不大,inode里面的i_block中,可以放得下一個ext4_extent_header和4項ext4_extent。所以這個時候,eh_depth為0,也即inode里面的就是葉子節點,樹高度為0。
如果文件比較大,4個extent放不下,就要分裂成為一棵樹,eh_depth>0的節點就是索引節點,其中根節點深度最大,在inode中。最底層eh_depth=0的是葉子節點。除了根節點,其他的節點都保存在一個塊4k里面,4k扣除ext4_extent_header的12個byte,剩下的能夠放340項,每個extent最大能表示128MB的數據,340個extent會使你的表示的文件達到42.5GB。
inode位圖和塊位圖
inode的位圖大小為4k,每一位對應一個inode。如果是1,表示這個inode已經被用了;如果是0,則表示沒被用。block的位圖同理。
在Linux操作系統里面,想要創建一個新文件,會調用open函數,并且參數會有O_CREAT。這表示當文件找不到的時候,我們就需要創建一個。那么open函數的調用過程大致是:要打開一個文件,先要根據路徑找到文件夾。如果發現文件夾下面沒有這個文件,同時又設置了O_CREAT,就說明我們要在這個文件夾下面創建一個文件。
創建一個文件,那么就需要創建一個inode,那么就會從文件系統里面讀取inode位圖,然后找到下一個為0的inode,就是空閑的inode。對于block位圖,在寫入文件的時候,也會有這個過程。
3.6文件系統的格式
數據塊的位圖是放在一個塊里面的,共4k。這個時候就需要用到塊組,數據結構為ext4_group_desc,這里面對于一個塊組里的inode位圖bg_inode_bitmap_lo、塊位圖bg_block_bitmap_lo、inode列表bg_inode_table_lo,都有相應的成員變量。、
這樣一個個塊組,就基本構成了我們整個文件系統的結構。因為塊組有多個,塊組描述符也同樣組成一個列表,我們把這些稱為塊組描述符表。
我們還需要有一個數據結構,對整個文件系統的情況進行描述,這個就是超級塊ext4_super_block。里面有整個文件系統一共有多少inode,s_inodes_count;一共有多少塊,s_blocks_count_lo,每個塊組有多少inode,s_inodes_per_group,每個塊組有多少塊,s_blocks_per_group等。這些都是這類的全局信息。
最終,整個文件系統格式就是下面這個樣子,如下圖所示:
圖片
默認情況下,超級塊和塊組描述符表都有副本保存在每一個塊組里面。防止這些數據丟失了,導致整個文件系統都打不開了。由于如果每個塊組里面都保存一份完整的塊組描述符表,一方面很浪費空間;另一個方面,由于一個塊組最大128M,而塊組描述符表里面有多少項,這就限制了有多少個塊組,128M * 塊組的總數目是整個文件系統的大小,就被限制住了。
因此引入Meta Block Groups特性,首先,塊組描述符表不會保存所有塊組的描述符了,而是將塊組分成多個組,我們稱為元塊組(Meta Block Group)。每個元塊組里面的塊組描述符表僅僅包括自己的,一個元塊組包含64個塊組,這樣一個元塊組中的塊組描述符表最多64項。
我們假設一共有256個塊組,原來是一個整的塊組描述符表,里面有256項,要備份就全備份,現在分成4個元塊組,每個元塊組里面的塊組描述符表就只有64項了,這就小多了,而且四個元塊組自己備份自己的。根據圖中,每一個元塊組包含64個塊組,塊組描述符表也是64項,備份三份,在元塊組的第一個,第二個和最后一個塊組的開始處。
如果開啟了sparse_super特性,超級塊和塊組描述符表的副本只會保存在塊組索引為0、3、5、7的整數冪里。所以上圖的超級塊只在索引為0、3、5、7等的整數冪里。
圖片
根據圖中,每一個元塊組包含64個塊組,塊組描述符表也是64項,備份三份,在元塊組的第一個,第二個和最后一個塊組的開始處。
如果開啟了sparse_super特性,超級塊和塊組描述符表的副本只會保存在塊組索引為0、3、5、7的整數冪里。所以上圖的超級塊只在索引為0、3、5、7等的整數冪里。
四、目錄的儲存格式
其實目錄本身也是個文件,也有inode。inode里面也是指向一些塊。和普通文件不同的是,普通文件的塊里面保存的是文件數據,而目錄文件的塊里面保存的是目錄里面一項一項的文件信息。這些信息我們稱為ext4_dir_entry。
在目錄文件的塊中,最簡單的保存格式是列表,每一項都會保存這個目錄的下一級的文件的文件名和對應的inode,通過這個inode,就能找到真正的文件。第一項是“.”,表示當前目錄,第二項是“…”,表示上一級目錄,接下來就是一項一項的文件名和inode。
如果在inode中設置EXT4_INDEX_FL標志,那么就表示根據索引查找文件。索引項會維護一個文件名的哈希值和數據塊的一個映射關系。
如果我們要查找一個目錄下面的文件名,可以通過名稱取哈希。如果哈希能夠匹配上,就說明這個文件的信息在相應的塊里面。然后打開這個塊,如果里面不再是索引,而是索引樹的葉子節點的話,那里面還是ext4_dir_entry的列表,我們只要一項一項找文件名就行。通過索引樹,我們可以將一個目錄下面的N多的文件分散到很多的塊里面,可以很快地進行查找。
4.1ext4 文件系統
ext4 文件系統作為 Linux 中常用的文件系統之一,是 ext 文件系統的后續版本,有著諸多顯著優勢。
在性能方面,它相比之前版本有了很大提升,例如支持更快的文件創建、刪除以及文件系統檢查等操作。其采用了 extents(連續的數據塊分配)特性,在處理大文件時效率更高,減少了元數據的碎片化,也讓大文件訪問速度得以加快。
對于文件支持大小上,ext4 有著強大的擴展性,能夠支持最大達到 1 EB(1,048,576 TB)的文件系統大小,單個文件最大尺寸可達 16 TB,這相比于 ext3 等之前版本有了極大的擴充,滿足了對大容量文件存儲的需求。
穩定性上,ext4 保留了 EXT3 的日志功能,還增加了如延遲分配日志、多塊分配日志等更多的日志類型,以此確保了數據的一致性和完整性,即便遇到如斷電、系統崩潰等異常情況,也能夠借助日志功能快速恢復文件系統。
而且,ext4 還具備向下兼容性,可以兼容 ext2 和 ext3,如果磁盤之前是用 Ext3 格式化的,用戶能夠在不損失數據的情況下升級到 Ext4 文件系統,極大方便了老用戶的升級使用,避免了數據遷移帶來的風險。
在擴展性方面,它突破了 ext3 的 32,000 子目錄限制,支持無限數量的子目錄,為文件和目錄管理提供了更廣闊的空間。同時,它還增加了更多的文件系統級別的加密和壓縮功能,進一步提高數據的安全性和存儲效率,并且支持在線文件系統檢查和在線文件系統碎片整理,可提高文件系統的可用性和性能。
ext4 文件系統憑借這些優勢,在眾多應用場景中被廣泛使用,特別適合對文件系統的性能、可靠性和擴展性要求較高的場景,像數據庫服務器、郵件服務器、文件共享服務器等,在金融、醫療、政府等對數據安全和存儲效率要求嚴格的領域也表現出色,也適用于云計算、虛擬化等需要在線文件系統檢查和碎片整理的環境。
4.2Btrfs 文件系統
Btrfs 文件系統是 Linux 系統中較為先進的文件系統,具備很多強大且實用的特性。
數據壓縮是其一大亮點,它支持透明的文件系統壓縮,壓縮后的文件對用戶來說就如同常規的未壓縮文件一樣,只是它們在硬盤上是以壓縮狀態存儲的。并且它提供了多個壓縮算法的選擇,例如 zstd、lz、zlib 等,用戶可按需選用,這種壓縮特性能夠有效節省存儲空間,尤其在存儲空間緊張時作用明顯。
寫時復制(Copy-on-Write,簡稱 CoW)是 Btrfs 的核心特性之一,采用這種策略,在對文件進行復制、更新及替換操作時,并非傳統的 “就地” 更新,而是通過復制、更新指針的方式來完成,這有助于保證文件系統的一致性,特別是在面對系統出現不可預料的硬件故障等情況時,能夠很好地避免數據出現問題。
Btrfs 還支持快照功能,用戶可以方便地創建文件系統或者單個文件的快照,便于進行備份或者系統恢復等操作,這在開發、測試環境中非常實用,比如可以快速創建某個項目文件的快照,方便在出現問題時回滾到之前的狀態。
此外,它還支持 RAID,能夠聯機添加、移除以及修改設備,方便管理多個物理設備,使得傳統的卷管理軟件變得多余,同時可以實現存儲池的靈活管理,能夠在線添加或移除磁盤,動態調整容量,滿足不同存儲規模的需求。
從參數方面來看,Btrfs 文件系統支持最大文件系統大小為 16 EB(18,446,744,073,709,551,616 字節),最大文件大小同樣為 16 EB,為處理海量數據提供了可能。
4.3XFS 文件系統
XFS 作為高性能日志文件系統,有著諸多獨特的特性使其在眾多文件系統中脫穎而出。
它具有強大的處理大容量文件系統和大文件的能力,最初由 SGI 公司開發,如今已成為 Linux 內核的一部分,最大支持 16 EB(艾字節)的文件系統和最大 8 EB 的單個文件,特別適合大規模數據存儲的場景,像大規模數據庫、文件服務器等應用場景中,XFS 都能很好地應對大量數據的管理需求。
其日志功能十分出色,通過記錄文件系統的操作,在系統異常重啟后,可以依據日志快速恢復,提高了文件系統的可靠性和穩定性,確保數據完整性不受太大影響。
動態增加文件系統大小這一特性也很實用,XFS 支持在線動態增加文件系統大小,無需卸載文件系統,這意味著在存儲需求不斷增長的過程中,能夠方便地對文件系統進行擴容,而不會影響到正在進行的業務操作,極大地提高了系統的靈活性和可用性。
另外,XFS 使用延遲分配機制,在寫入文件時并不會馬上為其分配磁盤空間,而是先寫入頁緩存并做標記,待特定條件滿足后再分配磁盤空間并將數據寫入磁盤,這種機制能夠將隨機 IO 盡量轉換為順序 IO,從而提高文件寫入性能。
五、Linux中的文件緩存
5.1ext4文件系統層
對于ext4文件系統來講,內核定義了一個ext4_file_operations
const struct file_operations ext4_file_operations = {
......
.read_iter = ext4_file_read_iter,
.write_iter = ext4_file_write_iter,
......
}
ext4_file_read_iter會調用generic_file_read_iter,ext4_file_write_iter會調用__generic_file_write_iter
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
......
if (iocb->ki_flags & IOCB_DIRECT) {
......
struct address_space *mapping = file->f_mapping;
......
retval = mapping->a_ops->direct_IO(iocb, iter);
}
......
retval = generic_file_buffered_read(iocb, iter, retval);
}
ssize_t __generic_file_write_iter(struct kiocb *iocb, struct iov_iter *from)
{
......
if (iocb->ki_flags & IOCB_DIRECT) {
......
written = generic_file_direct_write(iocb, from);
......
} else {
......
written = generic_perform_write(file, from, iocb->ki_pos);
......
}
}
generic_file_read_iter和__generic_file_write_iter有相似的邏輯,就是要區分是否用緩存。因此,根據是否使用內存做緩存,我們可以把文件的I/O操作分為兩種類型。
第一種類型是緩存I/O。大多數文件系統的默認I/O操作都是緩存I/O。對于讀操作來講,操作系統會先檢查,內核的緩沖區有沒有需要的數據。如果已經緩存了,那就直接從緩存中返回;否則從磁盤中讀取,然后緩存在操作系統的緩存中。對于寫操作來講,操作系統會先將數據從用戶空間復制到內核空間的緩存中。這時對用戶程序來說,寫操作就已經完成。至于什么時候再寫到磁盤中由操作系統決定,除非顯式地調用了sync同步命令。
第二種類型是直接IO,就是應用程序直接訪問磁盤數據,而不經過內核緩沖區,從而減少了在內核緩存和用戶程序之間數據復制。
如果在寫的邏輯__generic_file_write_iter里面,發現設置了IOCB_DIRECT,則調用generic_file_direct_write,里面同樣會調用address_space的direct_IO的函數,將數據直接寫入硬盤。
帶緩存的寫入操作
我們先來看帶緩存寫入的函數generic_perform_write。
ssize_t generic_perform_write(struct file *file,
struct iov_iter *i, loff_t pos)
{
struct address_space *mapping = file->f_mapping;
const struct address_space_operations *a_ops = mapping->a_ops;
do {
struct page *page;
unsigned long offset; /* Offset into pagecache page */
unsigned long bytes; /* Bytes to write to page */
status = a_ops->write_begin(file, mapping, pos, bytes, flags,
&page, &fsdata);
copied = iov_iter_copy_from_user_atomic(page, i, offset, bytes);
flush_dcache_page(page);
status = a_ops->write_end(file, mapping, pos, bytes, copied,
page, fsdata);
pos += copied;
written += copied;
balance_dirty_pages_ratelimited(mapping);
} while (iov_iter_count(i));
}
循環中主要做了這幾件事:
- 對于每一頁,先調用address_space的write_begin做一些準備;
- 調用iov_iter_copy_from_user_atomic,將寫入的內容從用戶態拷貝到內核態的頁中;
- 調用address_space的write_end完成寫操作;
- 調用balance_dirty_pages_ratelimited,看臟頁是否太多,需要寫回硬盤。所謂臟頁,就是寫入到緩存,但是還沒有寫入到硬盤的頁面。
對于第一步,調用的是ext4_write_begin來說,主要做兩件事:
第一做日志相關的工作
ext4是一種日志文件系統,是為了防止突然斷電的時候的數據丟失,引入了日志(Journal)模式。日志文件系統比非日志文件系統多了一個Journal區域。文件在ext4中分兩部分存儲,一部分是文件的元數據,另一部分是數據。元數據和數據的操作日志Journal也是分開管理的。你可以在掛載ext4的時候,選擇Journal模式。這種模式在將數據寫入文件系統前,必須等待元數據和數據的日志已經落盤才能發揮作用。這樣性能比較差,但是最安全。
另一種模式是order模式。這個模式不記錄數據的日志,只記錄元數據的日志,但是在寫元數據的日志前,必須先確保數據已經落盤。這個折中,是默認模式。
還有一種模式是writeback,不記錄數據的日志,僅記錄元數據的日志,并且不保證數據比元數據先落盤。這個性能最好,但是最不安全。
第二調用grab_cache_page_write_begin來,得到應該寫入的緩存頁。
struct page *grab_cache_page_write_begin(struct address_space *mapping,
pgoff_t index, unsigned flags)
{
struct page *page;
int fgp_flags = FGP_LOCK|FGP_WRITE|FGP_CREAT;
page = pagecache_get_page(mapping, index, fgp_flags,
mapping_gfp_mask(mapping));
if (page)
wait_for_stable_page(page);
return page;
}
在內核中,緩存以頁為單位放在內存里面,每一個打開的文件都有一個struct file結構,每個struct file結構都有一個struct address_space用于關聯文件和內存,就是在這個結構里面,有一棵樹,用于保存所有與這個文件相關的的緩存頁。
對于第二步,調用iov_iter_copy_from_user_atomic。先將分配好的頁面調用kmap_atomic映射到內核里面的一個虛擬地址,然后將用戶態的數據拷貝到內核態的頁面的虛擬地址中,調用kunmap_atomic把內核里面的映射刪除。
size_t iov_iter_copy_from_user_atomic(struct page *page,
struct iov_iter *i, unsigned long offset, size_t bytes)
{
char *kaddr = kmap_atomic(page), *p = kaddr + offset;
iterate_all_kinds(i, bytes, v,
copyin((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len),
memcpy_from_page((p += v.bv_len) - v.bv_len, v.bv_page,
v.bv_offset, v.bv_len),
memcpy((p += v.iov_len) - v.iov_len, v.iov_base, v.iov_len)
)
kunmap_atomic(kaddr);
return bytes;
}
第三步中,調用ext4_write_end完成寫入。這里面會調用ext4_journal_stop完成日志的寫入,會調用block_write_end->__block_commit_write->mark_buffer_dirty,將修改過的緩存標記為臟頁??梢钥闯觯鋵嵥^的完成寫入,并沒有真正寫入硬盤,僅僅是寫入緩存后,標記為臟頁。
第四步,調用balance_dirty_pages_ratelimited,是回寫臟頁
/**
* balance_dirty_pages_ratelimited - balance dirty memory state
* @mapping: address_space which was dirtied
*
* Processes which are dirtying memory should call in here once for each page
* which was newly dirtied. The function will periodically check the system's
* dirty state and will initiate writeback if needed.
*/
void balance_dirty_pages_ratelimited(struct address_space *mapping)
{
struct inode *inode = mapping->host;
struct backing_dev_info *bdi = inode_to_bdi(inode);
struct bdi_writeback *wb = NULL;
int ratelimit;
......
if (unlikely(current->nr_dirtied >= ratelimit))
balance_dirty_pages(mapping, wb, current->nr_dirtied);
......
}
在balance_dirty_pages_ratelimited里面,發現臟頁的數目超過了規定的數目,就調用balance_dirty_pages->wb_start_background_writeback,啟動一個背后線程開始回寫。
另外還有幾種場景也會觸發回寫:
- 用戶主動調用sync,將緩存刷到硬盤上去,最終會調用wakeup_flusher_threads,同步臟頁;
- 當內存十分緊張,以至于無法分配頁面的時候,會調用free_more_memory,最終會調用wakeup_flusher_threads,釋放臟頁;
- 臟頁已經更新了較長時間,時間上超過了設定時間,需要及時回寫,保持內存和磁盤上數據一致性。
5.2帶緩存的讀操作
看帶緩存的讀,對應的是函數generic_file_buffered_read。
static ssize_t generic_file_buffered_read(struct kiocb *iocb,
struct iov_iter *iter, ssize_t written)
{
struct file *filp = iocb->ki_filp;
struct address_space *mapping = filp->f_mapping;
struct inode *inode = mapping->host;
for (;;) {
struct page *page;
pgoff_t end_index;
loff_t isize;
page = find_get_page(mapping, index);
if (!page) {
if (iocb->ki_flags & IOCB_NOWAIT)
goto would_block;
page_cache_sync_readahead(mapping,
ra, filp,
index, last_index - index);
page = find_get_page(mapping, index);
if (unlikely(page == NULL))
goto no_cached_page;
}
if (PageReadahead(page)) {
page_cache_async_readahead(mapping,
ra, filp, page,
index, last_index - index);
}
/*
* Ok, we have the page, and it's up-to-date, so
* now we can copy it to user space...
*/
ret = copy_page_to_iter(page, offset, nr, iter);
}
}
在generic_file_buffered_read函數中,我們需要先找到page cache里面是否有緩存頁。如果沒有找到,不但讀取這一頁,還要進行預讀,這需要在page_cache_sync_readahead函數中實現。預讀完了以后,再試一把查找緩存頁。
如果第一次找緩存頁就找到了,我們還是要判斷,是不是應該繼續預讀;如果需要,就調用page_cache_async_readahead發起一個異步預讀。
最后,copy_page_to_iter會將內容從內核緩存頁拷貝到用戶內存空間。
六、Linux應用
標準的Linux系統一般都有一套都有稱為應用程序的程序集,它包括文本編輯器、編程語言、X Window、辦公套件、Internet工具和數據庫等。
七、Linux內核參數優化
內核參數是用戶和系統內核之間交互的一個接口,通過這個接口,用戶可以在系統運行的同時動態更新內核配置,而這些內核參數是通過Linux Proc文件系統存在的。因此,可以通過調整Proc文件系統達到優化Linux性能的目的。