最近在维护老项目时,发现项目中C/C++调用shell命令后,某系处理返回值的过程是以“临时文件”的方式进行;即shell命令执行后将返回值存放在临时文件(如temp.txt),C/C++程序再访问文件,获取shell的返回值。最经典的就是调用WiFi(iwlist wlan0 scan )扫描指令查询WiFi节点,然后解析获取WiFi数量、名称、信号强度、加密方式等信息。
通过“临时文件”的方式交互数据,是比较简单、易用和易理解的方式,在多进程间、多线程间也可以使用,但一般不会使用。共享“临时文件”有个弊端就是效率上不比较低,创建文件、删除文件然后是访问,都是访问存储器(磁盘、flash),加上文件系统的一层封装和存储介质映射,访问速度不如访问内存快。
“临时文件”的方式,个人觉得不是很好,通过该案例总结下C/C++调用shell命令知识。
1.C/C++调用shell命令方式
Linux 系统中使用 C/C++ 调用 shell 命令常用方式: 【1】system()函数 【2】popen()函数 【3】exec函数簇
system()函数最常用,简单高效; popen() 执行 shell 命令的开销比 system() 小;system()和popen()都封装了进程创建、释放,内部实质调用的是exec函数簇;exec需手动fork进程进,然后再调用exec函数簇个,过程比前两者稍微复杂。
1.1 system()
常见的是使用 system() ,system封装了进程创建、释放的过程,高效、使用简单。system内部调用的是exec函数簇。
函数原型int system(const char *command)引用头文件#include <stdlib.h>返回值成功返回执行值;调用bin/sh失败时返回127,其他情况返回-1;调用system成功与否应根据errno 来确认。注意事项在编写具SUID/SGID权限的程序时请尽量避免使用system(),system()会继承环境变量,通过环境变量可能会造成系统安全的问题。 int system(const char *cmdstring) { pid_t pid; int status; if (cmdstring == NULL) { return (1); } if ((pid = fork()) < 0) { status = -1; } else if (pid == 0) { execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); } else { while (waitpid(pid, &status, 0) < 0) { if (errno != EINTR) { status = -1; break; } } } return(status); }从函数源码看出,system() 执行时,首先 fork() 一个子进程,由子进程来调用 “/bin/sh -c string” 来执行形参“string”传递进来的shell命令,执行完退出子进程返回。需要注意的是在调用 system() 期间 SIGCHLD 信号会被暂时搁置,SIGINT 和 SIGQUIT 信号则会被忽略。
1.2 popen()
popen函数实际上是对管道操作的一个封装:
函数原型FILE * popen( const char * command,const char * type)引用头文件#include<stdio.h>返回值成功返回文件流指针,失败返回NULL,错误原因存于errno中。注意事项在编写具SUID/SGID权限的程序时请尽量避免使用popen(),popen()会继承环境变量,通过环境变量可能会造成系统安全的问题。 FILE * popen(const char *cmdstring, const char *type) { int i, pfd[2]; pid_t pid; FILE *fp; /* only allow "r" or "w" */ if ((type[0] != 'r' && type[0] != 'w') || type[1] != 0) { errno = EINVAL; /* required by POSIX.2 */ return(NULL); } if (childpid == NULL) { /* first time through */ /* allocate zeroed out array for child pids */ maxfd = open_max(); if ( (childpid = calloc(maxfd, sizeof(pid_t))) == NULL) return(NULL); } if (pipe(pfd) < 0) return(NULL); /* errno set by pipe() */ if ( (pid = fork()) < 0) return(NULL); /* errno set by fork() */ else if (pid == 0) { /* child */ if (*type == 'r') { close(pfd[0]); if (pfd[1] != STDOUT_FILENO) { dup2(pfd[1], STDOUT_FILENO); close(pfd[1]); } } else { close(pfd[1]); if (pfd[0] != STDIN_FILENO) { dup2(pfd[0], STDIN_FILENO); close(pfd[0]); } } /* close all descriptors in childpid[] */ for (i = 0; i < maxfd; i++) if (childpid[ i ] > 0) close(i); execl(SHELL, "sh", "-c", cmdstring, (char *) 0); _exit(127); } /* parent */ if (*type == 'r') { close(pfd[1]); if ( (fp = fdopen(pfd[0], type)) == NULL) return(NULL); } else { close(pfd[0]); if ( (fp = fdopen(pfd[1], type)) == NULL) return(NULL); } childpid[fileno(fp)] = pid; /* remember child pid for this fd */ return(fp); }执行过程 : ● 创建一个管道; ● fork 一个子进程,调用 /bin/sh -c 来执行参数 command 的指令,参数type“r”表示读,“w”表示写;根据形参 type 值,建立管道链接到子进程的标准输入/输出设备,然后返回一个文件指针; ● 主进程可以利用该文件指针来读取子进程的标准输出设备或是写入到子进程的标准输入设备中。除了 fclose() 函数以外,其他所有使用文件指针(FILE *)操作的函数都可以使用返回的文件指针。
1.3 exec()函数簇
exec函数族分别是:execl、execlp、execle、execv、execvp、execvpe 。
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[]); 函数原型int execl(const char *path, const char *arg, …);引用头文件#include <unistd.h>返回值exec函数执行成功后不会返回;失败时,会设置errno并返回-1,然后从原程序的调用处接着往下执行。注意事项输入参数准确,exec函数参数比较难记忆。参数说明:
函数参数说明path可执行文件的名称,包括路径 。arg可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束 。file如果参数file中包含/,则为路径名,否则在 PATH环境变量指定的各目录中搜寻可执行文件。示例:
#include<stdio.h> #include<unistd.h> int main(int argc, char *argv[]) { pid_t fpid; char *args[] = {"date"}; fpid = vfork(); if(fpid < 0) { perror("vfork:"); return -1; } if(fpid == 0) { execv("/bin/date", args); } else { ; /*父进程*/ } return 0; }2.获取返回值
Linux系统C/C++执行shell命令后,获取返回结果的方式有三种: 【1】使用临时文件,开头案例提及的; 【2】借助popen文件指针,推荐这种方式; 【3】使用匿名管道。
2.1 使用临时文件
采用临时文件的方式,比较简单和易实现,但效率不高,不推荐使用。
#include<stdlib.h> #include<stdio.h> int main(int argc, char **argv) { system("date > ./date.txt"); /* 其他进程可以通过访问date.txt获取返回值 */ return 0; }2.2 借助popen文件指针
popen()函数执行成功后,返回的是标准文件流指针,可以通过文件流函数(如fgets())获取执行命令的返回结果。这种方式比“临时文件”的方式效率要高,首推荐使用该方式。 popen执行完,必须调用“pclose”手动关闭文件流。
#include<iostream> #include<stdlib.h> #include<stdio.h> #include<string.h> int main(int argc, char **argv) { FILE *fp = NULL; char *buff = NULL; buff = (char*)malloc(20); if(buff == NULL) { perror("malloc:"); return -1; } memset(buff, 0, 20); fp = popen("date", "r"); if (fp == NULL) { perror("popen error:"); free(buff); return -1; } fgets(buff, 20, fp); std::cout << "run \"date\" return:"; std::cout << buff << std::endl; pclose(fp); free(buff); return 0; }运行结果: 2.3 使用匿名管道
匿名管道用于父、子进程间通信,我们可以fork一个子进程,调用“dup”将shell命令执行标准输出绑定到匿名管道的写端,父进程从匿名管道读端获取数据。这样也可以实现获取shell命令返回结果。这种方式稍微复杂,但个人觉得也比“临时文件”要更好使。
#include<iostream> #include<stdlib.h> #include<stdio.h> #include<string.h> int main(int argc, char **argv) { int fpipe[2] = {0}; pid_t fpid; char *buff = NULL; buff = (char*)malloc(20); if(buff == NULL) { perror("malloc:"); return -1; } memset(buff, 0, 20); if (pipe(fpipe) < 0) { perror("piple:"); free(buff); return -1; } fpid = fork(); if (fpid < 0) { perror("fork:"); free(buff); return -1; } if (fpid == 0) { close(fpipe[0]); dup2(fpipe[1],STDOUT_FILENO); system("date"); } else { fflush(stdout); close(fpipe[1]); read(fpipe[0], buff, 19); std::cout << "run \"date\" return:"; std::cout << buff << std::endl; } free(buff); return 0; }执行结果: