Linux exec 命令详解
一句话总结:
exec是 Shell 的内建命令,它不会创建新的进程,而是用新的程序替换当前进程。
一、什么是 exec
exec 是 Linux Shell(bash、sh、zsh 等)的一个 Builtin(内建命令)。
它最大的特点是:
使用新的程序替换当前 Shell 进程,而不是创建新的子进程。
理解 exec 的关键就是理解:
- 普通命令:fork + exec
- exec 命令:只有 exec,没有 fork
二、普通执行 VS exec 执行
假设当前 Shell 的 PID:
bash
$ echo $$
1000
1. 普通执行
例如:
bash
sleep 100
执行过程:
Shell(pid=1000)
│
├── fork()
▼
sleep(pid=1001)
进程树:
shell
└── sleep
sleep 结束以后:
shell
Shell 一直存在。
2. exec 执行
例如:
bash
exec sleep 100
执行过程:
Shell(pid=1000)
exec()
↓
sleep(pid=1000)
注意:
PID 完全没有变化!
Shell 已经不存在。
最终:
sleep(pid=1000)
sleep 退出以后:
整个终端结束。
因为:
Shell 已经被替换掉了。
三、exec 底层做了什么?
Linux 底层提供了一个系统调用:
c
execve()
它的作用是:
把当前进程的地址空间全部替换成新的程序。
例如:
原来:
bash
代码
数据
堆
栈
环境变量
调用:
c
execve("/bin/sleep")
以后:
sleep
代码
数据
堆
栈
整个 bash 消失。
但是:
PID 不变
因此:
exec 是替换当前进程,而不是创建新的进程。
四、为什么 exec 不会产生新进程?
很多人误以为:
exec == 启动程序
实际上:
Linux 启动程序通常都是:
fork()
↓
exec()
例如:
bash
│
fork
│
▼
child bash
│
exec(ls)
▼
ls
因此:
shell
└── ls
父 Shell 仍然存在。
而:
bash
exec ls
只有:
shell
↓
ls
没有 fork。
所以:
PID 不变
Shell 消失
五、exec 最常见的用途
用途一:替换当前进程
例如:
bash
#!/bin/bash
echo before
exec python app.py
echo after
输出:
before
然后:
python app.py
永远不会输出:
after
因为:
Shell 已经不存在了。
用途二:Docker ENTRYPOINT
官方推荐:
bash
#!/bin/sh
exec nginx -g 'daemon off;'
而不是:
bash
nginx -g 'daemon off;'
为什么?
不用 exec:
PID1
shell
└── nginx
Docker 发送:
SIGTERM
实际上先发给:
shell
很多 Shell 不会正确转发信号。
使用 exec:
PID1
nginx
Docker 发:
SIGTERM
直接到 nginx。
因此:
Docker 官方推荐:
exec nginx
用途三:systemd Service
例如:
ExecStart=/bin/bash start.sh
而:
start.sh
↓
exec java -jar app.jar
最终:
systemd
↓
java
而不是:
systemd
↓
bash
↓
java
这样:
- PID 更清晰
- 信号更容易管理
- systemd 能直接监控业务进程
用途四:Shell Wrapper
例如:
bash
#!/bin/bash
ulimit -n 65535
export A=1
exec "$@"
执行:
bash
wrapper.sh python app.py
最终:
python
wrapper 已经不存在。
因此:
wrapper 常用于:
- 初始化环境
- 设置 ulimit
- export 环境变量
- 最后 exec 真正程序
六、exec 的另一大用途:修改文件描述符(FD)
除了替换进程,
exec 还可以:
永久修改当前 Shell 的文件描述符。
重定向 stdout
bash
exec >output.log
以后:
bash
echo hello
echo world
都会进入:
output.log
重定向 stderr
bash
exec 2>error.log
以后:
所有错误:
stderr
↓
error.log
打开新的 FD
例如:
bash
exec 3<>data.txt
打开:
FD3
↓
data.txt
以后:
bash
echo hi >&3
直接写入:
FD3
很多高级 Shell 脚本都会这样管理文件。
七、为什么 Wrapper 都喜欢使用 exec?
例如:
wrapper.sh
↓
exec benchmark-cli
如果不用 exec:
bash(wrapper)
└── benchmark-cli
PID:
100 bash
101 benchmark
如果:
kill 100
很多情况下:
101
可能仍然存活(取决于信号处理、进程组、是否脱离终端等)。
如果:
bash
exec benchmark-cli
则变成:
100 benchmark
没有中间 Shell。
优势:
- 少一个进程
- PID 不变
- systemd 管理更简单
- Docker 管理更简单
- 信号直接发送到业务程序
- 避免 Wrapper 残留
八、exec 与 PTY(伪终端)的关系
例如:
PTY
↓
bash
↓
wrapper.sh
↓
exec benchmark-cli
最终实际上:
PTY
↓
benchmark-cli
wrapper 已经不存在。
因此:
关闭 PTY:
SIGHUP
↓
benchmark-cli
如果 benchmark-cli:
- 没有忽略 SIGHUP
- 没有调用 setsid()
- 没有脱离当前 Session
那么:
它通常会直接退出。
相比:
PTY
↓
bash
↓
benchmark-cli
少了一层 Shell,
整个任务生命周期更容易统一管理。
这也是很多压测平台、CI/CD、SSH Agent、远程执行框架喜欢使用:
bash
exec "$@"
作为最后一步的原因。
九、exec 与普通命令的区别总结
| 特性 | 普通命令 | exec |
|---|---|---|
| 创建新进程 | ✅ 是(通常 fork + exec) |
❌ 否 |
| 当前 Shell 是否存在 | ✅ 存在 | ❌ 被替换 |
| PID 是否变化 | 新程序拥有新 PID | PID 保持不变 |
| 后续脚本是否继续执行 | ✅ 会 | ❌ 不会 |
| 信号传递 | 多一层 Shell | 直接传递给目标程序 |
| Docker / systemd 推荐 | 一般不推荐 | ✅ 推荐 |
| 文件描述符管理 | 不修改当前 Shell | ✅ 可永久修改当前 Shell 的文件描述符 |
十、核心记忆
牢记一句话:
exec 的本质不是"启动一个程序",而是"让当前进程变成另一个程序"。
因此:
- 普通命令 = fork + exec
- exec 命令 = 只有 exec,没有 fork
它最大的价值体现在:
- 减少中间 Shell
- PID 保持不变
- 信号直接传递
- 便于 Docker / systemd 管理
- 便于 PTY 生命周期管理
- 能够修改当前 Shell 的文件描述符
因此,在编写 Wrapper、启动脚本、Docker ENTRYPOINT、systemd Service、远程执行 Agent 等场景中,exec 几乎都是最佳实践。