前言
`exec` 是 Shell 內建命令,也是系統調用族 (`execve` 等) 的用戶態入口。它獨特之處在於:**不創建新進程,而是用新程式映像替換當前進程映像** (PID 不變)。結合文件描述符複製 (`dup2`)、管道、重定向語法,構成了 Shell 腳本、服務管理器、容器運行時的核心執行引擎。
一、exec 核心機制
1. 進程映像替換
- **代碼段、數據段、堆、棧** 全部丟棄,重新從可執行文件加載。
- **保留不變**:PID、PPID、文件描述符表 (除 `FD_CLOEXEC`)、信號屏蔽集、當前工作目錄、用戶/組 ID、資源限制、環境變數 (可選替換)。
- **系統調用鏈**:`execve()` → `load_elf_binary()` / `load_script()` → `setup_new_exec()` → `start_thread()`。
2. exec 族函數對比
| 函數 | 參數 | 環境變數 | PATH 搜尋 |
| execl(path, arg0, ..., NULL) | 列表 | 繼承 | 否 |
| execlp(file, arg0, ..., NULL) | 列表 | 繼承 | 是 |
| execle(path, arg0, ..., NULL, envp[]) | 列表 | **指定** | 否 |
| execv(path, argv[]) | 陣列 | 繼承 | 否 |
| execvp(file, argv[]) | 陣列 | 繼承 | 是 |
| execvpe(file, argv[], envp[]) | 陣列 | **指定** | 是 |
3. Shell 內建 `exec` 命令
exec [-cl] [-a name] [command [arguments...]]
# 選項:
# -l : 在 argv[0] 前加 '-' (模擬登入 Shell)
# -c : 清空環境變數,僅保留傳入參數
# -a : 指定 argv[0] 顯示名稱
二、exec vs source vs fork
| 特性 | exec command | source script / . script | ./script (shebang) |
| 進程 | **替換當前進程** | **當前進程** (不創建子進程) | **創建子進程** (fork+exec) |
| 返回 | 成功不返回,失敗返回 | 執行完返回繼續 | 子進程結束返回父進程 |
| 環境影響 | 新程式決定 | **腳本中修改生效** (變量、函數、cd、trap) | 子進程修改不影響父進程 |
| 典型用途 | 啟動器、替換 Shell、容器入口 | 載入配置、函數庫、修改當前 Shell 狀態 | 獨立執行腳本、異步任務 |
三、I/O 重定向核心原理
1. 文件描述符 (FD) 基礎
- 0: STDIN (標準輸入,默認鍵盤/終端)
- 1: STDOUT (標準輸出,默認螢幕/終端)
- 2: STDERR (標準錯誤,默認螢幕/終端)
- 3~9: 用戶可用 FD
- 10~: 內部/動態分配
2. 重定向操作符語義
| 語法 | 含義 | 底層系統調用 |
| cmd > file | STDOUT 重定向到 file (截斷創建) | open(O_WRONLY|O_CREAT|O_TRUNC) → dup2(fd, 1) |
| cmd >> file | STDOUT 追加到 file | open(O_WRONLY|O_CREAT|O_APPEND) → dup2(fd, 1) |
| cmd < file | STDIN 從 file 讀取 | open(O_RDONLY) → dup2(fd, 0) |
| cmd 2> file | STDERR 重定向到 file | open(...) → dup2(fd, 2) |
| cmd 2>&1 | STDERR 複製 STDOUT 的**打開文件描述** | dup2(1, 2) — 共享偏移量、旗標 |
| cmd > file 2>&1 | STDOUT→file,STDERR→同一打開文件描述 | open(file)→dup2(fd,1)→dup2(1,2) **單次 open** |
| cmd > file 2> file | **兩次獨立 open**,互相覆蓋、競爭 | **錯誤用法** |
| cmd <&n / >&n | 複製 FD n 到 STDIN/STDOUT | dup2(n, 0/1) |
| cmd <&- / >&- | 關閉 STDIN/STDOUT | close(0/1) |
| cmd n<&- / n>&- | 關閉 FD n | close(n) |
| cmd n>file | FD n 重定向到 file | open(file)→dup2(fd, n) |
| cmd n>&m | FD n 複製 FD m 的打開文件描述 | dup2(m, n) |
3. exec 重定向持久化 (Shell 會話級)
# 將當前 Shell 會話的 STDOUT 永久重定向到日誌
exec > session.log
echo "這行寫入 session.log"
ls -l
# 恢復到終端
exec > /dev/tty
# 同時記錄 STDOUT 和 STDERR 到同一文件
exec > combined.log 2>&1
# 開啟 FD 3 讀取配置文件
exec 3< config.txt
while read -u 3 line; do
echo "處理: $line"
done
exec 3<&- # 關閉 FD 3
# 開啟 FD 4 寫入輸出
exec 4> output.txt
echo "直接寫入 FD 4" >&4
exec 4>&- # 關閉 FD 4
四、實戰範例
1. 守護進程模板 (雙重 fork + exec + 重定向)
#!/bin/bash
daemonize() {
# 第一次 fork
if [ $(fork) -ne 0 ]; then exit 0; fi
# 脫離控制終端
setsid
# 第二次 fork (防止重新獲得控制終端)
if [ $(fork) -ne 0 ]; then exit 0; fi
# 重定向 0/1/2 到 /dev/null
exec 0/dev/null
exec 2>/dev/null
# 切換工作目錄
cd /
# 設置 umask
umask 022
}
daemonize
exec /usr/sbin/mydaemon --config /etc/mydaemon.conf
2. 容器入口腳本 (exec 替換為主進程)
#!/bin/bash
# Dockerfile: ENTRYPOINT ["entrypoint.sh"]
set -e
# 初始化配置、數據庫遷移、權限修復...
init_config
# 信號傳遞給主進程
trap 'kill -TERM $PID' TERM INT
# exec 替換當前 Shell 為主進程 (PID 1)
exec "$@" # "$@" 為 CMD 參數,如 "nginx" "-g" "daemon off;"
# 此行後不再執行
3. 多路日誌分流
#!/bin/bash
exec 3> >(tee -a stdout.log) # FD 3 → tee 寫文件並顯示
exec 4> >(tee -a stderr.log >&2) # FD 4 → tee 寫錯誤文件並顯示
# 將腳本自身 STDOUT/STDERR 導向 FD 3/4
exec 1>&3
exec 2>&4
echo "正常輸出" # → stdout.log + 螢幕
echo "錯誤輸出" >&2 # → stderr.log + 螢幕
4. 捕獲命令輸出到變量 (不創建子 Shell)
# 傳統寫法:創建子 Shell
output=$(command)
# exec + 管道 + read (當前 Shell,效能更高)
exec 3< <(command)
read -u 3 -r output
exec 3<&-
echo "輸出: $output"
五、常見坑與避坑指南
| 現象 | 原因 | 解決 |
| exec 後腳本不再執行 | 進程映像已替換 | 預期行為;需在 exec 前完成所有準備 |
| 重定向順序錯誤導致 2>&1 不生效 | `cmd 2>&1 >file` 先複製 STDOUT(終端) 再重定向 STDOUT | **永遠寫 `cmd >file 2>&1`** |
| 管道符 `|` 隱式創建子 Shell | `cmd | while read; do ...; done` 循環在子 Shell,變量不回傳 | 用 `exec 3< <(cmd); while read -u 3; do ...; done` |
| FD 洩漏導致「Too many open files」 | 大量 `exec n>file` 未關閉 | 用完立即 `exec n>&-`;或用 `{ cmd; } n>file` 代碼塊自動關閉 |
| exec 關閉 STDIN 導致腳本掛起/退出 | `exec <&-` 後 `read` 失敗 | 謹慎關閉 0;改用 `exec 0 |
結語
`exec` 是「進程替換」的核心原語,重定向是「文件描述符操作」的語法糖。兩者結合,構成了 Unix 哲學中「組合小工具完成大任務」的執行骨架。深入理解 `dup2` 語義、重定向順序、FD 生命週期、exec 不返回特性,能讓你編寫出健壯、高效、符合 Unix 範式的 Shell 腳本與系統級程式。