1. 程式人生 > 實用技巧 >linux-file-system

linux-file-system

Linux 檔案系統

在 Linux 中,最直觀、最可見的部分就是 檔案系統(file system)。下面我們就來一起探討一下關於 Linux 中國的檔案系統,系統呼叫以及檔案系統實現背後的原理和思想。這些思想中有一些來源於 MULTICS,現在已經被 Windows 等其他作業系統使用。Linux 的設計理念就是 小的就是好的(Small is Beautiful) 。雖然 Linux 只是使用了最簡單的機制和少量的系統呼叫,但是 Linux 卻提供了強大而優雅的檔案系統。

Linux 檔案系統基本概念

Linux 在最初的設計是 MINIX1 檔案系統,它只支援 14 位元組的檔名,它的最大檔案只支援到 64 MB。在 MINIX 1 之後的檔案系統是 ext 檔案系統。ext 系統相較於 MINIX 1 來說,在支援位元組大小和檔案大小上均有很大提升,但是 ext 的速度仍沒有 MINIX 1 快,於是,ext 2 被開發出來,它能夠支援長檔名和大檔案,而且具有比 MINIX 1 更好的效能。這使他成為 Linux 的主要檔案系統。只不過 Linux 會使用 VFS

曾支援多種檔案系統。在 Linux 連結時,使用者可以動態的將不同的檔案系統掛載倒 VFS 上。

Linux 中的檔案是一個任意長度的位元組序列,Linux 中的檔案可以包含任意資訊,比如 ASCII 碼、二進位制檔案和其他型別的檔案是不加區分的。

為了方便起見,檔案可以被組織在一個目錄中,目錄儲存成檔案的形式在很大程度上可以作為檔案處理。目錄可以有子目錄,這樣形成有層次的檔案系統,Linux 系統下面的根目錄是 / ,它通常包含了多個子目錄。字元 / 還用於對目錄名進行區分,例如 /usr/cxuan 表示的就是根目錄下面的 usr 目錄,其中有一個叫做 cxuan 的子目錄。

下面我們介紹一下 Linux 系統根目錄下面的目錄名

  • /bin,它是重要的二進位制應用程式,包含二進位制檔案,系統的所有使用者使用的命令都在這裡
  • /boot,啟動包含引導載入程式的相關檔案
  • /dev,包含裝置檔案,終端檔案,USB 或者連線到系統的任何裝置
  • /etc,配置檔案,啟動指令碼等,包含所有程式所需要的配置檔案,也包含了啟動/停止單個應用程式的啟動和關閉 shell 指令碼
  • /home,本地主要路徑,所有使用者用 home 目錄儲存個人資訊
  • /lib,系統庫檔案,包含支援位於 /bin 和 /sbin 下的二進位制庫檔案
  • /lost+found,在根目錄下提供一個遺失+查詢系統,必須在 root 使用者下才能檢視當前目錄下的內容
  • /media,掛載可移動介質
  • /mnt,掛載檔案系統
  • /opt,提供一個可選的應用程式安裝目錄
  • /proc,特殊的動態目錄,用於維護系統資訊和狀態,包括當前執行中程序資訊
  • /root,root 使用者的主要目錄資料夾
  • /sbin,重要的二進位制系統檔案
  • /tmp, 系統和使用者建立的臨時檔案,系統重啟時,這個目錄下的檔案都會被刪除
  • /usr,包含絕大多數使用者都能訪問的應用程式和檔案
  • /var,經常變化的檔案,諸如日誌檔案或資料庫等

在 Linux 中,有兩種路徑,一種是 絕對路徑(absolute path) ,絕對路徑告訴你從根目錄下查詢檔案,絕對路徑的缺點是太長而且不太方便。還有一種是 相對路徑(relative path) ,相對路徑所在的目錄也叫做工作目錄(working directory)

如果 /usr/local/books 是工作目錄,那麼 shell 命令

cp books books-replica 

就表示的是相對路徑,而

cp /usr/local/books/books /usr/local/books/books-replica

則表示的是絕對路徑。

在 Linux 中經常出現一個使用者使用另一個使用者的檔案或者使用檔案樹結構中的檔案。兩個使用者共享同一個檔案,這個檔案位於某個使用者的目錄結構中,另一個使用者需要使用這個檔案時,必須通過絕對路徑才能引用到他。如果絕對路徑很長,那麼每次輸入起來會變的非常麻煩,所以 Linux 提供了一種 連結(link) 機制。

舉個例子,下面是一個使用連結之前的圖

以上所示,比如有兩個工作賬戶 jianshe 和 cxuan,jianshe 想要使用 cxuan 賬戶下的 A 目錄,那麼它可能會輸入 /usr/cxuan/A ,這是一種未使用連結之後的圖。

使用連結後的示意如下

現在,jianshe 可以建立一個連結來使用 cxuan 下面的目錄了。‘

當一個目錄被創建出來後,有兩個目錄項也同時被創建出來,它們就是 ... ,前者代表工作目錄自身,後者代表該目錄的父目錄,也就是該目錄所在的目錄。這樣一來,在 /usr/jianshe 中訪問 cxuan 中的目錄就是 ../cxuan/xxx

Linux 檔案系統不區分磁碟的,這是什麼意思呢?一般來說,一個磁碟中的檔案系統相互之間保持獨立,如果一個檔案系統目錄想要訪問另一個磁碟中的檔案系統,在 Windows 中你可以像下面這樣。

兩個檔案系統分別在不同的磁碟中,彼此保持獨立。

而在 Linux 中,是支援掛載的,它允許一個磁碟掛在到另外一個磁碟上,那麼上面的關係會變成下面這樣

掛在之後,兩個檔案系統就不再需要關心檔案系統在哪個磁碟上了,兩個檔案系統彼此可見。

Linux 檔案系統的另外一個特性是支援 加鎖(locking)。在一些應用中會出現兩個或者更多的程序同時使用同一個檔案的情況,這樣很可能會導致競爭條件(race condition)。一種解決方法是對其進行加不同粒度的鎖,就是為了防止某一個程序只修改某一行記錄從而導致整個檔案都不能使用的情況。

POSIX 提供了一種靈活的、不同粒度級別的鎖機制,允許一個程序使用一個不可分割的操作對一個位元組或者整個檔案進行加鎖。加鎖機制要求嘗試加鎖的程序指定其 要加鎖的檔案,開始位置以及要加鎖的位元組

Linux 系統提供了兩種鎖:共享鎖和互斥鎖。如果檔案的一部分已經加上了共享鎖,那麼再加排他鎖是不會成功的;如果檔案系統的一部分已經被加了互斥鎖,那麼在互斥鎖解除之前的任何加鎖都不會成功。為了成功加鎖、請求加鎖的部分的所有位元組都必須是可用的。

在加鎖階段,程序需要設計好加鎖失敗後的情況,也就是判斷加鎖失敗後是否選擇阻塞,如果選擇阻塞式,那麼當已經加鎖的程序中的鎖被刪除時,這個程序會解除阻塞並替換鎖。如果程序選擇非阻塞式的,那麼就不會替換這個鎖,會立刻從系統呼叫中返回,標記狀態碼錶示是否加鎖成功,然後程序會選擇下一個時間再次嘗試。

加鎖區域是可以重疊的。下面我們演示了三種不同條件的加鎖區域。

如上圖所示,A 的共享鎖在第四位元組到第八位元組進行加鎖

如上圖所示,程序在 A 和 B 上同時加了共享鎖,其中 6 - 8 位元組是重疊鎖

如上圖所示,程序 A 和 B 和 C 同時加了共享鎖,那麼第六位元組和第七位元組是共享鎖。

如果此時一個程序嘗試在第 6 個位元組處加鎖,此時會設定失敗並阻塞,由於該區域被 A B C 同時加鎖,那麼只有等到 A B C 都釋放鎖後,程序才能加鎖成功。

Linux 檔案系統呼叫

許多系統呼叫都會和檔案與檔案系統有關。我們首先先看一下對單個檔案的系統呼叫,然後再來看一下對整個目錄和檔案的系統呼叫。

為了建立一個新的檔案,會使用到 creat 方法,注意沒有 e

這裡說一個小插曲,曾經有人問 UNIX 創始人 Ken Thompson,如果有機會重新寫 UNIX ,你會怎麼辦,他回答自己要把 creat 改成 create ,哈哈哈哈。

這個系統呼叫的兩個引數是檔名和保護模式

fd = creat("aaa",mode);

這段命令會建立一個名為 aaa 的檔案,並根據 mode 設定檔案的保護位。這些位決定了哪個使用者可能訪問檔案、如何訪問。

creat 系統呼叫不僅僅建立了一個名為 aaa 的檔案,還會開啟這個檔案。為了允許後續的系統呼叫訪問這個檔案,這個 creat 系統呼叫會返回一個 非負整數, 這個就叫做 檔案描述符(file descriptor),也就是上面的 fd。

如果在已經存在的檔案上呼叫了 creat 系統呼叫,那麼該檔案中的內容會被清除,從 0 開始。通過設定合適的引數,open 系統呼叫也能夠建立檔案。

下面讓我們看一看主要的系統呼叫,如下表所示

系統呼叫 描述
fd = creat(name,mode) 一種建立一個新檔案的方式
fd = open(file, ...) 開啟檔案讀、寫或者讀寫
s = close(fd) 關閉一個開啟的檔案
n = read(fd, buffer, nbytes) 從檔案中向快取中讀入資料
n = write(fd, buffer, nbytes) 從快取中向檔案中寫入資料
position = lseek(fd, offset, whence) 移動檔案指標
s = stat(name, &buf) 獲取檔案資訊
s = fstat(fd, &buf) 獲取檔案資訊
s = pipe(&fd[0]) 建立一個管道
s = fcntl(fd,...) 檔案加鎖等其他操作

為了對一個檔案進行讀寫的前提是先需要開啟檔案,必須使用 creat 或者 open 開啟,引數是開啟檔案的方式,是隻讀、可讀寫還是隻寫。open 系統呼叫也會返回檔案描述符。開啟檔案後,需要使用 close 系統呼叫進行關閉。close 和 open 返回的 fd 總是未被使用的最小數量。

什麼是檔案描述符?檔案描述符就是一個數字,這個數字標示了計算機作業系統中開啟的檔案。它描述了資料資源,以及訪問資源的方式。

當程式要求開啟一個檔案時,核心會進行如下操作

  • 授予訪問許可權
  • 全域性檔案表(global file table)中建立一個條目(entry)
  • 向軟體提供條目的位置

檔案描述符由唯一的非負整陣列成,系統上每個開啟的檔案至少存在一個檔案描述符。檔案描述符最初在 Unix 中使用,並且被包括 Linux,macOS 和 BSD 在內的現代作業系統所使用。

當一個程序成功訪問一個開啟的檔案時,核心會返回一個檔案描述符,這個檔案描述符指向全域性檔案表的 entry 項。這個檔案表項包含檔案的 inode 資訊,位元組位移,訪問限制等。例如下圖所示

預設情況下,前三個檔案描述符為 STDIN(標準輸入)STDOUT(標準輸出)STDERR(標準錯誤)

標準輸入的檔案描述符是 0 ,在終端中,預設為使用者的鍵盤輸入

標準輸出的檔案描述符是 1 ,在終端中,預設為使用者的螢幕

與錯誤有關的預設資料流是 2,在終端中,預設為使用者的螢幕。

在簡單聊了一下檔案描述符後,我們繼續回到檔案系統呼叫的探討。

在檔案系統呼叫中,開銷最大的就是 read 和 write 了。read 和 write 都有三個引數

  • 檔案描述符:告訴需要對哪一個開啟檔案進行讀取和寫入
  • 緩衝區地址:告訴資料需要從哪裡讀取和寫入哪裡
  • 統計:告訴需要傳輸多少位元組

這就是所有的引數了,這個設計非常簡單輕巧。

雖然幾乎所有程式都按順序讀取和寫入檔案,但是某些程式需要能夠隨機訪問檔案的任何部分。與每個檔案相關聯的是一個指標,該指標指示檔案中的當前位置。順序讀取(或寫入)時,它通常指向要讀取(寫入)的下一個位元組。如果指標在讀取 1024 個位元組之前位於 4096 的位置,則它將在成功讀取系統呼叫後自動移至 5120 的位置。

Lseek 系統呼叫會更改指標位置的值,以便後續對 read 或 write 的呼叫可以在檔案中的任何位置開始,甚至可以超出檔案末尾。

lseek = Lseek ,段首大寫。

lseek 避免叫做 seek 的原因就是 seek 已經在之前 16 位的計算機上用於搜素功能了。

Lseek 有三個引數:第一個是檔案的檔案描述符,第二個是檔案的位置;第三個告訴檔案位置是相對於檔案的開頭,當前位置還是檔案的結尾

lseek(int fildes, off_t offset, int whence);

lseek 的返回值是更改檔案指標後文件中的絕對位置。lseek 是唯一從來不會造成真正磁碟查詢的系統呼叫,它只是更新當前的檔案位置,這個檔案位置就是記憶體中的數字。

對於每個檔案,Linux 都會跟蹤檔案模式(常規,目錄,特殊檔案),大小,最後修改時間以及其他資訊。程式能夠通過 stat 系統呼叫看到這些資訊。第一個引數就是檔名,第二個是指向要放置請求資訊結構的指標。這些結構的屬性如下圖所示。

儲存檔案的裝置
儲存檔案的裝置
i-node 編號
檔案模式(包括保護位資訊)
檔案連結的數量
檔案所有者標識
檔案所屬的組
檔案大小(位元組)
建立時間
最後一個修改/訪問時間

fstat 呼叫和 stat 相同,只有一點區別,fstat 可以對開啟檔案進行操作,而 stat 只能對路徑進行操作。

pipe 檔案系統呼叫被用來建立 shell 管道。它會建立一系列的偽檔案,來緩衝和管道元件之間的資料,並且返回讀取或者寫入緩衝區的檔案描述符。在管道中,像是如下操作

sort <in | head –40

sort 程序將會輸出到檔案描述符1,也就是標準輸出,寫入管道中,而 head 程序將從管道中讀入。在這種方式中,sort 只是從檔案描述符 0 中讀取並寫入到檔案描述符 1 (管道)中,甚至不知道它們已經被重定向了。如果沒有重定向的話,sort 會自動的從鍵盤讀入並輸出到螢幕中。

最後一個系統呼叫是 fcntl,它用來鎖定和解鎖檔案,應用共享鎖和互斥鎖,或者是執行一些檔案相關的其他操作。

現在我們來關心一下和整體目錄和檔案系統相關的系統呼叫,而不是把精力放在單個的檔案上,下面列出了這些系統呼叫,我們一起來看一下。

系統呼叫 描述
s = mkdir(path,mode) 建立一個新的目錄
s = rmdir(path) 移除一個目錄
s = link(oldpath,newpath) 建立指向已有檔案的連結
s = unlink(path) 取消檔案的連結
s = chdir(path) 改變工作目錄
dir = opendir(path) 開啟一個目錄讀取
s = closedir(dir) 關閉一個目錄
dirent = readdir(dir) 讀取一個目錄項
rewinddir(dir) 迴轉目錄使其在此使用

可以使用 mkdir 和 rmdir 建立和刪除目錄。但是需要注意,只有目錄為空時才可以刪除。

建立一個指向已有檔案的連結時會建立一個目錄項(directory entry)。系統呼叫 link 來建立連結,oldpath 代表已有的路徑,newpath 代表需要連結的路徑,使用 unlink 可以刪除目錄項。當檔案的最後一個連結被刪除時,這個檔案會被自動刪除。

使用 chdir 系統呼叫可以改變工作目錄。

最後四個系統呼叫是用於讀取目錄的。和普通檔案類似,他們可以被開啟、關閉和讀取。每次呼叫 readdir 都會以固定的格式返回一個目錄項。使用者不能對目錄執行寫操作,但是可以使用 creat 或者 link 在資料夾中建立一個目錄,或使用 unlink 刪除一個目錄。使用者不能在目錄中查詢某個特定檔案,但是可以使用 rewindir 作用於一個開啟的目錄,使他能在此從頭開始讀取。

Linux 檔案系統的實現

下面我們主要討論一下 虛擬檔案系統(Virtual File System)。 VFS 對高層程序和應用程式隱藏了 Linux 支援的所有檔案系統的區別,以及檔案系統是儲存在本地裝置,還是需要通過網路訪問遠端裝置。裝置和其他特殊檔案和 VFS 層相關聯。接下來,我們就會探討一下第一個 Linux 廣泛傳播的檔案系統: ext2。隨後,我們就會探討 ext4 檔案系統所做的改進。各種各樣的其他檔案系統也正在使用中。 所有 Linux 系統都可以處理多個磁碟分割槽,每個磁碟分割槽上都有不同的檔案系統。

Linux 虛擬檔案系統

為了能夠使應用程式能夠在不同型別的本地或者遠端裝置上的檔案系統進行互動,因為在 Linux 當中檔案系統千奇百種,比較常見的有 EXT3、EXT4,還有基於記憶體的 ramfs、tmpfs 和基於網路的 nfs,和基於使用者態的 fuse,當然 fuse 應該不能完全的檔案系統,只能算是一個能把檔案系統實現放到使用者態的模組,滿足了核心檔案系統的介面,他們都是檔案系統的一種實現。對於這些檔案系統,Linux 做了一層抽象就是 VFS 虛擬檔案系統,

下表總結了 VFS 支援的四個主要的檔案系統結構。

物件 描述
超級塊 特定的檔案系統
Dentry 目錄項,路徑的一個組成部分
I-node 特定的檔案
File 跟一個程序相關聯的開啟檔案

超級塊(superblock) 包含了有關檔案系統佈局的重要資訊,超級塊如果遭到破壞那麼就會導致整個檔案系統不可讀。

i-node 索引節點,包含了每一個檔案的描述符。

在 Linux 中,目錄和裝置也表示為檔案,因為它們具有對應的 i-node

超級塊和索引塊所在的檔案系統都在磁碟上有對應的結構。

為了便於某些目錄操作和路徑遍歷,比如 /usr/local/cxuan,VFS 支援一個 dentry 資料結構,該資料結構代表著目錄項。這個 dentry 資料結構有很多東西(http://books.gigatux.nl/mirror/kerneldevelopment/0672327201/ch12lev1sec7.html)這個資料結構由檔案系統動態建立。

目錄項被快取在 dentry_cache 快取中。例如,快取條目會快取 /usr 、 /usr/local 等條目。如果多個程序通過硬連線訪問相同的檔案,他們的檔案物件將指向此快取中的相同條目。

最後,檔案資料結構是代表著開啟的檔案,也代表著記憶體表示,它根據 open 系統呼叫建立。它支援 read、write、sendfile、lock 和其他在我們之前描述的系統呼叫中。

在 VFS 下實現的實際檔案系統不需要在內部使用完全相同的抽象和操作。 但是,它們必須在語義上實現與 VFS 物件指定的檔案系統操作相同的檔案系統操作。 四個 VFS 物件中每個物件的操作資料結構的元素都是指向基礎檔案系統中功能的指標。

Linux Ext2 檔案系統

現在我們一起看一下 Linux 中最流行的一個磁碟檔案系統,那就是 ext2 。Linux 的第一個版本用於 MINIX1 檔案系統,它的檔名大小被限制為最大 64 MB。MINIX 1 檔案系統被永遠的被它的擴充套件系統 ext 取代,因為 ext 允許更長的檔名和檔案大小。由於 ext 的效能低下,ext 被其替代者 ext2 取代,ext2 目前仍在廣泛使用。

一個 ext2 Linux 磁碟分割槽包含了一個檔案系統,這個檔案系統的佈局如下所示

Boot 塊也就是第 0 塊不是讓 Linux 使用的,而是用來載入和引導計算機啟動程式碼的。在塊 0 之後,磁碟分割槽被分成多個組,這些組與磁碟柱面邊界所處的位置無關。

第一個塊是 超級塊(superblock)。它包含有關檔案系統佈局的資訊,包括 i-node、磁碟塊數量和以及空閒磁碟塊列表的開始。下一個是 組描述符(group descriptor),其中包含有關點陣圖的位置,組中空閒塊和 i-node 的數量以及組中的目錄數量的資訊。這些資訊很重要,因為 ext2 會在磁碟上均勻分佈目錄。

圖中的兩個點陣圖用來記錄空閒塊和空閒 i-node,這是從 MINIX 1檔案系統繼承的選擇,大多數 UNIX 檔案系統使用點陣圖而不是空閒列表。每個點陣圖的大小是一個塊。如果一個塊的大小是 1 KB,那麼就限制了塊組的數量是 8192 個塊和 8192 個 i-node。塊的大小是一個嚴格的限制,塊組的數量不固定,在 4KB 的塊中,塊組的數量增大四倍。

在超級塊之後分佈的是 i-node 它們自己,i-node 取值範圍是 1 - 某些最大值。每個 i-node 是 128 位元組的 long ,這些位元組恰好能夠描述一個檔案。i-node 包含了統計資訊(包含了 stat 系統呼叫能獲得的所有者資訊,實際上 stat 就是從 i-node 中讀取資訊的),以及足夠的資訊來查詢儲存檔案資料的所有磁碟塊。

在 i-node 之後的是 資料塊(data blocks)。所有的檔案和目錄都儲存在這。如果一個檔案或者目錄包含多個塊,那麼這些塊在磁碟中的分佈不一定是連續的,也有可能不連續。事實上,大檔案塊可能會被拆分成很多小塊散佈在整個磁碟上。

對應於目錄的 i-node 分散在整個磁碟組上。如果有足夠的空間,ext2 會把普通檔案組織到與父目錄相同的塊組中,而把同一塊上的資料檔案組織成初始 i-node 節點。點陣圖用來快速確定新檔案系統資料的分配位置。在分配新的檔案塊時,ext2 也會給該檔案預分配許多額外的資料塊,這樣可以減少將來向檔案寫入資料時產生的檔案碎片。這種策略在整個磁碟上實現了檔案系統的 負載,後續還有對檔案碎片的排列和整理,而且效能也比較好。

為了達到訪問的目的,需要首先使用 Linux 系統呼叫,例如 open,這個系統呼叫會確定開啟檔案的路徑。路徑分為兩種,相對路徑絕對路徑。如果使用相對路徑,那麼就會從當前目錄開始查詢,否則就會從根目錄進行查詢。

目錄檔案的檔名最高不能超過 255 個字元,它的分配如下圖所示

每一個目錄都由整數個磁碟塊組成,這樣目錄就可以整體的寫入磁碟。在一個目錄中,檔案和子目錄的目錄項都是未經排序的,並且一個挨著一個。目錄項不能跨越磁碟塊,所以通常在每個磁碟塊的尾部會有部分未使用的位元組。

上圖中每個目錄項都由四個固定長度的屬性和一個長度可變的屬性組成。第一個屬性是 i-node 節點數量,檔案 first 的 i-node 編號是 19 ,檔案 second 的編號是 42,目錄 third 的 i-node 編號是 88。緊隨其後的是 rec_len 域,表明目錄項大小是多少位元組,名稱後面會有一些擴充套件,當名字以未知長度填充時,這個域被用來尋找下一個目錄項,直至最後的未使用。這也是圖中箭頭的含義。緊隨其後的是 型別域:F 表示的是檔案,D 表示的是目錄,最後是固定長度的檔名,上面的檔名的長度依次是 5、6、5,最後以檔名結束。

rec_len 域是如何擴充套件的呢?如下圖所示

我們可以看到,中間的 second 被移除了,所以將其所在的域變為第一個目錄項的填充。當然,這個填充可以作為後續的目錄項。

由於目錄是按照線性的順序進行查詢的,因此可能需要很長時間才能在大檔案末尾找到目錄項。因此,系統會為近期的訪問目錄維護一個快取。這個快取用檔名來查詢,如果快取命中,那麼就會避免執行緒搜尋這樣昂貴的開銷。組成路徑的每個部分都在目錄快取中儲存一個 dentry 物件,並且通過 i-node 找到後續的路徑元素的目錄項,直到找到真正的檔案 i - node。

比如說要使用絕對路徑來尋找一個檔案,我們暫定這個路徑是 /usr/local/file,那麼需要經過如下幾個步驟:

  • 首先,系統會確定根目錄,它通常使用 2 號 i -node ,也就是索引 2 節點,因為索引節點 1 是 ext2 /3/4 檔案系統上的壞塊索引節點。系統會將一項放在 dentry 快取中,以應對將來對根目錄的查詢。
  • 然後,在根目錄中查詢字串 usr,得到 /usr 目錄的 i - node 節點號。/usr 的 i - node 同樣也進入 dentry 快取。然後節點被取出,並從中解析出磁碟塊,這樣就可以讀取 /usr 目錄並查詢字串 local 了。一旦找到這個目錄項,目錄 /usr/local 的 i - node 節點就可以從中獲得。有了 /usr/local 的 i - node 節點號,就可以讀取 i - node 並確定目錄所在的磁碟塊。最後,從 /usr/local 目錄查詢 file 並確定其 i - node 節點呢號。

如果檔案存在,那麼系統會提取 i - node 節點號並把它作為索引在 i - node 節點表中定位相應的 i - node 節點並裝入記憶體。i - node 被存放在 i - node 節點表(i-node table) 中,節點表是一個核心資料結構,它會持有當前開啟檔案和目錄的 i - node 節點號。下面是一些 Linux 檔案系統支援的 i - node 資料結構。

屬性 位元組 描述
Mode 2 檔案屬性、保護位、setuid 和 setgid 位
Nlinks 2 指向 i - node 節點目錄項的數目
Uid 2 檔案所有者的 UID
Gid 2 檔案所有者的 GID
Size 4 檔案位元組大小
Addr 60 12 個磁碟塊以及後面 3 個間接塊的地址
Gen 1 每次重複使用 i - node 時增加的代號
Atime 4 最近訪問檔案的時間
Mtime 4 最近修改檔案的時間
Ctime 4 最近更改 i - node 的時間

現在我們來一起探討一下檔案讀取過程,還記得 read 函式是如何呼叫的嗎?

n = read(fd,buffer,nbytes);

當核心接管後,它會從這三個引數以及內部表與使用者有關的資訊開始。內部表的其中一項是檔案描述符陣列。檔案描述符陣列用檔案描述符 作為索引併為每一個開啟檔案儲存一個表項。

檔案是和 i - node 節點號相關的。那麼如何通過一個檔案描述符找到檔案對應的 i - node 節點呢?

這裡使用的一種設計思想是在檔案描述符表和 i - node 節點表之間插入一個新的表,叫做 開啟檔案描述符(open-file-description table)。檔案的讀寫位置會在開啟檔案描述符表中存在,如下圖所示

我們使用 shell 、P1 和 P2 來描述一下父程序、子程序、子程序的關係。Shell 首先生成 P1,P1 的資料結構就是 Shell 的一個副本,因此兩者都指向相同的開啟檔案描述符的表項。當 P1 執行完成後,Shell 的檔案描述符仍會指向 P1 檔案位置的開啟檔案描述。然後 Shell 生成了 P2,新的子程序自動繼承檔案的讀寫位置,甚至 P2 和 Shell 都不知道檔案具體的讀寫位置。

上面描述的是父程序和子程序這兩個 相關 程序,如果是一個不相關程序開啟檔案時,它將得到自己的開啟檔案描述符表項,以及自己的檔案讀寫位置,這是我們需要的。

因此,開啟檔案描述符相當於是給相關程序提供同一個讀寫位置,而給不相關程序提供各自私有的位置。

i - node 包含三個間接塊的磁碟地址,它們每個指向磁碟塊的地址所能夠儲存的大小不一樣。

Linux Ext4 檔案系統

為了防止由於系統崩潰和電源故障造成的資料丟失,ext2 系統必須在每個資料塊建立之後立即將其寫入到磁碟上,磁碟磁頭尋道操作導致的延遲是無法讓人忍受的。為了增強檔案系統的健壯性,Linux 依靠日誌檔案系統,ext3 是一個日誌檔案系統,它在 ext2 檔案系統的基礎之上做了改進,ext4 也是 ext3 的改進,ext4 也是一個日誌檔案系統。ext4 改變了 ext3 的塊定址方案,從而支援更大的檔案和更大的檔案系統大小。下面我們就來描述一下 ext4 檔案系統的特性。

具有記錄的檔案系統最基本的功能就是記錄日誌,這個日誌記錄了按照順序描述所有檔案系統的操作。通過順序寫出檔案系統資料或元資料的更改,操作不受磁碟訪問期間磁碟頭移動的開銷。最終,這個變更會寫入並提交到合適的磁碟位置上。如果這個變更在提交到磁碟前檔案系統宕機了,那麼在重啟期間,系統會檢測到檔案系統未正確解除安裝,那麼就會遍歷日誌並應用日誌的記錄來對檔案系統進行更改。

Ext4 檔案系統被設計用來高度匹配 ext2 和 ext3 檔案系統的,儘管 ext4 檔案系統在核心資料結構和磁碟佈局上都做了變更。儘管如此,一個檔案系統能夠從 ext2 檔案系統上解除安裝後成功的掛載到 ext4 檔案系統上,並提供合適的日誌記錄。

日誌是作為迴圈緩衝區管理的檔案。日誌可以儲存在與主檔案系統相同或者不同的裝置上。日誌記錄的讀寫操作會由單獨的 JBD(Journaling Block Device) 來扮演。

JBD 中有三個主要的資料結構,分別是 log record(日誌記錄)、原子操作和事務。一個日誌記錄描述了一個低級別的檔案系統操作,這個操作通常導致塊內的變化。因為像是 write 這種系統呼叫會包含多個地方的改動 --- i - node 節點,現有的檔案塊,新的檔案塊和空閒列表等。相關的日誌記錄會以原子性的方式分組。ext4 會通知系統呼叫程序的開始和結束,以此使 JBD 能夠確保原子操作的記錄都能被應用,或者一個也不被應用。最後,主要從效率方面考慮,JBD 會視原子操作的集合為事務。一個事務中的日誌記錄是連續儲存的。只有在所有的變更一起應用到磁碟後,日誌記錄才能夠被丟棄。

由於為每個磁碟寫出日誌的開銷會很大,所以 ext4 可以配置為保留所有磁碟更改的日誌,或者僅僅保留與檔案系統元資料相關的日誌更改。僅僅記錄元資料可以減少系統開銷,提升效能,但不能保證不會損壞檔案資料。其他的幾個日誌系統維護著一系列元資料操作的日誌,例如 SGI 的 XFS。

/proc 檔案系統

另外一個 Linux 檔案系統是 /proc (process) 檔案系統

它的主要思想來源於貝爾實驗室開發的第 8 版的 UNIX,後來被 BSD 和 System V 採用。

然而,Linux 在一些方面上對這個想法進行了擴充。它的基本概念是為系統中的每個程序在 /proc 中建立一個目錄。目錄的名字就是程序 PID,以十進位制數進行表示。例如,/proc/1024 就是一個程序號為 1024 的目錄。在該目錄下是程序資訊相關的檔案,比如程序的命令列、環境變數和訊號掩碼等。事實上,這些檔案在磁碟上並不存在磁碟中。當需要這些資訊的時候,系統會按需從程序中讀取,並以標準格式返回給使用者。

許多 Linux 擴充套件與 /proc 中的其他檔案和目錄有關。它們包含各種各樣的關於 CPU、磁碟分割槽、裝置、中斷向量、核心計數器、檔案系統、已載入模組等資訊。非特權使用者可以讀取很多這樣的資訊,於是就可以通過一種安全的方式瞭解系統情況。

NFS 網路檔案系統

從一開始,網路就在 Linux 中扮演了很重要的作用。下面我們會探討一下 NFS(Network File System) 網路檔案系統,它在現代 Linux 作業系統的作用是將不同計算機上的不同檔案系統連結成一個邏輯整體。

NFS 架構

NFS 最基本的思想是允許任意選定的一些客戶端伺服器共享一個公共檔案系統。在許多情況下,所有的客戶端和伺服器都會在同一個 LAN(Local Area Network) 區域網內共享,但是這並不是必須的。也可能是下面這樣的情況:如果客戶端和伺服器距離較遠,那麼它們也可以在廣域網上執行。客戶端可以是伺服器,伺服器可以是客戶端,但是為了簡單起見,我們說的客戶端就是消費服務,而伺服器就是提供服務的角度來聊。

每一個 NFS 服務都會匯出一個或者多個目錄供遠端客戶端訪問。當一個目錄可用時,它的所有子目錄也可用。因此,通常整個目錄樹都會作為一個整體匯出。伺服器匯出的目錄列表會用一個檔案來維護,這個檔案是 /etc/exports,當伺服器啟動後,這些目錄可以自動的被匯出。客戶端通過掛載這些匯出的目錄來訪問它們。當一個客戶端掛載了一個遠端目錄,這個目錄就成為客戶端目錄層次的一部分,如下圖所示。

在這個示例中,一號客戶機掛載到伺服器的 bin 目錄下,因此它現在可以使用 shell 訪問 /bin/cat 或者其他任何一個目錄。同樣,客戶機 1 也可以掛載到 二號伺服器上從而訪問 /usr/local/projects/proj1 或者其他目錄。二號客戶機同樣可以掛載到二號伺服器上,訪問路徑是 /mnt/projects/proj2。

從上面可以看到,由於不同的客戶端將檔案掛載到各自目錄樹的不同位置,同一個檔案在不同的客戶端有不同的訪問路徑和不同的名字。掛載點一般通常在客戶端本地,伺服器不知道任何一個掛載點的存在。

NFS 協議

由於 NFS 的協議之一是支援 異構 系統,客戶端和伺服器可能在不同的硬體上執行不同的作業系統,因此有必要在伺服器和客戶端之間進行介面定義。這樣才能讓任何寫一個新客戶端能夠和現有的伺服器一起正常工作,反之亦然。

NFS 就通過定義兩個客戶端 - 伺服器協議從而實現了這個目標。協議就是客戶端傳送給伺服器的一連串的請求,以及伺服器傳送回客戶端的相應答覆。

第一個 NFS 協議是處理掛載。客戶端可以向伺服器傳送路徑名並且請求伺服器是否能夠將伺服器的目錄掛載到自己目錄層次上。因為伺服器不關心掛載到哪裡,因此請求不會包含掛載地址。如果路徑名是合法的並且指定的目錄已經被匯出,那麼伺服器會將檔案 控制代碼 返回給客戶端。

檔案控制代碼包含唯一標識檔案系統型別,磁碟,目錄的i節點號和安全性資訊的欄位。

隨後呼叫讀取和寫入已安裝目錄或其任何子目錄中的檔案,都將使用檔案控制代碼。

當 Linux 啟動時會在多使用者之前執行 shell 指令碼 /etc/rc 。可以將掛載遠端檔案系統的命令寫入該指令碼中,這樣就可以在允許使用者登陸之前自動掛載必要的遠端檔案系統。大部分 Linux 版本是支援自動掛載的。這個特性會支援將遠端目錄和本地目錄進行關聯。

相對於手動掛載到 /etc/rc 目錄下,自動掛載具有以下優勢

  • 如果列出的 /etc/rc 目錄下出現了某種故障,那麼客戶端將無法啟動,或者啟動會很困難、延遲或者伴隨一些出錯資訊,如果客戶根本不需要這個伺服器,那麼手動做了這些工作就白費了。
  • 允許客戶端並行的嘗試一組伺服器,可以實現一定程度的容錯率,並且效能也可以得到提高。

另一方面,我們預設在自動掛載時所有可選的檔案系統都是相同的。由於 NFS 不提供對檔案或目錄複製的支援,使用者需要自己確保這些所有的檔案系統都是相同的。因此,大部分的自動掛載都只應用於二進位制檔案和很少改動的只讀的檔案系統。

第二個 NFS 協議是為檔案和目錄的訪問而設計的。客戶端能夠通過向伺服器傳送訊息來操作目錄和讀寫檔案。客戶端也可以訪問檔案屬性,比如檔案模式、大小、上次修改時間。NFS 支援大多數的 Linux 系統呼叫,但是 open 和 close 系統呼叫卻不支援。

不支援 open 和 close 並不是一種疏忽,而是一種刻意的設計,完全沒有必要在讀一個檔案之前對其進行開啟,也沒有必要在讀完時對其進行關閉。

NFS 使用了標準的 UNIX 保護機制,使用 rwx 位來標示所有者(owner)組(groups)其他使用者 。最初,每個請求訊息都會攜帶呼叫者的 groupId 和 userId,NFS 會對其進行驗證。事實上,它會信任客戶端不會發生欺騙行為。可以使用公鑰密碼來建立一個安全金鑰,在每次請求和應答中使用它驗證客戶端和伺服器。

NFS 實現

即使客戶端和伺服器的程式碼實現是獨立於 NFS 協議的,大部分的 Linux 系統會使用一個下圖的三層實現,頂層是系統呼叫層,系統呼叫層能夠處理 open 、 read 、 close 這類的系統呼叫。在解析和引數檢查結束後呼叫第二層,虛擬檔案系統 (VFS) 層。

VFS 層的任務是維護一個表,每個已經開啟的檔案都在表中有一個表項。VFS 層為每一個開啟的檔案維護著一個虛擬i節點 ,簡稱為 v - node。v 節點用來說明檔案是本地檔案還是遠端檔案。如果是遠端檔案的話,那麼 v - node 會提供足夠的資訊使客戶端能夠訪問它們。對於本地檔案,會記錄其所在的檔案系統和檔案的 i-node ,因為現代作業系統能夠支援多檔案系統。雖然 VFS 是為了支援 NFS 而設計的,但是現代作業系統都會使用 VFS,而不管有沒有 NFS。

關注公眾號 程式設計師cxuan 回覆 cxuan 領取優質資料。

我自己寫了六本 PDF ,非常硬核,連結如下

我自己寫了六本 PDF ,非常硬核,連結如下

我自己寫了六本 PDF ,非常硬核,連結如下

cxuan 嘔心瀝血肝了四本 PDF。

cxuan 又肝了兩本 PDF。