文章结构:


#exec函数族

通常所说的exec是指一组函数,如下共6个:

#include <unistd.h>
extern char **environ;

int execl(const char *path, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);

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

exec函数族的功能是根据指定的文件名找到可执行文件执行它并用它来取代调用进程的内容,即取代原调用进程的数据段、代码段和堆栈段。

当exec函数正常执行时,原调用进程的内容除了pid是被保留的,其他的全部被新进程替换了(旧瓶装新酒),且除非exec函数调用失败,否则exec函数后的所有代码都不会再被执行。

这里可执行文件可以是二进制文件,也可以是可执行的脚本文件。也就是上面的函数族中第一个参数filepath指向的内容。

注:man 3 exec出来的文档显示有一个execvpe函数,即文中的execve。由于在本人的Mac机中,unistd.h头文件中声明的是execve函数,所以这里会和 man 的文档有出入。


#函数分类

这6个函数在函数名和使用语法的规则上都有细微的区别,下面就从可执行文件查找方式参数传递方式环境变量这几个方面进行比较。

  • 查找方式
    函数族中的前4个函数的查找方式都是完整的文件目录路径,而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动按照环境变量PATH所指定的路径进行查找。

  • 参数传递方式
    exec函数族的参数传递有两种:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。

    在这里是以函数名的第5位字母来区分的,字母为 “l”(list)的表示逐个列举参数的方式,其语法为const char *arg0 , arg1, ..., argN;字母为“v”(vector)的表示将所有参数整体构造指针数组传递,其语法为char *const argv[]

    这里的参数实际上就是用户在使用这个可执行文件时所需的全部命令选项字符串(包括该可执行程序命令本身)。

  • 环境变量

    exec函数族可以默认系统的环境变量,也可以传入指定的环境变量。这里以 “e”(environment)结尾的两个函数 execle()execve()就可以在 envp[]中指定当前进程所使用的环境变量。

    这部分的详情讲解可以见下文的”??????????????”

下表即为对函数族的归纳小结,方便加深理解。

位数 含义
前4位 统一为 exec 开头
第5位 l: 参数传递为逐个列举方式
v: 参数传递为字符串数组方式
第6位 e: 可传递新进程环境变量
p: 可执行文件查找方式为文件名

#函数参数潜规则

在使用exec函数族时,不论使用的参数是以l方式逐个列举,还是以v方式传递的字符串数组,对参数的值有如下要求:
第一个参数指定可执行文件的绝对路径(非p类函数) 或 可执行文件文件名(p类函数)。
第二个参数照惯例要写明可执行文件的文件名。
最后一个参数要指定是(char *)NULL
中间的参数才是正常的向可执行文件传递的参数。

最后一个参数要指定是(char *)NULL的原因是:函数族中系统的真正实现是execve(),其它函数都是对execve()的封装,再看下execve()只支持char* const argv[],即字符串数组,由于在C语言中,无法获知当前字符串数组的长度,又为了避免数组越界造成其它不可知的错误,所以设定了当遍历参数数组时,遇到了(char *)NULL则表示数组已经遍历到结尾了。

同理,在使用和环境变量相关的execle()execvp()execve()等传递环境变量的字符串数组时,该数组envp[]也要以(char * )NULL结尾。


#函数示例

######execl()
以下代码演示了正常执行execl()函数的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <unistd.h>
static void testExecl() {
// 正常执行
int result = execl("/bin/echo", "echo", __FILE__, __func__, (char *)NULL);
// // 错误执行
// int result = execl("/bin/echo", "echo", __FILE__, __func__, __LINE__, (char *)NULL);
if (result == -1) {
perror("execl error");
}
}
int main/*08*/ (int argc, char ** argv) {
testExecl();
printf("this line can't be printed.\n");
return 0;
}

由于execl()需要填写的是可执行命令的完整路径,所以需要知道echo命令的完整路径。
可以通过在命令行终端运行which echo即可得知完整路径。

sodino:Define sodino$ which echo
/bin/echo

在上面的代码示例中,使用echo命令执行输出了当前代码的文件名、函数名;
在错误的代码示例中还试图去输出代码行数。

下图为分别执行正常执行与错误执行的两种效果截图:

exec.demo

在正常执行下,只输出了exec.c testExcel这两个字符串,而main()函数结束的printf()则没有被执行。
当在错误执行的情况下,则通过perror()输出了execl()执行错误的原因,且main()函数结束的printf()被执行输出了。

错误执行的原因是:execl()只接受字符串参数,而宏__LINE__则会向excel()传递整型数值,导致取址错误。宏定义的文章见【C/C++】宏(macro)定义与使用


######execv()

以下代码演示执行execv()函数的例子,与execl()是相似的,输出结果也与execl()一致:

1
2
3
4
5
6
7
static void testExecv() {
char *argv[] = {"echo", __FILE__, __func__, (char*)NULL};
int result = execv("/bin/echo", argv);
if (result == -1) {
perror("execl error");
}
}

######execlp() execvp()

exec函数族中函数名称带p的函数允许只可执行文件名称即可,函数在执行时会自动在系统设置的PATH环境变量中去寻找可执行文件。

PATH的值可以通过在命令行终端处输出echo $PATH$来查看当前的环境变量,对该环境变量下所有的文件夹路径下的可执行文件都生效。

exec的p类函数的第一个参数如果是以/开头的,则按绝对路径处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static void testExeclp() {
// 正常执行
int result = execlp("echo", "echo", __FILE__, __func__, (char *)NULL);
if (result == -1) {
perror("execl error");
}
}
static void testExecvp() {
char *argv[] = {"echo", __FILE__, __func__, (char*)NULL};
int result = execvp("echo", argv);
if (result == -1) {
perror("execl error");
}
}

#环境变量

在文章一开头中介绍exec函数族的函数原型中有一行如下:

extern char **environ;

这行代码是指程序可以通过声明该代码,获取到在系统内核中定义的所有的环境变量。要注意的是使用了extern的修饰符表示该变量environ是引用自的其它代码文件中的变量。

环境变量是以如下形式定义的:

key=value

通常key用来定义一些常用的路径名称,这些名称不但是在本机上通用的,而且在其它机子也是约定俗成的,如OSPATHHOME等。但由于在不同的机子上值可以不一致,如在Linux上我切换到另一个帐号,则HOME的值就会变成/Users/other_uid去了。所以系统在启动时每次都会把value加载进来与key绑定,要用时直接取key的值即可了。


######打印环境变量

下面的代码演示如何获取系统所有的环境变量:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <unistd.h>
extern char ** environ;
int main/*08*/ (int argc, char ** argv) {
for (int i = 0;environ[i] != NULL; i ++) { // 以"NULL"判断数组结束
printf("i=%d [%s] \n", i, environ[i]);
}
return 0;
}

在sodino的机子上输出为:
environ

也可以在命令行终端下通过env命令查看。

在这里先把上面的小段代码编译生成一个可执行的文件为“print_env.out”。下面将会用到。


######传递环境变量

接下来演示exec函数族中e类函数的使用。
e类函数的作用是将自定义的envp[]数组传递给要可执行文件,可执行文件通过extern char** environ获得envp[]中的字符串数组内容。

需要再一次强调的是,由于C语言中指针无法获知evnp[]的数组长度,所以是以出现NULL为判断认为遍历到数组结束位置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
static void testExecE(int type) {
printf("%s %s %s\n", __FILE__, __func__, ((type == 1)?"execle() ": "execve() "));
// 这里运行刚才编译好的'print_env.out'文件
const char * binary = "/Users/sodino/workspace/print_env.out";
char * envp[] = {"author=sodino", "mail=sodino@qq.com", (char*)NULL};
if (type == 1) {
execle(binary, "print_env.out", (char*)NULL, envp);
} else {
char * argv[] = {"print_env.out", (char*)NULL};
execve(binary, argv, envp);
}
}

env.demo

在上面的代码testExecE()中,envp数组中的字符串即使不是key=value结构,也是可以的。所以更加广义上来讲,exec函数族中的e类函数的一大功能是向调用的可执行文件传递参数的作用。