Linux系統(tǒng)編程-通用文件模型
寫(xiě)作目的:
-
學(xué)習(xí) Linux 文件模型相關(guān)的知識(shí)。
正文目錄:
1. Linux 的兩大抽象
2. 文件類型
3. 文件描述符
4. 通用文件模型:簡(jiǎn)介
4.1 演示 demo
4.2 相關(guān)要點(diǎn): 與 VFS 的關(guān)系
5. 通用文件模型:文件描述符和打開(kāi)文件的關(guān)系
5.1 相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)
5.2 列舉幾種打開(kāi)文件的情景
1. Linux 的兩大抽象
-
文件是 Linux 系統(tǒng)中最基礎(chǔ)最重要的抽象。Linux 遵循一切皆文件的理念。很多交互操作是通過(guò)讀寫(xiě)文件來(lái)完成,即使所涉及的對(duì)象看起來(lái)并非普通文件。
-
另外一大抽象是進(jìn)程。如果說(shuō)文件是 Linux 系統(tǒng)最重要的抽象概念,進(jìn)程則僅次于文件。
-
進(jìn)程相關(guān)的實(shí)現(xiàn)復(fù)雜且多變,而文件 IO 的實(shí)現(xiàn)則相對(duì)穩(wěn)定很多,且更貼近我們的日常操作,所以 以文件作為學(xué)習(xí) Linux 內(nèi)核的切入點(diǎn)是個(gè)更好的選擇。
2. 文件類型
Linux 系統(tǒng)的大多數(shù)文件是普通文件或目錄,但是也有另外一些文件類型,具體包括如下幾種:
-
普通文件 ( regular file )。
-
最常用的文件類型,包含了某種形式的數(shù)據(jù)。至于這種數(shù)據(jù)是文本還是二進(jìn)制數(shù)據(jù),對(duì)于 Linux 內(nèi)核而言并無(wú)區(qū)別。
-
文件中包含的字節(jié)可以是任意值,可以以任意方式進(jìn)行組織。在系統(tǒng)層,除了字節(jié)流,Linux 對(duì)文件結(jié)構(gòu)沒(méi)有特定要求。
-
對(duì)普通文件內(nèi)容的解釋由處理該文件的應(yīng)用程序進(jìn)行。
-
文件雖然是通過(guò)文件名訪問(wèn),但文件本身其實(shí)并沒(méi)有直接和文件名關(guān)聯(lián)。相反地,與文件關(guān)聯(lián)的是索引節(jié)點(diǎn) (inode,是index node 縮寫(xiě))。針對(duì)駐留于文件系統(tǒng)上的每個(gè)文件,文件系統(tǒng)都會(huì)為其分配一個(gè) inode。inode 中會(huì)保存和文件相關(guān)的元數(shù)據(jù),如文件修改時(shí)間戳、所有者、類型、長(zhǎng)度以及文件數(shù)據(jù)的位置,但不含文件名,文件名由目錄文件負(fù)責(zé)。
-
inode 由 inode number 來(lái)標(biāo)識(shí),可以通過(guò) “l(fā)s –li” 查看文件的 inode number。
# ls -li minicom.log
12582945 -rw-r--r-- 1 root root 665 Jul 10 18:47 minicom.log -
目錄文件 ( directory file )。
-
目錄也是一種文件類型,這種文件包含了其他文件的文件名以及 inode number。文件通常是通過(guò)文件名從用戶空間打開(kāi),目錄用于提供訪問(wèn)文件時(shí)需要的名稱。
-
文件名和 inode 之間的配對(duì)稱為鏈接 (link)。映射在物理磁盤(pán)上的形式,如簡(jiǎn)單的表或散列,是通過(guò)特定文件系統(tǒng)的內(nèi)核代碼來(lái)實(shí)現(xiàn)和管理的。
-
如果用戶空間的應(yīng)用請(qǐng)求打開(kāi)指定文件,內(nèi)核會(huì)打開(kāi)包含該文件名的目錄,然后根據(jù)文件名獲取 inode number。通過(guò) inode number 可以找到 inode。inode 包含和文件關(guān)聯(lián)的元數(shù)據(jù),其中包括文件數(shù)據(jù)在磁盤(pán)上的存儲(chǔ)位置。
-
硬鏈接 ( hard link )。
-
不同的文件名可以鏈接到到同一個(gè) inode。當(dāng)不同名稱的多個(gè)鏈接映射到同一個(gè)索引節(jié)點(diǎn)時(shí),我們稱該鏈接為硬鏈接。
-
硬鏈接通常要求鏈接和文件位于同一文件系統(tǒng)中。
-
在底層文件系統(tǒng)支持的前提下,也只有超級(jí)用戶才能創(chuàng)建指向目錄的硬鏈接。
-
符號(hào)鏈接 ( symbolic link )。
-
符號(hào)鏈接是對(duì)一個(gè)文件的間接指針,它與硬鏈接有所不同,硬鏈接直接指向文件的 inode。引入符號(hào)鏈接的原因是為了避開(kāi)硬鏈接的一些限制。
-
硬鏈接不能跨越多個(gè)文件系統(tǒng),因?yàn)?inode number在自己的文件系統(tǒng)之外沒(méi)有任何意義。為了跨越文件系統(tǒng)建立鏈接,Linux 系統(tǒng)實(shí)現(xiàn)了符號(hào)鏈接。
-
特殊文件 (special file)。
-
特殊文件是使得某些抽象可以適用于文件系統(tǒng),貫徹一切皆文件的理念。
-
Linux 只支持四種特殊文件:塊設(shè)備文件、字符設(shè)備文件、命名管道 以及 UNIX域套接字。
-
塊特殊文件 ( block device file )。提供對(duì)設(shè)備(如磁盤(pán))帶緩沖的訪問(wèn),每次訪問(wèn)以固定長(zhǎng)度為單位進(jìn)行。
-
字符特殊文件 ( character device file )。這種類型的文件提供對(duì)設(shè)備不帶緩沖的訪問(wèn),每次訪問(wèn)長(zhǎng)度可變。系統(tǒng)中的所有設(shè)備要么是字符特殊文件,要么是塊特殊文件。
-
命名管道 ( named pipes ),通常稱為 FIFO,是以文件描述符作為通信信道的 IPC 機(jī)制,它可以通過(guò)特殊文件來(lái)訪問(wèn)。
-
套接字 ( socket ) 是最后一種特殊文件。socket 是進(jìn)程間通信的高級(jí)形式,支持不同進(jìn)程間的通信,這兩個(gè)進(jìn)程可以在同一臺(tái)機(jī)器,也可以在不同機(jī)器。socket 是網(wǎng)絡(luò)和互聯(lián)網(wǎng)編程的基礎(chǔ)。
在 Linux,可以用 ls/stat 命令 和 stat() 系統(tǒng)調(diào)用確定文件類型。
$ 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 中,文件必須先打開(kāi)才能訪問(wèn)。對(duì)于內(nèi)核而言,所有打開(kāi)的文件都通過(guò)文件描述符 ( file descriptor,簡(jiǎn)稱fd ) 引用。文件描述符是一個(gè)非負(fù)整數(shù)。當(dāng)打開(kāi)一個(gè)現(xiàn)有文件或創(chuàng)建一個(gè)新文件時(shí),內(nèi)核向進(jìn)程返回一個(gè)文件描述符。當(dāng)讀、寫(xiě)一個(gè)文件時(shí),使用 open() 或 creat() 返回的文件描述符標(biāo)識(shí)該文件,將其作為參數(shù)傳送給 read() 或 write()。
-
Linux 系統(tǒng)編程的大部分工作都會(huì)涉及打開(kāi)、操縱、關(guān)閉以及其他文件描述符操作;
-
Linux 系統(tǒng)的 Shell 把文件描述符 0 與進(jìn)程的標(biāo)準(zhǔn)輸入 stdin 關(guān)聯(lián),文件描述符 1 與標(biāo)準(zhǔn)輸出 stdout 關(guān)聯(lián),文件描述符 2 與標(biāo)準(zhǔn)錯(cuò)誤 stderr 關(guān)聯(lián)。這是各種 Shell 以及很多應(yīng)用程序使用的慣例,與 Linux 內(nèi)核無(wú)關(guān)。如果不遵循這種慣例,很多 Linux 系統(tǒng)應(yīng)用程序就不能正常工作;
-
用戶可以重定向文件描述符,甚至可以通過(guò)管道把一個(gè)程序的輸出作為另一個(gè)程序的輸入。Shell 就是通過(guò)這種方式實(shí)現(xiàn)重定向和管道的。
-
在 POSIX 標(biāo)準(zhǔn)中,幻數(shù) 0、1、2 雖然已被標(biāo)準(zhǔn)化,但應(yīng)當(dāng)把它們替換成符號(hào)常量 STDIN_FILENO、STDOUT_FILENO 和 STDERR_FILENO 以提高可讀性;
-
文件描述符的范圍是 0 ~ OPEN_MAX-1;
-
文件描述符并非局限于訪問(wèn)普通文件。實(shí)際上,文件描述符也可以訪問(wèn)設(shè)備文件、管道、FIFO、Socket等。遵循一切皆文件的理念,幾乎任何能夠讀寫(xiě)的東西都可以通過(guò)文件描述符來(lái)訪問(wèn)。
4. 通用文件模型:簡(jiǎn)介
Linux 通用文件模型最為顯著的特性之一就是 I/O 通用性。也就是說(shuō),同一套系統(tǒng)調(diào)用 open()、read()、write()、close() 等所執(zhí)行的 I/O 操作,可施之于所有文件類型,包括設(shè)備文件在內(nèi)。應(yīng)用程序發(fā)起的I/O請(qǐng)求,內(nèi)核會(huì)將其轉(zhuǎn)化為相應(yīng)的文件系統(tǒng)操作,或者設(shè)備驅(qū)動(dòng)程序操作,以此來(lái)執(zhí)行針對(duì)目標(biāo)文件或設(shè)備的I/O操作。因此,采用這些系統(tǒng)調(dià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);
}
運(yùn)行效果:
$ ./copy test test.old
$ ./copy test /dev/tty
$ ./copy /dev/tty abc.txt
相關(guān)要點(diǎn):
-
要實(shí)現(xiàn)通用 I/O,就必須確保每一種文件系統(tǒng)和每一種文件類型(包括設(shè)備文件)都實(shí)現(xiàn)了相同的 I/O 系統(tǒng)調(diào)用集。由于文件系統(tǒng)或設(shè)備文件所特有的操作細(xì)節(jié)在內(nèi)核中處理,在編程時(shí)通??梢院雎栽O(shè)備專有的因素。一旦應(yīng)用程序需要訪問(wèn)文件系統(tǒng)或設(shè)備的專有功能時(shí),可以選擇瑞士軍刀般的 ioctl() 系統(tǒng)調(diào)用,該調(diào)用為通用 I/O 模型之外的專有特性提供了訪問(wèn)接口。
-
提到通用 I/O,就必須提起虛擬文件系統(tǒng) (VFS)。為支持各種本機(jī)文件系統(tǒng),且在同時(shí)允許訪問(wèn)其他操作系統(tǒng)的文件,Linux 內(nèi)核在用戶進(jìn)程和文件系統(tǒng)實(shí)現(xiàn)之間引入了一個(gè)抽象層 VFS。虛擬文件系統(tǒng)基于文件通用模型(common file model,簡(jiǎn)稱CFM)實(shí)現(xiàn)這種抽象,它是 Linux 上所有文件系統(tǒng)的基礎(chǔ)。
-
一方面,VFS 提供了一種操作文件、目錄及其他對(duì)象的統(tǒng)一方法。另一方面,它與各種具體的文件系統(tǒng)的實(shí)現(xiàn)達(dá)成妥協(xié)。我們可以認(rèn)為,是虛擬文件系統(tǒng) (VFS) 和通用文件模型 (CFM) 的共同作用為 Linux 提供了訪問(wèn)不同文件系統(tǒng)以及不同類型的文件的 統(tǒng)一API (open()、read()、write()、close())。在本文中,我們將重點(diǎn)放在文件上,忽略文件系統(tǒng)相關(guān)的東西。
-
在 VFS 中,并非所有文件系統(tǒng)都支持同樣的功能,有些操作對(duì)普通文件是不可缺少的,對(duì)某些對(duì)象則完全沒(méi)有意義。即并非每一種文件系統(tǒng)都支持 VFS 中的所有抽象。
-
Linux VFS 的實(shí)現(xiàn): 參考 ext2 文件系統(tǒng),提供一種結(jié)構(gòu)模型,該文件系統(tǒng)模型包含了一個(gè)強(qiáng)大文件系統(tǒng)所應(yīng)具備的所有組件。但該模型是虛擬的,它適應(yīng)于各種真實(shí)的文件系統(tǒng)。所有實(shí)現(xiàn)都必須提供可以適應(yīng) VFS 定義的結(jié)構(gòu)體的 routines,因此可以充當(dāng)兩個(gè)視圖之間的過(guò)渡。
-
在 VFS 中,每個(gè)文件都關(guān)聯(lián)到一個(gè) inode,我們可以 以 inode 和 inode->file_operations 作為學(xué)習(xí)通用文件模型和虛擬文件系統(tǒng)的切入點(diǎn)。
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. 通用文件模型:文件描述符和打開(kāi)文件的關(guān)系
5.1 相關(guān)的內(nèi)核數(shù)據(jù)結(jié)構(gòu)
內(nèi)核使用 3 種數(shù)據(jù)結(jié)構(gòu)來(lái)表示一個(gè)被打開(kāi)的文件:
-
進(jìn)程級(jí)的文件描述符表 ( file descriptor table )。
-
系統(tǒng)級(jí)的打開(kāi)文件表 ( open file table ) 。
-
文件系統(tǒng)的 i-node 表 ( i-node table )。
1) 進(jìn)程級(jí)的文件描述符表 ( file descriptor table )
每個(gè)進(jìn)程在進(jìn)程表 (process table) 中都有一個(gè)記錄項(xiàng) (process table entry),即 struct task_struct,內(nèi)核用它來(lái)描述一個(gè)進(jìn)程。在 struct task_struct 中包含了一張打開(kāi)文件描述符表 (open file descriptors table),由 struct files_struct 里的 struct fdtable 來(lái)表示 (Linux-4.14):
struct task_struct {
...
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
-> struct fdtable *fdt;
...
}
每個(gè)文件描述符包含:
-
1> 文件描述符標(biāo)志 ( file descriptor flags,目前只有一個(gè):close_on_exec,暫不關(guān)心 ); -
2> 指向一個(gè)打開(kāi)文件表項(xiàng) ( open file table entry) 的指針。
struct fdtable {
...
struct file **fd; /* current fd array */
unsigned long *close_on_exec;
...
};
2) 系統(tǒng)級(jí)的打開(kāi)文件表 ( open file table )
內(nèi)核為所有打開(kāi)文件維持一張打開(kāi)文件表。每個(gè)打開(kāi)文件表項(xiàng)包含:
-
1> 文件狀態(tài)標(biāo)志 ( file status flags,即 open() 的 flags 參數(shù));
-
2> 當(dāng)前文件偏移量 ( current file offset );
-
3> 指向該文件 inode 表項(xiàng)的指針 (在某些 UNIX 系統(tǒng)中是 vnode pointer,在 Linux 中是 inode pointer)。
inode 結(jié)構(gòu)體和 vnode 結(jié)構(gòu)體名稱雖然不同,但是 2 者其實(shí)是同一個(gè)概念,它們都用于描述存儲(chǔ)在硬盤(pán)中的文件系統(tǒng)的 inode 數(shù)據(jù)。注意區(qū)別內(nèi)存里的 inode 結(jié)構(gòu)體對(duì)象和硬盤(pán)中的 inode 數(shù)據(jù)。
3) 文件系統(tǒng)的 i-node 表 ( i-node table )
每個(gè)打開(kāi)文件都有一個(gè) inode 對(duì)象。inode 對(duì)象包含了:
-
文件類型和對(duì)此文件進(jìn)行各種操作函數(shù)的指針。
-
對(duì)于大多數(shù)文件,inode 對(duì)象還包含了指向該文件系統(tǒng) inode 數(shù)據(jù)的指針。
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;
}
這些信息是在打開(kāi)文件時(shí)從硬盤(pán)上讀入內(nèi)存的,所以,文件的所有相關(guān)信息都是隨時(shí)可用的。即 inode 對(duì)象包含了文件的所有者、文件長(zhǎng)度、指向文件實(shí)際數(shù)據(jù)塊在磁盤(pán)上所在位置的指針等。
上述三張表的完整關(guān)系如下:
5.2 列舉幾種打開(kāi)文件的情景
1) 兩個(gè)獨(dú)立進(jìn)程各自打開(kāi)同一個(gè)文件
兩個(gè)獨(dú)立進(jìn)程各自打開(kāi)了同一文件,則有如下關(guān)系:
第一個(gè)進(jìn)程在文件描述符 3 上打開(kāi)該文件,而另一個(gè)進(jìn)程在文件描述符 4 上打開(kāi)該文件。打開(kāi)該文件的每個(gè)進(jìn)程都獲得各自的一個(gè)打開(kāi)文件表項(xiàng),但對(duì)一個(gè)給定的文件只有一個(gè) inode 節(jié)點(diǎn)表項(xiàng)。
之所以每個(gè)進(jìn)程都獲得自己的打開(kāi)文件表項(xiàng),是因?yàn)檫@可以使每個(gè)進(jìn)程都有它自己的對(duì)該文件的當(dāng)前偏移量。
2) dup(1) 復(fù)制文件描述符
dup() 用來(lái)復(fù)制一個(gè)現(xiàn)有的文件描述符。
$ man 2 dup
#include <unistd.h>
int dup(int oldfd);
dup(1)后的內(nèi)核數(shù)據(jù)結(jié)構(gòu):
dup() 返回的新文件描述符與參數(shù) oldfd 共享同一個(gè)打開(kāi)文件表項(xiàng)。
3) fork 之后父進(jìn)程和子進(jìn)程之間對(duì)打開(kāi)文件的共享
假定所用的描述符是在fork之前打開(kāi)的,如果父進(jìn)程和子進(jìn)程寫(xiě)同一描述符指向的文件,但又沒(méi)有任何形式的同步,如使父進(jìn)程等待子進(jìn)程,那么它們的輸出就會(huì)相互混合。
三、總結(jié)
不好意思,這周身體不太舒服,文章拖更了,各位見(jiàn)諒。
鑒于大多數(shù)人的注意力無(wú)法在一篇文章里上集中太久,更多的內(nèi)容請(qǐng)大家先自行去閱讀吧,不是自己理解到的東西是消化不了的。有機(jī)會(huì)的話我會(huì)把更多的讀書(shū)心得放在后面的文章。
更多值得學(xué)習(xí)的知識(shí)點(diǎn)
-
stat() 的使用方法; -
復(fù)制文件描述符的方法 (dup, fcntl) 與使用場(chǎng)景; -
目錄相關(guān)的操作; -
高級(jí)文件 io 接口; -
文件 io 與標(biāo)準(zhǔn) io 的對(duì)比; -
VFS 的具體實(shí)現(xiàn); -
ext2 文件系統(tǒng)的實(shí)現(xiàn); -
...
四、相關(guān)參考
1. 參考書(shū)籍
-
《Linux 程序設(shè)計(jì)》(BLP)
-
3 - 文件操作 -
《Linux 系統(tǒng)編程》(LSP)
-
1.4.1 - 文件和文件系統(tǒng) -
《UNIX 環(huán)境高級(jí)編程》(APUE)
-
3.10 - 文件共享 -
3.12 - dup -
4.3 - 文件類型 -
8.3 - fork 文件共享 -
《Linux/UNIX 系統(tǒng)編程手冊(cè)》(TLPI)
-
2.5 - 文件I/O模型 -
4 - 文件I/O:通用的I/O模型 -
5.4 - 文件描述符和打開(kāi)文件之間的關(guān)系 -
《linux內(nèi)核設(shè)計(jì)與實(shí)現(xiàn)》(LKD)
-
13 - 虛擬文件系統(tǒng) -
《深入理解LINUX內(nèi)核》(ULK)
-
3.2 - 進(jìn)程描述符 -
12 - 虛擬文件系統(tǒng) -
《深入Linux內(nèi)核架構(gòu)》(PLKA)
-
6.3 - 與文件系統(tǒng)關(guān)聯(lián) -
8.2 - 通用文件模型 -
《UNIX 操作系統(tǒng)設(shè)計(jì)》
-
4 - 文件的內(nèi)部表示
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!