一、核心概念
exec 在 Linux 中有两个层面的含义:
| 层面 | 说明 |
|---|---|
| Shell 内置命令 | Bash/Zsh 等 shell 提供的 exec 命令,用于用新程序替换当前 shell 进程 |
| 系统调用家族 | execve() 等 C 库函数,是进程映像替换的底层机制 |
核心机制:进程替换(Process Replacement)。exec 不会创建新进程,而是将当前进程的地址空间完全替换为新程序的地址空间,PID 保持不变。
Shell 进程 (PID=1234) ──exec──> 新程序 (PID=1234)
二、Shell 内置 exec 命令
2.1 基本语法
exec [选项] [-a name] [command [arguments]]
exec [选项] [redirection]
2.2 两种模式
模式一:执行程序替换(Program Replacement)
exec /path/to/program [args...]
效果:当前 shell 进程被完全替换为指定程序。执行后,shell 不再存在,程序结束后直接返回到 shell 的父进程。
# 示例:当前 shell 被替换为 vim
exec vim /etc/hosts
# vim 退出后,不会回到原 shell,而是直接退出终端
模式二:仅重定向(Redirection Only)
exec > output.log 2>&1
效果:不执行程序,仅改变当前 shell 的文件描述符重定向。后续所有命令的输出都会重定向到指定文件。
三、详细用法与示例
3.1 程序替换场景
场景 1:在脚本中切换解释器
#!/bin/bash
echo "This is bash"
exec python3 << 'EOF'
print("Now running Python in the same process")
EOF
# 这行不会执行,因为 bash 已被替换
echo "Back to bash"
⚠️ 注意:
exec python3后,原 bash 脚本中后续命令不会执行。
场景 2:安全地降级权限(配合 suid)
#!/bin/bash
# 脚本以 root 运行,但需要用普通用户执行主程序
exec su -c "/path/to/app" normaluser
场景 3:守护进程启动(经典用法)
#!/bin/bash
# 关闭标准输入
exec 0</dev/null
# 重定向标准输出和错误到日志
exec 1>>/var/log/myapp.log
exec 2>&1
# 替换为实际程序
exec /usr/bin/myapp
3.2 文件描述符重定向场景
打开文件用于读写
# 打开文件描述符 3 用于写入
exec 3> /tmp/fd3.log
echo "Written to fd 3" >&3
exec 3>&- # 关闭 fd 3
读取文件
# 打开文件描述符 4 用于读取
exec 4< /etc/passwd
read line <&4
echo "$line"
exec 4<&- # 关闭 fd 4
同时读写
exec 5<> /tmp/dual.log
echo "Hello" >&5
exec 5<&- # 关闭读取端
exec 5>&- # 关闭写入端
经典:脚本日志重定向
#!/bin/bash
LOGFILE="/var/log/myscript.log"
exec > >(tee -a "$LOGFILE") 2>&1
echo "All output goes to both terminal and log file"
3.3 配合 source 与 . 的区别
| 命令 | 是否创建新进程 | 是否替换当前进程 | 用途 |
|---|---|---|---|
exec program |
否 | 是 | 替换当前 shell 为 program |
source script |
否 | 否 | 在当前 shell 中执行脚本 |
./script |
是 | 否 | 创建子进程执行脚本 |
四、系统调用层面的 exec 家族
4.1 函数家族概览
#include <unistd.h>int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
4.2 命名规则
| 后缀 | 含义 |
|---|---|
l |
list — 参数以可变参数列表形式传递(如 execl) |
v |
vector — 参数以数组形式传递(如 execv) |
e |
environment — 允许自定义环境变量 |
p |
pATH — 在 PATH 环境变量中搜索可执行文件 |
4.3 调用示例
#include <unistd.h>
#include <stdio.h>int main() {// execl: 参数逐个列出,以 NULL 结尾execl("/bin/ls", "ls", "-l", "/tmp", NULL);// 如果 exec 成功,下面这行不会执行perror("execl failed");return 1;
}
// execvp: 使用数组和 PATH 搜索
char *args[] = {"ls", "-l", "/tmp", NULL};
execvp("ls", args); // 会在 PATH 中查找 ls
4.4 exec 与 fork 的配合
exec 通常与 fork() 配合使用,这是创建新进程的标准模式:
pid_t pid = fork();
if (pid == 0) {// 子进程:替换为新程序execl("/bin/echo", "echo", "Hello from child", NULL);perror("execl failed");_exit(1); // exec 失败时必须退出
} else if (pid > 0) {// 父进程:等待子进程waitpid(pid, NULL, 0);
}
执行流程:
父进程 (PID=1000) ──fork()──> 子进程 (PID=1001)│└──exec()──> /bin/echo (PID=1001)
五、高级用法
5.1 使用 exec 改变脚本自身的文件描述符
#!/bin/bash# 保存原始 stdout
exec 3>&1# 将所有输出重定向到日志
exec > /tmp/script.logecho "This goes to log"# 恢复原始 stdout
exec 1>&3
echo "This goes to terminal"exec 3>&- # 关闭备份描述符
5.2 在函数中使用 exec
my_function() {local fdexec {fd}> /tmp/func.log # 自动分配文件描述符echo "Log entry" >&$fdexec {fd}>&- # 关闭
}
5.3 配合 trap 做清理
#!/bin/bash
cleanup() {exec 3>&- 2>/dev/null
}
trap cleanup EXITexec 3> /tmp/important.log
echo "Critical data" >&3
六、常见陷阱与注意事项
6.1 exec 后代码不再执行
#!/bin/bash
exec /bin/true
echo "NEVER REACHED" # 这行永远不会执行
6.2 exec 失败不会自动退出
#!/bin/bash
exec /nonexistent/program
echo "Still running!" # 如果 exec 失败,这行会执行!
正确做法:
exec /nonexistent/program || { echo "exec failed"; exit 1; }
6.3 文件描述符泄漏
使用 exec 打开的文件描述符不会自动关闭,必须显式关闭:
exec 3> /tmp/file
# ... 使用 fd 3 ...
exec 3>&- # 必须关闭,否则泄漏
6.4 与 nohup、disown 的区别
| 特性 | exec program |
nohup program |
disown |
|---|---|---|---|
| 进程替换 | 是 | 否 | 否 |
| SIGHUP 免疫 | 否 | 是 | 是 |
| 输出重定向 | 手动 | 自动到 nohup.out | 无 |
七、实际应用场景
场景 1:容器入口脚本
#!/bin/bash
# entrypoint.sh
set -e# 配置环境
export DB_HOST=${DB_HOST:-localhost}# 重定向日志
exec > >(tee /var/log/app/stdout.log)
exec 2> >(tee /var/log/app/stderr.log >&2)# 替换为实际应用(PID 1 保持为应用进程)
exec /usr/bin/myapp "$@"
在容器中,
exec确保应用进程接管 PID 1,从而正确接收 SIGTERM 信号。
场景 2:Shell 切换
# 从 bash 切换到 zsh,保持当前目录和环境
exec zsh
场景 3:权限隔离脚本
#!/bin/bash
# 先以 root 执行初始化,然后降级运行主程序
chown -R appuser:appuser /data
exec su -s /bin/sh -c '/usr/bin/myapp' appuser
八、总结速查表
| 用法 | 命令示例 | 效果 |
|---|---|---|
| 替换为程序 | exec vim file |
Shell → vim,PID 不变 |
| 输出重定向 | exec > log.txt |
后续命令输出到文件 |
| 输入重定向 | exec < input.txt |
后续命令从文件读取 |
| 打开 FD | exec 3> file |
创建文件描述符 3 |
| 关闭 FD | exec 3>&- |
关闭文件描述符 3 |
| 复制 FD | exec 3>&1 |
将 fd 3 指向 stdout |
| 自动分配 FD | exec {fd}> file |
Bash 自动分配 fd 编号 |
