Linux 檔案描述元

最後更新: 2021-10-06 12:25:39 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

Linux 將程序的整數控制代碼,與儲存開啟實例狀態的核心物件分離。這項區別解釋了偏移量共用、複製、繼承以及許多描述元洩漏。

描述元表與開啟檔案描述 #

檔案描述元是索引呼叫程序描述元表的非負整數。表格項目參照 開啟檔案描述,在 Linux 內部以 struct file 表示。開啟檔案描述記錄如下狀態:

  • 目前檔案偏移量;
  • O_APPEND 與 O_NONBLOCK 等檔案狀態旗標;
  • 開啟的路徑、存取模式、認證資料與檔案作業。

每次成功的 open() 通常會建立新的開啟檔案描述,並在程序表中安裝參照。因此開啟相同路徑兩次會建立獨立的偏移量:

c
int a = open("events.log", O_RDONLY);
int b = open("events.log", O_RDONLY); /* independent description and offset */

inode 識別檔案系統物件,而開啟檔案描述識別其中一次開啟。取消連結或重新命名路徑不會使既有描述元失效。

描述元旗標屬於表格項目,而非共用的開啟檔案描述。主要的描述元旗標是 FD_CLOEXEC,可使用 fcntl(fd, F_GETFD/F_SETFD) 讀取或變更。請優先使用 O_CLOEXEC、pipe2(O_CLOEXEC) 與 dup3(..., O_CLOEXEC) 等原子建立旗標,因為在後續 fcntl() 呼叫中設定旗標,會與另一個執行緒中的 fork() 加 execve() 競爭。

複製、fork() 與 execve() #

dup()、dup2() 與 dup3() 會建立另一個參照相同開啟檔案描述的描述元。因此複製的描述元共用偏移量與檔案狀態旗標:

c
int input = open("input", O_RDONLY | O_CLOEXEC);
int stdin_copy = dup(input);

read(input, buf, 16);      /* advances the shared offset */
read(stdin_copy, buf, 16); /* starts after those 16 bytes */

新描述元具有自己的描述元旗標。特別是,dup() 與 dup2() 會清除新描述元上的 close-on-exec;dup3() 可以原子地設定它。dup2(oldfd, newfd) 會在必要時原子關閉 newfd 並重複使用該編號,從而避免先關閉再複製的競爭。

fork() 後,父程序與子程序具有分離的描述元表,而繼承的項目參照相同的開啟檔案描述。偏移量與狀態旗標變更在這些參照之間仍然共用。成功 execve() 後,除非項目具有 FD_CLOEXEC,否則描述元仍保持開啟;描述元表本身會延續到新程式。

使用共用檔案表建立的執行緒(POSIX 執行緒即如此)會看到相同的描述元項目。因此,在一個執行緒中關閉或取代描述元可能影響另一個執行緒,而描述元編號可能立即重複使用。

匿名管線 #

pipe() 建立一個核心位元組串流,並回傳其讀取端與寫入端的描述元:

c
int fd[2];
if (pipe2(fd, O_CLOEXEC) == -1)
    /* handle error */;

/* fd[0] is readable; fd[1] is writable. */

傳統的 fork() 管線必須在每個程序中關閉未使用的複本:

c
if (fork() == 0) {
    close(fd[1]);
    consume(fd[0]);
    close(fd[0]);
    _exit(0);
}

close(fd[0]);
produce(fd[1]);
close(fd[1]); /* the reader can observe EOF after every writer closes */

關閉紀律是協定的一部分:

  • 當仍有寫入者存在時,對空管線執行 read() 會阻塞,除非設定 O_NONBLOCK。
  • 只有在緩衝區為空且所有參照寫入端的描述元都已關閉後,read() 才會回傳零。
  • 對已滿管線執行 write() 會阻塞,或在 O_NONBLOCK 下失敗/部分成功。
  • 所有讀取端都關閉後執行 write() 會引發 SIGPIPE;若忽略或處理該訊號,呼叫會以 EPIPE 失敗。

管線是位元組串流,不保留一般訊息邊界。POSIX 保證不超過 PIPE_BUF 位元組的寫入不會與其他程序的寫入交錯。Linux 將 PIPE_BUF 定義為 4096 位元組。較大的寫入可能被分割或交錯;非阻塞寫入也可能是部分寫入,因此呼叫端必須檢查回傳值。

O_NONBLOCK 是開啟檔案描述上的檔案狀態旗標。透過其中一個複製描述元設定它,會改變所有共用該描述的描述元所觀察到的行為。

FIFO 作為具名會合點 #

mkfifo() 建立 FIFO 類型的檔案系統項目:

c
if (mkfifo("/run/example/events.fifo", 0600) == -1 && errno != EEXIST)
    /* handle error */;

目錄項目不儲存串流內容。FIFO 開啟時,核心維護管線物件,而程序透過開啟路徑抵達它。開啟後,讀取、寫入、原子性、EOF 與 SIGPIPE 遵循管線語意。

開啟提供會合行為:

  • 阻塞的 open(path, O_RDONLY) 會等待寫入者。
  • 阻塞的 open(path, O_WRONLY) 會等待讀取者。
  • 沒有寫入者時,非阻塞唯讀開啟仍會成功;沒有寫入者時讀取回傳零,而存在寫入者但沒有可用資料時回傳 EAGAIN。
  • 在讀取者開啟 FIFO 前,非阻塞唯寫開啟會以 ENXIO 失敗。

Linux 允許在沒有對等端時以 O_RDWR 開啟 FIFO,但 POSIX 將該行為留作未定義。保持兩端開啟也可能抑制 EOF 或建立自我死結,因此可攜式協定應使用明確的讀取者與寫入者角色。

因為 FIFO 名稱可能在檢查與 open() 之間被取代,請將它放在受控目錄中,在信任邊界需要時驗證擁有權與型別,並避免先檢查再開啟的授權邏輯。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)