Linux支持各種各樣的檔案系統格式,如ext2、ext3、reiserfs、FAT、NTFS、iso9660等等,不同的磁碟分區、光盤或其它存儲設備都有不同的檔案系統格式,然而這些檔案系統都可以mount
到某個目錄下,使我們看到一個統一的目錄樹,各種檔案系統上的目錄和檔案我們用ls
命令看起來是一樣的,讀寫操作用起來也都是一樣的,這是怎麼做到的呢?Linux內核在各種不同的檔案系統格式之上做了一個抽象層,使得檔案、目錄、讀寫訪問等概唸成為抽象層的概念,因此各種檔案系統看起來用起來都一樣,這個抽象層稱為虛擬檔案系統(VFS,Virtual Filesystem)。上一節我們介紹了一種典型的檔案系統在磁碟上的存儲佈局,這一節我們介紹運行時檔案系統在內核中的表示。
Linux內核的VFS子系統可以圖示如下:
在第 28 章 檔案與I/O中講過,每個進程在PCB(Process Control Block)中都保存着一份檔案描述符表,檔案描述符就是這個表的索引,每個表項都有一個指向已打開檔案的指針,現在我們明確一下:已打開的檔案在內核中用file
結構體表示,檔案描述符表中的指針指向file
結構體。
在file
結構體中維護File Status Flag(file
結構體的成員f_flags
)和當前讀寫位置(file
結構體的成員f_pos
)。在上圖中,進程1和進程2都打開同一檔案,但是對應不同的file
結構體,因此可以有不同的File Status Flag和讀寫位置。file
結構體中比較重要的成員還有f_count
,表示引用計數(Reference Count),後面我們會講到,dup
、fork
等系統調用會導致多個檔案描述符指向同一個file
結構體,例如有fd1
和fd2
都引用同一個file
結構體,那麼它的引用計數就是2,當close(fd1)
時並不會釋放file
結構體,而只是把引用計數減到1,如果再close(fd2)
,引用計數就會減到0同時釋放file
結構體,這才真的關閉了檔案。
每個file
結構體都指向一個file_operations
結構體,這個結構體的成員都是函數指針,指向實現各種檔案操作的內核函數。比如在用戶程序中read
一個檔案描述符,read
通過系統調用進入內核,然後找到這個檔案描述符所指向的file
結構體,找到file
結構體所指向的file_operations
結構體,調用它的read
成員所指向的內核函數以完成用戶請求。在用戶程序中調用lseek
、read
、write
、ioctl
、open
等函數,最終都由內核調用file_operations
的各成員所指向的內核函數完成用戶請求。file_operations
結構體中的release
成員用於完成用戶程序的close
請求,之所以叫release
而不叫close
是因為它不一定真的關閉檔案,而是減少引用計數,只有引用計數減到0才關閉檔案。對於同一個檔案系統上打開的常規檔案來說,read
、write
等檔案操作的步驟和方法應該是一樣的,調用的函數應該是相同的,所以圖中的三個打開檔案的file
結構體指向同一個file_operations
結構體。如果打開一個字元設備檔案,那麼它的read
、write
操作肯定和常規檔案不一樣,不是讀寫磁碟的數據塊而是讀寫硬件設備,所以file
結構體應該指向不同的file_operations
結構體,其中的各種檔案操作函數由該設備的驅動程式實現。
每個file
結構體都有一個指向dentry
結構體的指針,“dentry”是directory entry(目錄項)的縮寫。我們傳給open
、stat
等函數的參數的是一個路徑,例如/home/akaedu/a
,需要根據路徑找到檔案的inode。為了減少讀盤次數,內核緩存了目錄的樹狀結構,稱為dentry cache,其中每個節點是一個dentry
結構體,只要沿著路徑各部分的dentry搜索即可,從根目錄/
找到home
目錄,然後找到akaedu
目錄,然後找到檔案a
。dentry cache只保存最近訪問過的目錄項,如果要找的目錄項在cache中沒有,就要從磁碟讀到內存中。
每個dentry
結構體都有一個指針指向inode
結構體。inode
結構體保存着從磁碟inode讀上來的信息。在上圖的例子中,有兩個dentry,分別表示/home/akaedu/a
和/home/akaedu/b
,它們都指向同一個inode,說明這兩個檔案互為硬連結。inode
結構體中保存着從磁碟分區的inode讀上來信息,例如所有者、檔案大小、檔案類型和權限位等。每個inode
結構體都有一個指向inode_operations
結構體的指針,後者也是一組函數指針指向一些完成檔案目錄操作的內核函數。和file_operations
不同,inode_operations
所指向的不是針對某一個檔案進行操作的函數,而是影響檔案和目錄佈局的函數,例如添加刪除檔案和目錄、跟蹤符號連結等等,屬於同一檔案系統的各inode
結構體可以指向同一個inode_operations
結構體。
inode
結構體有一個指向super_block
結構體的指針。super_block
結構體保存着從磁碟分區的超級塊讀上來的信息,例如檔案系統類型、塊大小等。super_block
結構體的s_root
成員是一個指向dentry
的指針,表示這個檔案系統的根目錄被mount
到哪裡,在上圖的例子中這個分區被mount
到/home
目錄下。
file
、dentry
、inode
、super_block
這幾個結構體組成了VFS的核心概念。對於ext2檔案系統來說,在磁碟存儲佈局上也有inode和超級塊的概念,所以很容易和VFS中的概念建立對應關係。而另外一些檔案系統格式來自非UNIX系統(例如Windows的FAT32、NTFS),可能沒有inode或超級塊這樣的概念,但為了能mount
到Linux系統,也只好在驅動程式中硬湊一下,在Linux下看FAT32和NTFS分區會發現權限位是錯的,所有檔案都是rwxrwxrwx
,因為它們本來就沒有inode和權限位的概念,這是硬湊出來的。
dup
和dup2
都可用來複制一個現存的檔案描述符,使兩個檔案描述符指向同一個file
結構體。如果兩個檔案描述符指向同一個file
結構體,File Status Flag和讀寫位置只保存一份在file
結構體中,並且file
結構體的引用計數是2。如果兩次open
同一檔案得到兩個檔案描述符,則每個描述符對應一個不同的file
結構體,可以有不同的File Status Flag和讀寫位置。請注意區分這兩種情況。
#include <unistd.h> int dup(int oldfd); int dup2(int oldfd, int newfd);
如果調用成功,這兩個函數都返回新分配或指定的檔案描述符,如果出錯則返回-1。dup
返回的新檔案描述符一定該進程未使用的最小檔案描述符,這一點和open
類似。dup2
可以用newfd
參數指定新描述符的數值。如果newfd
當前已經打開,則先將其關閉再做dup2
操作,如果oldfd
等於newfd
,則dup2
直接返回newfd
而不用先關閉newfd
再複製。
下面這個例子演示了dup
和dup2
函數的用法,請結合後面的連環畫理解程序的執行過程。
例 29.2. dup和dup2示常式序
#include <unistd.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int fd, save_fd; char msg[] = "This is a test\n"; fd = open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR); if(fd<0) { perror("open"); exit(1); } save_fd = dup(STDOUT_FILENO); dup2(fd, STDOUT_FILENO); close(fd); write(STDOUT_FILENO, msg, strlen(msg)); dup2(save_fd, STDOUT_FILENO); write(STDOUT_FILENO, msg, strlen(msg)); close(save_fd); return 0; }
重點解釋兩個地方:
第3幅圖,要執行dup2(fd, 1);
,檔案描述符1原本指向tty
,現在要指向新的檔案somefile
,就把原來的關閉了,但是tty
這個檔案原本有兩個引用計數,還有檔案描述符save_fd
也指向它,所以只是將引用計數減1,並不真的關閉檔案。
第5幅圖,要執行dup2(save_fd, 1);
,檔案描述符1原本指向somefile
,現在要指向新的檔案tty
,就把原來的關閉了,somefile
原本只有一個引用計數,所以這次減到0,是真的關閉了。