內核版本:linux-2.6.11
文件描述符(file descriptor)在Linux編程里隨處可見,設備讀寫、網絡通信、進程通信,fd可謂是關鍵中的關鍵。
深入理解可以增加我們使用它的信心。
該篇筆記主要解釋了文件描述符底層的多態實現和文件描述符的生命周期。希望對自己和大家有所幫助。
先看三段簡化后的內核代碼
sys_open
fd = get_unused_fd();
if (fd >= 0) {
struct file *f = filp_open(tmp, flags, mode);
fd_install(fd, f);
}
sys_socket
fd = get_unused_fd();
if (fd >= 0) {
struct file *file = get_empty_filp();
fd_install(fd, file)
}
do_pipe
struct file *f1 = get_empty_filp();
struct file *f2 = get_emtpy_filp();
i = get_unused_fd();
j = get_unused_fd();
fd_install(i, f1);
fd_install(j,f2);
fd[0] = i;
fd[1] = j;
這三段代碼分別是三個POSIX標准系統調用open
,socket
,pipe
的內核態例程,
簡化后的代碼可以清楚看到,文件描述符的獲取和安裝在不同模塊中都有着幾乎相同的套路。
-
get_unused_fd
顧名思義,它從當前進程描述符中的打開文件數組(current->files)里取得一個空閑的項,然后返回其數組下標。 -
filp_open
最終將調用get_empty_filp
,因此三段代碼都使用get_empty_filp
分配了一個新的文件對象(struct file)。 -
fd_install
將上一個步驟創建的文件對象指針存到該進程的打開文件數組中。
至此,文件描述符安裝完畢,返回的fd即為該文件對象在當前進程的task_struct中的files數組中的下標,也就是所謂的文件描述符,但究其本質,我們對文件描述符的所有使用其實就是在操作file對象。
接下來就是程序員最拿手的read,write,以及各種IO控制了。
file作為一個通用的結構體,在不同模塊表示的對象是不同的,在文件系統中,它表示為一個文件,在進程通信中,它表示為一個管道,在網絡通信中,它又表示為一個套接字。
那么問題來了,作為一個通用的結構體,在表示為不同的通信對象的時候,操作函數肯定是不同的,舉個例子來說,write一個文件和write一個socket肯定是不一樣的,那么同一個函數入口怎么做到多態的實現呢?
我們同樣用write來解釋,write()
->sys_write()
->vfs_write()
->file->f_op->write()
,重點在write調用流程的最后一步file->f_op->write()
,從這里開始,針對不同的file對象將進入不同的write實現。這是因為不同file里注冊的各類操作函數是不同的。
struct file中的f_op指針,指向一系列函數指針的集合,這個結構名叫file_operation
,這一系列函數指針都指向着當前這個file對象(設備也好,管道也好,套接字也好)對應的各種操作函數的具體實現。
在2.6.11的內核里看起來如下:
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 *);
int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
...
}
以ext3文件系統為例:
struct file_operations ext3_file_operations = {
.llseek = generic_file_llseek,
.read = do_sync_read,
.write = do_sync_write,
.ioctl = ext3_ioctl,
.mmap = generic_file_mmap,
.open = generic_file_open,
...
};
這里使用了C99的語法,始化了一個file_operations然后用自己的實現去填充一個個的函數指針,在open一個ext3文件系統中的文件時,會將這個file_operations注冊到新建的這個file對象里。
同樣,在pipe.c、socket.c里也可以看到類似的注冊file_operations的行為。
所以,一個file是文件、是設備、是管道還是套接字,就是根據其中存放的file_operations來區分,
在創建file的時候就會針對這個file的類型注冊不同的操作函數,這就解釋了同一個通用函數的多態實現。
文件描述符的生命周期:
- 創建file對象
- 根據IO對象的類型注冊各個操作函數(注冊file_operations)
- 將file對象的指針注冊到進程描述符的files數組里的fd下標處
- read、write等等IO操作
- 調用close(fd)釋放file對象