Linux文件描述符
Linux 系統中,把一切都看做是文件,當進程打開現有文件或創建新文件時,內核向進程返回一個文件描述符(file descriptor,fd)[1, 4],在windows下面,這玩意兒叫file handle,句柄。
文件描述符(file descriptor)就是內核為了高效管理這些已經被打開的文件所創建的索引,其是一個非負整數(通常是小整數),用於指代被打開的文件,所有執行I/O操作的系統調用都通過文件描述符來實現。同時還規定系統剛剛啓動的時候,0是標準輸入,1是標準輸出,2是標準錯誤。這意味着如果此時去打開一個新的文件,它的文件描述符會是3,再打開一個文件文件描述符就是4……[2]。
可以簡單理解成系統維護的文件描述符表是一個數組,下標就是索引(文件描述符),數組內容就是一個個指向文件的指針(如0 -> stdin,1 -> stdout,2-> stderr)。
掌握它,有助於深入理解 Linux 文件系統、I/O 操作,
以及進程間通信(如管道(pipe)、套接字(Socket))的實現,可以去Ubuntu系統中簡單完成下面的例子。<( ̄︶ ̄)↗[GO!]
優化界面的Blog:Linux文件描述符
用户程序與內核交互的基本過程
1. 打開文件
當一個用户程序需要訪問某個文件時,它會通過系統調用(如 open())請求內核打開該文件。
內核會根據文件路徑在文件系統中查找文件,併為該文件分配一個文件描述符。這個文件描述符是一個整數,表示該文件在內核中的唯一標識符。
內核維護着一個叫做 文件表(file table)的數據結構,文件描述符實際上就是對這個表中的一個條目的引用。
2. 使用文件描述符讀寫文件
用户程序使用文件描述符來進行後續的文件操作。例如:
讀取文件:用户程序調用 read(fd, ...) 系統調用,內核通過文件描述符 fd 查找對應的文件,並從磁盤中讀取數據,將數據返回給用户程序。
寫入文件:用户程序調用 write(fd, ...) 系統調用,內核通過文件描述符 fd 查找文件,向文件中寫入數據。
文件描述符使得內核能夠識別哪個文件需要被操作,從而實現文件與程序的交互。
3. 文件描述符與文件表
內核通過文件描述符和文件表來管理已打開的文件。每個進程都有一個 文件描述符表,它是一個數組,其中每個索引對應一個文件描述符。這個文件描述符指向內核的文件表項,每個文件表項包含文件的狀態信息(例如當前文件指針、文件權限等)。
4. 文件操作的內核層處理
用户程序和內核之間的交互通常通過系統調用來實現,文件描述符是這些系統調用的接口。內核會根據文件描述符執行相應的操作:
打開文件時,內核會創建或查找該文件的內核對象,並更新文件描述符。
對文件進行讀寫操作時,內核通過文件描述符在文件表中查找文件對象,然後執行 I/O 操作(例如讀取磁盤或寫入磁盤)。
5. 關閉文件
當用户程序完成文件操作後,它會通過系統調用 close(fd) 來關閉文件描述符。
內核會釋放文件描述符所佔用的資源,關閉文件的文件表項,並更新進程的文件描述符表。
通過文件描述符交互的具體例子
假設一個程序需要從文件中讀取數據並進行處理,下面的示例代碼:
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
int fd = open("example.txt", O_RDONLY);
if (fd == -1) {
perror("open");
return 1;
}
char buffer[128];
ssize_t bytesRead = read(fd, buffer, sizeof(buffer));
if (bytesRead == -1) {
perror("read");
close(fd);
return 1;
}
write(STDOUT_FILENO, buffer, bytesRead);
close(fd);
return 0;
}
在這個例子中,文件描述符的作用可以從以下幾個步驟看到:
- 打開文件時,
open()系統調用將返回一個文件描述符(fd),這個文件描述符在內核中表示 example.txt 文件的句柄。 - 讀取文件時,
read()系統調用使用文件描述符 fd 來訪問內核中的文件表項,執行 I/O 操作並將文件數據讀取到 buffer 中。 - 寫入數據時,通過
write(STDOUT_FILENO, ...)輸出數據,STDOUT_FILENO 是標準輸出的文件描述符(通常是 1)。 - 關閉文件時,
close(fd)系統調用會釋放文件描述符所佔用的資源,告知內核文件已經關閉。
通過文件描述符,程序可以與操作系統內核進行有效的通信,完成文件系統和其他 I/O 操作。
Linux上查看文件描述符列表
在 Linux 上使用 vim 打開文件時,操作系統通過文件描述符與文件系統進行交互。下面是一個具體例子。
打開文件(使用 Vim)
首先,你使用 vim 打開一個文件(例如 helloworld.cpp):
vim helloworld.cpp
這時,Vim 會啓動並打開 helloworld.cpp 文件。在內核中,Vim 會使用文件描述符來與文件系統交互,即讀取和編輯 helloworld.cpp 文件的內容。
在新 Shell 中查找 Vim 進程的 PID
接着,你可以打開另一個終端(Shell),通過 pidof 命令獲取正在運行的 Vim 進程的進程 ID(PID):
pidof vim
假設返回的 PID 是 40133,説明 Vim 進程的進程號是 40133。
查看 Vim 進程的文件描述符
Linux 系統中的每個進程都有一個對應的 /proc/[pid]/fd 目錄,裏面列出了該進程打開的所有文件的文件描述符。你可以通過以下命令查看 Vim 進程所使用的文件描述符列表:
ll /proc/40133/fd
這裏,40133 是你之前通過 pidof vim 命令獲得的 Vim 進程的 PID。
ll 命令會列出該目錄下的文件,其中每個文件都對應着一個打開的文件描述符(文件句柄)。輸出會類似於:
total 0
dr-x------ 2 allen allen 0 Dec 29 15:58 ./
dr-xr-xr-x 9 allen allen 0 Dec 29 15:58 ../
lrwx------ 1 allen allen 64 Dec 29 15:58 0 -> /dev/pts/8
lrwx------ 1 allen allen 64 Dec 29 15:58 1 -> /dev/pts/8
l-wx------ 1 allen allen 64 Dec 29 15:58 19 -> /home/allen/.vscode-server/data/logs/20241229T150828/ptyhost.log
lrwx------ 1 allen allen 64 Dec 29 15:58 2 -> /dev/pts/8
l-wx------ 1 allen allen 64 Dec 29 15:58 20 -> /home/allen/.vscode-server/data/logs/20241229T150828/remoteagent.log
lrwx------ 1 allen allen 64 Dec 29 15:58 21 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 22 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 23 -> /dev/ptmx
lrwx------ 1 allen allen 64 Dec 29 15:58 4 -> /home/allen/CPP/.helloworld.cpp.swp
這裏的輸出解釋如下:
- 0,1,2:這是標準輸入、標準輸出和標準錯誤,它們通常會指向終端設備(如
/dev/pts/8)。這些文件描述符是系統默認打開的,用於處理進程的 I/O 操作。 - 4:這個文件描述符指向你用 Vim 打開的文件
helloworld.cpp。可以看到,/home/allen/CPP/.helloworld.cpp.swp是文件描述符 4 對應的目標文件。
説明:
- 在 Linux 中,每個進程都會為打開的文件、管道、設備、套接字等分配一個文件描述符,文件描述符的值通常是從 0 開始遞增的。
- 標準輸入(0)、標準輸出(1)、標準錯誤(2)是系統自動打開的,而打開的文件
helloworld.cpp在 Vim 進程中會被分配到文件描述符 3 及以後。
可以看到新打開的 helloworld.cpp 的文件描述符,竟然是4,而不是從3開始,這裏面有一番學問,涉及 vim 的原理。因為vim這種編輯器的原理是先打開源文件並拷貝,然後關閉源文件再打開自己的副本,修改完文件保存的時候直接將副本重命名覆蓋源文件。所以打開源文件的時候用的文件描述符3,然後打開自己的副本是時候就該用文件描述符4了,然後關閉源文件,文件描述符3就被釋放了,我們查看的時候就只剩下了4,這裏它指向的是vim創建的副本文件[3](這裏有更詳細的解釋,這裏是一個通俗的理解)。
檢查文件描述符的具體信息
可以通過查看符號鏈接來了解更多細節,例如,可以用 readlink 命令查看文件描述符指向的文件路徑:
readlink /proc/40133/fd/4
深入理解 Linux 中的文件描述符及其背後的數據結構
要深入理解 Linux 中的文件描述符及其背後的數據結構,我們需要了解內核如何通過三個核心數據結構來管理文件描述符:
- 進程級的文件描述符表(Process File Descriptor Table)
- 系統級的打開文件描述符表(System-wide Open File Table)
- 文件系統的 i-node 表(File System i-node Table)
這三個數據結構[4]共同工作,使得 Linux 系統能夠高效地管理文件 I/O 操作,並確保每個進程對文件的訪問是獨立且有序的。
1. 進程級的文件描述符表
每個運行中的進程都有一個 進程控制塊(PCB),它包含了與進程相關的各種信息。在這個 PCB 中,文件描述符表(也稱為 文件描述符數組)是一個非常重要的數據結構。
- 文件描述符表的功能:每個進程的文件描述符表記錄着該進程所打開的文件描述符。文件描述符是一個整數,它對應着進程所打開的文件、套接字、管道等資源。
- 表的結構:文件描述符表是一個數組,每個文件描述符對應數組中的一個位置。例如,標準輸入、標準輸出、標準錯誤默認分別對應文件描述符 0、1、2,而其他文件則由內核為每個進程分配一個較大的文件描述符,如 3、4、5 等。
- 進程獨立性:進程級文件描述符表是進程私有的。不同進程之間是獨立的,進程 A 使用文件描述符 3 打開的文件,進程 B 如果也打開了一個文件,可能也會被分配文件描述符 3。它們的文件描述符對應的是不同的文件資源。
進程級文件描述符表的關鍵點:
- 每個進程都維護一個自己的文件描述符表。
- 文件描述符表的每個條目對應一個打開的文件或資源。
- 文件描述符表存儲的只是文件描述符與內核內部文件對象的引用。
2. 系統級的打開文件描述符表
系統級的 打開文件描述符表 是內核維護的一個全局數據結構,用來管理系統中所有進程共享的文件資源。每當一個進程打開文件時,內核會在此表中創建一項記錄,表示這個文件被打開。
該表中的每項記錄包含以下信息:
- 當前文件偏移量:每個文件都有一個當前的讀取或寫入位置。當進程執行
read()或write()操作時,內核會根據該文件的偏移量進行相應的讀寫操作。在每次讀取時,偏移量會自動更新,也可以通過lseek()系統調用顯式地修改偏移量。 - 打開文件時的標識:由
open()系統調用的flags參數指定,如只讀、只寫、讀寫等。內核在打開文件時會記錄這些標識,用於後續的訪問控制。 - 文件訪問模式:當進程通過
open()打開文件時,內核會記錄文件的訪問模式(如只讀模式O_RDONLY,只寫模式O_WRONLY,讀寫模式O_RDWR)以及其他訪問權限(如O_APPEND、O_NONBLOCK等)。 - 與信號驅動相關的設置:某些文件(如終端設備)可以通過信號驅動模式進行 I/O 操作。這些設置會記錄在系統級的打開文件表中,以便內核在合適的時機處理信號。
- 與文件的 i-node 關聯:系統級的打開文件描述符表項會保存指向文件系統 i-node 表項的指針。i-node 表項包含了該文件的元數據(如文件大小、權限、時間戳等)。
關鍵點:
- 每個進程打開文件時,系統級打開文件表會創建相應的記錄。
- 所有進程共享系統級的打開文件描述符表,通過這個表來管理文件的偏移量和訪問模式等信息。
3. 文件系統的 i-node 表
i-node 是 索引節點(Index Node)的縮寫,是文件系統用來存儲文件元數據的一種數據結構。每個文件都有一個對應的 i-node,i-node 不存儲文件的內容,而是存儲與文件相關的各種屬性和元數據。
i-node 表包含以下內容:
- 文件類型:例如普通文件、目錄文件、符號鏈接、套接字、FIFO 等。
- 文件權限:表示文件的訪問權限(如讀、寫、執行權限)。
- 文件大小:文件的實際大小(字節數)。
- 時間戳:包括文件的創建時間、修改時間和訪問時間(如
ctime、mtime、atime等)。 - 指向文件數據塊的指針:i-node 會存儲指向文件實際數據塊的指針(對於小文件直接存儲指針,對於大文件使用間接塊)。這些指針幫助操作系統在磁盤上定位文件內容。
- 文件鎖列表:如果文件被加鎖,i-node 會存儲一個指向鎖信息的指針。
- 引用計數:記錄有多少個進程或文件描述符正在使用該文件。如果引用計數為 0,則表示該文件可以被刪除。
i-node 的關鍵點:
- i-node 存儲文件的元數據,而不存儲文件的實際內容。
- 每個文件在文件系統中都有唯一的 i-node。
- 文件的內容由磁盤上的數據塊來存儲,而 i-node 中存儲的是指向這些數據塊的指針。
文件描述符如何協同工作
文件描述符表、系統級的打開文件描述符表和 i-node 表相互協作來管理文件資源:
- 進程級文件描述符表 存儲進程所打開的文件描述符,它是進程私有的。當進程通過
open()打開一個文件時,內核會在進程的文件描述符表中分配一個文件描述符,並且該文件描述符指向 系統級的打開文件描述符表 中的一個記錄。 - 系統級的打開文件描述符表 存儲所有打開文件的狀態信息,如文件偏移量、訪問模式等,並且每個表項都會指向對應文件的 i-node。
- i-node 表 存儲文件的元數據(如權限、大小等)以及文件內容所在的磁盤位置。每個打開的文件都會通過 i-node 來訪問文件的實際數據。
當進程進行文件操作時(如 read()、write()),操作首先通過進程級的文件描述符表查找對應的文件描述符,然後在系統級的打開文件描述符表中查找該文件的狀態信息,並通過 i-node 訪問文件的實際數據。
總結
- 進程級的文件描述符表:每個進程獨立維護,記錄當前進程打開的文件描述符。
- 系統級的打開文件描述符表:所有進程共享,記錄文件的狀態信息和 i-node 引用。
- 文件系統的 i-node 表:記錄文件的元數據和實際數據的位置信息。
這三個數據結構協作,使得 Linux 系統能夠高效且靈活地管理文件 I/O 操作,確保進程之間的文件訪問獨立且有序,並且能夠在多進程環境中正確地管理文件資源。
參考
1、[理解Linux的文件描述符FD與Inode]
2、[理解linux中的file descriptor(文件描述符)]
3、[徹底弄懂 Linux 下的文件描述符(fd)]
4、[Linux文件描述符到底是什麼?]