参考资料:《Linux环境编程:从应用到内核》
execve函数
execve函数接口如下:
#include <unistd.h>
int execve(const char *filename, char *const argv[],
char *const envp[]);
参数:
- 第一个参数:filename是可执行的新程序的路径名,可以是绝对路径,也可以是相对于当前工作目录的相对路径;
- 第二个参数:字符串指针组成的数组,以NULL结束。argv[0]一般对应可执行文件的文件名,也就是filename中的basename(路径名最后一个/后面的部分)。当然如果argv[0]不遵循这个约定也无妨,因为execve可以从第一个参数获取到要执行文件的路径,只要不是NULL即可。
- 第三个参数:与C语言的main函数中的第三个参数envp一样,也是字符串指针数组,以NULL结束,指针指向的字符串的格式为name=value。
一般来说,execve()函数总是紧随fork函数之后。父进程调用fork之后,子进程执行execve函数,抛弃父进程的程序段,和父进程分道扬镳,从此天各一方,各走各路。但是也可以不执行fork,单独调用execve函数:
返回值:
execve函数返回值是特殊的,如果失败,返回-1,但如果成功,永不返回。
所以无须检查execve的返回值,只要返回,就必然是-1。可以从errno判断出出错的原因。出错的可能性非常多,手册提供了19种不同的errno,罗列了22种失败的情景。很难记住,好在大部分都不常见,常见的情况有以下几种:
- EACCESS:这个是我们最容易想到的,就是第一个参数filename,不是个普通文件,或者该文件没有赋予可执行的权限,或者目录结构中某一级目录不可搜索,或者文件所在的文件系统是以MS_NOEXEC标志挂载的。
- ENOENT:文件不存在。
- ETXTBSY:存在其他进程尝试修改filename所指代的文件。
- ENOEXEC:这个错误其实是比较高端的一种错误了,文件存在,也可以执行,但是无法执行,比如说,Windows下的可执行程序,拿到Linux下,调用execve来执行,文件的格式不对,就会返回这种错误。
// 准备外部程序 main.cpp
#include <iostream>
int main(int argc, char* args[])
{
if (argc < 3)
{
std::cout << "函数参数太少" << std::endl;
return -1;
}
int a = std::stoi(args[1]);
int b = std::stoi(args[2]);
std::cout << "a + b = " << (a + b) << std::endl;
return 0;
}
// 示例
// execve函数
#include <iostream>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
// 执行外部程序main
void func1()
{
// 为什么使用const char*,之后又转一次?
// 这是因为 "./main"、"1"、"2"在C++中都是字符串字面值,属于const char*,如果赋值给char*,就会用警告
// 所以用const char*接收,之后通过const_cast去除const属性。
const char* args[] = { "./main", "1", "2", nullptr };
if (execve(const_cast<char*>(args[0]), const_cast<char**>(args), nullptr) == -1)
{
perror("execve");
exit(EXIT_FAILURE);
}
}
// 在终端执行 pwd -L 命令
void func2()
{
const char* args[] = { "/bin/pwd", "-L", nullptr };
if (execve(const_cast<char*>(args[0]), const_cast<char**>(args), nullptr) == -1)
{
perror("execve");
exit(EXIT_FAILURE);
}
}
int main()
{
func1();
// func2();
std::cout << "never get here" << std::endl;
return 0;
}
func1()和func2()不能同时测试,因为execve函数永不返回,无法执行完func1(),再执行func2()。
可以看到,我们在func1()里使用execve函数调用外部程序main,输出结果也是正确。
exec家族
从内核的角度来说,提供execve系统调用就足够了,但是从应用层编程的角度来讲,execve函数就并不那么好使了:
- 第一个参数必须是绝对路径或是相对于当前工作目录的相对路径。习惯在shell下工作的用户会觉得不太方便,因为日常工作都是写ls和mkdir之类命令的,没有人会写/bin/ls或/bin/mkdir。shell提供了环境变量PATH,即可执行程序的查找路径,对于位于查找路径里的可执行程序,我们不必写出完整的路径,很方便,而execve函数享受不到这个福利,因此使用不便。
- execve函数的第三个参数是环境变量指针数组,用户使用execve编程时不得不自己负责环境变量,书写大量的“key=value”,但大部分情况下并不需要定制环境变量,只需要使用当前的环境变量即可。
正是为了提供相应的便利,所以用户层提供了6个函数,当然,这些函数本质上都是调用execve系统调用,只是使用的方法略有不同,代码如下:
#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 execve(const char *path, char *const argv[],char *const envp[]);
上述6个函数分成上下两个半区。分类的依据是参数采用列表(l,表示list)还是数组(v,表示vector)。上半区采用列表,它们会罗列所有的参数,下半区采用数组。在每个半区之中,带p的表示可以使用环境变量PATH,带e的表示必须要自己维护环境变量,而不使用当前环境变量,
// 示例 返回值自行判断
char* const ps_argv[] = { "ps","-ax",NULL };
char* const ps_envp[] = { "PATH=/bin:/usr/bin","TERM=console",NULL };
execl("/bin/ps", "ps", "-ax", NULL);
/*带p的,可以使用环境变量PATH,无须写全路径*/
execlp("ps", "ps", "-ax", NULL);
/*带e的需要自己组拼环境变量*/
execle("/bin/ps", "ps", "-ax", NULL, ps_envp);
execv("/bin/ps", ps_argv);
/*带p的,可以使用环境变量PATH,无须写全路径*/
execvp("ps", ps_argv);
/*带e的需要自己组拼环境变量*/
execve("/bin/ps", ps_argv, ps_envp);
system函数
程序可以调用system函数,来执行任意的shell命令,可以使C程序很方便地调用其他语言编写的程序。
函数接口如下:
#include <stdlib.h>
int system(const char *command);
这里将需要执行的命令作为command参数,传给system函数,该函数就帮你执行该命令。
这样看来system最大的好处就在于使用方便。不需要自己来调用fork、exec和waitpid,也不需要自己处理错误,处理信号,方便省心。
但是system函数的缺点也是很明显的。首先是效率,使用system运行命令时,一般要创建两个进程,一个是shell进程,另外一个或多个是用于shell所执行的命令。如果对效率要求比较高,最好是自己直接调用fork和exec来执行既定的程序。