进程管理之system 详解
system定义
#includeint system(const char *command);
首先要知道,system函数是c库中的函数,而不是系统调用。其实system函数使用起来并不复杂,难就难在对其返回值的理解。这个问题,下文会详细分析。参数的话,很简单,就是终端的命令即可。这是因为system函数的实现中调用了shell的缘故。
system优缺点
- 优点:可以让c程序猿很方便地调用其他语言编写的程序,当然调用c程序自然也没问题;
- 缺点:第一:效率低,第二:返回值难理解;效率低是因为system函数的实现过程至少要创建两个进程,一个是shell进程,还有一个或着多个shell命令运行的进程。所以在对效率高有要求的程序中,直接用fork和exec函数族比较合适;返回值的问题下文会讲;
system函数返回值
先写个简化版的system函数的实现过程。简化是没有考虑处理信号的问题。代码如下:
#include#include #include int system(char * command) { int status; pid_t child; swicth(child = fork()) { case -1: return -1; case 0: execl("/bin/sh","sh","-c",command,NULL); _exit(127); default: while(waitpid(child,&status,0)<0) { if(errno != EINTR) { status = -1; break; } } return status; } }
1)返回值为 “0” 或 “1”
这中情况一般不会出现,只有当写成system(NULL)时,才会出现这种结果。目的是为了检测系统的shell是否可用。返回1表示shell可用,返回0表示shell不可用;这种情况在上诉代码中看不出来,通过glibc库中的源码可以看出:glibc-2.17/sysdeps/posix/system.cint __libc_system(const char *line){ if(line == NULL) return do_system("exit 0") == 0; ...... }
line指针指向的就是command命令行参数,system函数调用do_system系统调用,当执行“exit 0”(表示结束当前shell),执行成功do_system返回0,说明shell可用。反之,shell不可用。
2)返回值为 -1
有两个原因造成这样的结果。第一,因为fork创建子进程失败导致的。即“case -1”的情况,这中情况比较少见;第二,是不能正常处理子进程的“墓志铭”导致的,说白了,就是子进程的进程表在子进程结束时,没有经过父进程处理,而自己销毁了。这样的效果是因为父进程中安插的处理SIGCHLD信号的处理函数是SIG_IGN,或者用户设置了SA_NOCLDWAIT标志位导致,waitpid函数返回值为-1且全局变量errno的值为ECHLD;例如:signal(SIGCHLD,SIG_IGN); //出错的根源if( (status = system(command)) < 0 ){ fprintf(stderr,"system return %d (%s)\n",status,strerror(errno)); return -2; }
所以在使用system函数时,一定要判断SIGCHLD是否被设置为SIG_IGN。
3)返回值为 _exit(127)的返回值
这种情况是因为程序运行到“case 0”中,execl函数执行失败后,执行_exit函数导致的。说明子进程无法执行shell该shell命令。4)返回值为shell执行的进程的返回值
shell的终止状态是其执行最后一条命令的退出状态。这种情况下和获取子进程的退出状态一样。通过如下宏获取其退出状态:- WIFEXITED(status)
- WEXITSATUS(status)
- WIFSIGNALED(status)
- WTERMSIG(status)
- WCOREDUMP(satus)
综上所述,可以给出一个system函数返回值判断的例程:
if( (status == system(command))==-1 ){ fprintf(stderr,"system() function return -1 (%s)\n",strerror(errno)); } else if(WIFEXITED(status)&&WEXITSTATUS(status) == 127) { fprintf(stderr,"cannot invoke shell to exec command (%s)\n",strerror(enrrno)); } else print_wait_exit(status);
print_wait_exit函数就是上文中提到的获取shell退出状态的宏,根据需要去实现即可。
system函数和信号
影响system函数执行的信号有三个:SIGCHLD、SIGINT和SIGQUIT信号。
SIGCHLD信号:上文已经提过,它会影响waitpid函数,产生竞争现象的出现。调用system函数的进程可能还有其他的子进程,当然同样也会有wait或waitpid函数。当有SIGCHLD信号到来时,system函数中的waitpid函数和其他的wait或waitpid函数产生竞争状态,从而使得system执行异常。所以system执行期间会暂时屏蔽SIGCHLD信号;
SIGINT信号和SIGQUIT信号:调用system函数的进程会屏蔽这两个信号。system函数创建的进程会恢复这两个信号的默认处理状态。调用system函数的进程其实已经放弃了控制权,所以不能够去终止进程。反之,system函数创建的进程当然就有了控制权,所以要恢复控制权。
#includeint system(const char *command);
system() executes a command specified in command by calling /bin/sh -c command, and returns after the command has been completed. During execution of the command, SIGCHLD will be blocked, and SIGINT and SIGQUIT will be ignored.
int system(const char * cmdstring) { pid_t pid; int status; if(cmdstring == NULL) { return (1); //如果cmdstring为空,返回非零值,一般为1 } if((pid = fork())<0) { status = -1; //fork失败,返回-1 } else if(pid == 0) { execl("/bin/sh", "sh", "-c", cmdstring, (char *)0); _exit(127); // exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦~~ } else //父进程 { while(waitpid(pid, &status, 0) < 0) { if(errno != EINTR) { status = -1; //如果waitpid被信号中断,则返回-1 break; } } } return status; //如果waitpid成功,则返回子进程的返回状态 }
仔细看完这个system()函数的简单实现,那么该函数的返回值就清晰了吧,那么什么时候system()函数返回0呢?只在command命令返回0时。
int status;if(NULL == cmdstring) //如果cmdstring为空趁早闪退吧,尽管system()函数也能处理空指针 { return XXX; } status = system(cmdstring); if(status < 0) { printf("cmd: %s\t error: %s", cmdstring, strerror(errno)); // 这里务必要把errno信息输出或记入Log return XXX; } if(WIFEXITED(status)) { printf("normal termination, exit status = %d\n", WEXITSTATUS(status)); //取得cmdstring执行结果 } else if(WIFSIGNALED(status)) { printf("abnormal termination,signal number =%d\n", WTERMSIG(status)); //如果cmdstring被信号中断,取得信号值 } else if(WIFSTOPPED(status)) { printf("process stopped, signal number =%d\n", WSTOPSIG(status)); //如果cmdstring被信号暂停执行,取得信号值 }
到于取得子进程返回值的相关介绍可以参考另一篇文章:
system()函数用起来很容易出错,返回值太多,而且返回值很容易跟command的返回值混淆。这里推荐使用popen()函数替代,关于popen()函数的简单使用也可以通过上面的链接查看。
popen()函数较于system()函数的优势在于使用简单,popen()函数只返回两个值:
成功返回子进程的status,使用WIFEXITED相关宏就可以取得command的返回结果; 失败返回-1,我们可以使用perro()函数或strerror()函数得到有用的错误信息。这篇文章只涉及了system()函数的简单使用,还没有谈及SIGCHLD、SIGINT和SIGQUIT对system()函数的影响,事实上,之所以今天写这篇文章,是因为项目中因有人使用了system()函数而造成了很严重的事故。现像是system()函数执行时会产生一个错误:“No child processes”。
先看一下问题
简单封装了一下system()函数:
int pox_system(const char *cmd_line) { return system(cmd_line); }
int ret = 0; ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z"); if(0 != ret) { Log("zip file failed\n"); }问题现象:每次执行到此处,都会zip failed。而单独把该命令拿出来在shell里执行却总是对的,事实上该段代码已运行了很长时间,从没出过问题。
糟糕的日志
分析log时,我们只能看到“zip file failed”这个我们自定义的信息,至于为什么fail,毫无线索。
int ret = 0; ret = pox_system("gzip -c /var/opt/I00005.xml > /var/opt/I00005.z"); if(0 != ret) { Log("zip file failed: %s\n", strerror(errno)); //尝试打印出系统错误信息 }我们增加了log,通过system()函数设置的errno,我们得到一个非常有用的线索:system()函数失败是由于“ No child processes”。继续找Root Cause。
谁动了errno
我们通过上面的线索,知道system()函数设置了errno为ECHILD,然而从system()函数的man手册里我们找不到任何有关EHILD的信息。我们知道system()函数执行过程为:fork()->exec()->waitpid()。很显然waitpid()有重大嫌疑,我们去查一下man手册,看该函数有没有可能设置ECHILD:
如此处理问题是你的风格吗
正当我们急于check in 代码时,一个疑问出现了:“这个错误为什么以前没发生”?是啊,运行良好的程序怎么突然就挂了呢?首先我们代码没有改动,那么肯定是外部因素了。一想到外部因素,我们开始抱怨:“肯定是其他组的程序影响我们了!”但抱怨这是没用的,如果你这么认为,那么请拿出证据!但静下来分析一下不难发现,这不可能是其他程序的影响,其他进程不可能影响我们进程对信号的处理方式。
system()函数之前没出错,是因为systeme()函数依赖了系统的一个特性,那就是内核初始化进程时对SIGCHLD信号的处理方式为SIG_DFL,这是什么什么意思呢?即内核发现进程的子进程终止后给进程发送一个SIGCHLD信号,进程收到该信号后采用SIG_DFL方式处理,那么SIG_DFL又是什么方式呢?SIG_DFL是一个宏,定义了一个信号处理函数指针,事实上该信号处理函数什么也没做。这个特性正是system()函数需要的,system()函数首先fork()一个子进程执行command命令,执行完后system()函数会使用waitpid()函数对子进程进行收尸。
通过上面的分析,我们可以清醒的得知,system()执行前,SIGCHLD信号的处理方式肯定变了,不再是SIG_DFL了,至于变成什么暂时不知道,事实上,我们也不需要知道,我们只需要记得使用system()函数前把SIGCHLD信号处理方式显式修改为SIG_DFL方式,同时记录原来的处理方式,使用完system()后再设为原来的处理方式。这样我们可以屏蔽因系统升级或信号处理方式改变带来的影响。
验证猜想
我们公司采用的是持续集成+敏捷开发模式,每天都会由专门的team负责自动化case的测试,每次称为一个build,我们分析了本次build与上次build使用的系统版本,发现版本确实升级了。于是我们找到了相关team进行验证,我们把问题详细的描述了一下,很快对方给了反馈,下面是邮件回复原文:
LIBGEN 里新增加了SIGCHLD的处理。将其ignore。为了避免僵尸进程的产生。看来我们的猜想没错!问题分析到这里,解决方法也清晰了,于是我们修改了我们的pox_system()函数:
typedef void (*sighandler_t)(int); int pox_system(const char *cmd_line) { int ret = 0; sighandler_t old_handler; old_handler = signal(SIGCHLD, SIG_DFL); ret = system(cmd_line); signal(SIGCHLD, old_handler); return ret; }
我想这是调用system()比较完美的解决方案了,同时使用pox_system()函数封装带来了非常棒的易维护性,我们只需要修改此处一个函数,其他调用处都不需要改。
后来,查看了对方修改的代码,果然从代码上找到了答案:
/* Ignore SIGCHLD to avoid zombie process */ if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) { return -1; } else { return 0; }
其他思考
我们公司的代码使用SVN进程管理的,到目前为止有很多branch,逐渐的,几乎每个branch都出现了上面的问题,于是我逐个在各个branchc上fix这个问题,几乎忙了一天,因为有的branch已被锁定,再想merge代码必须找相关负责人说明问题的严重性,还要在不同的环境上测试,我边做这些边想,系统这样升级合适吗?
首先,由于系统的升级导致我们的代码在测试时发现问题,这时再急忙去fix,造成了我们的被动,我想这是他们的一个失误。你做的升级必须要考虑到对其他team的影响吧?何况你做的是系统升级。升级前需要做个风险评估,对可能造成的影响通知大家,这样才职业嘛。
再者,据他们的说法,修改信号处理方式是为了避免僵尸进程,当然初衷是好的,但这样的升级影响了一些函数的使用方式,比如system()函数、wait()函数、waipid()、fork()函数,这些函数都与子进程有关,如果你希望使用wait()或waitpid()对子进程收尸,那么你必须使用上面介绍的方式:在调用前(事实上是fork()前)将SIGCHLD信号置为SIG_DFL处理方式,调用后(事实上wait()/waitpid()后)再将信号处理方式设置为从前的值。你的系统升级,强制大家完善代码,确实提高了代码质量,但是对于这种升级我不是很认同,试想一下,你见过多少fork()->waitpid()前后都设置SIGCHLD信号的代码?
使用system()函数的建议
上在给出了调用system()函数的比较安全的用法,但使用system()函数还是容易出错,错在哪?那就是system()函数的返回值,关于其返回值的介绍请见上篇文章。system()函数有时很方便,但不可滥用!
1、建议system()函数只用来执行shell命令,因为一般来讲,system()返回值不是0就说明出错了;
2、建议监控一下system()函数的执行完毕后的errno值,争取出错时给出更多有用信息;
3、建议考虑一下system()函数的替代函数popen();其用法在我的另一篇文章有介绍。
标准I/O函数库提供了popen函数,它启动另外一个进程去执行一个shell命令行。
这里我们称调用popen的进程为父进程,由popen启动的进程称为子进程。
popen函数还创建一个管道用于父子进程间通信。父进程要么从管道读信息,要么向管道写信息,至于是读还是写取决于父进程调用popen时传递的参数。下在给出popen、pclose的定义:
#include/* 函数功能:popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。 参数type可使用“r”代表读取,“w”代表写入。 依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。 随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中 返回值:若成功则返回文件指针,否则返回NULL,错误原因存于errno中 */ FILE * popen( const char * command,const char * type); /* 函数功能:pclose()用来关闭由popen所建立的管道及文件指针。参数stream为先前由popen()所返回的文件指针 返回值:若成功返回shell的终止状态(也即子进程的终止状态),若出错返回-1,错误原因存于errno中 */ int pclose(FILE * stream);
下面通过例子看下popen的使用:
假如我们想取得当前目录下的文件个数,在shell下我们可以使用:
ls | wc -l
我们可以在程序中这样写:
/*取得当前目录下的文件个数*/#include#include #include #include #define MAXLINE 1024 int main() { char result_buf[MAXLINE], command[MAXLINE]; int rc = 0; // 用于接收命令返回值 FILE *fp; /*将要执行的命令写入buf*/ snprintf(command, sizeof(command), "ls ./ | wc -l"); /*执行预先设定的命令,并读出该命令的标准输出*/ fp = popen(command, "r"); if(NULL == fp) { perror("popen执行失败!"); exit(1); } while(fgets(result_buf, sizeof(result_buf), fp) != NULL) { /*为了下面输出好看些,把命令返回的换行符去掉*/ if('\n' == result_buf[strlen(result_buf)-1]) { result_buf[strlen(result_buf)-1] = '\0'; } printf("命令【%s】 输出【%s】\r\n", command, result_buf); } /*等待命令执行完毕并关闭管道及文件指针*/ rc = pclose(fp); if(-1 == rc) { perror("关闭文件指针失败"); exit(1); } else { printf("命令【%s】子进程结束状态【%d】命令返回值【%d】\r\n", command, rc, WEXITSTATUS(rc)); } return 0; }
$ gcc popen.c
$ ./a.out
命令【ls ./ | wc -l】 输出【2】
命令【ls ./ | wc -l】子进程结束状态【0】命令返回值【0】
上面popen只捕获了command的标准输出,如果command执行失败,子进程会把错误信息打印到标准错误输出,父进程就无法获取。比如,command命令为“ls nofile.txt” ,事实上我们根本没有nofile.txt这个文件,这时shell会输出“ls: nofile.txt: No such file or directory”。这个输出是在标准错误输出上的。通过上面的程序并无法获取。
注:如果你把上面程序中的command设成“ls nofile.txt”,编译执行程序你会看到如下结果:
$ gcc popen.c
$ ./a.out
ls: nofile.txt: No such file or directory
命令【ls nofile.txt】子进程结束状态【256】命令返回值【1】
需要注意的是第一行输出并不是父进程的输出,而是子进程的标准错误输出。
有时子进程的错误信息是很有用的,那么父进程怎么才能获取子进程的错误信息呢?
这里我们可以重定向子进程的错误输出,让错误输出重定向到标准输出(2>&1),这样父进程就可以捕获子进程的错误信息了。例如command为“ls nofile.txt 2>&1”,输出如下:
命令【ls nofile.txt 2>&1】 输出【ls: nofile.txt: No such file or directory】
命令【ls nofile.txt 2>&1】子进程结束状态【256】命令返回值【1】
附:子进程的终止状态判断涉及到的宏,设进程终止状态为status.
WIFEXITED(status)如果子进程正常结束则为非0值。
WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED 来判断是否正常结束才能使用此宏。
WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真。
WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED 来判断后才使用此宏。
WIFSTOPPED(status)如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。
WSTOPSIG(status)取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED 来判断后才使用此宏。