Linux 文件系統之入門必看!

  • 2020 年 9 月 28 日
  • 筆記

在 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) 機制。

舉個例子,下面是一個使用鏈接之前的圖

21

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

使用鏈接後的示意如下

22

現在,jianshe 可以創建一個鏈接來使用 cxuan 下面的目錄了。『

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

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

23

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

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

24

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

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

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

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

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

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

25

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

26

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

27

如上圖所示,進程 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 資訊,位元組位移,訪問限制等。例如下圖所示

28

默認情況下,前三個文件描述符為 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 作用於一個打開的目錄,使他能在此從頭開始讀取。

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

我自己寫了六本 PDF ,非常硬核,鏈接如下

我自己寫了六本 PDF ,非常硬核,鏈接如下

我自己寫了六本 PDF ,非常硬核,鏈接如下

cxuan 嘔心瀝血肝了四本 PDF。

cxuan 又肝了兩本 PDF。