有 7 种不同的 exec 函数可供使用。
#include <unistd.h> int execl(const char *pathname, const char *arg0, ... /* (char *)0 */); int execv(const char *pathname, char *const argv[]); int execle(const char *pathname, const char *arg0, ... /* (char *)0, char *const envp[] */ ); int execve(const char *pathname, char *const argv[], char *const envp[]); int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ ); int execvp(const char *filename, char *const argv[]); int fexecve(int fd, char *const argv[], char *const envp[]); /* 7 个函数返回值:若成功,不返回;否则,返回 -1 */
其中,字母 p 表示取 filename 作为参数,并用 PATH 环境变量寻找可执行文件;字母 l 表示取一个参数表;v 表示取一个 argv[] 矢量,与字母 l 互斥;字母 e 表示取 envp[]
数组,而不使用当前环境,即可以传递一个指向环境字符串指针数组的指针,否则就使用调用进程中的 environ 变量为新程序复制现有的环境。
当指定 filename 作为参数时,如果 filename 中包含“/”,则将其视为路径名,否则就在 PATH 环境变量指定的各目录中搜寻可执行文件。如果找到了一个可执行文件,但是该文件不是由连接编辑器产生的机器可执行文件,则就认为该文件是一个 shell 脚本,于是试着调用 /bin/sh,并以该 filename 作为 shell 的输入。
fexecve 函数避免了寻找正确的可执行文件,而是依赖调用进程来完成这项工作。调用进程可以使用文件描述符验证所需要的文件并且无竞争地执行该文件。否则,拥有特权的恶意用户就可以在找到文件位置并且验证之后,但在调用进程执行之前替换可执行文件或可执行文件的部分路径。
execl、execlp 和 execle 要求将新程序的每个命令行参数都说明为一个单独的参数,这种参数表以空指针结尾,execv、execvp、execve 和 fexecve 函数则以一个指向各命令行参数的指针数组的地址作为参数。
在使用 ISO C 原型之前,对 execl、execle 和 execlp 三个函数表示命令行参数的一般方法是:
char *arg0, char *arg1, ..., char *argn, (char *)0
这种语法显示地说明了最后一个命令行参数之后跟了一个空指针(如果用 0 来表示一个空指针,则必须将它强制转换为一个指针。如果一个整型数的长度与 char * 的长度不同,那么 exec 的实际参数将出错)。
通常,一个进程允许将其环境传播给其子进程,但有时进程也想为子进程指定某一个确定的环境。例如,在初始化一个新登录的 shell 时,login 程序通常创建一个只定义少数几个变量的特殊环境,而在登录时,可以通过 shell 启动文件,将其他变量添加到环境中。
在使用 ISO C 原型之前,execle 的参数是:
char *pathname, char *arg0, ..., char *argn, (char *)0, char *envp[]
可见最后一个参数是指向环境字符串的各字符指针构成的数组的指针。而在 ISO C 中,所有命令行参数、空指针和 envp 指针都使用省略号表示。
每个系统对参数表和环境表的总长度都有一个限制,该限制是由 ARG_MAX 给出的。在 POSIX.1 系统中,此值至少是 4096 字节。为摆脱对参数表长度的限制,可以使用 xargs(1) 命令,将长参数表断开成几部分。比如,为了寻找所用系统手册页中的 getrlimit,可用:
find /usr/share/man -type f -print | xargs grep getrlimit
如果系统手册页是压缩过的,则可使用:
find /usr/share/man -type f -print | xargs bzgrep getrlimit
这里使用 find 的“-type f”选项的原因是,grep 命令不能在目录中进行模式搜索,同时也可避免不必要的出错信息。
执行 exec 后,新程序从调用进程继承了下列属性:
* 进程 ID 和父进程 ID
* 实际用户 ID 和实际组 ID
* 附属组 ID
* 进程组 ID
* 会话 ID
* 控制终端
* 闹钟尚余留的时间
* 当前工作目录
* 根目录
* 文件模式创建屏蔽字
* 文件锁
* 进程信号屏蔽
* 未处理信号
* 资源限制
* nice 值
* tms_utime、tms_stime、tms_cutime 以及 tms_cstime 值
对打开文件的处理与每个描述符的执行时关闭(close-on-exec)FD_CLOEXEC 标志值有关:若设置了此标志,则在执行 exec 时关闭该描述符;否则该描述符仍打开。除非特地使用 fcntl 设置了此标志,否则系统的默认操作是在 exec 后仍保持这种描述符打开。
POSIX.1 明确要求在 exec 时关闭打开目录流,这通常是由 opendir 函数实现的,它调用 fcntl 函数为对应于打开目录流的描述符设置执行时关闭。
在 exec 前后实际用户 ID 和实际组 ID 保持不变,而有效 ID 是否改变则取决于所执行程序文件的设置用户 ID 位和设置组 ID 位是否设置:如果已设置,则有效用户 ID 变成程序文件所有者的 ID;否则有效用户 ID 不变。对组 ID 的处理方式类似。
在很多 UNIX 系统中,只有 execve 是系统调用,另外 6 个是库函数,它们最终都要调用该系统调用。它们之间的关系如图所示。
在这种安排中,库函数 execlp 和 execvp 使用 PATH 环境变量,查找第一个包含名为 filename 的可执行文件的路径名前缀,fexecve 使用 /proc 把文件描述符转换成路径名,execve 用该路径名去执行程序。这描述了在 FressBSD 8.0 和 Linux 3.2.0 中是如何实现 fexecve 的,其他系统采用的方法可能不同。例如,没有 /proc 和 /dev/fd 的系统可能把 fexecve 实现为系统调用,把文件描述符参数转换成 i 节点指针,把 execve 实现为系统调用,把路径名参数转换成 i 节点指针,然后把 execve 和 fexecve 中剩余的 exec 公共代码放到单独的函数中,调用该函数时传入执行文件的 i 节点指针。
下面这个程序演示了 exec 函数。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> char *env_init[] = {"USER=unknown", "PATH=/tmp", NULL}; int main(void){ pid_t pid; if((pid=fork()) < 0){ printf("fork 1 error\n"); exit(2); } if(pid == 0){ // specify pathname, specify environment if(execle("./echoall.out", "echoall.out","myarg1","MY ARG2", (char *)0, env_init) < 0){ printf("execle error\n"); exit(2); } } if(waitpid(pid, NULL, 0) < 0){ printf("waitpid 1 error\n"); exit(2); } printf("==================================\n"); if((pid=fork()) < 0){ printf("fork 2 error\n"); exit(2); } if(pid == 0){ if(execlp("./echoall.out", "echoall.out", "only 1 arg", (char *)0) < 0){ printf("execlp error\n"); exit(2); } } if(waitpid(pid, NULL, 0) < 0){ printf("waitpid 2 error\n"); exit(2); } exit(0); }
在该程序中先调用 execle,它要求一个路径名和一个特定的环境。下一个调用的 execlp使用一个文件名,并将调用者的环境传送给新程序。另外,这里将新程序中的 argv[0] 参数设置为路径名的文件名分量,某些 shell 将此参数设置为完全的路径名,也可将其设置为任何字符串。当 login 命令执行 shell 时就是这样做的。在执行 shell 前,login 在 argv[0] 前加一个“/”作为前缀,这向 shell 指明它是作为登录 shell 被调用的。登录 shell 将执行启动配置文件命令,而非登录 shell 则不会执行这些命令。
上面这个程序中执行的 echoall 程序会回显所有命令行参数及环境表,其代码如下。
#include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]){ int i; char **ptr; extern char **environ; for(i=0; i<argc; i++) // echo all command-line args printf("argv[%d]: %s\n", i, argv[i]); for(ptr=environ; *ptr!=0; ptr++) // echo all env strings printf("%s\n", *ptr); exit(0); }
exec 演示程序的执行结果如下。
$ ./execDemo.out argv[0]: echoall.out argv[1]: myarg1 argv[2]: MY ARG2 USER=unknown PATH=/tmp ================================== argv[0]: echoall.out argv[1]: only 1 arg IMSETTINGS_INTEGRATE_DESKTOP=yes TERM=xterm SHELL=/bin/bash HISTSIZE=1000 XDG_SESSION_COOKIE=c0ff14d609262918aa50a54a00000014-1493237231.38046-1794705392 WINDOWID=82012945 ... # 省略了多行输出