Linux exec命令详解

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

它最大的价值体现在:

  1. 减少中间 Shell
  2. PID 保持不变
  3. 信号直接传递
  4. 便于 Docker / systemd 管理
  5. 便于 PTY 生命周期管理
  6. 能够修改当前 Shell 的文件描述符

因此,在编写 Wrapper、启动脚本、Docker ENTRYPOINT、systemd Service、远程执行 Agent 等场景中,exec 几乎都是最佳实践。