数字营销 · Web开发 · 基础设施

Linux中exec系列函数的应用

通过模拟 Shell 执行外部命令的过程,理解 Linux 中 fork 与 exec 的配合方式,以及 exec 系列函数的基本区别和参数传递方式。

这段时间在研究 Linux 中用户登录和 Shell 执行程序的原理。

我们知道,Shell 命令分为内部命令和外部命令。内部命令有诸如 cd、history、exit、echo 等,常见的外部命令有 ls、ping、netstat 等。通过 type 命令可以查看一个命令是内部命令还是外部命令。

当执行的命令是内部命令时,Shell 可以直接执行。那么,Shell 是如何执行一个外部命令的呢?Shell 本身也是一个程序,程序之间调用的方式有很多,例如 popen。但是,Shell 要正确地执行一个外部命令,需要满足几个条件:

  1. 首先是 Shell 能够找到并调用这个程序,这是最基本的前提。
  2. Shell 需要等待外部程序执行完成之后再继续。
  3. 外部程序执行时,需要继承 Shell 的一些运行状态,例如当前工作目录、控制终端、根目录等。

实际上,在 Linux 中,Shell 执行外部命令通常是通过创建子进程的方式完成的。通过 pstree 可以看到 Linux 的进程树,例如:

[root@home ~]# pstree

systemd-+-2*[agetty]
|-avahi-daemon---avahi-daemon
|-avahi-dnsconfd
|-crond
|-dbus-daemon
|-haveged
|-ifplugd
|-master---pickup
|-python
|-rngd
|-sshd-+-3*[sshd]
| `-sshd---bash---pstree
|-systemd---(sd-pam)
|-systemd-journal
|-systemd-logind
|-systemd-udevd
`-wpa_supplicant

可以看到,这里执行的 pstree 就是 bash 的子进程。那么,Shell 执行外部命令的原理是什么呢?当 Shell 执行一个外部命令时,第一步通常是 fork 出一个子进程。Shell 获取需要执行的程序以及相关参数之后,子进程再调用 exec 系列函数执行另一个程序。这里需要注意:

exec 并不会创建新的进程,而是直接用新的程序替换当前进程。

因此,执行 exec 前后的进程 ID 并不会发生变化。整个过程可以简单理解成:

Shell

fork()

子进程

exec()

新的程序

下面的代码可以简单模拟 Shell 执行外部程序的过程:

#!/usr/bin/env python

import os
import sys

if sys.argv[1:]:
pid = os.fork()
else:
sys.exit(0)

if not pid:

filename = sys.argv[1]

argv1 = filename

argvs = ",".join(sys.argv[2:])

os.execlp(
filename,
argv1,
argvs
)

os.waitpid(pid, 0)

把这个文件保存为 ba,增加执行权限,然后运行:

./ba ping www.hitoy.org

程序可以正常执行。

再运行:

pstree

可以看到 Python 进程产生了一个 ping 子进程。

Linux 中,一个程序调用另一个程序时,经常会采用这种:

fork + exec

的方式。

exec是什么?

再来看一下 exec。

exec 并不是一个单独的函数,而是一组用于执行新程序的函数。Linux 中常见的有 6 个:

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ...,
char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[],
char *const envp[]);

这几个函数虽然名字比较多,但实际上是按照几个不同的功能组合出来的。

其中需要注意的是,带有 p 的函数,例如:

execlp()execvp()

当传入的文件名不是完整路径时,会根据 PATH 环境变量去查找对应的程序。

而不带 p 的函数,参数中的 path 一般需要指定程序的路径。

另外一个需要特别注意的地方是参数的传递。

例如:

execlp()
execvp()

这里的 arg 并不是简单地理解成“传给程序的第一个参数”。

按照程序实际接收到的参数列表来看,通常需要从第 0 个参数开始传递,也就是把程序本身的名称也作为第一个参数传进去。

例如:

argv[0] = 程序名称
argv[1] = 第一个参数
argv[2] = 第二个参数
...

所以,在使用 exec 系列函数的时候,需要特别注意参数的组织方式。通过这几个函数,可以看到 Linux 中“启动另外一个程序”的一个基本过程:

先通过 fork() 创建子进程,再通过 exec() 让这个子进程执行新的程序。

这也是理解 Linux Shell、进程以及程序调用关系时一个非常重要的基础。

评论0

欢迎分享你的看法,也欢迎补充不同的实践经验。