曾经的曾经,被system()函数折磨过,之所以这样,是因为对system()函数了解不够深入。只是简单的知道用这个函数执行一个系统命令,这远远不够,它的返回值、它所执行命令的返回值以及命令执行失败原因如何定位,这才是重点。当初因为这个函数风险较多,故抛弃不用,改用其他的方法。这里先不说我用了什么方法,这里必须要搞懂system()函数,因为还是有很多人用了system()函数,有时你不得不面对它。

先来看一下system()函数的简单介绍:

int 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.

system()函数调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell,比如bash,-c选项是告诉shell从字符串command中读取命令;

在该command执行期间,SIGCHLD是被阻塞的,好比在说:hi,内核,这会不要给我送SIGCHLD信号,等我忙完再说;

在该command执行期间,SIGINT和SIGQUIT是被忽略的,意思是进程收到这两个信号后没有任何动作。

再来看一下system()函数返回值:

The value returned is - on error (e.g. fork() failed), and the return status of the command otherwise. This latter return status is in the format specified in wait(). Thus, the exit code of the command will be WEXITSTATUS(status). In case /bin/sh could not be executed, the exit status will be that of a command that does exit().
If the value of command is NULL, system() returns nonzero if the shell is available, and zero if not.

为了更好的理解system()函数返回值,需要了解其执行过程,实际上system()函数执行了三步操作:

1.fork一个子进程;

2.在子进程中调用exec函数去执行command;

3.在父进程中调用wait去等待子进程结束。

对于fork失败,system()函数返回-1。

如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。

(注意,command顺利执行不代表执行成功,比如command:”rm debuglog.txt”,不管文件存不存在该command都顺利执行了)

如果exec执行失败,也即command没有顺利执行,比如被信号中断,或者command命令根本不存在,system()函数返回127.

如果command为NULL,则system()函数返回非0值,一般为1.

popen和system都可以执行外部命令。

popen相当于是先创建一个管道,fork,关闭管道的一端,执行exec,返回一个标准的io文件指针。

system相当于是先后调用了fork, exec,waitpid来执行外部命令

popen本身是不阻塞的,要通过标准io的读取使它阻塞

system本身就是阻塞的。

看一下system()函数的源码

看完这些,我想肯定有人对system()函数返回值还是不清楚,看源码最清楚,下面给出一个system()函数的实现:

int system(const char * cmdstring)
{
pid_t pid;
int status; if(cmdstring == NULL)
{
return (); //如果cmdstring为空,返回非零值,一般为1
} if((pid = fork())<)
{
status = -; //fork失败,返回-1
}
else if(pid == )
{
execl("/bin/sh", "sh", "-c", cmdstring, (char *));
_exit(); // exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话现在的进程就不存在啦~~
}
else //父进程
{
while(waitpid(pid, &status, ) < )
{
if(errno != EINTR)
{
status = -; //如果waitpid被信号中断,则返回-1
break;
}
}
} return status; //如果waitpid成功,则返回子进程的返回状态
}

仔细看完这个system()函数的简单实现,那么该函数的返回值就清晰了吧,那么什么时候system()函数返回0呢?只在command命令返回0时。

看一下该怎么监控system()函数执行状态

这里给我出的做法:

if(NULL == cmdstring) //如果cmdstring为空趁早闪退吧,尽管system()函数也能处理空指针popen和system都可以执行外部命令。
popen相当于是先创建一个管道,fork,关闭管道的一端,执行exec,返回一个标准的io文件指针。
system相当于是先后调用了fork, exec,waitpid来执行外部命令
popen本身是不阻塞的,要通过标准io的读取使它阻塞
system本身就是阻塞的。popen和system都可以执行外部命令。
popen相当于是先创建一个管道,fork,关闭管道的一端,执行exec,返回一个标准的io文件指针。
system相当于是先后调用了fork, exec,waitpid来执行外部命令
popen本身是不阻塞的,要通过标准io的读取使它阻塞
system本身就是阻塞的。 status = system(cmdstring); printf(“cmd: %s\t error: %s”, cmdstring, strerror(errno)); // 这里务必要把errno信息输出或记入Log 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被信号暂停执行,取得信号值

到于取得子进程返回值的相关介绍可以参考另一篇文章:http://my.oschina.net/renhc/blog/35116

system()函数用起来很容易出错,返回值太多,而且返回值很容易跟command的返回值混淆。这里推荐使用popen()函数替代,关于popen()函数的简单使用也可以通过上面的链接查看。

popen()函数较于system()函数的优势在于使用简单,popen()函数只返回两个值:
成功返回子进程的status,使用WIFEXITED相关宏就可以取得command的返回结果;
失败返回-1,我们可以使用perro()函数或strerror()函数得到有用的错误信息。

这篇文章只涉及了system()函数的简单使用,还没有谈及SIGCHLD、SIGINT和SIGQUIT对system()函数的影响,事实上,之所以今天写这篇文章,是因为项目中因有人使用了system()函数而造成了很严重的事故。现像是system()函数执行时会产生一个错误:“No child processes”。

关于这个错误的分析,感兴趣的朋友可以看一下:http://my.oschina.net/renhc/blog/54582

——————————————————————————————————————-------------------------------------------

今天,一个运行了近一年的程序突然挂掉了,问题定位到是system()函数出的问题,关于该函数的简单使用在我上篇文章做过介绍: 
http://my.oschina.net/renhc/blog/53580

先看一下问题

简单封装了一下system()函数:

int pox_system(const char *cmd_line)
return system(cmd_line);

函数调用:

ret = pox_system(“gzip -c /var/opt/I00005.xml > /var/opt/I00005.z”);
Log(“zip file failed\n”);

问题现象:每次执行到此处,都会zip failed。而单独把该命令拿出来在shell里执行却总是对的,事实上该段代码已运行了很长时间,从没出过问题。

糟糕的日志

system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。分析log时,我们只能看到“zip file failed”这个我们自定义的信息,至于为什么fail,毫无线索。

那好,我们先试着找出更多的线索:

ret = pox_system(“gzip -c /var/opt/I00005.xml > /var/opt/I00005.z”);
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:

ECHILD

(for waitpid() or waitid()) The process specified by pid (waitpid()) or idtype and id (waitid()) does not exist or is not a child of the calling process. (This can happen for one’s own child if the action for SIGCHLD is set to SIG_IGN. See also the Linux Notes section about threads.)

果然有料,如果SIGCHLD信号行为被设置为SIG_IGN时,waitpid()函数有可能因为找不到子进程而报ECHILD错误。似乎我们找到了问题的解决方案:在调用system()函数前重新设置SIGCHLD信号为缺省值,即signal(SIGCHLD, SIG_DFL)。我们很兴奋,暂时顾不上看Linux Notes部分,直接加上代码测试!乖乖,问题解决了!

如此处理问题是你的风格吗

正当我们急于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)
sighandler_t old_handler;
old_handler = signal(SIGCHLD, SIG_DFL);
ret = system(cmd_line);
signal(SIGCHLD, old_handler);

我想这是调用system()比较完美的解决方案了,同时使用pox_system()函数封装带来了非常棒的易维护性,我们只需要修改此处一个函数,其他调用处都不需要改。

后来,查看了对方修改的代码,果然从代码上找到了答案:

/* Ignore SIGCHLD to avoid zombie process */
if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。

其他思考

我们公司的代码使用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();其用法在我的另一篇文章有介绍。

ps:

1.如果waitpid()函数是被信号中断而返回负数的,则继续调用waitpid()函数。

这个包括SIGINT的啊,不违反POSIX.1定义啊

2.system非阻塞方式注意点:’&’转后台,同时将输出重定向。否则变为阻塞方式。

3,

popen和system都可以执行外部命令。

popen相当于是先创建一个管道,fork,关闭管道的一端,执行exec,返回一个标准的io文件指针。

system相当于是先后调用了fork, exec,waitpid来执行外部命令

popen本身是不阻塞的,要通过标准io的读取使它阻塞

system本身就是阻塞的。

[转载]Linux下关于system调用的更多相关文章

  1. linux下代替system的基于管道的popen和pclose函数

    linux下使用system需要谨慎,那么代替它的方法是什么呢? 标准I/O函数库提供了popen函数,它启动另外一个进程去执行一个shell命令行. 这里我们称调用popen的进程为父进程,由pop ...

  2. linux下c程序调用reboot函数实现直接重启【转】

    转自:http://www.blog.chinaunix.net/uid-20564848-id-73878.html linux下c程序调用reboot函数实现直接重启 当然你也可以直接调用syst ...

  3. 【C/C++】Linux下使用system()函数一定要谨慎

    [C/C++]Linux下使用system()函数一定要谨慎 http://my.oschina.net/renhc/blog/53580 曾经的曾经,被system()函数折磨过,之所以这样,是因为 ...

  4. 在linux下sh批处理文件调用java的方法

    解密 java -classpath collection-impl-0.0.1.jar com.ai.toptea.collection.message.DESEncrypt 1EFE4663895 ...

  5. [转载]Linux下非root用户如何安装软件

    [转载]Linux下非root用户如何安装软件 来源:https://tlanyan.me/work-with-linux-without-root-permission/ 这是本人遇到的实际问题,之 ...

  6. linux 下cmake 编译 ,调用,调试 poco 1.6.0 小记

    上篇文章 小记了: 关于 Poco::TCPServer框架 (windows 下使用的是 select模型) 学习笔记. http://www.cnblogs.com/bleachli/p/4352 ...

  7. [转载] Linux下高并发socket最大连接数所受的各种限制

    原文: http://mp.weixin.qq.com/s?__biz=MzAwNjMxNjQzNA==&mid=207772333&idx=1&sn=cfc8aadb422f ...

  8. [转载]Linux下getopt()函数的简单使用

    转载源地址:https://www.cnblogs.com/qingergege/p/5914218.html 1.getopt()函数的出处就是unistd.h头文件(哈哈),写代码的时候千万不要忘 ...

  9. [转载]linux下svn常用指令

    一下内容转载于:http://blog.chinaunix.net/space.php?uid=22976768&do=blog&id=1640924.这个总结的很好~ windows ...

随机推荐

  1. Python 数据类型--字典类型

    字典 dict 字典是Python的另一种有序的可变数据结构,且可存储任意类型对象. 字典是一种键值对的数据容器,每个键值(key:value)对用冒号(:)分割,每个对之间用逗号(,)分割,整个字典 ...

  2. iOS 9 通用链接(Universal Links)

    什么是Universal Links? 在iOS9之前,对于从各种从浏览器.Safari中唤醒APP的需求,我们通常只能使用scheme.但是这种方式需要提前判断系统中是否安装了能够响应此scheme ...

  3. ubuntu16安装mysql图形界面

    之前在windows用sqlyog当做图形界面连接mysql,现在在ubuntu上需要连接测试环境的数据库,需要安装mysql图形界面.安装只需要条命令: sudo apt-get update su ...

  4. vue 之 Vue.extend()

    Vue.extend( options ) 参数: {Object} options 用法: 使用基础 Vue 构造器,创建一个“子类”.参数是一个包含组件选项的对象. data 选项是特例,需要注意 ...

  5. 算法训练 P1103

      算法训练 P1103   时间限制:1.0s   内存限制:256.0MB      编程实现两个复数的运算.设有两个复数 和 ,则他们的运算公式为: 要求:(1)定义一个结构体类型来描述复数. ...

  6. Mac锁屏快捷键

    Mac 锁屏快捷键 直接盖 -- 推荐,因为帅 control + command + Q -- 真正意义上的锁屏.可行 control + shift + Power -- 只是黑屏,再次打开不需要 ...

  7. 【linux基础】区块选择VisualBlock

    前言 有时候使用linux需要选择某一块区域进行处理,比如对某些列的某些行,类似于Ultraedit的列模式,其实vim中就有相关的功能,此时可以使用vim的区块选择进行处理. 区块选择 1.在一般模 ...

  8. 【计算机视觉】交并比IOU概念理解

    前言 交并比IOU(Intersection over Union)是一种测量在特定数据集中检测相应物体准确度的一个标准. 图示 很简单,IoU相当于两个区域重叠的部分除以两个区域的集合部分得出的结果 ...

  9. hdu 1723 DP/递推

    题意:有一队人(人数 ≥ 1),开头一个人要将消息传到末尾一个人那里,规定每次最多可以向后传n个人,问共有多少种传达方式. 这道题我刚拿到手没有想过 DP ,我觉得这样传消息其实很像 Fibonacc ...

  10. Mysql隔离级别 sql示例理解

    前言 事务要解决的是多线程并发修改数据库的问题.Mysql innodb 引擎支持事务.类似 Java 中的各种锁,例如乐观锁(CAS),读写锁,悲观锁.事务也有很多级别. 每个隔离级别要解决的问题都 ...