什麼是檔案?

在 Unix 系統中,有「everything is a file」的概念,如硬體設備、進程資訊、網路連線、處理器狀態等都抽象化為檔案系統中的一個路徑。透過這種設計,使用者只需要使用一套統一的 API(例如 open(), read(), write(), close()),就能操作各種截然不同的系統資源,而不需要為每一種硬體另外撰寫專屬的操作介面。比方說 /proc/cpuinfo 這個路徑包含了 CPU 資訊, /dev/sda 則代表整顆實體硬碟。

一個檔案通常用一個路徑表示,在程式內則用一個指標(或者稱作 File Descriptor, FD)指著。在作業系統中,一個檔案可能會有很多不同指標指著。當程式開啟一個檔案時, Kernel 會維護三種結構:File Descriptor Table、System-Wide Open File Table、V-node Table(或 I-node Table)。

  1. File Descriptor Table:
    每個 Process 都有自己獨立的 FD Table,FD Table 實際上只是一個陣列,每個 FD 是一個非零整數用來存取 FD Table。預設情況下,會開啟三個 FD,分別是 stdinstdoutstderr,由 0、1、2 表示。再往後開啟的檔案,都由 3 往上加。
  2. System-wide Open File Table:
    當呼叫 open(2) 時,Kernel 會建立一個 File Description 結構,這個結構儲存了本次開啟的操作狀態,包含讀寫的 Offset、開啟模式(如唯讀)、Reference Count(有多少個 FD 指向這個 File Description) 等等。
  3. V-node Table:
    這裡面存的資料代表實體或靜態檔案資源本身,紀錄檔案的 Metadata,包含檔案大小、檔案類型、存取權限、所有者、指向實體磁區的指標等等。

下圖展示了上面三個數據結構的關係:

Source Code Trace

/include/linux/sched.h

struct task_struct {
    ...
    struct files_struct *files;
    ...
};

task_struct 是 Process Control Block ,包含該進程的各種資訊。其中的欄位 files 指向下面的結構,這就是每個 Process 的 File Descriptor Table:

/include/linux/fdtable.h

struct files_struct {
    ...
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
    struct file __rcu *fd_array[NR_OPEN_DEFAULT];
    ...
};

/include/linux/fdtable.h

struct fdtable {
    ...
    struct file __rcu **fd;
    ...
};

初始狀態下, files_struct.fdt 指向 files_struct.fdtab ,而 files_struct.fdtab.fd 又指向 files_struct.fd_array 。這樣實作的好處是,大部分進程開啟的檔案數量都很少(<64),所以先以靜態分配出 NR_OPEN_DEFAULT 大小,讓 fdtab.fd 指向 fd_array ,後續擴展再更換 files_struct.fdt 所指向的位置,存取時只需要統一讀取 files_struct.fdt.fd ,此外還能供多執行緒在無鎖狀態下讀取。

下面的 struct file 是存在 Open File Table 中的結構:

struct file {
    ...
    struct inode *f_inode;
    ...
}

/include/linux/fs.h

struct file 結構內則有 f_inode 指向該檔案的 metadata。實際上, linux 中沒有所謂的 System-wide Open File Table 這個"結構",他是靠 Mempool 來分配 struct file 實際上的空間,以及利用 f_ref 追蹤有多少指標指向這塊記憶體,再加上 fdtable 來達成 $O(1)$ 存取 struct file

V-node vs. I-node

V-node (Virtual Node)由 Sun Microsystems 為 Solaris / BSD 系統開發,主要由 Unix 系統使用。早期的 Unix 只支援本地傳統檔案系統(UFS),可以直接使用實體磁碟上的 I-node。但後來為了支援網路檔案系統(NFS)以及其他非 Unix 檔案系統,引入了 VFS (Virtual File System) 與 V-node 抽象介面。V-node 代表記憶體中的通用檔案物件,封裝跨檔案系統的統一操作介面,並指向底層具體檔案系統的實體 I-node。 I-node 是實際存在磁碟上的,由 OS 從磁碟讀取到記憶體中。

Linux 採用的 I-node (Index Node)繼承並改進了 VFS 的思想。Linux 沒有獨立命名為新的結構,而是直接將 V-node 的抽象操作介面整合進記憶體的 I-node 。其包含儲存檔案的 Metadata,例如:檔案大小、權限、修改時間、存取控制等等。此外,Linux 將路徑結構從 I-node 拆分出來,引入了 Dentry(Directory Entry),而 Dentry 結構再指向 I-node。

傳統 V-node 機制在解析路徑時,需透過檔案系統遞迴執行 VOP_LOOKUP;而 Linux 透過 Dentry Cache 的 Hash Table,能快速找到對應的 I-node。這也是為什麼 Linux 在記憶體層面可以輕鬆處理硬連結,只要將 Dentry 中的不同路徑指向同一個 I-node 就好。

什麼是 IO?

  • Unbuffered I/O: syscall everytime, buffered by OS.
  • Buffered I/O:no syscall everytime, buffered by C library.