1. execve()函数
系统调用 execve()可以将新程序加载到某一进程的内存空间,通过调用 execve()函数将一个外部的可执行文件加载到进程的内存空间运行,使用新的程序替换旧的程序,而进程的栈、数据、以及堆数据会被新程序的相应部件所替换,然后从新程序的 main()函数开始执行。
#include <unistd.h>
int execve(const char *filename, char *const argv[], char *const envp[]);
参数含义:
参数 | 含义 |
---|---|
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 族函数。
下面准备了两个demo案例:
testApp:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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);
}
newApp:
#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);
}
由上图打印结果可知,在我们的 testApp 程序中,成功通过 execve()运行了另一个新的程序 newApp,当newApp 程序运行完成退出后,testApp 进程就结束了。
说到这里,我们来分析一个问题,为什么需要在子进程中执行新程序?其实这个问题非常简单,虽然可以直接在子进程分支编写子进程需要运行的代码,但是不够灵活,扩展性不够好,直接将子进程需要运行的代码单独放在一个可执行文件中不是更好吗,所以就出现了 exec 操作。
2. exec全家桶
exec 族函数包括多个不同的函数,这些函数命名都以 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参数相同,也是字符串指针数组。
3. exec库函数的使用方法
就拿Linux中一个很简单的shell命令来做举例
ls -al
- execl()函数运行 ls 命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
execl("/bin/ls", "ls", "-a", "-l", NULL);
perror("execl error");
exit(-1);
}
- execv()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execv("/bin/ls", arg_arr);
perror("execv error");
exit(-1);
}
- execlp()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
execlp("ls", "ls", "-a", "-l", NULL);
perror("execlp error");
exit(-1);
}
- execvp()函数运行 ls 命令。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execvp("ls", arg_arr);
perror("execvp error");
exit(-1);
}
- execle()函数运行 ls 命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
execle("/bin/ls", "ls", "-a", "-l", NULL, environ);
perror("execle error");
exit(-1);
}
- execvpe()函数运行 ls 命令
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern char **environ;
int main(void)
{
char *arg_arr[5];
arg_arr[0] = "ls";
arg_arr[1] = "-a";
arg_arr[2] = "-l";
arg_arr[3] = NULL;
execvpe("ls", arg_arr, environ);
perror("execvpe error");
exit(-1);
}
以上全部案例,结果都一样的:
4. system()函数
使用 system()函数可以很方便地在我们的程序当中执行任意 shell 命令,我们来学习下 system()函数的用法,以及介绍 system()函数的实现方法。
#include <stdlib.h>
int system(const char *command);
参数介绍:
参数 | 含义 |
---|---|
command | 参数 command 指向需要执行的 shell 命令,以字符串的形式提供,譬如"ls -al"、"echo HelloWorld"等 |
system()的返回值如下:
- 当参数 command 为 NULL,如果 shell 可用则返回一个非 0 值,若不可用则返回 0;
- 如果无法创建子进程或无法获取子进程的终止状态,那么 system()返回-1;
- 如果子进程不能执行 shell,则 system()的返回值就好像是子进程通过调用_exit(127)终止了;
- 如果所有的系统调用都成功,system()函数会返回执行 command 的 shell 进程的终止状态;
system()函数其内部的是通过调用 fork()、execl()以及 waitpid()这三个函数来实现它的功能,首先 system()会调用 fork()创建一个子进程来运行 shell(可以把这个子进程成为 shell 进程),并通过 shell 执行参数command 所指定的命令;
system()的主要优点在于使用上方便简单,编程时无需自己处理对 fork()、exec 函数、waitpid()以及 exit()等调用细节,system()内部会代为处理;当然这些优点通常是以牺牲效率为代价的,使用 system()运行 shell命令需要至少创建两个进程,一个进程用于运行 shell、另外一个或多个进程则用于运行参数 command 中解析出来的命令,每一个命令都会调用一次 exec 函数来执行;所以从这里可以看出,使用 system()函数其效率会大打折扣,如果我们的程序对效率或速度有所要求,那么建议大家不是直接使用 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);
}
本文参考正点原子的嵌入式LinuxC应用编程。