先看一下问题 shell
简单封装了一下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里执行却老是对的,事实上该段代码已运行了很长时间,从没出过问题。
糟糕的日志 app
分析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: ui
如此处理问题是你的风格吗 spa
正当咱们急于check in 代码时,一个疑问出现了:“这个错误为何之前没发生”?是啊,运行良好的程序怎么忽然就挂了呢?首先咱们代码没有改动,那么确定是外部因素了。一想到外部因素,咱们开始抱怨:“确定是其余组的程序影响咱们了!”但抱怨这是没用的,若是你这么认为,那么请拿出证据!但静下来分析一下不难发现,这不多是其余程序的影响,其余进程不可能影响咱们进程对信号的处理方式。 .net
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()函数有时很方便,但不可滥用!
一、建议system()函数只用来执行shell命令,由于通常来说,system()返回值不是0就说明出错了;
二、建议监控一下system()函数的执行完毕后的errno值,争取出错时给出更多有用信息;
三、建议考虑一下system()函数的替代函数popen();其用法在个人另外一篇文章有介绍。
qdurenhongcai@163.com
转载请注明出处。