16.exec函数详解
1.exec函数说明
fork函数是用于创建一个子进程,该子进程几乎是父进程的副本,而有时我们希望子进程去执行另外的程序,exec函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
2.在Linux中使用exec函数族主要有以下两种情况
当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生。
如果一个进程想执行另一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生。
3.exec函数族语法
实际上,在Linux中并没有exec函数,而是有6个以exec开头的函数族,下表列举了exec函数族的6个成员函数的语法。
这6 个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数表传递方式及环境变量这几个方面进行比较说明。
①查找方式:上表其中前4个函数的查找方式都是完整的文件目录路径(pathname),而最后2个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
前4个取路径名做参数,后2个则取文件名做参数。
当指定filename做参数时:
a. 如果filename中包含/,则将其视为路径名
b. 否则就按PATH环境变量搜索可执行文件。
② 参数传递方式:exec函数族的参数传递有两种方式,一种是逐个列举(l)的方式,而另一种则是将所有参数整体构造成指针数组(v)进行传递。
在这里参数传递方式是以函数名的第5位字母来区分的,字母为“l”(list)的表示逐个列举的方式,字母为“v”(vector)的表示将所有参数整体构造成指针数组传递,然后将该数组的首地址当做参数传给它,数组中的最后一个指针要求是NULL。读者可以观察execl、execle、execlp的语法与execv、execve、execvp的区别。
③ 环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”(environment)结尾的两个函数execle、execve就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所以环境变量。
4.PATH环境变量说明
PATH环境变量包含了一张目录表,系统通过PATH环境变量定义的路径搜索执行码,PATH环境变量定义时目录之间需用用“:”分隔,以“.”号表示结束。PATH环境变量定义在用户的.profile或.bash_profile中,下面是PATH环境变量定义的样例,此PATH变量指定在“/bin”、“/usr/bin”和当前目录三个目录进行搜索执行码。
PATH=/bin:/usr/bin:.
export $PATH
5.进程中的环境变量说明
在Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Shell进程堆栈中存放着该用户下的所有环境变量,使用execl、execv、execlp、execvp函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用execle、execve时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。
6.exec函数族关系
事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用,调用关系如下图12-11所示:
7.exec
调用举例如下
//NULL在此上下文中是一个指针常量,用于表示指针数组的结尾。在处理像exec*这样的函数时,这很有用,因为它们期望参数列表以NULL指针作为结束标志。
char *const ps_argv[] ={"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
//"PATH=/bin:/usr/bin" 和 "TERM=console" 是字符串字面量,代表环境变量的名字和值。
//"PATH=/bin:/usr/bin" 表示路径变量PATH被设置为“/bin:/usr/bin”。
//"TERM=console" 表示终端类型变量TERM被设置为“console”。
//NULL在这里同样用于表示指针数组的结尾,这对于许多C函数来说都是必要的,因为它们使用NULL来识别参数列表的结束。
char *const ps_envp[] ={"PATH=/bin:/usr/bin", "TERM=console", NULL};
//"/bin/ps":这是你想要执行的程序的路径。这行代码想要执行系统上的 ps 命令,这个命令通常位于 /bin/ 目录下。
//"ps":这是传递给 /bin/ps 的第一个参数。当你在命令行上执行一个程序时,第一个参数(也叫做 argv[0])通常是该程序的名字。很多程序都依赖这个参数来获取自己的名字。
//"-o" 和 "pid,ppid,pgrp,session,tpgid,comm":这些是传递给 ps 的参数。特别是,-o 选项允许用户指定要显示的输出格式,后面的字符串则是这个格式的具体定义。在这里,这些参数意味着 ps 将会输出每个进程的 PID(进程ID)、PPID
//(父进程ID)、PGRP(进程组ID)、session(会话ID)、TPGID(前台进程组ID)和 comm(命令名或程序名)。
//NULL:这是参数列表的终止符。execl() 函数使用这个 NULL 来判断参数列表何时结束。
execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execv("/bin/ps", ps_argv);
execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);
execve("/bin/ps", ps_argv, ps_envp);
//execlp() 是 exec 函数家族中的另一个成员。这个函数用于执行程序,但与前面的 execl() 和 execv() 有一个主要区别:它会在环境变量 PATH 指定的目录中查找要执行的程序。这意味着你不必给出完整路径,只需要给出可执行文件的名字。
//"ps": 这是你想要执行的程序的名字。由于使用了 execlp(), 系统会在 PATH 环境变量中指定的目录列表中查找这个名字。
//"ps": 这是传递给 ps 的第一个参数。在命令行上执行程序时,第一个参数(也被称为 argv[0])通常是程序的名字。许多程序都依赖这个参数来知道自己是如何被调用的。
//"-o" 和 "pid,ppid,pgrp,session,tpgid,comm": 这些是传递给 ps 的其他参数。特别是,-o 选项允许用户指定要显示的输出格式,后面的字符串是这种格式的具体定义。
//NULL: 这是参数列表的终止符。execlp() 和其他 exec 函数使用这个 NULL 来确定参数列表何时结束。
//当 execlp() 函数成功时,当前进程的映像会被替换为指定的程序,并从该程序的 main 函数开始执行。如果 execlp() 失败,则函数会返回,并且进程不会被替换。
//总之,这行代码的目的是使用给定的参数执行名为 "ps" 的命令,并期望该命令位于 PATH 环境变量指定的某个目录中。
execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);
execvp("ps", ps_argv);
请注意exec函数族形参展开时的前两个参数,第一个参数是带路径的执行码(execlp、execvp函数第一个参数是无路径的,系统会根据PATH自动查找然后合成带路径的执行码),第二个是不带路径的执行码,执行码可以是二进制执行码和Shell脚本。
8.exec函数族使用注意点
在使用exec函数族时,一定要加上错误判断语句。因为exec很容易执行失败,其中最常见的原因有:
① 找不到文件或路径,此时errno被设置为ENOENT。
② 数组argv和envp忘记用NULL结束,此时errno被设置为EFAULT。
③ 没有对应可执行文件的运行权限,此时errno被设置为EACCES。
9.exec后新进程保持原进程以下特征
环境变量(使用了execle、execve函数则不继承环境变量);
进程ID和父进程ID;
实际用户ID和实际组ID;
附加组ID;
进程组ID;
会话ID;
控制终端;
当前工作目录;
根目录;
文件权限屏蔽字;
文件锁;
进程信号屏蔽;
未决信号;
资源限制;
tms_utime、tms_stime、tms_cutime以及tms_ustime值。
对打开文件的处理与每个描述符的exec关闭标志值有关,进程中每个文件描述符有一个exec关闭标志(FD_CLOEXEC),若此标志设置,则在执行exec时关闭该描述符,否则该描述符仍打开。除非特地用fcntl设置了该标志,否则系统的默认操作是在exec后仍保持这种描述符打开,利用这一点可以实现I/O重定向。
10.execl函数
头文件 | #include<unistd.h> |
---|---|
功能 | 为进程重载0-3G的用户空间,可与fork函数搭配使用 |
语法 | int execl("绝对路径", “标识符”, “需要的参数”(需要多少传入多少) ,NULL); |
返回值 | 成功的话无返回值,失败的话返回 -1 |
我们来说明一下execl函数所需要的四个参数
参数 | 变量类型 | 解释 |
---|---|---|
绝对路径 | const char* | 文件存储的绝对路径,可通过pwd命令查看 |
标识符 | const char* | ① |
参数 | ------ | ② |
NULL | ------ | 最后这个必须传NULL,否则函数会报错 |
①标识符可以理解为编程时使用的“名字”,像命令 ls -a 中的ls就是标识符,是这个命令的“名字”,文件的文件名就是标识符,是这个文件的“名字”。
②参数很好理解,像命令 ls -a 中的 -a 就是参数,函数move(int a, int b)中的整型变量a和整形变量b就是参数
我们下面来写一个代码
所用函数:execl函数、fork函数
功能:创建三个子进程,并分别对三个子进程进行重载,第一个子进程实现使用火狐浏览器打开百度网页,第二个子进程创建一个名为huala的文件,第三个子进程显示当前目录下的文件,下图为使用火狐浏览器打开百度网页的大概流程,其余两个子进程类似该步骤
父进程通过fork函数创建子进程,子进程调用execl函数重载用户空间,来实现三个功能,以下是代码实现
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
int main(void)
{
pid_t pid;
int i;
for (i = 0; i < 3; i++)
{
pid = fork();
/*
这个地方要判断pid是否为0是因为fork函数的实现原理,fork函数最后的return 0是子进程进行
的,所以进入这个判断的是子进程,而子进程返回的pid就是0,如果这个地方不加上该判断,子进
程也会进入该for循环来创造进程,子又生孙孙又生子,而我们只希望父进程来创建三个子进程,
所以加上了该判断
*/
if (pid == 0)
{
break;
}
}
/*
首先父进程进入下面的三个判断,因为父进程pid大于0,所以会进入第一个判断,打印出父进程的
pid,然后我们用while循环一直sleep(1)来阻塞父进程,让子进程进入三个判断,因为子进程的pid
是0,所以会进入第二个判断,第一个子进程先进入判断,进入if(i == 0)用execl函数重载来实现功
能,firefox是命令标识符,www.baidu.com是参数,后面执行同样的步骤,也是父进程先进入判断,
之后两个进程分别进入判断并使用execl函数重载来实现功能
*/
if (pid > 0)
{
printf("parent pid %d\nsleeping..\n", getpid());
while (1)
{
sleep(1);
}
}
else if (pid == 0)
{
if (i == 0)
{
printf("child no.%d pid %d exec firefox..\n", i, getpid());
execl("/usr/bin/firefox", "firefox", "www.baidu.com", NULL);
}
if (i == 1)
{
printf("child no.%d pid %d touch files..\n", i, getpid());
execl("/usr/bin/touch", "touch", "huala", NULL);
}
if (i == 2)
{
printf("child no.%d pid %d exec ls -l..\n", i, getpid());
execl("/bin/ls", "ls", "-l", NULL);
}
}
return 0;
}
这样我们就实现了我们所想要达到的功能,记住exec函数一定要在fork函数之后执行
exec函数族的日常应用
其实exec在linux中的应用非常的广泛,就比如第一个终端的创建,还有终端下.c文件的执行,我们讲解一下这两个过程中exec函数族的应用
1.Linux中第一个终端的创建
具体过程:
1.init(1)是系统启动初始化后的第一个进程
2.当init进程初始化完成后系统会进行硬件检测,之后系统调用login函数
3.检查存放在/etc/passwd中的密码与用户输入的密码是否一致,一致的话init进程就调用fork函数创建子进程
4.子进程调用execl函数将子进程重载成bash终端,这样就实现了终端的创建
2.终端下.c文件的执行
在bash终端下我们先写一个world.c文件,然后将编译后的文件命名为app,看一下这个编译后的文件和bash终端的亲缘关系,我们可以通过命令 ps -ef|grep [进程名] 来查看对应该进程名的进程id与父进程id,大概流程如下
注意:
第一个数据是用户名,第二个数字是进程id,第三个数字是父进程id
我们可以发现,app的父进程就是bash终端,那么这是为什么呢?
原因就是bash终端调用了fork函数创建了一个子进程,子进程调用了execl函数,将文件app重载到了子进程中,所以app的父进程就是bash终端
11.execlp函数举例
execlp.c源代码如下:
#include <stdio.h>
#include <unistd.h>
int main()
{
if (fork() == 0)
{
if (execlp("/usr/bin/env", "env", NULL) < 0)
{
perror("execlp error!");
return -1;
}
}
return 0;
}
编译 gcc execlp.c –o execlp。
执行 ./execlp,执行结果如下:
HOME=/home/test
DB2DB=test
SHELL=/bin/bash
……
由执行结果看出,execlp函数使执行码重生时继承了Shell进程的所有环境变量,其他三个不以e结尾的函数同理。
我们来详细讲解这段代码及其执行结果。
- 包含头文件
#include <stdio.h>
#include <unistd.h>
这里包含了两个头文件。stdio.h
用于输入输出函数,如 printf
和 perror
;unistd.h
提供了Unix标准函数,如 fork
和 execlp
。
- 主函数
int main()
{
开始定义主函数。
- 创建子进程
if (fork() == 0)
{
fork()
是UNIX系统中用来创建新进程的函数。这个函数的特点是调用一次,返回两次:一次是在父进程中,一次是在新创建的子进程中。在子进程中,fork()
返回0;在父进程中,它返回新创建的子进程的进程ID。
因此,if (fork() == 0)
这段代码的意图是仅在子进程中执行以下的代码块。
- 执行新程序
if (execlp("/usr/bin/env", "env", NULL) < 0)
{
execlp()
是 exec
函数家族的一员。它会替换当前进程的映像为新的程序映像。在这里,它尝试执行 /usr/bin/env
程序,同时将第一个参数(即 argv[0]
)设置为 "env"。
/usr/bin/env
是一个常用的Unix命令,用于打印出所有的环境变量。所以,当这个程序被执行时,它会输出所有设置的环境变量及其值,就像你在执行结果中看到的那样。
- 错误处理
perror("execlp error!");
return -1;
}
如果 execlp()
调用失败(例如,因为 /usr/bin/env
程序不存在),它将返回一个负值。这段代码会捕捉这种情况,并使用 perror
打印一个错误消息,然后使子进程返回一个 -1
的错误代码。
- 结束子进程代码块
这一部分结束了仅在子进程中执行的代码块。
- 主函数结束
return 0;
}
主函数结束,返回0表示成功。
- 执行过程
当你执行 ./execlp
,以下步骤将发生:
- 程序开始在父进程中运行。
- 当执行到
fork()
时,创建一个子进程。 - 子进程开始执行,并进入
if (fork() == 0)
代码块。 - 子进程试图用
/usr/bin/env
程序替换自己。如果成功,/usr/bin/env
程序将开始运行,并打印所有的环境变量。 - 一旦
env
完成并打印所有的环境变量,子进程将结束。 - 同时,父进程将继续执行,跳过
if (fork() == 0)
代码块,并直接结束。
这就是为什么当你执行 ./execlp
时,你会看到环境变量的列表输出。
将第一个参数(即 argv[0])设置为 "env"。什么意思?
在Unix和Linux系统中,当一个程序被执行时,它不仅仅接收到明确传递的参数,还会收到一个隐式的第一个参数,通常称为argv[0]
。这个参数是一个字符串,通常是用来启动程序的命令或程序的路径。
当你使用exec
系列的函数(例如execlp()
)来执行一个程序时,你可以明确地设置这个argv[0]
的值。这个值并不一定要与实际的程序名或路径匹配。事实上,它可以是任何字符串。程序可以访问这个argv[0]
值,并基于它进行不同的行为。
在给出的代码中:
execlp("/usr/bin/env", "env", NULL);
第一个参数指定了要执行的程序的路径,即/usr/bin/env
。
第二个参数指定了argv[0]
的值,即"env"。这里,我们简单地将它设置为与程序名相同的值,但它可以是任何其他的字符串。
很多程序会使用argv[0]
来查找自己是如何被调用的,这有时用于确定程序的行为。例如,一些Unix工具,如busybox
,会根据argv[0]
来确定要执行哪种功能。
12.execle函数举例
利用函数execle,将环境变量添加到新建的子进程中去。
execle.c源代码如下:
#include <unistd.h>
#include <stdio.h>
int main()
{
/*命令参数列表,必须以 NULL 结尾*/
char* envp[] = { "PATH=/tmp","USER=sun",NULL };
if (fork() == 0) {
/*调用 execle 函数,注意这里也要指出 env 的完整路径*/
if (execle("/usr/bin/env", "env", NULL, envp) < 0)
{
perror("execle error!");
return -1;
}
}
return 0;
}
编译:gcc execle.c –o execle。
执行./execle,执行结果如下:
PATH=/tmp
USER=sun
可见,使用execle和execve可以自己向执行进程传递环境变量,但不会继承Shell进程的环境变量,而其他四个exec函数则继承Shell进程的所有环境变量。
总结示例exec.c:
/* exec函数族的语法 */
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
int main(int argc, char* argv[])
{
/* 字符串指针数组传递参数,使用包含v的exec函数参数 */
char* arg[] = { "ls","-a",NULL };
char* arg1[] = { "env",NULL }; //只用于execve函数
char* envp[] = { "NAME=amoscykl","[email protected]","PATH=/tmp",NULL };
char** ptr; //指向环境表
// 打印出环境表
printf("自定义环境表\n");
for (ptr = envp; *ptr != 0; ptr++)
printf("%s \n", *ptr);
printf("\n");
sleep(2);
/* 子进程调用execl函数 */
if (fork() == 0)
{
//child1
printf("1-----execl-----\n");
if (execl("/bin/ls", "ls", "-a", NULL) == -1)
{
perror("execl error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execv函数 */
if (fork() == 0)
{
//child2
printf("2-----execv-----\n");
if (execv("/bin/ls", arg) == -1)
{
perror("execv error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execlp函数 */
if (fork() == 0)
{
//child3
printf("3-----execlp-----\n");
if (execlp("ls", "ls", "-a", NULL) == -1)
{
perror("execlp error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execvp函数 */
if (fork() == 0)
{
//child4
printf("4-----execvp-----\n");
if (execvp("ls", arg) == -1)
{
perror("execvp error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execle函数 */
if (fork() == 0)
{
//child5
printf("5-----execle-----\n");
if (execle("/usr/bin/env", "env", NULL, envp) == -1) //使用自定义的环境表,并打印出自定义环境变量
{
perror("execle error!");
exit(1);
}
}
sleep(2);
/* 子进程调用execve函数 */
if (fork() == 0)
{
//child6
printf("6-----execve-----\n");
if (execve("/usr/bin/env", arg1, envp) == -1) //使用自定义的环境表,并打印出自定义环境变量
{
perror("execve error!");
exit(1);
}
}
sleep(2);
printf("over!\n");
return 0;
}
运行结果:
参考资料:
Linux中execl函数详解与日常应用(附图解与代码实现)
标签:bin,函数,16,exec,详解,进程,NULL,环境变量 From: https://www.cnblogs.com/codemagiciant/p/17649363.html