0 前言
当你不得不在 C++ 程序中调用脚本的时候,将就着用 system()?No,我有更好的方法,不仅可以捕获脚本输出,还能实现调用超时。
1 fork + execlp + pipe 优雅搞定脚本调用
-
C++ 中几种创建进程和调用脚本方式:
- system() 建立独立进程,拥有独立的代码空间和内存空间。
- fork() 创建进程,克隆父进程的代码。
- execlp() 代替当前进程的代码空间中的代码数据,函数本身不创建新的进程。
-
system() 无能为力的地方:
- system() 只能建的的通过执行命令的返回状态来判断脚本的执行情况,如果脚本有复杂的文本返回值则无法处理。
- system() 无法设置超时时间,不够灵活。
-
使用 fork + execlp + pipe 虽然复杂一些,但是可以通过进程间的管道来重定向脚本的标准输出,从而拿到脚本的执行日志和返回值,同时可以设置超时时间,比 system() 简单的调用要优雅不少。
-
小技巧
- 如果脚本中有后台调用,则后台调用会把 pipe 的输出 hang 住,直到后台调用也执行完成,这种情况往往不是大家想要的。可以在脚本执行结束时打印 ‘\0’,调用程序通过识别 ‘\0’ 来结束调用,而不单单等 pipe 的数据流结束。
- 在调用 waitpid 等待脚本执行结束时使用 WNOHANG,实现调用超时,但是要使用从超时功能的话,就不能同时使用 pipe 捕获脚本数据了,因为 read 可能会 hang 住。
-
实例代码
int pipefd[2];
ret = pipe(pipefd);
if((pid = fork()) == 0) {
// child process
close(pipefd[0]);
dup2(pipefd[1], 1);
switch(param_count) {
case 0:
ret = execlp(cmd, cmd, NULL);
break;
case 2:
ret = execlp(cmd, cmd, param[0], param[1], NULL);
break;
default:
break;
}
if (ret) {
printf("execlp run error %s\n", cmd);
}
} else if(pid > 0) {
// parent process
int read_size;
char buffer[32]="";
char *ptr = res_buf;
int overflow = 0;
close(pipefd[1]);
int res_len = 0;
while ((read_size = read(pipefd[0], buffer, sizeof(buffer)-1)) != 0) {
buffer[read_size] = '\0';
if (read_size + res_len < res_buf_len) {
strcpy(ptr + res_len, buffer);
if (buffer[read_size - 1] == 0) {
break;
}
} else {
overflow = 1;
break;
}
res_len += read_size;
}
if (overflow) {
while ((read_size = read(pipefd[0], buffer, sizeof(buffer)-1)) != 0) {}
log2(g_log, "%s output overflow\n", cmd);
}
waitpid(pid, &status, 0);
//调用超时的实现逻辑: while(timeout){waitpid(pid, &status, WNOHANG);sleep(1);}
close(pipefd[0]);
} else {
close(pipefd[0]);
close(pipefd[1]);
snprintf(res_buf, res_buf_len, "fork error");
log2(g_log, "%s fork error\n", cmd);
return -1;
}