目的:
學習 Linux 檔案模型相關的知識。
正文目錄:
Linux 的兩大抽象
檔案型別
檔案描述符
通用檔案模型:簡介
4.1 演示 demo
4.2 相關要點: 與 VFS 的關係
通用檔案模型:檔案描述符和開啓檔案的關係
5.1 相關的內核數據結構
5.2 列舉幾種開啓檔案的情景
Linux 的兩大抽象
檔案是 Linux 系統中最基礎最重要的抽象。Linux 遵循一切皆檔案的理念。很多互動操作是通過讀寫檔案來完成,即使所涉及的物件看起來並非普通檔案。
另外一大抽象是進程。如果說檔案是 Linux 系統最重要的抽象概念,進程則僅次於檔案。
進程相關的實現複雜且多變,而檔案 IO 的實現則相對穩定很多,且更貼近我們的日常操作,所以 以檔案作爲學習 Linux 內核的切入點是個更好的選擇。
檔案型別
Linux 系統的大多數檔案是普通檔案或目錄,但是也有另外一些檔案型別,具體包括如下幾種:
普通檔案 ( regular file )。
最常用的檔案型別,包含了某種形式的數據。至於這種數據是文字還是二進制數據,對於 Linux 內核而言並無區別。
檔案中包含的位元組可以是任意值,可以以任意方式進行組織。在系統層,除了位元組流,Linux 對檔案結構沒有特定要求。
對普通檔案內容的解釋由處理該檔案的應用程式進行。
檔案雖然是通過檔名存取,但檔案本身其實並沒有直接和檔名關聯。相反地,與檔案關聯的是索引節點 (inode,是index node 縮寫)。針對駐留於檔案系統上的每個檔案,檔案系統都會爲其分配一個 inode。inode 中會儲存和檔案相關的元數據,如檔案修改時間戳、所有者、型別、長度以及檔案數據的位置,但不含檔名,檔名由目錄檔案負責。
inode 由 inode number 來標識,可以通過 「ls –li」 檢視檔案的 inode number。
12582945 -rw-r–r-- 1 root root 665 Jul 10 18:47 minicom.log
目錄檔案 ( directory file )。
目錄也是一種檔案型別,這種檔案包含了其他檔案的檔名以及 inode number。檔案通常是通過檔名從使用者空間開啓,目錄用於提供存取檔案時需要的名稱。
檔名和 inode 之間的配對稱爲鏈接 (link)。對映在物理磁碟上的形式,如簡單的表或雜湊,是通過特定檔案系統的內核程式碼來實現和管理的。
如果使用者空間的應用請求開啓指定檔案,內核會開啓包含該檔名的目錄,然後根據檔名獲取 inode number。通過 inode number 可以找到 inode。inode 包含和檔案關聯的元數據,其中包括檔案數據在磁碟上的儲存位置。
硬鏈接 ( hard link )。
不同的檔名可以鏈接到到同一個 inode。當不同名稱的多個鏈接對映到同一個索引節點時,我們稱該鏈接爲硬鏈接。
硬鏈接通常要求鏈接和檔案位於同一檔案系統中。
在底層檔案系統支援的前提下,也只有超級使用者才能 纔能建立指向目錄的硬鏈接。
符號鏈接 ( symbolic link )。
符號鏈接是對一個檔案的間接指針,它與硬鏈接有所不同,硬鏈接直接指向檔案的 inode。引入符號鏈接的原因是爲了避開硬鏈接的一些限制。
硬鏈接不能跨越多個檔案系統,因爲 inode number在自己的檔案系統之外沒有任何意義。爲了跨越檔案系統建立鏈接,Linux 系統實現了符號鏈接。
特殊檔案 (special file)。
特殊檔案是使得某些抽象可以適用於檔案系統,貫徹一切皆檔案的理念。
Linux 只支援四種特殊檔案:塊裝置檔案、字元裝置檔案、命名管道 以及 UNIX域通訊端。
塊特殊檔案 ( block device file )。提供對裝置(如磁碟)帶緩衝的存取,每次存取以固定長度爲單位進行。
字元特殊檔案 ( character device file )。這種型別的檔案提供對裝置不帶緩衝的存取,每次存取長度可變。系統中的所有裝置要麼是字元特殊檔案,要麼是塊特殊檔案。
命名管道 ( named pipes ),通常稱爲 FIFO,是以檔案描述符作爲通訊通道的 IPC 機制 機製,它可以通過特殊檔案來存取。
通訊端 ( socket ) 是最後一種特殊檔案。socket 是進程間通訊的高階形式,支援不同進程間的通訊,這兩個進程可以在同一臺機器,也可以在不同機器。socket 是網路和網際網路程式設計的基礎。
在 Linux,可以用 ls/stat 命令 和 stat() 系統呼叫確定檔案型別。
$ ls -li
12587634 drwxr-xr-x 26 root root 4096 Mar 16 07:49 1.opensource
27396428 lrwxrwxrwx 1 root root 12 Nov 17 2017 Link to ssd_dvd -> /mnt/ssd_dvd
12582945 -rw-r–r-- 1 root root 665 Jul 10 18:47 minicom.log
$ stat minicom.log
File: ‘minicom.log’
Size: 665 Blocks: 8 IO Block: 4096 regular file
Device: 822h/2082d Inode: 12582945 Links: 1
Access: (0644/-rw-r–r--) Uid: ( 0/ root) Gid: ( 0/ root)
Access: 2020-01-09 09:44:07.101177618 +0800
Modify: 2020-07-10 18:47:20.073532673 +0800
Change: 2020-07-10 18:47:20.073532673 +0800
3. 檔案描述符
在 Linux 中,檔案必須先開啓才能 纔能存取。對於內核而言,所有開啓的檔案都通過檔案描述符 ( file descriptor,簡稱fd ) 參照。檔案描述符是一個非負整數。當開啓一個現有檔案或建立一個新檔案時,內核向進程返回一個檔案描述符。當讀、寫一個檔案時,使用 open() 或 creat() 返回的檔案描述符標識該檔案,將其作爲參數傳送給 read() 或 write()。
Linux 系統程式設計的大部分工作都會涉及開啓、操縱、關閉以及其他檔案描述符操作;
Linux 系統的 Shell 把檔案描述符 0 與進程的標準輸入 stdin 關聯,檔案描述符 1 與標準輸出 stdout 關聯,檔案描述符 2 與標準錯誤 stderr 關聯。這是各種 Shell 以及很多應用程式使用的慣例,與 Linux 內核無關。如果不遵循這種慣例,很多 Linux 系統應用程式就不能正常工作;
使用者可以重定向檔案描述符,甚至可以通過管道把一個程式的輸出作爲另一個程式的輸入。Shell 就是通過這種方式實現重定向和管道的。
在 POSIX 標準中,幻數 0、1、2 雖然已被標準化,但應當把它們替換成符號常數 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可讀性;
檔案描述符的範圍是 0 ~ OPEN_MAX-1;
檔案描述符並非侷限於存取普通檔案。實際上,檔案描述符也可以存取裝置檔案、管道、FIFO、Socket等。遵循一切皆檔案的理念,幾乎任何能夠讀寫的東西都可以通過檔案描述符來存取。
4. 通用檔案模型:簡介
Linux 通用檔案模型最爲顯著的特性之一就是 I/O 通用性。也就是說,同一套系統呼叫 open()、read()、write()、close() 等所執行的 I/O 操作,可施之於所有檔案型別,包括裝置檔案在內。應用程式發起的I/O請求,內核會將其轉化爲相應的檔案系統操作,或者裝置驅動程式操作,以此來執行鍼對目標檔案或裝置的I/O操作。因此,採用這些系統呼叫的程式能夠處理任何型別的檔案。
演示 demo (copy.c):
int main(int argc, char *argv[])
{
int inputFd, outputFd, openFlags;
mode_t filePerms;
ssize_t numRead;
char buf[BUF_SIZE];
if (argc != 3 || strcmp(argv[1], "--help") == 0)
usageErr("%s old-file new-file\n", argv[0]);
/* Open input and output files */
inputFd = open(argv[1], O_RDONLY);
if (inputFd == -1)
errExit("opening file %s", argv[1]);
openFlags = O_CREAT | O_WRONLY | O_TRUNC;
filePerms = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP |
S_IROTH | S_IWOTH; /* rw-rw-rw- */
outputFd = open(argv[2], openFlags, filePerms);
if (outputFd == -1)
errExit("opening file %s", argv[2]);
/* Transfer data until we encounter end of input or an error */
while ((numRead = read(inputFd, buf, BUF_SIZE)) > 0)
if (write(outputFd, buf, numRead) != numRead)
fatal("write() returned error or partial write occurred");
if (numRead == -1)
errExit("read");
if (close(inputFd) == -1)
errExit("close input");
if (close(outputFd) == -1)
errExit("close output");
exit(EXIT_SUCCESS);
}
執行效果:
$ ./copy test test.old
$ ./copy test /dev/tty
$ ./copy /dev/tty abc.txt
相關要點:
要實現通用 I/O,就必須確保每一種檔案系統和每一種檔案型別(包括裝置檔案)都實現了相同的 I/O 系統呼叫集。由於檔案系統或裝置檔案所特有的操作細節在內核中處理,在程式設計時通常可以忽略裝置專有的因素。一旦應用程式需要存取檔案系統或裝置的專有功能時,可以選擇瑞士軍刀般的 ioctl() 系統呼叫,該呼叫爲通用 I/O 模型之外的專有特性提供了存取介面。
提到通用 I/O,就必須提起虛擬檔案系統 (VFS)。爲支援各種本機檔案系統,且在同時允許存取其他操作系統的檔案,Linux 內核在使用者進程和檔案系統實現之間引入了一個抽象層 VFS。虛擬檔案系統基於檔案通用模型(common file model,簡稱CFM)實現這種抽象,它是 Linux 上所有檔案系統的基礎。
一方面,VFS 提供了一種操作檔案、目錄及其他物件的統一方法。另一方面,它與各種具體的檔案系統的實現達成妥協。我們可以認爲,是虛擬檔案系統 (VFS) 和通用檔案模型 (CFM) 的共同作用爲 Linux 提供了存取不同檔案系統以及不同類型的檔案的 統一API (open()、read()、write()、close())。在本文中,我們將重點放在檔案上,忽略檔案系統相關的東西。
在 VFS 中,並非所有檔案系統都支援同樣的功能,有些操作對普通檔案是不可缺少的,對某些物件則完全沒有意義。即並非每一種檔案系統都支援 VFS 中的所有抽象。
Linux VFS 的實現: 參考 ext2 檔案系統,提供一種結構模型,該檔案系統模型包含了一個強大檔案系統所應具備的所有元件。但該模型是虛擬的,它適應於各種真實的檔案系統。所有實現都必須提供可以適應 VFS 定義的結構體的 routines,因此可以充當兩個檢視之間的過渡。
在 VFS 中,每個檔案都關聯到一個 inode,我們可以 以 inode 和 inode->file_operations 作爲學習通用檔案模型和虛擬檔案系統的切入點。
struct inode {
umode_t i_mode;
…
const struct file_operations *i_fop;
…
}
struct file_operations {
struct module *owner;
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 *);
…
long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
int (*mmap) (struct file *, struct vm_area_struct *);
int (*open) (struct inode *, struct file *);
int (*flush) (struct file *, fl_owner_t id);
…
} __randomize_layout;
5. 通用檔案模型:檔案描述符和開啓檔案的關係
5.1 相關的內核數據結構
內核使用 3 種數據結構來表示一個被開啓的檔案:
進程級的檔案描述符表 ( file descriptor table )。
系統級的開啓檔案表 ( open file table ) 。
檔案系統的 i-node 表 ( i-node table )。
struct task_struct {
…
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
-> struct fdtable *fdt;
…
}
每個檔案描述符包含:
1> 檔案描述符標誌 ( file descriptor flags,目前只有一個:close_on_exec,暫不關心 );
2> 指向一個開啓檔案表項 ( open file table entry) 的指針。
struct fdtable {
…
struct file *fd; / current fd array */
unsigned long *close_on_exec;
…
};
1> 檔案狀態標誌 ( file status flags,即 open() 的 flags 參數);
2> 當前檔案偏移量 ( current file offset );
3> 指向該檔案 inode 表項的指針 (在某些 UNIX 系統中是 vnode pointer,在 Linux 中是 inode pointer)。
inode 結構體和 vnode 結構體名稱雖然不同,但是 2 者其實是同一個概念,它們都用於描述儲存在硬碟中的檔案系統的 inode 數據。注意區別記憶體裡的 inode 結構體物件和硬碟中的 inode 數據。
檔案型別和對此檔案進行各種操作函數的指針。
對於大多數檔案,inode 物件還包含了指向該檔案系統 inode 數據的指針。
struct inode {
…
/* Stat data, not accessed from path walking */
unsigned long i_ino;
...
/* former ->i_op->default_file_ops */
const struct file_operations *i_fop;
}
這些資訊是在開啓檔案時從硬碟上讀入記憶體的,所以,檔案的所有相關資訊都是隨時可用的。即 inode 物件包含了檔案的所有者、檔案長度、指向檔案實際數據塊在磁碟上所在位置的指針等。
上述三張表的完整關係如下:
5.2 列舉幾種開啓檔案的情景
第一個進程在檔案描述符 3 上開啓該檔案,而另一個進程在檔案描述符 4 上開啓該檔案。開啓該檔案的每個進程都獲得各自的一個開啓檔案表項,但對一個給定的檔案只有一個 inode 節點表項。
之所以每個進程都獲得自己的開啓檔案表項,是因爲這可以使每個進程都有它自己的對該檔案的當前偏移量。
$ man 2 dup
#include <unistd.h>
int dup(int oldfd);
dup(1)後的內核數據結構:
dup() 返回的新檔案描述符與參數 oldfd 共用同一個開啓檔案表項。
假定所用的描述符是在fork之前開啓的,如果父進程和子進程寫同一描述符指向的檔案,但又沒有任何形式的同步,如使父進程等待子進程,那麼它們的輸出就會相互混合。
有相同愛好的可以進來一起討論哦:企鵝羣號:1046795523
學習視訊資料:http://www.makeru.com.cn/course/details/2058?s=143793