Linux 檔案系統之入門必看!
阿新 • • 發佈:2020-09-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 命令
```shell
cp books books-replica
```
就表示的是相對路徑,而
```shell
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 ,哈哈哈哈。
這個系統呼叫的兩個引數是檔名和保護模式
```shell
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` 有三個引數:第一個是檔案的檔案描述符,第二個是檔案的位置;第三個告訴檔案位置是相對於檔案的開頭,當前位置還是檔案的結尾
```c
lseek(int fildes, off_t offset, int whence);
```
lseek 的返回值是更改檔案指標後文件中的絕對位置。lseek 是唯一從來不會造成真正磁碟查詢的系統呼叫,它只是更新當前的檔案位置,這個檔案位置就是記憶體中的數字。
對於每個檔案,Linux 都會跟蹤檔案模式(常規,目錄,特殊檔案),大小,最後修改時間以及其他資訊。程式能夠通過 `stat` 系統呼叫看到這些資訊。第一個引數就是檔名,第二個是指向要放置請求資訊結構的指標。這些結構的屬性如下圖所示。
| 儲存檔案的裝置 |
| ------------------------ |
| 儲存檔案的裝置 |
| i-node 編號 |
| 檔案模式(包括保護位資訊) |
| 檔案連結的數量 |
| 檔案所有者標識 |
| 檔案所屬的組 |
| 檔案大小(位元組) |
| 建立時間 |
| 最後一個修改/訪問時間 |
`fstat` 呼叫和 `stat` 相同,只有一點區別,fstat 可以對開啟檔案進行操作,而 stat 只能對路徑進行操作。
`pipe` 檔案系統呼叫被用來建立 shell 管道。它會建立一系列的`偽檔案`,來緩衝和管道元件之間的資料,並且返回讀取或者寫入緩衝區的檔案描述符。在管道中,像是如下操作
```shell
sort