从Linux system()函数谈起

今天我们主要来谈谈linux下的system这个函数,之所以从这个函数谈起,是因为这两天被这个函数坑惨了。于是做了一些system函数的相关学习,并小结于此。

1.system函数

(1)system介绍

简言之,这个函数就是用来执行一个shell命令的函数。函数原型如下:

该函数会通过调用/bin/sh -c [command]来执行参数command指定的shell命令。并且,在执行command期间,SIGCHLD信号会被阻塞、SIGINT和SIGQUIT信号会被忽略——具体原因,我们稍后再谈。

为了更直观的感受,我们不妨写一个简易的小示例:

编译、运行:

可以发现,通过调用system函数执行ls命令和直接在shell下执行ls命令的输出结果完全相同。

嗯~这就是system函数简单的应用,接着,我们再深入一点…

(2)system源码

知其然,知其所以然。我们来看看system函数的实现,包括未对信号处理和对信号处理两个版本,为了便于理解,我们先看第一个未对信号进行处理的版本即可:

如上所示,system函数的实现主要调用了三个函数——fork()、execl()、waitpid():
1)首先,调用fork()函数去生成一个子进程;
2)如果pid==0,说明子进程创建成功,子进程调用execl()函数去执行shell命令;
3)如果pid>0(父进程),则父进程调用waitpid()函数获取子进程退出的状态,存入status中。

(3)system返回值

system函数的返回值相对于其他系统函数是比较复杂的。
1)首先,若命令为空==>返回1;如果fork()函数失败==》返回-1;
2)fork()成功,那么子进程调用execl()函数执行相应命令,如果execl()函数执行失败==》返回127;(execl函数也是一个“有个性”的函数,至于为什么,不妨动手查查)
3)execl()函数执行command成功,退出状态被父进程获取存入status==》返回status。

2.system函数使用

因此,在使用system函数时,有两点是需要特别注意的:一是,system函数返回值的判断及处理;二是,system函数执行期间会阻塞SIGCHLD信号(有信号处理的system函数)。

(1)system函数返回值

在这之前,我们先来了解两个检查wait和waitpid返回的终止状态的宏:WIFEXITED、WEXITSTATUS。

  • WIFEXITED(status):用来判断子进程是否正常退出,若是==》返回真。
  • WEXITSTATUS(status):若WIFEXITED(status)非零,即返回值为真,那么这个宏才有意义,该宏可以用来获取子进程正常退出时的退出状态。确切的说是获取子进程传递给exit()或_exit()参数的低8位。(比如,如果子进程调用exit(0)退出,则WEXITSTATUS宏就返回0)

好了,知道这些之后,我们就可以对system函数的返回值进行一些判断处理了。如下示例:运行一个未赋予执行权限的hello.sh脚本。

编译、运行(其中hello.sh未赋执行权限):

报错,返回值和报错信息一目了然。

接着,我们给hello.sh赋予执行权限,再运行:

程序正常运行,返回值也是我们所期望的。不信?我们换个方法验证脚本的返回值:

hello.sh的返回值确实是0。

接着我们再改一改,让hello.sh以12退出:

执行:

systemFun02报错,shell命令行执行sh hello.sh却正常。

这是因为源程序在判断WEXITSTATUS(status)的值时,我们是以0为正确的返回值来设计的,所以,修改相应的判断语句即可:
if(WEXITSTATUS(nRet) != 12)        /*若不为12,则错误*/
不过还是建议一般情况下以0为正确的退出码。

(2)system函数信号

什么是SIGCHLD信号?SIGCHLD信号就是由内核在任何一个进程终止时发给它的父进程的一个信号。父进程收到该信号后会做出相应的处理(当然,父进程可以选择忽略该信号),在父进程处理该信号之前,子进程处于僵尸状态——即我们常说的僵尸进程。处于僵尸状态的目的是以便父进程在以后某个时候获取。这些信息包括子进程的进程ID、终止状态以及资源利用信息等等。

所以,如果我们不在意子进程退出时的状态等信息时,我们常常会选择忽略SIGCHLD信号避免僵尸进程:
signal(SIGCHLD, SIG_IGN);
那么问题就来了,当我们在调用system函数时,system的内部实现会先fork()再execl(),最后waitpid()给子进程收尸,而并非是对子进程进行SIG_IGN忽略。所以,这就要求我们在使用system()函数时需要格外小心SIGCHLD信号的处理方式了。

1)情形一:

还是针对上面的例子,我们对main函数做出一点改动,执行system_without_sig版本的system函数之前,调用signal()函数忽略SIGCHLD信号:

编译、运行:

虽然hello.sh正常输出了,但是返回值却是不正确的,这是不能容忍的。

为什么返回值会出错呢?这就是前面我们提过的带有信号处理版本的system函数之所以会阻塞SIGCHLD信号的重要原因了。
详细点的说是因为:system函数在fork()一个子进程去执行shell命令之后,在父进程中会调用waitpid()函数获取子进程的退出状态然后给子进程收尸(释放资源等等)。但是,我们上面的例子在main函数中进行了忽略SIGCHLD信号的操作,所以父进程势必将获取不到子进程的状态,导致程序异常。

那是不是只要不忽略SIGCHLD信号就可以了呢?如果上面这个例子不够说服你阻塞SIGCHLD信号的重要性,那么,我们接着往下谈:

2)情形二:

如果我们不是对SIGCHLD信号进行忽略,而是捕捉该信号并进行相应的处理:

现在基于这样一种假设:

我们已经知道父进程调用system函数之后,system函数内部会fork()一个子进程了。在子进程结束的时候会发送一个SIGCHLD信号给父进程,父进程收到该信号后进行信号处理函数。比如,调用上述的信号处理函数handle_sigchld(),该信号处理函数在做出相应操作之后调用wait()函数为子进程收尸。那么,问题来了,handle_sigchld()函数调用wait函数为子进程收尸了,system函数中的waitpid()函数就无法获取子进程的退出状态了==》进而导致system函数异常。

这是system函数需要阻塞SIGCHLD信号的另一个重要的原因。而system函数之所以还要忽略SIGINT和SIGQUIT信号也是为了避免出现类似的情况。

基于以上两点,在使用system函数时的正确方式是,对其进行封装,并且需要对信号进行简单的处理:

3.system函数注意事项

(1)调用system时,小到一个内部的shell命令,大到一个百行、千行的shell脚本。避免使用相对路径,而应尽量使用绝对路径。

(2)system通过调用/bin/sh来执行相应命令,但是sh对连字符()并不是很友好,所以,system函数执行的shell脚本中,不应该出现以命名的变量名,比如有如下shell脚本:

通过system函数调用该shell脚本:

如上所述,system调用出错:hello-world是一个无效的标识符。

但是,诸如./hello.sh、. hello.sh、source hello.sh等命令是可以正常运行的:

所以,应该避免用符号命名shell脚本的变量。

4.小结

本文重点介绍了system函数的使用方法,返回值的错误检查方法,以及使用system函数的几点注意事项,希望大家在使用该函数的时候可以尽量避免一些错误。

5.附man system

 

 

《从Linux system()函数谈起》有2个想法

发表评论

电子邮件地址不会被公开。 必填项已用*标注