從《APUE》第 3 章文件 I/O 入手,結合內核視角梳理 read/write 無緩衝本質、文件描述符表、文件表、v-node 表三層關係、O_APPEND 原子追加語義、dup/dup2 共享文件表差異、lseek 僅修改偏移不觸發 I/O 等核心知識點,並附帶可運行驗證代碼。
進程 A 內核
+------------------+ +-------------------------+
| fd 0,1,2,3,4... | ---> | 文件描述符表 (每進程) |
+------------------+ | fd -> 指向 文件表項 |
+-----------+-------------+
|
v
+-------------------------+
| 文件表 (文件表項) | <-- 存放: 文件狀態標誌、當前偏移量、指向 v-node 指針
| 每次 open() 生成新項 |
+-----------+-------------+
|
v
+-------------------------+
| v-node 表 (每文件一項) | <-- 存放: i-node 資訊、鎖狀態、文件系統操作向量
+-------------------------+
read/write:用戶態無緩衝 — 直接發起系統調用,讀滿/寫滿/遇 EOF/錯誤即返回。內核仍有頁面緩存 (delayed write)。fread/fwrite:用戶態帶緩衝 (FILE*) — 以塊為單位調用 read/write,減少系統調用次數。open() → 兩個獨立文件表項int fd1 = open("temp", O_RDWR);
int fd2 = open("temp", O_RDWR);
// fd1, fd2 各自擁有獨立「當前偏移量」
// write(fd1) 不影響 fd2 的偏移
dup2(fd1, fd2) → 共享同一文件表項int fd1 = open("temp", O_RDWR);
int fd2 = dup2(fd1, fd2); // 或 dup(fd1)
// fd1, fd2 共享「當前偏移量」
// write(fd1) 導致 fd2 讀取位置同步前移
O_APPEND 原子追加語義驗證結論:O_APPEND 只影響 write,不影響 lseek 設置的偏移量,也不影響 read。
// temp 內容: "aaaaabbbbbccccc"
int fd = open("temp", O_RDWR | O_APPEND);
lseek(fd, 5, SEEK_SET); // 偏移設為 5
read(fd, buf, 5); // 讀到 "bbbbb" (lseek 生效)
write(fd, "abcde", 5); // 實際寫入文件末尾 (O_APPEND 生效)
// 文件變為: "aaaaabbbbbcccccabcde"
內核實現:每次 write 前,內核檢測 O_APPEND 旗標,強制將文件表項的偏移量設為文件當前大小,再寫入 — 原子操作,多進程並發追加不會覆蓋。
| 調用 | 關鍵行為 | 備註 |
|---|---|---|
lseek(fd, off, SEEK_SET) | 僅修改文件表項偏移,無 I/O | 返回新偏移 |
open(file, O_RDWR|O_APPEND) | 原子追加模式 | write 前強制偏移=文件末尾 |
fcntl(fd, F_GETFL)/F_SETFL | 獲取/設置文件狀態標誌 | 可動態切換 O_APPEND/O_NONBLOCK |
dup(fd) | 復制 fd,共享文件表項 | 原子操作 |
dup2(fd1, fd2) | 指定新 fd 編號,關閉舊 fd2 | 原子;close+fcntl 非原子 |
creat(file, mode) | 等價 open(file, O_CREAT|O_WRONLY|O_TRUNC, mode) | 若文件存在則截斷 |
off_t = long int;size_t = unsigned int;ssize_t = signed size_t。cat 不直接讀 stdin,需 cat - file 或 cat /dev/fd/0 file。O_SYNC 每次 write 同步到磁盤;fsync/fdatasync 一次性同步。2>&1 將 fd 2 重定向到 fd 1 指向的文件 (合併 stderr/stdout)。link(dir)。ln -s ../foo bar 即使 ../foo 不存在也成功。來源:Neil的博客