前言
文件描述符 (File Descriptor, FD) 是 Linux 進程訪問文件、網路套接字、管道等資源的核心抽象。理解 FD 的分配機制、標準流 (0/1/2)、dup 系列系統調用的複製語義,是掌握 I/O 重定向、管道通信、守護進程編程的基石。
一、Linux 文件類型與描述符基礎
1. 四大文件類型
| 類型 | 首字符 | 說明 | 常見命令 |
| 普通文件 | - | 文本、二進制、腳本、數據 | cp, rm, vim |
| 目錄文件 | d | 包含檔名與 inode 指針 | mkdir, ls, rmdir |
| 鏈接文件 | l | 符號鏈接 (軟鏈接),指向目標路徑 | ln -s |
| 設備文件 | b/c | 塊設備 (磁碟) / 字符設備 (終端、網卡) | mknod, dd |
2. 文件描述符本質
- **非負整數**,進程內核打開文件表的索引。
- **進程私有**:每個進程獨立擁有 FD 表 (默認上限 1024,可 `ulimit -n` 調整)。
- **三大預設 FD**:
| FD | 宏定義 | 預設設備 | 用途 |
| 0 | STDIN_FILENO | 鍵盤/終端 | 標準輸入 |
| 1 | STDOUT_FILENO | 螢幕/終端 | 標準輸出 |
| 2 | STDERR_FILENO | 螢幕/終端 | 標準錯誤 |
3. 查看進程 FD
# 系統級限制
ulimit -n
# 查看特定進程已打開 FD
ls -l /proc//fd/
# 系統全局 FD 使用統計
sysctl -a | grep fs.file-nr
二、dup 與 dup2 系統調用
函數原型
#include
int dup(int oldfd); // 返回最小可用 FD
int dup2(int oldfd, int newfd); // 指定新 FD,若被佔用先關閉
核心語義:共享「打開文件描述」
- 複製的是 **FD 表項指向的同一個「打開文件描述」 (open file description)**,包含:文件偏移量、存取模式 (O_RDONLY 等)、狀態旗標 (O_APPEND 等)。
- **不共享**:FD 旗標 (FD_CLOEXEC),每個 FD 獨立持有。
- 任一 FD 執行 `lseek`、`read`、`write` 移動偏移量,**另一 FD 可見**。
代碼示範
int fd1 = open("test.txt", O_RDWR | O_CREAT, 0644);
int fd2 = dup(fd1); // fd2 = 3 (假設)
write(fd1, "hello", 5); // 偏移量 → 5
write(fd2, "world", 5); // 實際寫入位置 5-10,非 0-5
// 文件內容:helloworld
三、實戰應用場景
1. I/O 重定向 (Shell 內部實現)
// 將標準輸出重定向到文件
int fd = open("log.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO); // fd=3 → newfd=1
close(fd); // 關閉原 fd,STDOUT 現指向 log.txt
printf("This goes to log.txt\n");
2. 管道通信 (父子進程)
int pipefd[2];
pipe(pipefd); // pipefd[0] 讀端, pipefd[1] 寫端
pid_t pid = fork();
if (pid == 0) { // 子進程
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO); // 標準輸出 → 管道寫端
close(pipefd[1]);
execlp("ls", "ls", "-l", NULL);
} else { // 父進程
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO); // 標準輸入 → 管道讀端
close(pipefd[0]);
execlp("wc", "wc", "-l", NULL);
}
3. 守護進程脫離終端
// 雙重 fork 後,重定向 0/1/2 到 /dev/null
int fd = open("/dev/null", O_RDWR);
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
4. 保存/恢復標準流
int saved_stdout = dup(STDOUT_FILENO); // 備份
freopen("temp.log", "w", stdout); // 臨時重定向
// ... 執行輸出日誌的庫函數 ...
dup2(saved_stdout, STDOUT_FILENO); // 恢復
close(saved_stdout);
四、進階:fcntl 與 FD 旗標
// 獲取/設置 FD 旗標 (如 FD_CLOEXEC)
int flags = fcntl(fd, F_GETFD);
fcntl(fd, F_SETFD, flags | FD_CLOEXEC); // exec 時自動關閉
// 獲取/設置文件狀態旗標 (O_APPEND, O_NONBLOCK 等)
int flags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, flags | O_APPEND);
五、總結對比表
| 調用 | 新 FD 分配 | 原子性 | 典型用途 |
| dup(old) | 內核選最小可用 | 是 | 簡單複製、備份標準流 |
| dup2(old, new) | 指定 new (佔用先關閉) | 是 | 精確重定向 0/1/2、管道綁定 |
| fcntl(old, F_DUPFD, min) | ≥ min 的最小可用 | 是 | 指定範圍分配、避開低位 FD |
結語
dup/dup2 看似簡單,實則是 Unix 「一切皆文件」哲學下 I/O 多路複用、重定向、進程間通信的核心樞紐。深入理解「打開文件描述」與「文件描述符」的區別,能避免並發寫入偏移量競爭、FD 洩漏等隱蔽 Bug,是系統級編程必修內功。