你真的了解 bash 的 exec 命令吗?

前言

bash exec 命令不太常用,我对它提供的功能也仅知一二,没有掌握全貌。最近遇到了几个问题,从问题出发研究了一通找到 exec 命令头上,问题迎刃而解后,才对 exec 命令提供的功能有了进一步的认识。

在本文中,我将从 bash manual 中 exec 命令的帮助信息出发,完整的描述 exec 命令提供的功能以及我过去几个月遇到的两个与 exec 命令相关的问题。

exec 函数的 manual 信息

       exec [-cl] [-a name] [command [arguments]]
              If command is specified, it replaces the shell.  No new process is created.  The arguments become  the
              arguments  to  command.  If the -l option is supplied, the shell places a dash at the beginning of the
              zeroth argument passed to command.  This is what login(1) does.  The -c option causes  command  to  be
              executed  with  an empty environment.  If -a is supplied, the shell passes name as the zeroth argument
              to the executed command.  If command cannot be executed  for  some  reason,  a  non-interactive  shell
              exits,  unless the shell option execfail is enabled, in which case it returns failure.  An interactive
              shell returns failure if the file cannot be executed.  If command is not specified,  any  redirections
              take  effect  in  the current shell, and the return status is 0.  If there is a redirection error, the
              return status is 1.

exec 命令提供的基本功能加载新的程序替换 shell 并执行,除此之外它还有如下几个重要的功能:

  1. -l 参数,在命令的第 0 个参数前面添加一个 -
  2. -a 参数,将命令的第 0 个参数替换为指定的名称
  3. -c 参数,使用一个空的环境执行程序

下面我分别对这三个功能进行解释。

1. exec -l

-l 选项在命令的第 0 个参数前面添加一个 -,这一功能不常用。这里添加的 - 与 login shell 有关。

bash 登录时读配置文件问题 这篇文章中,我描述过 login shell 与交互式 shell 初始化过程中读配置文件的区别。

login shell 的定义如下:

A login shell is one whose first character of argument zero is a -, or one started with the --login option.

当执行 shell 程序的第 0 个参数的第一个字符为 - 时,此 shell 为 login shell,login shell 执行的时候会读取 ~/.bash_profile 文件。

使用如下命令进行测试:

[longyu@debian-10:19:21:18] ~ $ echo "echo test" >> ~/.bash_profile
[longyu@debian-10:19:21:25] ~ $ exec -l bash
test
[longyu@debian-10:19:21:38] ~ $ exec bash
[longyu@debian-10:19:21:47] ~ $ 

上述命令首先在 ~/.bash_profile 文件中追加一行打印,然后执行 exec -l bash 命令,执行后可以看到终端打印了 test 字符串,表明 ~/.bash_profile 文件被加载执行。这之后执行 exec bash 命令,终端没有任何输出,这时执行的 shell 不是 login shell 故而不读取 ~/.bash_profile 文件内容。

2. exec -a

-a 参数可以称为 -l 参数的扩展,它能够直接替换第 0 个参数的内容。使用如下脚本进行测试:

#!/bin/bash

exec -a test ls

使用 strace 跟踪执行并在输出中搜索 execve 系统调用,得到了如下信息:

[longyu@debian-10:19:27:14] ~ $ strace ./test.sh 2>&1 | grep execve
execve("./test.sh", ["./test.sh"], 0x7ffc3d432a10 /* 73 vars */) = 0
execve("/usr/bin/ls", ["test"], 0x55782a206150 /* 73 vars */) = 0

第一次系统调用执行了 ./test.sh 脚本,第二次系统调用执行了 /usr/bin/ls 程序,且程序的第 0 个参数被替换为了 test。

3. exec -c

-c 选项将会在空的环境中执行程序,这里说的环境指的是环境变量

在一个终端中执行如下命令在空环境中执行 awk 程序:

[longyu@debian-10:19:36:56] ~ $ exec -c awk '{print $1}'

在另外一个终端中查看 awk 命令的环境变量:

[longyu@debian-10:19:37:25] ~ $ ps aux | grep awk
longyu   19374  0.0  0.0  10180  2500 pts/2    Ss+  19:36   0:00 awk {print $1}
longyu   19425  0.0  0.0  18976   884 pts/3    S+   19:38   0:00 grep --color=auto awk
[longyu@debian-10:19:38:14] ~ $ cat /proc/19374/environ 
[longyu@debian-10:19:38:21] ~ $ 

可以确认环境变量为空

在一个终端中正常执行 awk 程序:

[longyu@debian-10:19:38:40] ~ $ awk '{print $1}'

在另外一个终端中查看 awk 程序的环境变量内容:

[longyu@debian-10:19:38:49] ~ $ ps aux | grep awk
longyu   19464  0.0  0.0  26064  3052 pts/3    S+   19:38   0:00 awk {print $1}
longyu   19483  0.0  0.0  18976   888 pts/2    S+   19:38   0:00 grep --color=auto awk
[longyu@debian-10:19:38:53] ~ $ cat /proc/19464/environ 
SHELL=/bin/bashSESSION_MANAGER=local/debian-10:@/tmp/.ICE-unix/1891,unix/debian-10:/tmp/.ICE-unix/1891QT_ACCESSIBILITY=1COLORTERM=truecolorXDG_MENU_PREFIX=gnome-GNOME_DESKTOP_SESSION_ID=this-is-deprecatedGTK_IM_MODULE=fcitxHISTSIZE=-1QT4_IM_MODULE=fcitxLC_ADDRESS=zh_CN.UTF-8JAVA_HOME=/usr/local/jdk1.7.0_67LC_NAME=zh_CN.UTF-8SSH_AUTH_SOCK=/run/user/1000/keyring/ssh_ZL_MATCH_MODE=1XMODIFIERS=@im=fcitxDESKTOP_SESSION=gnome-xorgLC_MONETARY=zh_CN.UTF-8SSH_AGENT_PID=1940GTK_MODULES=gail:atk-bridgeXDG_SEAT=seat0PWD=/home

可以看到此时 awk 的环境变量有许多,这些环境变量是从父进程(bash)中继承到的!

在某些特定场景中,需要避免从父进程继承环境变量对子进程执行的影响,这时候 -c 参数就能够派上用场。

exec 与文件描述符的关闭问题

bash 的 exec 命令最终将会调用到 execve 系统调用,execve 系统调用在执行的时候将会关闭设置 FD_CLOEXEC 标志的文件描述符。有这样的认识后,当我遇到 为啥 bash 没有按照我想象的样子执行脚本呢? 这篇博文中描述的问题时,我想到了 exec,测试确定能够解决问题。

exec -a 修改命令的第一个参数问题

常见发行版中的 poweroff、reboot 命令都是个软链接,指向 systemctl 命令,与之类似的有 insmod、modinfo 命令,这两个命令也是个软链接,指向 kmod 命令。

以 reboot 为例,其命令信息如下:

[longyu@debian-10:19:59:13] ~ $ ls -lh /usr/sbin/reboot
lrwxrwxrwx 1 root root 14 1月  29 22:16 /usr/sbin/reboot -> /bin/systemctl

能够看到它指向了 systemctl 命令。

现在你需要在 reboot 命令执行前做某些操作,你不能添加一个新的命令,只能通过修改原有的 reboot 命令来实现,并且基于可维护性考虑你必须使用 bash 脚本来实现。

你可能会立马写出如下脚本:

#!/bin/bash

export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# do something
systemctl reboot $@

执行 reboot -f 命令能够成功,然而当你尝试指定其它选项时却发现了问题。

例如你尝试执行 --help 选项时,终端却输出了 systemctl 的帮助信息,部分内容如下:

[longyu@debian-10:20:06:20] ~ $ ./reboot --help
systemctl [OPTIONS...] {COMMAND} ...

Query or send control commands to the systemd manager.

你期望的内容如下:

reboot [OPTIONS...] [ARG]

Reboot the system.

     --help      Show this help
     --halt      Halt the machine
  -p --poweroff  Switch off the machine
     --reboot    Reboot the machine
  -f --force     Force immediate halt/power-off/reboot
  -w --wtmp-only Don't halt/power-off/reboot, just write wtmp record
  -d --no-wtmp   Don't write wtmp record
     --no-wall   Don't send wall message before halt/power-off/reboot

See the halt(8) man page for details.

你首先会想为什么 systemctl 不支持 systemctl reboot --help 这种参数呢?这与 systemctl 命令的实现有关。

上文我已经说过了 reboot 与 poweroff 都是个链接文件且都指向了 systemctl 命令,当你执行 reboot 命令的时候第 0 个参数内容为 reboot,执行的时候却由于 execve 会 follows symbolic link,最终执行到 systemctl 命令systemctl 命令会判断第 0 个参数,执行不同的功能。

如果你想让你的脚本完全兼容 reboot 命令的参数,你需要做的就是替换执行命令的 argv[0],将它改为 reboot。按照这个思路搜索互联网,很快就能发现 exec -a 就能够提供这一功能。

修改后的脚本内容如下:

#!/bin/bash

export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# do something
exec -a reboot systemctl $@

至此,问题得到解决!

总结

exec 命令看似简单却也不太简单,它充其量也不过是 bash 的冰山一角。我可以通过阅读 bash 命令的手册来确定 exec 命令支持的功能,这纯粹是记忆的过程且很容易遗忘。

我也可以从实际的问题出发,通过分析明确解决问题需要什么知识或功能,再去寻找这些知识与功能的实例,在这里 exec 就是满足上文描述的两个问题功能的实例。

参考链接

option -l of exec shell command

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值