exec族函数是Linux进程控制原语中非常重要的一部分,往往和fork/vfork配合使用。
当一个进程调用一种exec函数时,该进程执行的程序完全替换未新程序,而新程序则从其main函数开始执行(原程序后续部分不会再执行了)。但是调用exec并不创建新进程(不同于fork和vfork),所以调用exec前后进程ID并未改变。exec只是用一个全新的程序替换了当前进程的正文、数据、堆和栈。
下面是exec族的函数:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *path, const char *arg, ...
/*, (char *) NULL, char * const envp[] */);
int execv(const char *path, char *const argv[]);
int execve(const char *path, char *const argv[],
char *const envp[]);
int execvp(const char *file, char *const argv[]);
函数失败返回-1,成功则不返回。
这个家族挺大的,挺难记忆的。我们可以稍微总结一下特征:
特点1. 以p结尾的两个函数(execlp,execvp)取文件名作为第一个参数,其它几个取路径名作为参数。这里的文件名可以带绝对路径,也可以不带。这样的话,程序将会取PATH环境变量里面找可执行文件。而其它几个不以p结尾只能使用绝对路径。
程序1:使用不以p结尾的,且不使用路径名作为参数:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
printf ( "begin ...n" );
/* 使用不带p版本的 */
if (execl("ls", "ls", "-rt", NULL) == -1)
{
perror("execl error");
}
printf ( "end ...n" );
return 0;
}
执行结果:
allan@NYC:~/test$ ./exec_test
begin ...
execl error: No such file or directory
end ...
程序2:将程序1改为使用以p结尾的
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
int main()
{
printf ( "begin ...n" );
if (execlp("ls", "ls", "-rt", NULL) == -1)
{
perror("execl error");
}
printf ( "end ...n" );
return 0;
}
执行结果:
allan@NYC:~/test$ ./exec_test
begin ...
apue.c bug.c test1 test1.out test.dat file.c file c-7.c c-7 bug_exp exec_test.c exec_test
可以看到,程序2可以正常执行。要注意的是,后面的printf并没有执行到。因为exec替换了原有的进程映像。
所以,对于以p结尾的这两个exec函数,我们使用的时候,可以不用指定可执行文件(如果不是ld产生的二进制文件,exec就会尝试用/bin/sh取执行,即把它当做一个shell脚本取执行)的路径,让系统取PATH里面查找。当然也可以指定,但如果指定的话,就一定要指定对,因为系统只会去指定的路径查找,不会再去PATH指定的路径找了。举个例子,ls名字的完整路径是/bin/ls,那么我们可以写成execlp("ls", "ls", "-rt", NULL)
,或者execlp("/bin/ls", "ls", "-rt", NULL)
,但不能写成execlp("/usr/bin/ls", "ls", "-rt", NULL)
。而对于不带p后缀的,第一个参数只能写成"/bin/ls"。即,带不带p决定exec族函数的第一个参数如何写——如果带p,可以不用写正确的绝对路径;如果不带,就必须写正确的绝对路径。
特点2. 参数中的l(list)和v(vector)决定其它参数如何传递(l和v是互斥的,只会包含其中之一)简单说就是,包含l的(execl,execlv,execlp),将要执行的可执行文件的参数直接写到exec函数参数里面;包含v的(execv,execve,execvp),必须先将要执行的可执行文件的参数写到一个字符串数组里面,然后把这个数组作为exec的参数。需要注意的是,包含l的,最后一个参数必须是(char *)0,当然,就是C里面的NULL。
程序2就是包含l的例子,我们举一个包含v的例子,程序3:包含v的
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
char* argv[] = { "ls", "-rt" };
int main()
{
printf ( "begin ...n" );
if (execv("/bin/ls", argv) == -1)
{
perror("exec error");
}
printf ( "end ...n" );
return 0;
}
执行结果:
allan@NYC:~/test$ ./exec_test
begin ...
apue.c bug.c test1 test1.out test.dat file.c file c-7.c c-7 bug_exp exec_test.c exec_test
特点3:以e结尾的exec函数(execle,execve)可以(选择性的)传递一个指针数组,该指针数组是一些环境变量的定义。其它的不以e结尾的则使用调用进程总的environ变量(一个系统预定义的全局变量)为新程序复制现有的环境。
下面我们举一个例子来看一下以e结尾exec函数和其它的区别:
程序4:echoall程序,打印命令行参数以及环境变量,在后面的程序中会使用
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
char **ptr;
extern char ** environ;
for (int i = 0; i < argc; i++)
{
printf ( "argv[%d]: %sn", i, argv[i] );
}
for (ptr = environ; *ptr != 0; ptr++)
{
printf ( "%sn", *ptr );
}
exit(0);
}
程序5:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
char *env_init[] = { "USER=known", "PATH=/tmp", NULL };
int main()
{
pid_t pid;
if ((pid = fork()) < 0)
{
printf ( "fork errorn" );
exit(1);
}
else if (pid == 0)
{
if (execle("/home/allan/test/echoall", "echoall", "myarg1", "MY_ARG2", NULL, env_init) < 0)
{
perror ( "execle error" );
exit(2);
}
}
if (waitpid(pid, NULL, 0) < 0)
{
perror("wait error");
exit(3);
}
if ((pid = fork()) < 0)
{
printf ( "fork error 2n" );
exit(4);
}
else if (pid == 0)
{
if (execlp("/home/allan/test/echoall", "echoall", "only 1 arg", NULL) < 0)
{
perror("execlp error");
exit(5);
}
}
exit(0);
}
程序5运行结果:
allan@NYC:~/test$ ./exec_test
argv[0]: echoall
argv[1]: myarg1
argv[2]: MY_ARG2
USER=known
PATH=/tmp
allan@NYC:~/test$ argv[0]: echoall
argv[1]: only 1 arg
XDG_VTNR=7
LC_PAPER=zh_CN.UTF-8
/* 中间省略 */
GTK_IM_MODULE=fcitx
LESSCLOSE=/usr/bin/lesspipe %s %s
LC_TIME=zh_CN.UTF-8
LC_NAME=zh_CN.UTF-8
XAUTHORITY=/home/allan/.Xauthority
_=./exec_test
可以看到,对于带e的,使用了我们指定的环境变量,不带e的继承了父进程的环境变量(即系统默认的环境变量)。再强调一点:对于带e的exec函数,我们也可以不传递最后一个envp这个参数,这个和传一个空的envp指针数组是一样的,只是编译的时候会报一个WARNING。当然,这样做的后果是我们新的进程映像就没有任何环境变量了。所以,如果不是要为新的进程指定某个特定的环境(变量),就不要使用带e的exec函数。
最后,我们再说两个知识点:
- 虽然exec族有很多函数,但具体的实现中往往只有execve是内核的系统调用,其它的都是库函数,它们最终都要调用execve系统调用。
- exec族函数中都有一个arg0参数,习惯上指定为要执行的文件名,但其实没有什么用。程序并不使用这个参数。
看下面两个例子:
程序6:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
if (execlp("ls", "abc", NULL) < 0)
{
perror("execle error");
exit(1);
}
exit(0);
}
程序7:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
char *argv[] = { "abc" , NULL};
int main()
{
if (execvp("ls", argv) < 0)
{
perror("execle error");
exit(1);
}
exit(0);
}
上面程序6和程序7中的arg0参数我们都指定为了"abc",但它们都可以正常运行。当然,我们还是遵从习惯,指定为可执行文件名比较好。
至此,exec家族就算介绍完了。
评论已关闭