今天我们主要来谈谈linux下的system这个函数,之所以从这个函数谈起,是因为这两天被这个函数坑惨了。于是做了一些system函数的相关学习,并小结于此。
1.system函数
(1)system介绍
简言之,这个函数就是用来执行一个shell命令的函数。函数原型如下:
1 2 3 |
#include <stdlib.h> int system(const char *command); |
该函数会通过调用/bin/sh -c [command]来执行参数command指定的shell命令。并且,在执行command期间,SIGCHLD信号会被阻塞、SIGINT和SIGQUIT信号会被忽略——具体原因,我们稍后再谈。
为了更直观的感受,我们不妨写一个简易的小示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//filename:systemFun.c #include <stdlib.h> #include <stdio.h> int main() { char szShellCmd[64] = {0}; //用于存放待执行的shell命令 sprintf(szShellCmd, "ls"); //将"ls"命令存入szShellCmd中,当然也可以使用其他的命令 system(szShellCmd); //调用system函数执行"ls"命令 return 0; } |
编译、运行:
1 2 3 4 5 |
[root@localhost system-function]# gcc -Wall -g systemFun.c -o systemFun [root@localhost system-function]# ./systemFun systemFun systemFun.c [root@localhost system-function]# ls systemFun systemFun.c |
可以发现,通过调用system函数执行ls命令和直接在shell下执行ls命令的输出结果完全相同。
嗯~这就是system函数简单的应用,接着,我们再深入一点…
(2)system源码
知其然,知其所以然。我们来看看system函数的实现,包括未对信号处理和对信号处理两个版本,为了便于理解,我们先看第一个未对信号进行处理的版本即可:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
//filename:my-system.h #include <sys/types.h> #include <sys/wait.h> #include <errno.h> #include <unistd.h> #include <signal.h> int system_without_sig(const char *cmdstring) //version without signal handling { pid_t pid; int status; if(cmdstring == NULL) return(1); if((pid = fork()) < 0){ status = -1; //probably out of process } else if(pid == 0){ //child execl("/bin/sh", "sh", "-c", cmdstring, (char*)0); _exit(127); //execl error } else{ while(waitpid(pid, &status, 0) < 0) if(errno != EINTR){ status = -1; //error other than EINTR from waitpid() break; } } return(status); } int system_with_sig(const char *cmdstring) //with appropriate signal handling { pid_t pid; int status; struct sigaction ignore, saveintr, savequit; sigset_t chldmask, savemask; if(cmdstring == NULL) return(1); //always a command processor with UNIX ignore.sa_handler = SIG_IGN; //ignore SIGINT and SIGQUIT sigemptyset(&ignore.sa_mask); ignore.sa_flags = 0; if(sigaction(SIGINT, &ignore, &saveintr) < 0) return(-1); if(sigaction(SIGQUIT, &ignore, &savequit) < 0) return(-1); sigemptyset(&chldmask); //now block SIGCHLD sigaddset(&chldmask, SIGCHLD); if(sigprocmask(SIG_BLOCK, &chldmask, &savemask) < 0) return (-1); if((pid = fork()) < 0){ status = -1; //probably out of process } else if(pid == 0){ //child //restore previous signal actions & reset signal mask sigaction(SIGINT, &saveintr, NULL); sigaction(SIGQUIT, &savequit, NULL); sigprocmask(SIG_SETMASK, &savemask, NULL); execl("/bin/sh", "sh", "-c", cmdstring, (char*)0); _exit(127); //exec error } else{ //parent while(waitpid(pid, &status, 0) < 0) if(errno != EINTR){ status = -1; //error other than EINTR from waitpid() break; } } //restore previous signal actions & reset signal mask if(sigaction(SIGINT, &saveintr, NULL) < 0) return(-1); if(sigaction(SIGQUIT, &savequit, NULL) < 0) return(-1); if(sigprocmask(SIG_SETMASK, &savemask, NULL) < 0) return(-1); return(status); } |
如上所示,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脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
//filename:systemFun02.c #include <stdlib.h> #include <stdio.h> #include "my-system.h" int MySystem(const char *pShellCmd) { /*变量初始化*/ int nRet; /*参数有效性验证*/ if(NULL == pShellCmd) { printf("MySystem 参数无效。\n"); return -1; } /*调用system函数*/ nRet = system_without_sig(pShellCmd); /*打印调试信息*/ printf("system返回值: %d\n", nRet); printf("WIFEXITED返回值: %d\n", WIFEXITED(nRet)); printf("WEXITSTATUS返回值:%d\n", WEXITSTATUS(nRet)); /*首先,判断system函数是否正常返回*/ if(-1 == nRet){ printf("system 返回值错误。\n"); return -1; } else{ /*然后,判断子进程是否正常退出,即pShellCmd是否正常执行完毕*/ if(WIFEXITED(nRet)){ /*最后,判断pShellCmd命令的返回值是否正确,假设子进程调用exit(0)退出*/ if(WEXITSTATUS(nRet) != 0){ printf("pShellCmd 返回值错误。\n"); return -1; } } else{ printf("pShellCmd 命令未正常执行完毕。\n"); return -1; } } return 0; } int main() { char szShellCmd[64] = {0}; /*用于存放待执行的shell命令*/ sprintf(szShellCmd, "hello.sh"); /*将命令存入szShellCmd中*/ MySystem(szShellCmd); /*调用system函数执行"ls"命令*/ return 0; } |
编译、运行(其中hello.sh未赋执行权限):
1 2 3 4 5 6 |
[root@localhost system-function]# ./systemFun02 sh: ./hello.sh: Permission denied system返回值: 32256 WIFEXITED返回值: 1 WEXITSTATUS返回值:126 pShellCmd 返回值错误。 |
报错,返回值和报错信息一目了然。
接着,我们给hello.sh赋予执行权限,再运行:
1 2 3 4 5 6 |
[root@localhost system-function]# chmod 755 hello.sh [root@localhost system-function]# ./systemFun02 HELLO WORLD! system返回值: 0 WIFEXITED返回值: 1 WEXITSTATUS返回值:0 |
程序正常运行,返回值也是我们所期望的。不信?我们换个方法验证脚本的返回值:
1 2 3 4 |
[root@localhost system-function]# sh hello.sh HELLO WORLD! [root@localhost system-function]# echo $? 0 |
hello.sh的返回值确实是0。
接着我们再改一改,让hello.sh以12退出:
1 2 3 |
[root@localhost system-function]# cat hello.sh echo "HELLO WORLD!" exit 12 |
执行:
1 2 3 4 5 6 7 8 9 10 11 |
[root@localhost system-function]# ./systemFun02 HELLO WORLD! system返回值: 3072 WIFEXITED返回值: 1 WEXITSTATUS返回值:12 pShellCmd 返回值错误。 [root@localhost system-function]# sh hello.sh HELLO WORLD! [root@localhost system-function]# echo $? 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信号:
1 2 3 4 5 6 7 8 9 10 11 |
int main() { signal(SIGCHLD, SIG_IGN); /*忽略SIGCHLD信号*/ char szShellCmd[64] = {0}; /*用于存放待执行的shell命令*/ sprintf(szShellCmd, "hello.sh"); /*将命令存入szShellCmd中*/ MySystem(szShellCmd); /*调用system函数执行"ls"命令*/ return 0; } |
编译、运行:
1 2 3 4 5 6 |
[root@localhost system-function]# ./systemFun02 HELLO WORLD! system返回值: -1 WIFEXITED返回值: 0 WEXITSTATUS返回值:255 system 返回值错误。 |
虽然hello.sh正常输出了,但是返回值却是不正确的,这是不能容忍的。
为什么返回值会出错呢?这就是前面我们提过的带有信号处理版本的system函数之所以会阻塞SIGCHLD信号的重要原因了。
详细点的说是因为:system函数在fork()一个子进程去执行shell命令之后,在父进程中会调用waitpid()函数获取子进程的退出状态然后给子进程收尸(释放资源等等)。但是,我们上面的例子在main函数中进行了忽略SIGCHLD信号的操作,所以父进程势必将获取不到子进程的状态,导致程序异常。
那是不是只要不忽略SIGCHLD信号就可以了呢?如果上面这个例子不够说服你阻塞SIGCHLD信号的重要性,那么,我们接着往下谈:
2)情形二:
如果我们不是对SIGCHLD信号进行忽略,而是捕捉该信号并进行相应的处理:
1 2 3 4 5 6 7 8 9 |
void handle_sigchld(int sig) { /*捕获信号后做的一系列操作*/ ... /*释放子进程的资源*/ wait(NULL); } signal(SIGCHLD, handle_sigchld); |
现在基于这样一种假设:
我们已经知道父进程调用system函数之后,system函数内部会fork()一个子进程了。在子进程结束的时候会发送一个SIGCHLD信号给父进程,父进程收到该信号后进行信号处理函数。比如,调用上述的信号处理函数handle_sigchld(),该信号处理函数在做出相应操作之后调用wait()函数为子进程收尸。那么,问题来了,handle_sigchld()函数调用wait函数为子进程收尸了,system函数中的waitpid()函数就无法获取子进程的退出状态了==》进而导致system函数异常。
这是system函数需要阻塞SIGCHLD信号的另一个重要的原因。而system函数之所以还要忽略SIGINT和SIGQUIT信号也是为了避免出现类似的情况。
基于以上两点,在使用system函数时的正确方式是,对其进行封装,并且需要对信号进行简单的处理:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
int MySystem(const char *pShellCmd) { /*变量初始化*/ int nRet; /*参数有效性验证*/ if(NULL == pShellCmd) { printf("MySystem 参数无效。\n"); return -1; } /*先恢复SIGCHLD信号的默认处理方式*/ signal(SIGCHLD, SIG_DFL); /*调用system函数*/ nRet = system(pShellCmd); /*首先,判断system函数是否正常返回*/ if(-1 == nRet){ printf("system 返回值错误。\n"); return -1; } else{ /*然后,判断子进程是否正常退出,即pShellCmd是否正常执行完毕*/ if(WIFEXITED(nRet)){ /*最后,判断pShellCmd命令的返回值是否正确,假设子进程调用exit(0)退出*/ if(WEXITSTATUS(nRet) != 0){ printf("pShellCmd 返回值错误。\n"); return -1; } } else{ printf("pShellCmd 命令未正常执行完毕。\n"); return -1; } } /*恢复SIGCHLD信号到忽略状态*/ /*如果先前对SIGCHLD信号的处理方式不是忽略而是调用相应的处理函数, 那么在设置SIGCHLD为默认SIG_DFL前,需要保存其处理函数然后在此处恢复即可*/ signal(SIGHLD, SIG_IGN); return 0; } |
3.system函数注意事项
(1)调用system时,小到一个内部的shell命令,大到一个百行、千行的shell脚本。避免使用相对路径,而应尽量使用绝对路径。
(2)system通过调用/bin/sh来执行相应命令,但是sh对连字符(–)并不是很友好,所以,system函数执行的shell脚本中,不应该出现以–命名的变量名,比如有如下shell脚本:
1 2 3 4 5 6 7 |
[root@localhost system-function]# cat hello.sh function hello-world() { echo "HELLO WORLD!" } hello-world |
通过system函数调用该shell脚本:
1 2 3 4 5 6 |
[root@localhost system-function]# ./systemFun02 ./hello.sh: line 4: `hello-world': not a valid identifier system返回值: 512 WIFEXITED返回值: 1 WEXITSTATUS返回值:2 pShellCmd 返回值错误。 |
如上所述,system调用出错:hello-world是一个无效的标识符。
但是,诸如./hello.sh、. hello.sh、source hello.sh等命令是可以正常运行的:
1 2 3 4 5 6 |
[root@localhost system-function]# ./hello.sh HELLO WORLD! [root@localhost system-function]# source hello.sh HELLO WORLD! [root@localhost system-function]# . hello.sh HELLO WORLD! |
所以,应该避免用符号–命名shell脚本的变量。
4.小结
本文重点介绍了system函数的使用方法,返回值的错误检查方法,以及使用system函数的几点注意事项,希望大家在使用该函数的时候可以尽量避免一些错误。
5.附man system
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
NAME system - execute a shell command SYNOPSIS #include <stdlib.h> int system(const char *command); DESCRIPTION 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. RETURN VALUE The value returned is -1 on error (e.g. fork(2) failed), and the return status of the command otherwise. This latter return status is in the format specified in wait(2). Thus, the exit code of the command will be WEXIT- STATUS(status). In case /bin/sh could not be executed, the exit status will be that of a command that does exit(127). If the value of command is NULL, system() returns non-zero if the shell is available, and zero if not. system() does not affect the wait status of any other children. |
总结的很好,学习了
总结的很好,学习了