在前面已经大家提到了 exec 函数,当子进程的工作不再是运行父进程的代码段,而是运行另一个新程序的代码,那么这个时候子进程可以通过 exec 函数来实现运行另一个新的程序。本小节我们就来学习下, 如何在程序中运行一个新的程序,从新程序的 main()函数开始运行。
execve()函数
系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。
execve()函数原型如下所示:
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
函数参数和返回值含义如下:
filename:参数 filename 指向需要载入当前进程空间的新程序的路径名,既可以是绝对路径、也可以是相对路径。
argv:参数 argv 则指定了传递给新程序的命令行参数。是一个字符串数组,该数组对应于 main(int argc, char *argv[])函数的第二个参数 argv,且格式也与之相同,是由字符串指针所组成的数组,以 NULL 结束。 argv[0]对应的便是新程序自身路径名。
envp:参数 envp 也是一个字符串指针数组,指定了新程序的环境变量列表,参数 envp 其实对应于新程序的 environ 数组,同样也是以 NULL 结束,所指向的字符串格式为 name=value。
返回值:execve 调用成功将不会返回;失败将返回-1,并设置 errno。
对 execve()的成功调用将永不返回,而且也无需检查它的返回值,实际上,一旦该函数返回,就表明它发生了错误。
基于系统调用 execve(),还提供了一系列以 exec 为前缀命名的库函数,虽然函数参数各异,当其功能相同,通常将这些函数(包括系统调用 execve())称为 exec 族函数,所以 exec 函数并不是指某一个函数、 而是 exec 族函数,下一小节将会向大家介绍这些库函数。
通常将调用这些 exec 函数加载一个外部新程序的过程称为 exec 操作。
使用示例
编写一个简单的程序,在测试程序 testApp 当中通过 execve()函数运行另一个新程序 newApp。
int main(int argc, char *argv[])
{
char *arg_arr[5];
char *env_arr[5] = {"NAME=app", "AGE=25",
"SEX=man", NULL};
if (2 > argc)
exit(-1);
arg_arr[0] = argv[1];
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execve(argv[1], arg_arr, env_arr);
perror("execve error");
exit(-1);
}
将上述程序编译成一个可执行文件 testApp。
接着编写新程序,在新程序当中打印出环境变量和传参,如下所示:
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[]){
char **ep = NULL;
int j;
for (j = 0; j < argc; j++)
printf("argv[%d]: %s\n", j, argv[j]);
puts("env:");
for (ep = environ; *ep != NULL; ep++)
printf(" %s\n", *ep);
exit(0);
}
将新程序编译成 newApp 可执行文件,接下来进行测试,运行 testApp 程序,传入一个参数,该参数便是新程序 newApp 的可执行文件路径:
由上图打印结果可知,在我们的 testApp 程序中,成功通过 execve()运行了另一个新的程序 newApp,当 newApp程序运行完成退出后,testApp 进程就结束了。
然而,上文测试代码中 execve()函数的使用并不是它真正的应用场景,通常由 fork()生成的子进程对 execve() 的调用最为频繁,也就是子进程执行 exec 操作。
为什么需要在子进程中执行新程序?其实这个问题非常简单,虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作。
exec 库函数
exec 族函数包括多个不同的函数,这些函数命名都以 exec 为前缀,上一小节给大家介绍的 execve()函数也属于exec 族函数中的一员,但它属于系统调用;本小节我们介绍 exec 族函数中的库函数,这些库函数都是基于系统调用 execve()而实现的,虽然参数各异、但功能相同,包括:execl()、execlp()、execle()、execv()、 execvp()、execvpe(),它们的函数原型如下所示:
#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 execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
接下来简单地介绍下它们之间的区别:
execl()和 execv()都是基本的 exec 函数,都可用于执行一个新程序,它们之间的区别在于参数格式不同;参数 path 意义和格式都相同,与系统调用 execve()的 filename 参数相同,指向新程序的路径名,既可以是绝对路径、也可以是相对路径。execl()和 execv()不同的在于第二个参数,execv()的 argv 参数与 execve()的 argv 参数相同,也是字符串指针数组;而 execl()把参数列表依次排列,使用可变参数形式传递,本质上也是多个字符串,以 NULL 结尾,如下所示:
// execv 传参
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execv("./newApp", arg_arr);
// execl 传参
execl("./newApp", "./newApp", "Hello", "World", NULL);
execlp()和 execvp()在 execl()和 execv()基础上加了一个 p,这个 p 其实表示的是 PATH;execl()和 execv()要求提供新程序的路径名,而 execlp()和 execvp()则允许只提供新程序文件名,系统会在由环境变量 PATH 所指定的目录列表中寻找相应的可执行文件,如果执行的新程序是一个 Linux 命令,这将很有用;当然,execlp()和 execvp()函数也兼容相对路径和绝对路径的方式。
execle()和 execvpe()这两个函数在命名上加了一个 e,这个 e 其实表示的是 environment 环境变量,意味着这两个函数可以指定自定义的环境变量列表给新程序,参数envp与系统调用execve()的envp参数相同,也是字符串指针数组,使用方式如下所示:
// execvpe 传参
char *env_arr[5] = {"NAME=app", "AGE=25",
"SEX=man", NULL};
char *arg_arr[5];
arg_arr[0] = "./newApp";
arg_arr[1] = "Hello";
arg_arr[2] = "World";
arg_arr[3] = NULL;
execvpe("./newApp", arg_arr, env_arr);
// execle 传参
execle("./newApp", "./newApp", "Hello", "World", NULL, env_arr);
system()函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令,本小节来学习下 system()函数的用法,以及介绍 system()函数的实现方法。
首先来看看 system()函数原型,如下所示:
#include <stdlib.h>
int system(const char *command);
函数参数和返回值含义如下:
command:参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等。
返回值:关于 system()函数的返回值有多种不同的情况,稍后给大家介绍。
system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system() 会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数 command 所指定的命令。譬如:
system("ls -la")
system("echo HelloWorld")
system()的返回值如下:
- 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;针对一些非 UNIX 系统,该系统上可能是没有 shell 的,这样会导致 shell 不可能;如果 command参数不为 NULL,则返回值从以下的各种情况所决定。
- 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
- 如果子进程不能执行shell,则system()的返回值就好像是子进程通过调用_exit(127)终止了;
- 如果所有的系统调用都成功,system()函数会返回执行command 的 shell 进程的终止状态。
system()的主要优点在于使用上方便简单,编程时无需自己处理对 fork()、exec 函数、waitpid()以及 exit() 等调用细节,system()内部会代为处理;当然这些优点通常是以牺牲效率为代价的,使用 system()运行 shell 命令需要至少创建两个进程,一个进程用于运行 shell、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行;所以从这里可以看出,使用 system()函数其效率会大打折扣,如果我们的程序对效率或速度有所要求,那么建议不要直接使用 system()。
使用示例
以下示例代码演示了 system()函数的用法,执行测试程序时,将需要执行的命令通过参数传递给 main() 函数,在 main 函数中调用 system()来执行该条命令。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
int ret;
if (2 > argc)
exit(-1);
ret = system(argv[1]);
if (-1 == ret)
fputs("system error.\n", stderr);
else {
if (WIFEXITED(ret) && (127 == WEXITSTATUS(ret)))
fputs("could not invoke shell.\n", stderr);
}
exit(0);
}
运行结果