每個進程在Linux內核中都有一個task_struct結構體來維護進程相關的 信息,稱為進程描述符(Process Descriptor),而在操作系統理論中稱為進程控制塊 (PCB,Process Control Block)。task_struct中有一個指針(struct files_struct *files; )指向files_struct結構體,稱為文件 描述符表,其中每個表項包含一個指向已打開的文件的指針,如下圖所示。
用戶程序不能直接訪問內核中的文件描述符表,而只能使用文件描述符表的索引 (即0、1、2、3這些數字),這些索引就稱為文件描述符(File Descriptor),用int 型變量保存。 當調用open 打開一個文件或創建一個新文件時,內核分配一個文件描述符並返回給用戶程序,該文件描述符表項中的指針指向新打開的文件。當讀寫文件時,用戶程序把文件描述符傳給read 或write ,內核根據文件描述符找到相應的表項,再通過表項中的指針找到相應的文件。
已打開的文件在內核中用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 成員所指向的內核函數(如內核代碼中實現函數可能為sys_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和權限位的概念,這是硬湊出來的。
在UNIX系統中,用戶通過終端登錄系統後得到一個Shell進程,這個終端成為Shell進程的控制終端 (Controlling Terminal),控制終端是保存在PCB中的信息,而我們知 道fork 會復制PCB中的信息,因此由Shell進程啟動的其它進程的控制終端也是這個終端。
默認情況 下(沒有重定向),每個進程的標准輸入(stdin)、標准輸出(stdout)和標准錯誤輸出(stdout)都指向控制終端,因為在程序啟動時(在main 函數還 沒開始執行之前)會自動把控制終端打開三次,分別賦給三個FILE *指 針stdin 、stdout和stderr,這三個文件指針是libc 中定義的全局變量,這三個文件的描述符分別是0、1、2,保存在相應的FILE 結構體中。進程從標准輸入讀也就是讀用戶的鍵盤輸入,進程往標准輸出或標准錯誤輸出寫也就是輸出到顯示器上
頭文件unistd.h 中有如下的宏定義來表示這三個文件描述符:
#define STDIN_FILENO 0
#define STDOUT_FILENO 1
#define STDERR_FILENO 2
每個進程都可以通過一個特殊的設備文件/dev/tty(字符設備c) 訪問它的控制終端。事實上每個終端設備都對應一個不同的設備文件,/dev/tty 提供了一個通用的接口,一個進程要訪問它的控制終端既可以通過/dev/tty 也可以通過該終端設備所對應的設備文件來訪問。ttyname函數可以由文件描述符查出對應的文件名,該文件描述符必須指向一個終端設備而不 能是任意文件。不同的終端所對應的設備文件名可以是/dev/pts/?, /dev/tty?等
simba@simba-Aspire-4752:~$ ls -l /dev/tty
crw-rw-rw- 1 root tty 5, 0 Jan 29 09:46 /dev/tty
開頭的c表示文件類型是字符設備。中間的5, 0是它的設備號,主設備號5,次設備號0,主設備號 標識內核中的一個設備驅動程序,次設備號標識該設備驅動程序管理的一個設備。內核通過設備號 找到相應的驅動程序,完成對該設備的操作。我們知道常規文件的這一列應該顯示文件尺寸,而設 備文件的這一列顯示設備號,這表明設備文件是沒有文件尺寸這個屬性的,因為設備文件在磁盤上 不保存數據,對設備文件做讀寫操作並不是讀寫磁盤上的數據,而是在讀寫設備。
由open 返回的文件描述符一定是該進程尚未使用的最小描述符。由於程序啟動時自動打開文件描述符0、1、2,因此第一次調用open打開文件通常會返回描述符3,再調用open 就會返回4。可以利用 這一點在標准輸入、標准輸出或標准錯誤輸出上打開一個新文件,實現重定向的功能。例如,首先 調用close 關閉文件描述符1,然後調用open 打開一個常規文件,則一定會返回文件描述符1,這時候標准輸出就不再是終端,而是一個常規文件了,再調用printf就不會打印到屏幕上,而是寫到這 個文件中了。
需要說明的是,當一個進程終止時,內核對該進程所有尚未關閉的 文件描述符調用close 關閉,所以即使用戶程序不調用close ,在終止時內核也會自動關閉它打開的 所有文件。但是對於一個長年累月運行的程序(比如網絡服務器),打開的文件描述符一定要記得 關閉,否則隨著打開的文件越來越多,會占用大量文件描述符和系統資源。
------------------------------------------------------------------------------------------------------------------------------------
傳統的Unix既有v節點(vnode)也有i節點(inode),vnode的數據結構中包含了inode信息。但在Linux中沒有使用vnode,而使用了通用inode。“實現雖不同,但在概念上是一樣的。”
vnode (“virtual node”)僅在文件打開的時候,才出現的;而inode定位文件在磁盤的位置,它的信息本身是存儲在磁盤等上的,當打開文件的時候從磁盤上讀入內存。
inode結構體記錄了很多關於文件的信息,比如文件長度,文件所在的設備,文件的物理位置,創建、修改和更新時間等等,特別的,它不包含文件名!目錄下的所有文件名和目錄名都存儲在目錄的數據塊中,即如下圖的目錄塊。對於常規文件,文件的數據存儲在數據塊中,一個文件通常占用一個inode,但往往要占用多個數據塊,數據塊是在分區進行文件系統格式化時所指定的“最小存儲單位”,塊的大小為扇區的2^n倍,一個扇區512B。
如果多個inode指向同一個數據塊的時候,是不是就可以實現熟悉的鏈接了?!這就是軟連接的原理,新建一個文件(一個符號鏈接文件,文件的屬性中有明確說明它是一個符號鏈接文件),為需要鏈接的文件分配一個新的inode,然後指向同一個數據塊。 當我們用ls 查看某個目錄或文件時,如果加上-i 參數,就可以看到inode節點了;比如ls -li lsfile.sh ,最前面的數值就是inode信息。
多個文件共用一個inode,同樣可以實現鏈接?!這就是硬鏈接的原理,inode中有鏈接計數器,當增加一個文件指向這個inode時,計數器增1。特別的,當計數器為0時候,文件才真正從磁盤刪除。即ls -l 命令輸出中的第二欄。