不懂 exec 不好意思说会 Linux

0 阅读2分钟

命令简介

exec 是 Linux 中非常重要的一个命令,它允许一个进程在运行的过程中用另外一个程序的代码和数据来替换自己原有的代码和数据,从而使得不创建新的进程就可以运行另外一个程序的效果。

Tips:进程的替换并不是创建一个新的进程,然后使用新进程替换旧进程,而是在原进程的基础上,对其代码段、数据段和堆栈进行替换,进程的 PID 保持不变。(可以看后面的实验证明)

$ exec [-cl] [-a 名称] [命令 [参数 ...]] [重定向 ...]

使用 <命令> 来代替当前的 Shell。

$ exec ls

如果没有指定 <命令>,则任何的 <重定向> 都在当前 Shell 中生效。

$ exec > output.txt # 此后的命令输出将重定向到 output.txt
$ ls
# 不会直接在当前 Shell 中输出 ls 内容,而是重定向到 output.txt 中

这样,在当前 Shell 中时,此后所有的命令输出都不会直接在终端显示,而是直接重定向到 output.txt 文件中。

选项:

  • -a 名称<名称> 作为第 0 个参数传递给 <命令>
  • -c 在一个空的环境中执行 <命令>
  • -l<命令> 的第 0 个参数中放置一个短横线(-)。

选项讲解

-a

一条命令的第 0 个参数,通常都是进程的名称,如 sleep 就叫作 sleep。如下代码会将 sleep 的命令名称替换成 ddz_sleep.

$ exec -a ddz_sleep sleep 100

然后查看进程,就会发现存在那么一个名为 ddz_sleep 的进程。

$ ps aux | grep sleep
wwj       134013  0.0  0.0  17388  2420 pts/0    S+   10:10   0:00 ddz_sleep 100

用途:

  • 伪造进程名称
  • 便于调试

-a 参数允许你将自定义名称作为命令的第 0 个参数进行传递。这会改变进程在 ps 等工具中显示的名称,而不会影响实际执行的命令。

-c

当我们执行 env 命令时,会列出很多环境变量。

$ env
SHELL=/bin/bash
SESSION_MANAGER=local/wwj:@/tmp/.ICE-unix/3327,unix/wwj:/tmp/.ICE-unix/3327
...

当我们 exec -c 时会清空大部分环境变量,并不是全部清空,还会保留一部分。

首先准备一个脚本 exec_c_test.sh

#!/bin/bash

echo '========================'
echo "PATH=$PATH"
echo '========================'
echo "Number of env vars: $(env | wc -l)"
echo '========================'
echo "I_LIKE_LINUX=$I_LIKE_LINUX"
echo '========================'
ls

~/.bashrc 中定义一个环境变量:

export I_LIKE_LINUX="I like linux."
$ source ~/.bashrc 
$ echo $I_LIKE_LINUX
I like linux.

在当前 Shell 窗口先执行一次该脚本。

观察四个地方:

  • PATH 环境变量
  • Number of env vars(环境变量数量)
  • I_LIKE_LINUX 自定义环境变量是否成功输出?
  • 最后的 ls 输出内容(思考:如果环境变量清空了,那么 ls 还可以执行成功吗?)
image-20260411113140387

使用 exec -c 在清空环境变量后,在干净的环境中再次执行脚本。

$ exec -c ./exec_c_test.sh > output.txt

当再次查看 cat output.txt 时,会发现环境变量的数量仅剩 3 个了,并且 PATH 也只保留了系统默认的那部分,用户自带的就给清空了。

观察四个地方:

  • PATH 环境变量

    保留了系统默认的环境变量,清空了用户自定义的环境变量

  • Number of env vars(环境变量数量)

    仅剩 3 个了

  • I_LIKE_LINUX 自定义环境变量是否成功输出?

    没有输出,因为清空了用户自定义的环境变量,因此找不到该环境变量了

  • 最后的 ls 输出内容(思考:如果环境变量清空了,那么 ls 还可以执行成功吗?)

    可以输出,因为 /usr/bin 属于系统默认的环境变量

image-20260411113505330

-l

-l 将一个短横线(-)放置在命令的第 0 个参数的前面。

如下例子:Bash 的行为像是一个 shell 登录,会去读取相关的配置文件。

$ echo $0
bash
$ exec -l bash
login wwj
$ echo $0
-bash

为什么说 exec -l 且命令是 bash 时是有意义的?

因为 login shell 的触发的条件有两种:

  1. argv[0] 的第一个字符是 -
  2. 启动时采用 --login-l

为什么输出 login wwj ?去阅读我的另外一篇文章 Linux 登录时加载了哪些配置文件?

如下这种,就是没意义的。相当于单纯改了个进程名而已。

$ exec -l sleep 100
image-20260411133712468

证明 exec 不会创建一个新的进程

你说 exec 不会创建一个新的进程?你有啥证据?!

image-20260412111512627

在当前 Shell 窗口先 echo $$ 查看当前 shell 的 PID 是 133772

image-20260412110232937

另外新开一个窗口,用于查看 sleep 这个进程的 PID 是多少。发现 sleep 的 PID 和上面那个 shell 窗口的 PID 是不一样!此时 sleep 的 PID 是 244798

image-20260412110158703

OK,那我们现在使用 exec 来执行 sleep 命令。

image-20260412110634313

当我们再次查看 sleep 的 PID 时,发现 sleep 的 PID 和执行它的那个 shell 一致!都是 133772

这就是说:在执行 exec sleep 100 时,当前的 shell 的代码段和数据段被替换成了 sleep 程序的代码段和数据段。

image-20260412110754970

最终,当 sleep 程序执行完成后,那个被替换的 shell 窗口也会消失!因为 shell 的代码段和数据段已经被 sleep 的代码段和数据段代替了,shell 已经不再是 shell,而是 sleep!(我已经不再是当年的我)

适用场景

重定向文件

$ exec > output.txt

作用:当前 shell 后续的所有输出都写入 output.txt 文件。

当然,也可以指定特定的输出流。

$ exec 2> error.log
# or
$ exec 1> stdout.log

批量执行命令

exec < file当前 shell 的标准输入重定向到文件,之后所有从 stdin 读取的命令都会从这个文件读取数据。

准备一个 command.txt 文件

ls
ll
sleep 100

然后执行如下命令

$ exec < command.txt

这将从 command.txt 中读取内容,并且在 shell 中执行。

执行程序替换自身

用 AI 生成了一份用于处理退出信号的 Python 脚本 python_test.py

import time
import signal
import sys

running = True

def signal_handler(sig, frame):
    """处理退出信号"""
    global running
    print("\n收到退出信号,正在关闭...")
    running = False
    sys.exit(0)

def write_current_time():
    """每隔2秒向文件输出当前时间"""
    # 注册信号处理
    signal.signal(signal.SIGINT, signal_handler)
    signal.signal(signal.SIGTERM, signal_handler)
    
    with open('python_log.txt', 'a') as f:
        while running:
            current_time = time.strftime('%Y-%m-%d %H:%M:%S')
            f.write(f"{current_time}\n")
            f.flush()
            print(f"已写入: {current_time}")  # 控制台输出,便于观察
            time.sleep(2)

if __name__ == '__main__':
    write_current_time()

在 Shell 1 中查看 PID

$ echo $$
275055

在 Shell 1 中运行该脚本

$ python3 python_test.py

在 Shell 2 中查看日志

$ tail -n 20 -f python_log.txt

在 Shell 3 中向 Shell 1(PID=275055)发送优雅退出信号

$ kill -TERM 275055

你会发现,Shell 2 中的日志还是不断的输出。

此时的结构:

Bash [PID = 275055]
  python3 python_test.py [PID = 276763]

我们是对 275055 发送的优雅退出信号,Bash 虽然收到了该信号,但 Bash 默认不会转发信号,因此 python_test.py 也就收不到信号。

采用 exec 方式来执行 python 脚本:

$ exec python3 python_test.py

再次查看 python_test.py 的 PID,会发现就是一样的了!

image-20260412132116335

此时的结构:

Bash [PID = 275055] = python3 python_test.py [PID = 275055]

再次尝试对 PID=275055 发送信号

$ kill -TERM 275055

会发现在 Shell 2 中的日志就不再输出了,并且 Shell 1 窗口也随之退出了。

安全环境、沙箱环境

假设有黑客或者不怀好意的人,在你系统的环境变量中注入了恶意环境变量。

# 攻击者设置恶意环境变量
export LD_PRELOAD=/tmp/malicious.so
export PATH=/tmp/evil:$PATH
export BASH_ENV=/tmp/evil.sh

那么如果在这种环境下执行脚本或程序,就会非常危险!

./application_program

因此,我们需要在一个相对安全的环境下来执行程序或脚本。

exec -c ./application_program

调试和伪装进程

使用 -a Name 选项,前文已有示例,这里不再演示。

不得不使用 exec 的情况

  • 替换当前进程

  • 操作文件描述符

    # 1. 打开文件描述器(读)
    $ exec 3< input.txt
    
    # 2. 打开文件描述器(写)
    $ exec 4> output.txt
    
    # 3. 打开文件描述器(追加)
    $ exec 5>> log.txt
    
    # 4. 复制文件描述器
    $ exec 6<&3      # 复制 FD 3 到 FD 6
    $ exec 7>&4      # 复制 FD 4 到 FD 7
    
    # 5. 移动文件描述器
    $ exec 8<&5-     # 移动 FD 5 到 FD 8(FD 5 被关闭)
    
    # 6. 关闭文件描述器
    $ exec 3<&-      # 关闭读 FD
    $ exec 4>&-      # 关闭写 FD
    
  • 永久改变 Shell 的 I/O

    # 永久重定向输入流:对所有命令有效
    $ exec < input.txt
    # 永久重定向输出流:对所有命令有效
    $ exec > output.txt
    
  • “几乎”完全清空环境变量,需要一个相对干净的环境