---
# System prepended metadata

title: 第九講：檔案系統概念及實作手法
tags: [Linux]

---

# 第九講：檔案系統概念及實作手法

> 本筆記僅為個人紀錄，相關教材之 Copyright 為[jserv](http://wiki.csie.ncku.edu.tw/User/jserv)及其他相關作者所有
* 直播：
    * ==[Linux 核心設計：檔案系統概念及實作手法 (上) - 2020/10/25](https://www.youtube.com/watch?v=d8ZN5-XTIJM)==
    * ==[Linux 核心設計：檔案系統概念及實作手法 (下) - 2020/11/19](https://www.youtube.com/watch?v=BCPe0EICfwI)==
* 詳細共筆：[Linux 核心設計：檔案系統概念及實作手法](https://hackmd.io/@sysprog/linux-file-system?type=view)
* 主要參考資料：
    * [I/O Multiplexing](http://www.cs.toronto.edu/~krueger/csc209h/f05/lectures/Week11-Select.pdf)
    * [Virtual File System](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/vfs.pdf)
    * [Ext3/4 file systems](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/ext4.pdf)
    * [Encrypted File Systems](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/cryptfs.pdf)

---

## 前言：檔案系統的重要性與目標

檔案系統是作業系統中歷史最悠久且最核心的概念之一。自 1969 年 Ken Thompson 進行 UNIX 早期實驗以來，其基本設計哲學歷經五十餘年，仍為今日的 Linux 核心所繼承。檔案系統作為一種邏輯抽象，將各式各樣的實體儲存裝置 (如硬碟、網路儲存、主記憶體) 統一管理，並以檔案、目錄、路徑等概念呈現給使用者。
<center>
  <img src="https://developer.ibm.com/developer/default/tutorials/l-linux-filesystem/images/figure1.gif" alt="image" width="70%" />
</center>

本課深入探討 Linux 檔案系統的關鍵概念與實作手法，從 UNIX 的歷史淵源、核心設計哲學，到現代 Linux 核心中強大的 Virtual File System (VFS) 介面，再到基於此介面的各種檔案系統實作，建構一個完整而深入的知識體系。

---

## 核心概念釐清

在深入技術細節之前，我們首先需要釐清幾個 foundational 的概念，這些概念是理解 UNIX/Linux 檔案系統設計哲學的基石。

### `file` 的真義：「檔案」而非「文件」

在作業系統的語境中，將 `file` 翻譯為「文件」會偏離其核心意義。
* 根據 [Cambridge Dictionary](https://dictionary.cambridge.org/dictionary/english/file)，`file` 指的是用於有序儲存文件的「容器 (container)」。
* 根據 [國家教育研究院的解釋](http://terms.naer.edu.tw/detail/1278441/?index=9)，它是一個「資訊集合」。

因此，將 `file` 翻譯為 **檔案** 更能體現其作為「保存資訊的容器」的本質，這也是作業系統看待 `file` 的視角。

### UNIX 哲學："Everything is a file" vs. "Everything is a file descriptor"

UNIX 一個廣為人知的設計哲學是 **Everything is a file (一切皆檔案)**。在 [The Linux Programming Interface](http://man7.org/tlpi/) 一書中，此概念被稱為 **Universality of I/O (I/O 的通用性)**，意指所有類型的 I/O 操作，無論是讀寫磁碟上的檔案，還是與終端機、網路等裝置互動，都可以透過一組統一的系統呼叫 (System Calls) 來完成：`open()` 、 `read()` 、 `write()` 、 `close()` 、 `link()` 、 `unlink()` 、 `mount()` 等，大大簡化了程式設計模型。

> 然而，在實際的 UNIX 和 Linux 系統中，此理念並未完全落實。例如，早期的訊號 (signal) 或行程 (process) 管理就無法用檔案操作來處理。

因此，一個更精確的描述是 **Everything is a file descriptor (一切皆檔案描述符)**。File Descriptor (檔案描述符，簡稱 `fd`) 是一個非負整數，由核心在打開檔案時回傳給 process，作為後續操作該檔案的唯一識別碼，後續所有操作都憑此號碼進行。

Linux 核心貫徹了此理念，並將其發揚光大，實作出多種以 `fd` 結尾的核心機制，將原本非檔案的物件也納入檔案描述符的管理體系 (如 `select`, `poll`, `epoll`)：

*   **[eventfd](http://man7.org/linux/man-pages/man2/eventfd.2.html)**：將 **核心的事件通知機制** 封裝，讓應用程式可以像讀寫檔案一樣等待或觸發事件。
*   **[timerfd](http://man7.org/linux/man-pages/man2/timerfd_settime.2.html)**：將 **計時器** 封裝，時間到期時，該 `fd` 變得可讀。
*   **[signalfd](http://man7.org/linux/man-pages/man2/signalfd.2.html)**：將 **傳統的 signal 機制** 封裝，讓程式可以透過 `read()` 來同步等待並接收訊號，而非使用有風險的 signal handler。
*   **[pidfd](https://lwn.net/Articles/801319/)**：將 **PID** 封裝，使得監控 process 狀態 (如等待其結束) 可以整合到 `poll`/`epoll` 等 I/O 多工機制中。

### Plan 9 的影響：真正的分散式檔案系統

真正徹底實現「Everything is a file」哲學的是 Bell Labs 在 UNIX 之後開發的 [Plan 9 from Bell Labs](https://en.wikipedia.org/wiki/Plan_9_from_Bell_Labs) 作業系統。在 Plan 9 中，所有系統資源，包括網路連線 (Telnet, FTP)、process、服務等，**都以檔案的形式呈現在一個統一的階層式命名空間 (namespace) 中**。儘管 Plan 9 在商業上未獲成功，其前瞻性的設計思想卻深刻影響了後來的系統，**特別是 Linux 與虛擬化技術**。
* **9P (protocol)**
Plan 9 透過一個 **統一的通訊協定 [9P](https://en.wikipedia.org/wiki/9P_(protocol))**，讓任何程式都能將其提供的服務呈現為一組虛擬檔案，供其他程式存取。這種設計使得行程間通訊 (IPC) 和網路通訊都能簡化為檔案的 I/O 操作。
* **Virtio**
Linux 核心中的 **[Virtio](https://developer.ibm.com/articles/l-virtio/)** 為虛擬化環境 (如 QEMU/KVM) 中的各種裝置 (網路、block 儲存等) 提供了一個通用的 I/O 虛擬化框架。
    * 為了實現 Host 與 Guest 之間的檔案分享，Virtio 最初便採用了基於 9P 協定的 [v9fs](https://www.kernel.org/doc/Documentation/filesystems/9p.txt)，將 Plan 9 的資源共享精神在 Linux 虛擬化中發揚光大。
    * 後來發展的 [VirtIO-FS](https://virtio-fs.gitlab.io/) 效能更佳，它結合了 FUSE 與共享記憶體，讓 Guest OS 能高效存取 Host 端的檔案。

> 延伸閱讀：
> * [An Updated Overview of the QEMU Storage Stack](https://events.static.linuxfound.org/slides/2011/linuxcon-japan/lcj2011_hajnoczi.pdf)
> * [溫柔的教你用 QEMU 開始寫系統軟體](https://hackmd.io/@VIRqdo35SvekIiH4p76B7g/rkVzoomyx)
> * [測試 Linux 核心的虛擬化環境](https://hackmd.io/@sysprog/linux-virtme) - jserv

---

## 檔案存取的內部機制

當應用程式透過 `open()`、`read()` 等系統呼叫操作檔案時，其背後涉及三個核心的資料結構，它們共同構成了 Linux **檔案存取** 的骨架。

### 三大核心元件：File Descriptor、Open File Table、Inode

1.  **Per-process File Descriptor Table (檔案描述符表)：一切操作的起點**
    *   **定義**：每個 process 內部都有一個 **File Descriptor Table**，它是一個陣列，儲存了指向 **System-wide Open File Table** 的指標。`fd` 本身就是這個陣列的索引，是一個非負整數。
    *   **範疇**：`fd` 是 **行程級別 (per-process)** 的。不同的 process 可以擁有相同的 `fd` 數字，但它們指向的檔案實體可以完全不同。依照慣例：
        *   `fd 0`：Standard Input (標準輸入)
        *   `fd 1`：Standard Output (標準輸出)
        *   `fd 2`：Standard Error (標準錯誤)
3.  **System-wide Open File Table (開啟檔案表)：共享與隔離的關鍵**
    *   **定義**：這是由核心維護的一個 **系統級別 (system-wide)** 的全域表格。當任何 process 呼叫 `open()` 時，核心會在此表格中建立一個條目 (entry)，這個條目被稱為 `file` 物件。
    *   **內容**：`file` 物件包含了檔案的狀態資訊，如：
        *   **檔案的開啟模式**：控制存取權限，例如 `O_RDONLY`, `O_WRONLY`, `O_RDWR` 等，在 `open()` 時傳入。
        *   **檔案狀態旗標 (Status Flags)**：控制行為/特性，例如 `O_APPEND`、`O_NONBLOCK` 等，在 `open()` 時傳入。
        *   **目前讀寫位移 (Current file offset/cursor)**：記錄下一次 `read()` 或 `write()` 將從檔案的哪個位置開始。
        *   **指向 inode 物件的指標**：連結到該檔案的具體資訊。
    *   **共享**：多個 `fd` (可能來自不同 process) 可以指向同一個 Open File Table 中的條目，實現了檔案共享。
4.  **Inode (索引節點)**：
    *   **定義**：`inode` 代表了儲存在 disk 上的一個 **實際檔案或目錄**。
    *   **儲存內容 (檔案的 metadata)**：
        *   檔案的 **類型、擁有者、大小、時間戳**
        *   **檔案權限 (Permission)**：「read、write、execute」權限，以及對應「user、group、others」的權限劃分。在 C 語言中，常使用八進位數值 (如 `0755`) 來表示這些權限組合 (幾乎是八進位在作業系統中的唯一用途，除了加密系統)。
        *   指向 **儲存檔案內容的資料區塊 (data blocks)** 的指標
        *   **連結計數 (Link  count)**：有多少個檔案名稱 (dentry) 指向這個 `inode`
    *   **唯一性**：在同一個檔案系統中，每個檔案對應唯一的 `inode`。不同的 `file` 物件可以指向同一個 `inode`，這意味著多個 process 開啟了同一個檔案。
![檔案描述符、開啟檔案表與 Inode 的關係](https://hackmd.io/_uploads/rk_olD7AT.png)

### 行程與檔案：`fork()` 與 `dup()` 的角色

*   **`fork()`：複製描述符，共享檔案狀態**
    當一個父行程透過 `fork()` 系統呼叫建立一個子行程時：
    *   子行程會「複製」父行程的檔案描述符表 (File Descriptor Table)。
    *   **關鍵點**：父子行程中對應的 `fd`，會指向 **同一個 Open File Table Entry**，使該條目的引用計數 (reference count) 加一。
    *   **結果**：父子行程 **共享** 同一個檔案的位移量 (offset) 和狀態旗標 (如讀寫模式)。如果父行程讀取了 100 個位元組，子行程接著讀取將會從第 101 個位元組開始。這種共享行為在某些協作場景中有用，但也可能導致非預期的讀寫混亂。
*   **`dup()`/`dup2()`：在「同一個 process 內」複製描述符**
    在 **同一個 process 內** 建立一個新的檔案描述符 (`new_fd`)。
    *   **關鍵點**：新描述符 (`new_fd`) 和舊描述符 (`old_fd`)，都會指向 **同一個 Open File Table Entry**，使該條目的引用計數 (reference count) 加一。
    *   **主要用途**：**I/O 重導向 (I/O Redirection)**
    最常見的場景是在 `fork()` 之後、`exec()` 之前，將子行程的標準輸入/輸出 (stdin/stdout) 重新導向到一個檔案或管道 (pipe)。例如，執行 `ls > output.txt` 命令。

---

## I/O 事件模型

在 UNIX 這類從設計之初就支援多人多工的系統中，I/O 操作極為頻繁且是效能的關鍵。為了應對不同的 I/O 場景，發展出了多種事件處理模型。
<!-- ![](https://rickhw.github.io/images/ComputerScience/IO-Multiplexing/IO-Multiplexing_CompleteThink.png) -->

### Blocking (阻塞式) vs. Non-blocking (非阻塞式) I/O
:::info
**區分點**：在 **等待資料準備就緒** 的階段，應用程式 process 是否會被核心阻塞 (blocked)。
:::
*   **Blocking I/O**：當應用程式發起 `read()` 請求，若核心緩衝區沒有資料，則應用程式會 **被阻塞，直到資料到達為止**。這是最常見的預設模式。
*   **Non-blocking I/O**：若透過 `fcntl()` 設定了 `O_NONBLOCK` 旗標，發起 `read()` 時如果沒有資料，核心會立刻返回一個錯誤碼 (如 `EAGAIN`)，應用程式不會被阻塞，而是 **需要自己不斷地輪詢 (polling) 檢查資料是否就緒**。

### Synchronous (同步) vs. Asynchronous (非同步) I/O
:::info
**區分點**：在 **實際進行資料傳輸** (從 kernel 複製到 user) 的階段，應用程式 process 是否會被阻塞。
:::
*   **Synchronous I/O**：即便資料已準備就緒，應用程式在呼叫 `read()` 後，仍需等待資料從核心複製到應用程式緩衝區的過程完成，此期間應用程式是阻塞的。**Blocking I/O**、**Non-blocking I/O**、**I/O Multiplexing** 都屬於同步 I/O。
*   **Asynchronous I/O**：應用程式發起 `aio_read()` 後，核心會立即返回，應用程式可以繼續執行其他任務。核心會在背景完成資料的讀取與複製，**完成後再透過訊號 (signal) 或回呼函式 (callback) 通知應用程式**。在整個過程中，應用程式都不會被阻塞。
![](https://blog.function12.io/content/images/2023/11/----.png)

### I/O Multiplexing (I/O 多路復用)

I/O Multiplexing 是一種常見的高效能 I/O 模型，它屬於 **同步 I/O** 的一種。其核心思想是 **允許單一 process 同時監視多個 `fd` 的狀態**。
*   **select()**：
    ```c
    int select(int nfds, fd_set *readfds, fd_set *writefds,
               fd_set *exceptfds, struct timeval *timeout);
    ```
    *   **運作模式**：
        1.  應用程式將所有感興趣的 `fd` 集合 (`readfds`, `writefds` 等) 傳遞給 `select()`。
        2.  `select()` 呼叫本身是 **阻塞** 的，它會阻塞應用程式，直到集合中有至少一個 `fd` 準備就緒 (例如，有資料可讀)，或等待超時。
        3.  當 `select()` 返回後，應用程式需要遍歷 `fd` 集合，找出是哪些 `fd` 準備就緒了。
        4.  接著對這些就緒的 `fd` 呼叫 `read()` 或 `write()` 進行實際的 I/O 操作。此時的讀寫通常不會阻塞，因為資料已經準備好了。
![](https://images0.cnblogs.com/blog/305504/201308/17201205-8ac47f1f1fcd4773bd4edd947c0bb1f4.png)
    *   **優勢**：能用單一 process 高效管理大量連線，避免了為每個連線建立一個 process 或 thread 的巨大開銷。
    *   **限制**：
        *   可監控的 `fd` 數量有上限 (通常是 `FD_SETSIZE` ==  1024)。
        *   每次呼叫都需要將整個 `fd` 集合從 user space 複製到 kernel space。
        *   每次返回後，應用程式都需要線性掃描整個 `fd` 集合，當 `fd` 數量龐大時效率低下。
    > 為了解決這些問題，Linux 後續引入了更高效的 `poll()` 以及 `epoll()` 機制，使得單一 thread 的程式也能高效地處理成千上萬的並行連線，成為 **現代高效能網路伺服器 (如 Nginx) 的基石**。
*   **poll()**：本質與 `select()` 相同，但它基於 Linked-list 來管理 `fd`，**沒有了 `FD_SETSIZE` 的數量限制**。但它仍有 `select()` 的後兩個缺點。
*   **epoll()**：Linux 特有的機制，是目前最高效的 I/O Multiplexing 實作。透過 `epoll_create()`, `epoll_ctl()`, `epoll_wait()` 三個系統呼叫來運作，解決了 `select()` 的所有缺點：
    1.  沒有理論上的 `fd` 數量限制 (Linked-list)。
    2.  `fd` 集合由核心維護，並 **利用 `mmap()` 加速 user 與 kernel 的訊息傳遞**，不需每次都複製。
    3.  `epoll_wait()` **只返回已就緒的 `fd`**，不需應用程式遍歷。
> 參考：
> * [I/O Multiplexing](http://www.cs.toronto.edu/~krueger/csc209h/f05/lectures/Week11-Select.pdf)
> * [I/O Models 中文解說](https://rickhw.github.io/2019/02/27/ComputerScience/IO-Models/)
> * [IO 多路複用之 select、poll、epoll 詳解](https://www.cnblogs.com/jeakeven/p/5435916.html)

> 延伸閱讀：
> * [高效 Web 伺服器開發](https://hackmd.io/@sysprog/fast-web-server) - jserv
> * [透過 timerfd 處理週期性任務](https://hackmd.io/@sysprog/linux-timerfd) - jserv
> * [signalfd is useless](https://ldpreload.com/blog/signalfd-is-useless)

---

## Linux 虛擬檔案系統 (VFS)

為了讓核心能同時支援多種不同的檔案系統 (如 ext4, XFS, Btrfs, NFS 等)，Linux 引入了一個抽象層，即 **Virtual File System (VFS)**，有時也稱為 Virtual Filesystem Switch。
<center>
  <img src="https://i.imgur.com/UJ9oSGe.png" alt="image" width="80%" />
</center>

### VFS 的設計目標與架構

* **對上層應用程式**：
VFS 的核心目標是 **定義一組通用的、抽象的檔案操作介面**。任何實體的檔案系統，只要遵循這組介面規範來開發，就能夠在執行時期動態地掛載到 Linux 核心中，並被應用程式以統一的方式存取。
* **對下層檔案系統**
從開發者的角度來看，VFS 是一個 **實現了物件導向思想的框架**。它定義了幾個核心的物件類型，以及與這些物件相關的操作函式指標集合 (類似於 C++ 中的虛擬函式表)。一個新的檔案系統實作者，其主要工作就是提供這些物件的具體實例，並填寫對應的操作函式。
    <center>
      <img src="https://hackmd.io/_uploads/BJfOX7dNxl.png" alt="image" width="85%" />
    </center>
> [Linux Storage Stack Diagram](https://www.thomas-krenn.com/en/wiki/Linux_Storage_Stack_Diagram)

### 核心物件：`superblock`, `inode`, `dentry`, `file`

<center>
  <img src="https://i.sstatic.net/daHCZ.gif" alt="image" width="70%" />
</center>

VFS 主要透過以下四個核心物件來抽象檔案系統：
*   **superblock 物件**：
    *   代表一個 **已掛載的檔案系統實例**。
    *   儲存 **整個檔案系統的全域性資訊**，例如：
        *   檔案系統類型、區塊大小、總空間
        *   inode 總數、可用 inode 數
        *   一組操作函數指標，指向由具體檔案系統 (如 ext4) 實作的管理整個檔案系統的函數 (如 `alloc_inode()`, `destroy_inode()`) 等
    *   核心會維護一個 `superblock` 物件的 linked-list，來追蹤所有已掛載的檔案系統。
* **inode 物件**：
    * 代表一個 **檔案或目錄的 metadata**，是檔案系統中區分每個檔案的唯一標識。
    * 儲存了 **檔案的通用屬性**，例如：權限、擁有者、大小、時間戳 等。
    * 在核心中，inode 的實作被分為兩個層次：
        * **VFS inode**：一個通用的 in-memory 結構，為上層提供了一致的抽象介面。它包含所有檔案系統都共通的資訊。
        * **特定檔案系統的 inode**：儲存在 disk 的具體結構 (如 ext4 的 ext4_inode)。除了包含 VFS inode 的資訊外，還額外加入了該檔案系統特有的資料，例如指向實體資料區塊 (data blocks) 的指標。
*   **dentry (Directory Entry) 物件**：
    *   代表一個 **目錄項**，它是一個檔案名稱與其對應 `inode` 之間的連結。
        >   例如，路徑 `/home/user/file.txt` 會被 VFS 解析成 `dentry` `/`、`home`、`user` 和 `file.txt` 的 linked-list。
    *   `dentry` 的存在極大地優化了路徑查詢的效能。核心會維護一個 `dentry` 快取 (dcache)，加速路徑解析過程。
*   **file 物件**：
    *   代表一個 **由 process 開啟的檔案實例**。
    *   由 `open()` 在核心創建，並與一個 `fd` 關聯。
    *   儲存 **檔案的動態資訊**，例如：
        *   指向對應 `dentry` 的指標
        *   指向一組檔案操作函數 (file_operations) 的指標 (如 `read()` 、 `write()` 等)
        *   讀寫位移量 (curser/offset)
    *   同一個檔案可以被開啟多次，每次都會產生一個新的 `file` 物件，但它們都指向同一個 `dentry` 和 `inode`。
    ```graphviz
    digraph VFS_structure {
        graph [rankdir=LR, fontname="Arial", splines=polyline];
        node [shape=plain, fontname="Arial"];
        task_struct [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0">
                <TR><TD PORT="task_struct" WIDTH="100"><b>task_struct</b></TD></TR>
                <TR><TD height="30"></TD></TR>
                <TR><TD PORT="files">files</TD></TR>
                <TR><TD height="120"></TD></TR>
            </TABLE>
        >];
        files_struct [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0">
                <TR><TD PORT="files" WIDTH="100"><b>files_struct</b></TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="fd0">fd[0]</TD></TR>
                <TR><TD>fd[1]</TD></TR>
                <TR><TD>fd[2]</TD></TR>
                <TR><TD>...</TD></TR>
                <TR><TD>...</TD></TR>
                <TR><TD>fd[N-1]</TD></TR>
                <TR><TD height="10"></TD></TR>
            </TABLE>
        >];
        file [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#e1f3fd">
                <TR><TD PORT="file" WIDTH="120"><b>file</b></TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="dentry">dentry</TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD>pos</TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="f_ops">f_ops</TD></TR>
                <TR><TD height="10"></TD></TR>
            </TABLE>
        >];
        file_operations [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#e1f3fd">
                <TR><TD PORT="f_ops" WIDTH="120"><b>file_operations</b></TD></TR>
                <TR><TD>read()</TD></TR>
                <TR><TD>write()</TD></TR>
                <TR><TD>open()</TD></TR>
                <TR><TD>release()</TD></TR>
                <TR><TD>mmap()</TD></TR>
                <TR><TD>fsync()</TD></TR>
                <TR><TD>...</TD></TR>
            </TABLE>
        >];
        dentry [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#cde7ca">
                <TR><TD PORT="dentry" WIDTH="150"><b>dentry</b></TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD>d_name</TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="d_inode">d_inode</TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="d_sb">d_sb</TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD PORT="d_ops">d_ops</TD></TR>
                <TR><TD height="10"></TD></TR>
            </TABLE>
        >];
        dentry_operations [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#cde7ca">
                <TR><TD PORT="d_ops" WIDTH="150"><b>dentry_operations</b></TD></TR>
                <TR><TD>hash()</TD></TR>
                <TR><TD>compare()</TD></TR>
                <TR><TD>...</TD></TR>
            </TABLE>
        >];
        super_block [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#ffe0bf">
                <TR><TD  PORT="sb" WIDTH="140"><b>super_block</b></TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD>s_dirt</TD></TR>
                <TR><TD PORT="s_root">s_root</TD></TR>
                <TR><TD>s_type</TD></TR><TR><TD height="10"></TD></TR>
                <TR><TD PORT="s_op">s_op</TD></TR>
                <TR><TD height="10"></TD></TR>
            </TABLE>
        >];
        super_operations [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#ffe0bf">
                <TR><TD PORT="s_op" WIDTH="140"><b>super_operations</b></TD></TR>
                <TR><TD>alloc_inode()</TD></TR>
                <TR><TD>dirty_inode()</TD></TR>
                <TR><TD>write_inode()</TD></TR>
                <TR><TD>freeze_fs()</TD></TR>
                <TR><TD>statfs()</TD></TR>
                <TR><TD>...</TD></TR>
            </TABLE>
        >];
        inode [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#fffbcc">
                <TR><TD  PORT="inode" WIDTH="140"><b>inode</b></TD></TR>
                <TR><TD height="10"></TD></TR>
                <TR><TD>i_mode</TD></TR>
                <TR><TD>i_uid</TD></TR>
                <TR><TD>i_gid</TD></TR>
                <TR><TD>i_ino</TD></TR>
                <TR><TD>i_nlink</TD></TR>
                <TR><TD>i_size</TD></TR>
                <TR><TD>i_atime</TD></TR>
                <TR><TD>i_mtime</TD></TR>
                <TR><TD>i_ctime</TD></TR>
                <TR><TD>...</TD></TR>
                <TR><TD PORT="i_sb_list">i_sb_list</TD></TR>
                <TR><TD>...</TD></TR>
                <TR><TD PORT="i_ops">i_ops</TD></TR>
                <TR><TD height="10"></TD></TR>
            </TABLE>
        >];
        inode_operations [label=<
            <TABLE BORDER="1" CELLBORDER="1" CELLSPACING="0" BGCOLOR="#fffbcc">
                <TR><TD  PORT="i_ops" WIDTH="140"><b>inode_operations</b></TD></TR>
                <TR><TD>lookup()</TD></TR>
                <TR><TD>create()</TD></TR>
                <TR><TD>mkdir()</TD></TR>
                <TR><TD>getattr()</TD></TR>
                <TR><TD>setattr()</TD></TR>
                <TR><TD>...</TD></TR>
            </TABLE>
        >];
        task_struct:task_struct:e -> files_struct:files:w [style="invis"];
        task_struct:files:e -> files_struct:files:w;
        files_struct:files:e -> file:file:w [style="invis"];
        files_struct:fd0:e -> file:file:w;
        file:file:e -> dentry:dentry:w [style="invis"];
        file:dentry:e -> dentry:dentry:w;
        file:f_ops:s -> file_operations:f_ops:n;
        dentry:dentry:e -> super_block:sb:w [style="invis"];
        super_block:sb:e -> inode:inode:w [style="invis"];
        dentry:d_inode:e -> inode:inode:w;
        dentry:d_sb -> super_block:sb;
        dentry:d_ops:s -> dentry_operations:d_ops:n;
        super_block:s_root -> dentry:dentry:e;
        super_block:s_op:s -> super_operations:s_op:n;
        inode:i_ops:s -> inode_operations:i_ops:n;
        inode:i_sb_list:w -> super_block:sb:e;
        {rank=same; file; file_operations;}
        {rank=same; dentry; dentry_operations;}
        {rank=same; super_block; super_operations;}
        {rank=same; inode; inode_operations;}
    }
    ```
<!--     <center>
      <img src="https://www.coins.tsukuba.ac.jp/~yas/coins/os2-2013/2014-01-30/images/task-files-file-dentry-inode.png" alt="image" width="70%" />
    </center> -->

### Virtual 檔案系統 vs. Pseudo 檔案系統

*   **Virtual File System (VFS)**：是一個 **抽象介面框架**。它本身不儲存資料，而是 **定義了一套規則，讓真正的檔案系統能夠接入**。它著重於「構成現實的要素」，即定義了檔案系統應具備的標準操作。
*   **Pseudo File System**：是一種 **特殊的檔案系統實作**。它們看起來像檔案系統，但其 **內容並非儲存在持久化的儲存裝置上，而是由核心在記憶體中動態生成**，用以揭露核心或 process 的狀態資訊。它們存在的目的是為了特定目標，例如：
    *   `/proc` (**procfs**)：揭露 process 相關資訊。
    *   `/sys` (**sysfs**)：揭露裝置驅動程式和系統匯流排的階層結構。
    *   `/tmp` (**tmpfs**)：一種基於記憶體的檔案系統，利用核心的 Page Cache 來儲存檔案，重開機後內容會消失，但讀寫效能極高。

### Hard link vs. Soft link

|                | 硬連結 (Hard link)                               | 符號連結 (Soft / Symbolic link)                                   |
|:--------------:|------------------------------------------------|----------------------------------------------------------|
| **定義**       | 多個目錄項 **指向完全相同的 inode**                    | **一種特殊的檔案**，內容為指向目標檔案路徑的字串                  |
| **鏈結目標**   | inode (實體檔案內容)                             | 另一個檔案或目錄的路徑                                      |
| **自身 inode** | 無 (目錄項僅是指向既有 inode)                    | 有 (在目錄中作為獨立檔案，可被 `ls` 辨認)               |
| **跨檔案系統** | 不行                                             | 可以                                                     |
| **修改內容**   | 改動即改變同一 inode 的資料，所有硬連結同步可見    | 只能修改目標檔案，無法直接在符號連結上改動指向 (需重建)     |
| **刪除目標**   | 刪除單一硬連結只減 refcount，refcount 清零後才釋放空間 | 刪除目標後符號連結仍在，但變成 broken link，需手動或工具清理 |
| **用途**       | 為同一檔案建立多個別名 (aliases)                   | 1. 類似 Windows 捷徑；<br>2. 方便重命名或移動目標而不改動 link 本身 |


### 實作解析：`fibdrv` char device 驅動程式如何掛鉤 VFS
> 參照：[2020 年春季 Linux 核心設計課程作業 —— fibdrv](https://hackmd.io/@sysprog/linux2020-fibdrv)

可以透過一個簡單的字元裝置驅動程式 [fibdrv](https://github.com/sysprog21/fibdrv) 來理解裝置如何與 VFS 互動。`fibdrv` 的功能是計算並回傳費波那契數列。

1.  **觀察 inode 結構**：
    `include/linux/fs.h`
    ```c=672
    struct inode {
        ...    
        dev_t            i_rdev;
        ...    
        union {
            struct pipe_inode_info *i_pipe;
            struct cdev            *i_cdev;
            char                   *i_link;
            unsigned                i_dir_seq;
        };
        ...
    } __randomize_layout;
    ```
    若這個 `inode` 指向一個裝置，其 `i_rdev` 會儲存裝置的 major number 和 minor number。並根據裝置的類型，`inode` 內的 union 會指向對應的結構。以 `fibdrv` 來說，它屬於字元裝置 (char device)，所以核心會將 `i_cdev` 指向對應的 `struct cdev`。
2.  **觀察 file_operations 結構**：
    ```c=2268
    struct file_operations {
        struct module *owner;
        fop_flags_t fop_flags;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ...
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ...
    } __randomize_layout;
    ```
    對裝置進行 (系統呼叫) 操作時，底層會呼叫的函式。
3.  **定義檔案操作集合**：
    驅動程式首先需要定義一個 `struct file_operations` 結構的實例，並填充它所支援的操作函式指標。
    `fibdrv.c`
    ```c=112
    const struct file_operations fib_fops = {
        .owner = THIS_MODULE,
        .read = fib_read,           // 對應 read() 系統呼叫
        .write = fib_write,         // 對應 write() 系統呼叫
        .open = fib_open,           // 對應 open() 系統呼叫
        .release = fib_release,     // 對應 close() 系統呼叫
        .llseek = fib_device_lseek, // 對應 lseek() 系統呼叫
    };
    ```
3.  **註冊字元裝置 (Character Device)**：
    在模組初始化函式中，驅動程式使用 `register_chrdev()` 函式來註冊此字元裝置。這個函式會動態分配一個主裝置號 (major number)，並將 `fib_fops` 註冊到核心。這是一種較傳統的方法，核心會自動處理 `cdev` 的相關設定 (如 `cdev_alloc()`、`cdev_add()` 等)。
    `fibdrv.c`
    ```c=121
    static int __init init_fib_dev(void)
    {
        int rc = 0;
        mutex_init(&fib_mutex);

        // Let's register the device
        // This will dynamically allocate the major number
        rc = major = register_chrdev(major, DEV_FIBONACCI_NAME, &fib_fops);
        ...
    ```
4.  **建立裝置節點**：
    最後，驅動程式透過 `class_create()` 建立裝置類別，再用 `device_create()` 建立裝置。使用者空間的服務 (如 udev) 會偵測到這些操作，並在 `/dev` 目錄下建立對應的裝置檔案 (例如 `/dev/fibonacci`)。程式碼中使用了預處理器指令來相容不同版本的核心。
    `fibdrv.c`
    ```c=136
        fib_class = class_create(DEV_FIBONACCI_NAME);
        if (!fib_class) {
            ...
        }

        if (!device_create(fib_class, NULL, fib_dev, NULL, DEV_FIBONACCI_NAME)) {
            ...
    }
    ```

當 User space 的應用程式對 `/dev/fibonacci` 呼叫 `open()`、`read()` 等系統呼叫時，VFS 會：
1.  根據路徑找到對應的 `inode`。
2.  發現這個 `inode` 是一個字元裝置。
3.  從 `inode` 中找到對應的 `cdev` 結構。
4.  從 `cdev` 結構中找到 `file_operations` 指標 (`fib_fops`)。
5.  呼叫 `file_operations` 中對應的函式 (如 `fib_open`, `fib_read`)，從而將控制權轉交給我們的驅動程式。

這個過程展示了 VFS 作為一個 **執行時期分派中心 (Runtime Dispatcher)** 的角色，將通用的系統呼叫路由到特定的驅動程式實作。

---

## 依循 VFS 的檔案系統實作

基於 VFS 的靈活架構，Linux 生態系中發展出各式各樣的檔案系統，以滿足不同場景的需求。

### Ext2/3/4 的演進：從 FSCK 到 Journaling

*   **Ext2**：一個經典的傳統檔案系統。
    *   **缺點**：系統意外斷電或崩潰後，檔案系統的 metadata 可能不一致。為了修復這種不一致，每次開機時都需要執行一個耗時的 **`fsck` (file system check)** 工具來掃描並修復整個 disk partition，對於需要快速啟動的伺服器或嵌入式裝置是不可接受的。
![](https://www.idiskhome.com/resource/images/partition/ext2-file-system-structure-1.jpg)
*   **Ext3**：為了解決 `fsck` 的效能問題，Ext3 引入 **Journaling (日誌)** 機制：
    *   **核心思想**：對檔案系統的 metadata 進行實際修改前，先將這次操作的意圖 (一個交易, transaction) 寫入一個稱為 `journal` 的專門日誌 disk 區域。
    *   **運作流程**：
        1.  **Journal Write**：將「我要修改 inode A 和 data block B」這個操作寫入 journal。
        2.  **Commit**：確認 journal 寫入成功。
        3.  **Checkpoint**：將 journal 中的操作實際寫入到disk 的 inode 和 data block 區域。
    *   **修復方式**：如果系統在操作中途崩潰，下次啟動時，只需檢查 `journal` 的內容，就能恢復檔案系統的一致性。
        *   **replay (重放)**：未完成的交易
        *   **undo (回滾)**：已部分執行但未提交的交易
    *   **優點**：只需處理一小塊日誌區域，無需掃描整個檔案系統，大大縮短了恢復時間。
![](https://www.idiskhome.com/resource/images/partition/ext3-file-system-structure-2.jpg)
*   **Ext4**：作為 Ext3 的後繼者，Ext4 進行了多項重要改進，以支援更大的儲存容量和更高的效能，是目前許多 Linux 發行版的預設檔案系統：
    *   **Extents**：取代了傳統的間接區塊對映 (indirect block mapping)，`extent` 將大量連續的資料區塊表示為「起始區塊 + 長度」，大大提高了大檔案的存取效率並減少了 metadata 的碎片化。
    *   **更大的檔案系統支援**：將 32 位元的區塊編號擴展到 48 位元，使得檔案系統的最大容量從 16TB 提升到 1EB。
        > 值得注意的是，Ext4 選擇 48 位元而非 64 位元是因為還有一些技術限制需要在未來版本中解決。
    *   **目錄索引**：針對包含大量檔案的目錄，引入了基於 hashing 的 HTree 索引，將目錄查詢的演算法從線性掃描 $O(n)$ 優化到對數時間 $O(log n)$。
![](https://www.idiskhome.com/resource/images/partition/ext4-file-system-structure-3.jpg)
        > 圖源：[What Is Ext2/Ext3/Ext4 File System Format?](https://www.idiskhome.com/resource/partition/ext2-ext3-ext4-file-system-format-and-difference.shtml)
> 延伸閱讀：[Ext3/4 file systems](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/ext4.pdf)

### 加密檔案系統 (Encrypted File System)

為了保護 disk 上靜態資料的機密性，加密檔案系統應運而生。主要有兩種實現方式：

1.  **區塊裝置加密 (Block Device Encryption)**：
    *   **層級**：在 VFS 和底層檔案系統 (如 Ext4) **之下** 運作 (Overlay)。
    *   **原理**：建立一個虛擬的區塊裝置，所有寫入這個虛擬裝置的資料都會被透明地加密，然後再寫入到實體 disk。讀取時則反向操作。
    *   **優點**：對上層的檔案系統完全透明，可以 **加密整個分割區**，隱藏目錄結構和空間使用情況。
    *   **範例**：Linux 的 `dm-crypt`、Windows 的 `BitLocker`、MacOS 的 `FileVault 2`。
    <center>
      <img src="https://hackmd.io/_uploads/rJItmZYNgl.png" alt="image" width="30%" />
    </center>
2.  **檔案級加密 (File System Encryption)**：
    *   **層級**：在 VFS 和底層檔案系統 **之間** 運作 (Stackable)。
    *   **原理**：應用程式透過 VFS 寫入檔案，這個請求會先被加密檔案系統攔截，將資料加密後，再以普通檔案的形式交給底層的 Ext4 等檔案系統儲存。
    *   **優點**：更具靈活性，可以實現 **基於使用者、目錄或單一檔案的加密**，且可以將已加密的檔案直接透過網路傳輸，在遠端解密。
    *   **範例**：Linux 的 `eCryptFS`、Windows 的 `EFS`、MacOS 的 `FileVault 1`。
    <center>
      <img src="https://hackmd.io/_uploads/HJPCQ-KElg.png" alt="image" width="30%" />
    </center>
> 延伸閱讀：[Encrypted File Systems](http://www.cs.unc.edu/~porter/courses/cse506/s16/slides/cryptfs.pdf)

### 使用者空間檔案系統 (FUSE) 應用案例

**FUSE (Filesystem in Userspace)** 是一個強大的框架，允許開發者在 **User space** 使用任何偏好的高階語言 (如 Python, Go) 編寫檔案系統邏輯，而無需修改核心程式碼。極大地降低了開發門檻並催生了眾多創新的檔案系統。

*   **[MezzFS](https://medium.com/netflix-techblog/mezzfs-mounting-object-storage-in-netflixs-media-processing-platform-cda01c446ba)**：Netflix 開發的 FUSE 檔案系統，用於將其後端的雲端物件儲存 (如 AWS S3) 掛載為本地檔案系統。使其龐大的 **媒體處理平台** 可以像操作本地檔案一樣，無縫地存取雲端上的海量媒體資源。
*   **[VirtIO-FS](https://virtio-fs.gitlab.io/)**：Red Hat 為了提升 **虛擬機** 中的檔案分享效能而開發的技術。利用 FUSE 結合 VirtIO 的共享記憶體機制，讓 Guest OS 可以直接存取 Host OS 的 Page Cache，從而避免了傳統 `v9fs` 中頻繁的資料複製，顯著提高了 I/O 效能。
*   **[vramfs](https://github.com/Overv/vramfs)**: 一個實驗性的 FUSE 檔案系統，利用 OpenCL 將 GPU 的視訊記憶體 (Video RAM) 掛載為一個儲存裝置，展示了 FUSE 的極高彈性。
*   **[simplefs](https://github.com/jserv/simplefs)**：jserv 團隊研發的簡易檔案系統，全部程式碼僅約 1500 行，具備完整基礎功能。主要的目的是讓學生可以理解一個小型的檔案系統是怎麼運作。

---

## 延展性 (Scalability) 議題

隨著多核心處理器的普及，作業系統的 **延展性 (Scalability,** 系統在增加硬體資源 (如 CPU 核心) 時，其整體效能能否隨之線性增長的能力) 變得至關重要。檔案系統中的鎖 (lock) 機制是影響延展性的常見瓶頸。

### 掛載表 (Mount Table) 的鎖定問題

Linux 核心需要維護一個全域的掛載表 (mount table)，以追蹤所有已掛載的檔案系統。當程式需要 **解析路徑時，可能會頻繁地查詢這個掛載表**。在早期的核心中，對這個表的存取是透過一個全域性的 **spinlock** 來保護的。

spinlock 的問題在於，當多個核心高併發地嘗試獲取同一個鎖時，未獲取到鎖的核心會處於「忙碌等待 (busy-waiting)」狀態，不斷消耗 CPU 週期，導致嚴重的效能下降。這是一個典型的延展性瓶頸：核心數越多，鎖的競爭越激烈，整體效能不增反減。

### 從 spinlock 到 per-cpu 資料的優化策略

為了解決這個問題，核心開發者觀察到掛載表的特性：**讀取頻繁，寫入稀少**。基於此，採用了 per-cpu 資料的優化策略：

1.  為每個 CPU 核心建立一個掛載表資訊的 **本地快取**。
2.  當需要查詢時，首先檢查本地 per-cpu 快取。由於存取的是本地資源，無需加鎖，速度極快。
3.  只有 **在本地快取未命中 (cache miss) 時，才去獲取全域的鎖**，查詢全域掛載表。
4.  查詢到結果後，將其更新到本地 per-cpu 快取中，以備後續快速查詢。

這種優化策略，將高成本的全域鎖定操作，轉化為絕大多數情況下的無鎖本地操作，從而極大地提升了檔案系統在多核心環境下的延展性。

> 延伸閱讀：[Linux 核心設計：Scalability 議題](https://hackmd.io/@sysprog/linux-scalability) - jserv

---

## 總結

Linux 的檔案系統是一個層次分明、設計精巧且充滿演化智慧的複雜系統。從 UNIX 繼承的「一切皆檔案描述符」哲學，到 VFS 提供的強大抽象介面，再到 FUSE 帶來的無限創新可能，它不僅僅是儲存資料的工具，更是整個作業系統資源管理和擴充的核心樞紐。理解其背後的概念、I/O 模型、核心物件以及效能優化策略，是深入掌握 Linux 系統運作原理的必經之路。

---

回[主目錄](https://hackmd.io/@Jaychao2099/Linux-kernel)