C/C++调用外部程序

C/C++中调用外部程序的接口:exec系列函数、system、popen

一、exec库函数

基于系统调用execve(),提供了一系列冠以exec来命名的上层库函数,虽然接口方式各异,但功能一致

1.1 执行新程序:execve()

系统调用execve()可以将新的程序加载到调用者进程的内存空间。这一操作过程中,进程的栈、数据以及堆段会被新的程序相应部件替换。一般由fork()生成的子进程对execve()的调用最常见。

execve()系统调用定义如下:

text
1
2
3
4
#include <unistd.h>

int execve(const char *pathname, char *const argv[], char *const envp[]);
/* 成功不返回,失败返回-1 */

execve()系统调用参数说明:

  • pathname是准备载入当前进程空间的新程序的路径名。pathname可以是绝对路径,也可以是相对调用进程当前工作目录(current working directory)的相对路径
  • argv指定了传递给新进程的命令行参数。argv是由字符串指针组成的列表,并且以NULL值结尾。其中argv[0]的值对应于命令名,通常来说与pathname中的basename(路径的最后部分)值相同。
  • envp指定了新程序的环境列表。对应于新程序的environ数组,也是由字符串指针组成的列表,格式为name=value,并且以NULL值结尾

调用execve()之后,因为同一进程依然存在,所以进程ID不变

1.2 exec()库函数

基于系统调用,库函数提供了多种API选择,这些API都构建于execve()之上,只是在为新程序指定程序名,参数列表以及环境变量的方式上有所不同。

exec()库函数API如下:

text
1
2
3
4
5
6
7
8
#include <unistd.h>

int execle(const char *pathname, const char * arg, ... /*, (char*)NULL, char *const envp[] */);
int execlp(const char *filename, const char * arg, ... /*, (char*)NULL */);
int execvp(const char *filename, char *const argv[]);
int execv(const char *pathname, char *const argv[]);
int execl(const char *pathname, const char * arg, ... /*, (char*)NULL */);
/* 成功不返回,失败返回-1 */

exec()库函数之间的差异总结

函数 对程序文件的描述(-,p) 对参数的描述(v,l) 环境变量来源(e,-)
execve() 路径名 数组 envp参数
execle() 路径名 列表 envp参数
execlp() 文件名+PATH 列表 调用者的environ
execvp() 文件名+PATH 数据 调用者的environ
execv() 路径名 数组 调用者的environ
execl() 路径名 列表 调用者的environ

fexecve()执行由文件描述符指定的程序。有些应用程序需要打开某个文件,通过执行校验和之后再运行该程序,这一场景就比较适合使用fexecve(),该接口定义如下:

text
1
2
3
4
5
#define _GNU_SOURCE
#include <unistd.h>

int fexecve(int fd, char *const argv[], char *const envp[]);
/* 成功不返回,失败返回-1 */

1.3 解释器脚本

解释器(interpreter)是能够读取并执行文本格式命令的程序。(相形之下,编译器是将源代码译为可在真实或者虚拟机上执行的机器语言)解释器通常可以从被称为脚本的文件中读取和执行命令。

UNIX内核运行解释器脚本的方式与二进制程序无异,前提是:1、必须赋予脚本可执行权限;2、文件的起始行必须执行运行脚本的解释器路径名,格式如下:

text
1
2
3
4
5
#! interpreter
# 其中
# 1、#!必须置于改行起始处,这两个字符可以与解释器的路径名使用空格分割
# 2、在解释解释器路径名时不会使用环境变量PATH,所以解释器路径一般使用绝对路径
# 3、解释器路径名后可以跟随可选参数,二者以空格分隔,但是可选参数中不应包含空格(Linux系统不会将对可选参数中的中的空格做特殊解释,从参数起始到行尾视为一个单词,并且要求脚本的#!起始行不超过127个字节,包括换行符)

当调用execve()来运行脚本时,execve()如果检测到传入的文件以两字节序列开始,就会析取该行剩余的部分(路径名和参数)。然后按照如下格式执行解释器程序:

text
1
interpreter-path [optional-arg] script-path arg

interpreter-path(解释器路径)和optional-arg(可选参数)都取自脚本的#!行,script-path时传递给execve()的路径名,arg是通过argv传递给execve()的参数列表(argv[0]排除在外)。

二、执行shell命令:system

程序可以通过调用system()函数来执行任意的shell命令,函数定义如下:

text
1
2
3
4
5
6
7
8
#include <stdlib.h>
int system(const char*command);

# 返回值定义如下
# 1、当command命令为NULL指针时,如果shell可用则system()返回非0值,如果不可用则返回0
# 2、如果无法创建子进程或者无法获取其终止状态,system返回-1
# 3、如果子进程不能执行shell,则system返回值与子shell调用_exit(127)终止时一样
# 4、如果所有的系统调用都成功,system返回执行command的子shell的终止状态。shell的终止状态是执行最后一条命令时的退出状态;如果命令被信号所杀,大多数shell会以128+n的值退出,其中n为信号编号

函数system()创建一个子进程来运行shell,并以之执行命令command。所以使用system()函数运行命令至少会创建两个进程,一个用于运行shell,另一个或者多个用于运行shell所执行的命令(如果对效率或者速度有要求,最好直接调用fork()exec()来执行既定程序)。

三、通过管道与shell命令通信popen

管道的一个常见的用途是执行shell命令并读取或向其发送一些输入。popen()pclose()简化了这个任务,函数定义如下:

text
1
2
3
4
5
6
#include <stdio.h>

FILE *popen(const char *command, const char *mode);
/* 成功时返回文件流指针,失败时返回NULL并设置errno */
int pclose(FILE *stream);
/* 成功时返回子进程结束状态,失败返回-1 */

popen()函数创建了一个管道,然后创建了一个子进程来执行shell,而shell又创建了一个子进程来执行command字符串。其中mode是一个字符串,它确定调用进程是从管道中读取数据(mode是r)或者是写入到管道中(mode是w),由于管道是单向的,所以无法在执行的command中进行双向通信

与使用pipe()创建的管道一样,当从管道中读取数据时,调用进程在command关闭管道的写入端之后会看到文件结束;当向管道写入数据时,如果command已经关闭了管道的读取端,那么调用进程会收到SIGPIPE并的到EPIPE错误。

当IO结束之后可以使用pclose()函数关闭管道并等待子进程中的shell终止(不应该使用fclose()函数,因为它不会等待子进程)。pclose()在成功时会返回子进程中shell的终止状态(即shell命令执行最后一条命令的终止状态,除非是被信号杀死的)。与system()一样,如果无法执行shell,pclose()会返回一个值就像是子进程中的shell调用_exit(127)来终止一样。如果发生了其它错误,那么pclose()返回-1。

参考文献

【0】Linux/UNIX系统编程手册