(1)exec函数说明:
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
在Linux中使用exec函数族主要有以下两种情况:
- 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec 函数族让自己重生。
- 如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
(2)exec函数族语法
在linux中并没有exec()函数,而是由6个以exec开头的函数,所以称之为族。他们之间的语法有着细微的差别,下面是在man册中查看“man 3 execl ”库函数,函数原型如下:
#include <unistd.h>
extern char **environ;
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 execvpe(const char *file, char *const argv[], char *const envp[]);
为了方便记忆,可以通过对这6个函数进行对比记忆,方法如下:
execl()与execv() 这两个函数是最基本的exec,都可以用来执行一个程序,区别是传参的格式不同。execl是把参数列表(本质上是多个字符串,必须以NULL结尾)依次排列而成(l其实就是list的缩写),execv是把参数列表事先放入一个字符串数组中,再把这个字符串数组传给execv函数。
execlp和execvp 这两个函数在上面2个基础上加了p,较上面2个来说,区别是:上面2个执行程序时必须指定可执行程序的全路径(如果exec没有找到path这个文件则直接报错),而加了p的传递的可以是file(也可以是path,只不过兼容了file。加了p的这两个函数会首先去找file,如果找到则执行执行,如果没找到则会去环境变量PATH所指定的目录下去找,如果找到则执行如果没找到则报错)
execle和execvpe 这两个函数较execl()与execv()来说加了e (注意execvpe函数命名中并没有将p去掉,原因后面有说明),函数的参数列表中也多了一个字符串数组envp形参,e就是environment环境变量的意思,和基本版本的exec的区别就是:执行可执行程序时会多传一个环境变量的字符串数组给待执行的程序。如果用户在执行这个程序时没有传递第三个参数,则程序会自动从父进程继承一份环境变量(默认的,最早来源于OS中的环境变量);如果我们exec的时候使用execlp或者execvpe去给传一个envp数组,则程序中的实际环境变量是我们传递的这一份(取代了默认的从父进程继承来的那一份)
(3)函数使用实例
execl() 函数:
int main(void)
{
pid_t pid = -1;
pid = fork();
if (pid > 0)
{
// 父进程
printf("parent, 子进程id = %d.\n", pid);
}
else if (pid == 0)
{
// 子进程
execl("/bin/ls", "ls", "-l", "-a", NULL); // ls -l -a
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
注意:execl("/bin/ls", "ls", "-l", "-a", NULL);这条语句中的参数传递方式,第一个参数是要执行的程序的绝对路径,后面跟的“ls”这个参数,在实际测试中并没有发现有什么实际的用处(即使填写的不是ls命令,是别的命令也没有关系比如第二个参数是“pwd”,具体原因不解,望高手指教),但是这里还必须填入一个字符串,这样才可以正常进行命令的解析。
execv()函数
int main(void)
{
pid = fork();
if (pid > 0)
{
// 父进程
printf("parent, 子进程id = %d.\n", pid);
}
else if (pid == 0)
{
// 子进程
char * const arg[] = {"ls", "-l", "-a", NULL};
execv("/bin/ls", arg);
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
该函数以execl()的区别就是将命令以及字符串赋值给一个字符串数组,然后作为函数的参数传入。其实本质是一样的。
execlp()与execvp函数
int main(void)
{
pid = fork();
if (pid > 0)
{
// 父进程
printf("parent, 子进程id = %d.\n", pid);
}
else if (pid == 0)
{
// 子进程
/*execlp函数*/
execlp("ls", "ls", "-l", "-a", NULL); // ls -l -a
/*execvp函数*/
char * const arg[] = {"ls", "-l", "-a", NULL};
execvp("ls", arg);
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
这两个函数以前面的区别就是函数名不同,第一个参数使用的是相对路径。
execle()与execvpe()函数
int main(void)
{
pid = fork();
if (pid > 0)
{
// 父进程
printf("parent, 子进程id = %d.\n", pid);
}
else if (pid == 0)
{
// 子进程
char *envp[] = {"PATH=/tmp", "USER=David", NULL}; //自己定义的环境变量
/*execlp函数*/
execle("/bin/ls", "ls", "env", NULL, envp);
/*execvp函数*/
char *const arg[] = {"env", NULL};
execvpe("/bin/ls", arg, envp);
return 0;
}
else
{
perror("fork");
return -1;
}
return 0;
}
实际上,真正的系统调用只有execve(),(这就是为什么在execvpe这个系统调用中命名的时候没有将"p"去掉的原因)函数原型如下,上面的函数都是库函数,他们最终都会调用execve()这个系统调用
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
在使用exec函数族时,一定要加上错误判断语句。exec很容易执行失败,其中最常见的原因如下:
- 找不到文件或路径,此时errno被置为ENOENT
- 数组argv与envp忘记用NULL结束,此时errno被置为EFAULT
- 没有对应执行文件的执行权限,此时errno被置为EACCES
可见内核的开发人员想的多么的细致,将一下使用者常见的错误都能想得到。
补充:关于main函数的传参。
main函数的原型其实不止是int main(int argc, char **argv) 或者nt main(int argc, char *argv[ ])
还可以是 int main(int argc, char **argv, char **env) 第三个参数是一个字符串数组,内容是环境变量。
注明:本博客是根据朱有鹏老师的l《linux嵌入式核心课程》视频所作的总结