Linux 將程序的整數控制代碼,與儲存開啟實例狀態的核心物件分離。這項區別解釋了偏移量共用、複製、繼承以及許多描述元洩漏。
描述元表與開啟檔案描述 #
檔案描述元是索引呼叫程序描述元表的非負整數。表格項目參照 開啟檔案描述,在 Linux 內部以 struct file 表示。開啟檔案描述記錄如下狀態:
- 目前檔案偏移量;
O_APPEND與O_NONBLOCK等檔案狀態旗標;- 開啟的路徑、存取模式、認證資料與檔案作業。
每次成功的 open() 通常會建立新的開啟檔案描述,並在程序表中安裝參照。因此開啟相同路徑兩次會建立獨立的偏移量:
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() 會建立另一個參照相同開啟檔案描述的描述元。因此複製的描述元共用偏移量與檔案狀態旗標:
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() 建立一個核心位元組串流,並回傳其讀取端與寫入端的描述元:
int fd[2];
if (pipe2(fd, O_CLOEXEC) == -1)
/* handle error */;
/* fd[0] is readable; fd[1] is writable. */傳統的 fork() 管線必須在每個程序中關閉未使用的複本:
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 類型的檔案系統項目:
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 上閱讀或評論(發表評論需要登入)