Linux文件描述符
與linux打交道,盡管可能你只是一個高級語言的碼農,還是或多或少的要和遇到d這種術語。今天抽空看了下傳說中的fd,雖然還沒有深入了解linux操作系統,因此也談不上真的深刻理解了fd,但還是掃盲了些許,至少以后再碰到相關術語,不至於一臉茫然。
說明:看了一些網上的文章,大多數語句都是在理解的基礎上直接搬過來的,感謝那些整理的人們,就不一一列舉出處了。
1. Linu文件系統簡介
文件系統是操作系統用於明確存儲設備(常見的是磁盤,也有基於NAND Flash的固態硬盤)或分區上的文件的方法和數據結構;即在存儲設備上組織文件的方法。
Unix可以把一個能隨機存取的存儲介質(如:硬盤、軟盤和光盤)上的存儲空間划分成一致多個區域,每個區域都可以像獨立的物理設備一樣單獨進行管理和數 據存取,這樣的存儲區域,即是邏輯設備。在邏輯設備上按照一定的格式進行划分,就構成了邏輯文件系統,簡稱文件系統。
- 普通文件 這種文件包含了某種形式的數據,這些數據無論是文件還是二進制對於 UNIX 內核而言都是一樣的。對普通文件內容的解釋有處理該文件的應用程序進行。
- 目錄文件 目錄文件包含了其他文件的名字以及指向與這些文件有關信息的指針。對一個目錄文件具有讀權限的任一進程都可以讀取該目錄的內容,但是只有內核才能直接寫目錄文件。
- 塊特殊文件 這種文件類型提供對設備帶緩沖的訪問,每次訪問以固定長度為單位進行。
- 字符特殊文件 這種文件類型提供對設備不帶緩沖的訪問,每次訪問長度可變。系統中的所有設備要么是字符特殊文件,要么是塊特殊文件。
- FIFO 這種類型文件用於進程間通信。也稱為命名管道(namedpipe)。
- 套接字(socket) 這種文件類型用於進程間的網絡通信。
- 符號鏈接(symbolic link) 這種文件類型指向另一個文件。
2. 什么是文件描述符fd
在Linux系統中一切皆可以看成是文件,文件又可分為:普通文件、目錄文件、鏈接文件和設備文件。文件描述符(file descriptor)是內核為了高效管理已被打開的文件所創建的索引,其是一個非負整數(通常是小整數),用於指代被打開的文件,所有執行I/O操作的系統調用都通過文件描述符。程序剛剛啟動的時候,0是標准輸入,1是標准輸出,2是標准錯誤。如果此時去打開一個新的文件,它的文件描述符會是3。POSIX標准要求每次打開文件時(含socket)必須使用當前進程中最小可用的文件描述符號碼。
也就是說,在linux系統中,所有的文件操作,都是通過fd來定位資源和狀態的,不管是讀寫文件,還是進行網絡通信,都需要與相應的fd打交道。
理解具體情況,需要了解由內核維護的3個數據結構:
- 進程級文件描述符表(file descriptor table)
- 系統級打開文件表(open file table)
- 文件系統i-node表(i-node table)
這3個數據結構之間的關系如下圖所示:
文件描述符表
系統為每個進程維護一份文件描述符表,該表的每一個條目都記錄了單個文件描述符的相關信息,包括:
- 控制標志(flags),目前內核僅定義了一個,即close-on-exec
- 打開文件描述體指針
打開文件列表
內核對所有打開的文件維護一個系統級別的打開文件描述表(open file description table)。表中的條目稱為打開文件描述體(open file description),存儲了與一個打開的文件相關的全部信息,包括:
- 文件偏移量(current file offset),調用read()和write()更新,調用lseek()直接修改
- 訪問模式(file status flags),由open()調用設置,例如:只讀、只寫或讀寫等
- i-node對象指針(v-node ptr),指向一個inode元素,從而關聯物理文件
i-node表
就像進程用pid來描述和定位一樣,在linux系統中,文件使用inode號來描述,inode存儲了文件的很多元信息。
每個文件系統會為存儲於其上的所有文件(包括目錄)維護一個i-node表,單個i-node包含以下信息:
- 文件類型(file type),可以是常規文件、目錄、套接字或FIFO
- 文件的字節數
- 文件擁有者的User ID
- 文件的Group ID
- 文件的讀、寫、執行權限
- 文件的時間戳,共有三個:ctime指inode上一次變動的時間,mtime指文件內容上一次變動的時間,atime指文件上一次打開的時間。
- 鏈接數,即有多少文件名指向這個inode
- 文件數據block的位置
i-node存儲在磁盤設備上,內核在內存中維護了一個副本,這里的i-node表為后者。副本除了原有信息,還包括:引用計數(從打開文件描述體)、所在設備號以及一些臨時屬性,例如文件鎖。
舉個例子
open系統調用執行的操作:新建一個i-node表元素,讓其對應打開的物理文件(如果對應於該物理文件的inode元素已經建立,就不做任何操作);新建一個文件表的元素,根據open的第2個參數設置file status flags字段,將current file offset字段置0,將v-node ptr指向剛建立的i節點表元素;在文件描述符表中,尋找1個尚未使用的元素,在該元素中填入一個指針值,讓其指向剛建立的文件表元素。最重要的是:將該元素的下標作為open的返回值返回。
這樣一來,當調用read(write)時,根據傳入的文件描述符,OS就可以找到對應的文件描述符表元素,進而找到文件表的元素,進而找到i節點表元素,從而完成對物理文件的讀寫。
3. 文件描述限制
在編寫文件操作的或者網絡通信的軟件時,初學者一般可能會遇到“Too many open files”的問題。這主要是因為文件描述符是系統的一個重要資源,雖然說系統內存有多少就可以打開多少的文件描述符,但是在實際實現過程中內核是會做相應的處理的,一般最大打開文件數會是系統內存的10%(以KB來計算)(稱之為系統級限制),查看系統級別的最大打開文件數可以使用sysctl -a | grep fs.file-max命令查看。與此同時,內核為了不讓某一個進程消耗掉所有的文件資源,其也會對單個進程最大打開文件數做默認值處理(稱之為用戶級限制),默認值一般是1024,使用ulimit -n命令可以查看,我們也可以通過命令去修改該限制。
4.fork等操作對fd的影響
我們還是以這幅圖為例:
在進程A中,文件描述符1和30都指向了同一個打開的文件句柄(標號23)。這可能是通過調用dup()、dup2()、fcntl()或者對同一個文件多次調用了open()函數而形成的。
進程A的文件描述符2和進程B的文件描述符2都指向了同一個打開的文件句柄(標號73)。這種情形可能是在調用fork()后出現的(即,進程A、B是父子進程關系,子進程繼承父進程打開的文件),或者當某進程通過UNIX域套接字將一個打開的文件描述符傳遞給另一個進程時,也會發生。再者是不同的進程獨自去調用open函數打開了同一個文件,此時進程內部的描述符正好分配到與其他進程打開該文件的描述符一樣。
此外,進程A的描述符0和進程B的描述符3分別指向不同的打開文件句柄,但這些句柄均指向i-node表的相同條目(1976),換言之,指向同一個文件。發生這種情況是因為每個進程各自對同一個文件發起了open()調用。同一個進程兩次打開同一個文件,也會發生類似情況。
總結
- 由於進程級文件描述符表的存在,不同的進程中會出現相同的文件描述符,它們可能指向同一個文件,也可能指向不同的文件
- 兩個不同的文件描述符,若指向同一個打開文件句柄,將共享同一文件偏移量。因此,如果通過其中一個文件描述符來修改文件偏移量(由調用read()、write()或lseek()所致),那么從另一個描述符中也會觀察到變化,無論這兩個文件描述符是否屬於不同進程,還是同一個進程,情況都是如此。
- 要獲取和修改打開的文件標志(例如:O_APPEND、O_NONBLOCK和O_ASYNC),可執行fcntl()的F_GETFL和F_SETFL操作,其對作用域的約束與上一條頗為類似。
- 文件描述符標志(即,close-on-exec)為進程和文件描述符所私有。對這一標志的修改將不會影響同一進程或不同進程中的其他文件描述符
