8.2.4 與進程相關的文件結構
在具體介紹這幾個結構以前,我們需要解釋一下文件描述符、打開的文件描述、系統打開文件表、用戶打開文件表的概念以及它們的聯繫。
1.文件對象
在Linux中,進程是通過文件描述符(file descriptors,簡稱fd)而不是文件名來訪問文件的,文件描述符實際上是一個整數。Linux中規定每個進程能最多能同時使用NR_OPEN個文件描述符,這個值在fs.h中定義,爲1024*1024(2.0版中僅定義爲256)。
每個文件都有一個32位的數字來表示下一個讀寫的字節位置,這個數字叫做文件位置。每次打開一個文件,除非明確要求,否則文件位置都被置爲0,即文件的開始處,此後的讀或寫操作都將從文件的開始處執行,但你可以通過執行系統調用LSEEK(隨機存儲)對這個文件位置進行修改。Linux中專門用了一個數據結構file來保存打開文件的文件位置,這個結構稱爲打開的文件描述(open file description)。這個數據結構的設置是煞費苦心的,因爲它與進程的聯繫非常緊密,可以說這是VFS中一個比較難於理解的數據結構。
首先,爲什麼不把文件位置乾脆存放在索引節點中,而要多此一舉,設一個新的數據結構呢?我們知道,Linux中的文件是能夠共享的,假如把文件位置存放在索引節點中,則如果有兩個或更多個進程同時打開同一個文件時,它們將去訪問同一個索引節點,於是一個進程的LSEEK操作將影響到另一個進程的讀操作,這顯然是不允許也是不可想象的。
另一個想法是既然進程是通過文件描述符訪問文件的,爲什麼不用一個與文件描述符數組相平行的數組來保存每個打開文件的文件位置?這個想法也是不能實現的,原因就在於在生成一個新進程時,子進程要共享父進程的所有信息,包括文件描述符數組。
我們知道,一個文件不僅可以被不同的進程分別打開,而且也可以被同一個進程先後多次打開。一個進程如果先後多次打開同一個文件,則每一次打開都要分配一個新的文件描述符,並且指向一個新的file結構,儘管它們都指向同一個索引節點,但是,如果一個子進程不和父進程共享同一個file結構,而是也如上面一樣,分配一個新的file結構,會出現什麼情況了?讓我們來看一個例子:
假設有一個輸出重定位到某文件A的shell script(shell腳本),我們知道,shell是作爲一個進程運行的,當它生成第一個子進程時,將以0作爲A的文件位置開始輸出,假設輸出了2K的數據,則現在文件位置爲2K。然後,shell繼續讀取腳本,生成另一個子進程,它要共享shell的file結構,也就是共享文件位置,所以第二個進程的文件位置是2K,將接着第一個進程輸出內容的後面輸出。如果shell不和子進程共享文件位置,則第二個進程就有可能重寫第一個進程的輸出了,這顯然不是希望得到的結果。
至此,已經可以看出設置file結構的原因所在了。
file結構中主要保存了文件位置,此外,還把指向該文件索引節點的指針也放在其中。file結構形成一個雙鏈表,稱爲系統打開文件表,其最大長度是NR_FILE,在fs.h中定義爲8192。
file結構在include/linux/fs.h中定義如下:
struct file
{
struct list_head f_list; /*所有打開的文件形成一個鏈表*/
struct dentry *f_dentry; /*指向相關目錄項的指針*/
struct vfsmount *f_vfsmnt; /*指向VFS安裝點的指針*/
struct file_operations *f_op; /*指向文件操作表的指針*/
mode_t f_mode; /*文件的打開模式*/
loff_t f_pos; /*文件的當前位置*/
unsigned short f_flags; /*打開文件時所指定的標誌*/
unsigned short f_count; /*使用該結構的進程數*/
unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;
/*預讀標誌、要預讀的最多頁面數、上次預讀後的文件指針、預讀的字節數以及
預讀的頁面數*/
int f_owner; /* 通過信號進行異步I/O數據的傳送*/
unsigned int f_uid, f_gid; /*用戶的UID和GID*/
int f_error; /*網絡寫操作的錯誤碼*/
unsigned long f_version; /*版本號*/
void *private_data; /* tty驅動程序所需 */
};
每個文件對象總是包含在下列的一個雙向循環鏈表之中:
· “未使用”文件對象的鏈表。該鏈表既可以用做文件對象的內存高速緩存,又可以當作超級用戶的備用存儲器,也就是說,即使系統的動態內存用完,也允許超級用戶打開文件。由於這些對象是未使用的,它們的f_count域是NULL,該鏈表首元素的地址存放在變量free_list中,內核必須確認該鏈表總是至少包含NR_RESERVED_FILES個對象,通常該值設爲10。
· “正在使用”文件對的象鏈表:該鏈表中的每個元素至少由一個進程使用,因此,各個元素的f_count域不會爲NULL,該鏈表中第一個元素的地址存放在變量anon_list中。
如果VFS需要分配一個新的文件對象,就調用函數get_empty_filp( )。該函數檢測“未使用”文件對象鏈表的元素個數是否多於NR_RESERVED_FILES,如果是,可以爲新打開的文件使用其中的一個元素;如果沒有,則退回到正常的內存分配。
2.用戶打開文件表
每個進程用一個files_struct結構來記錄文件描述符的使用情況,這個files_struct結構稱爲用戶打開文件表,它是進程的私有數據。files_struct結構在include/linux/sched.h中定義如下:
struct files_struct {
atomic_t count; /* 共享該表的進程數 */
rwlock_t file_lock; /* 保護以下的所有域,以免在
tsk->alloc_lock中的嵌套*/
int max_fds; /*當前文件對象的最大數*/
int max_fdset; /*當前文件描述符的最大數*/
int next_fd; /*已分配的文件描述符加1*/
struct file ** fd; /* 指向文件對象指針數組的指針 */
fd_set *close_on_exec; /*指向執行exec( )時需要關閉的文件描述符*/
fd_set *open_fds; /*指向打開文件描述符的指針*/
fd_set close_on_exec_init;/* 執行exec( )時需要關閉的文件描述符的初 值集合*/
fd_set open_fds_init; /*文件描述符的初值集合*/
struct file * fd_array[32];/* 文件對象指針的初始化數組*/
};
fd域指向文件對象的指針數組。該數組的長度存放在max_fds域中。通常,fd域指向files_struct結構的fd_array域,該域包括32個文件對象指針。如果進程打開的文件數目多於32,內核就分配一個新的、更大的文件指針數組,並將其地址存放在fd域中;內核同時也更新max_fds域的值。
對於在fd數組中有入口地址的每個文件來說,數組的索引就是文件描述符(file descriptor)。通常,數組的第一個元素(索引爲0)是進程的標準輸入文件,數組的第二個元素(索引爲1)是進程的標準輸出文件,數組的第三個元素(索引爲2)是進程的標準錯誤文件(參見圖8.3)。請注意,藉助於dup( )、dup2( )和 fcntl( ) 系統調用,兩個文件描述符就可以指向同一個打開的文件,也就是說,數組的兩個元素可能指向同一個文件對象。當用戶使用shell結構(如2>&1)將標準錯誤文件重定向到標準輸出文件上時,用戶總能看到這一點。
open_fds域包含open_fds_init域的地址,open_fds_init域表示當前已打開文件的文件描述符的位圖。max_fdset域存放位圖中的位數。由於數據結構fd_set有1024位,通常不需要擴大位圖的大小。不過,如果確實必須的話,內核仍能動態增加位圖的大小,這非常類似文件對象的數組的情形。
當開始使用一個文件對象時調用內核提供的fget( )函數。這個函數接收文件描述符fd作爲參數,返回在current->files->fd[fd]中的地址,即對應文件對象的地址,如果沒有任何文件與fd對應,則返回NULL。在第一種情況下,fget( )使文件對象引用計數器f_count的值增1。
fd
stdin 0
stdin 1
stdin 2
3
圖8.3 文件描述符數組
當內核完成對文件對象的使用時,調用內核提供的fput( ) 函數。該函數將文件對象的地址作爲參數,並遞減文件對象引用計數器f_count的值,另外,如果這個域變爲NULL,該函數就調用文件操作的“釋放”方法(如果已定義),釋放相應的目錄項對象,並遞減對應索引節點對象的i_writeaccess域的值(如果該文件是寫打開),最後,將該文件對象從“正在使用”鏈表移到“未使用”鏈表。
3.關於文件系統信息的fs_struct結構
第三個結構是fs_struct ,在2.4以前的版本中在include/linux/sched.h 中定義爲:
struct fs_struct {
atomic_t count;
int umask;
struct dentry * root, * pwd;
};
在2.4中,單獨定義在include/linux/fs_struct.h中:
struct fs_struct {
atomic_t count;
rwlock_t lock;
int umask;
struct dentry * root, * pwd, * altroot;
struct vfsmount * rootmnt, * pwdmnt, * altrootmnt;
};
count域表示共享同一fs_struct 表的進程數目。umask域由umask( )系統調用使用,用於爲新創建的文件設置初始文件許可權。
fs_struct中的dentry結構是對一個目錄項的描述,root、pwd及 altroot三個指針都指向這個結構。其中,root所指向的dentry結構代表着本進程所在的根目錄,也就是在用戶登錄進入系統時所看到的根目錄;pwd指向進程當前所在的目錄;而altroot則是爲用戶設置的替換根目錄。實際運行時,這三個目錄不一定都在同一個文件系統中。例如,進程的根目錄通常是安裝於“/”節點上的Ext2文件系統,而當前工作目錄可能是安裝於/msdos的一個DOS文件系統。因此,fs_struct結構中的rootmnt、 pwdmnt及 altrootmnt就是對那三個目錄的安裝點的描述,安裝點的數據結構爲vfsmount。