system(cmd)的使用注意事项

本文深入解析system()函数的工作原理及其风险,包括返回值解释、执行过程和常见错误。同时,提供了一个简单的实现代码示例,并推荐使用popen()函数作为更安全的替代方案。
曾经的曾经,被system()函数折磨过,之所以这样,是因为对system()函数了解不够深入。只是简单的知道用这个函数执行一个系统命令,这远远不够,它的返回值、它所执行命令的返回值以及命令执行失败原因如何定位,这才是重点。当初因为这个函数风险较多,故抛弃不用,改用其他的方法。这里先不说我用了什么方法,这里必须要搞懂system()函数,因为还是有很多人用了system()函数,有时你不得不面对它。

先来看一下system()函数的简单介绍:
?
1
2
#include <stdlib.h>
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 -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 WEXITSTATUS(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 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.

看一下system()函数的源码
看完这些,我想肯定有人对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
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时。

看一下该怎么监控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
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被信号暂停执行,取得信号值
}

到于取得子进程返回值的相关介绍可以参考另一篇文章: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

<think>我们正在分析`system(cmd)`函数,这是一个在C/C++中用于执行shell命令的标准库函数。根据引用[1]和引用[2],我们可以详细分析该函数的用法、工作原理以及注意事项。 ## 1. `system`函数的功能和原型 `system`函数用于执行一个shell命令。其原型定义在`<stdlib.h>`头文件中: ```c int system(const char *command); ``` - **参数**:`command` 是要执行的shell命令字符串。 - **返回值**:返回命令执行后的状态。返回值比较复杂,通常: - 如果命令成功执行且正常退出,返回命令的退出状态(通常是0表示成功)。 - 如果`command`为NULL,则返回非零值(表示系统中存在shell可用)。 - 如果无法启动shell,则返回错误码(通常为127)。 - 如果出现其他错误,返回-1。 ## 2. `system`函数的工作原理 在Linux/Unix系统中,`system`函数通过以下步骤执行命令(参考引用[2]): 1. 调用`fork`创建一个子进程。 2. 在子进程中调用`execl`来启动`/bin/sh`,并将命令字符串作为参数传递给shell。 3. 父进程调用`wait`(或`waitpid`)等待子进程结束。 4. 父进程获取子进程的退出状态,并返回。 在Windows系统中,`system`函数会调用命令解释器(通常为`cmd.exe`)来执行命令。 ## 3. 使用示例 下面的C程序使用`system`函数列出当前目录下的文件: ```c #include <stdlib.h> int main() { int ret = system("ls -l"); if (ret == -1) { // 处理错误 } return 0; } ``` ## 4. 返回值处理 由于`system`的返回值是命令的退出状态,我们需要使用宏来解析返回值: - 使用`WIFEXITED(status)`判断命令是否正常退出。 - 使用`WEXITSTATUS(status)`获取命令的退出码。 例如: ```c #include <sys/wait.h> #include <stdlib.h> int main() { int status = system("ls -l"); if (status == -1) { // 执行失败(例如无法创建子进程) } else if (WIFEXITED(status)) { int exit_status = WEXITSTATUS(status); // 根据退出状态处理 } else { // 命令未正常退出(例如被信号终止) } return 0; } ``` ## 5. 安全性和性能注意事项 1. **安全性问题**:`system`函数存在安全风险,特别是当命令字符串来自用户输入时,可能导致命令注入攻击(Command Injection)。例如: ```c system("echo " + user_input); // 如果user_input是`; rm -rf /`,则可能执行恶意命令 ``` 因此,应避免使用用户输入直接构造命令,或者对用户输入进行严格的过滤和转义。 2. **性能开销**:每次调用`system`都会创建新的进程(shell和命令进程),在频繁调用的场景下(如循环中),会导致较大的性能开销。 3. **依赖环境**:`system`函数依赖于系统的shell环境(如Linux的`/bin/sh`,Windows的`cmd.exe`),不同系统上的行为可能不同。例如,在Windows上路径分隔符为反斜杠`\`,而在Linux上为斜杠`/`。 ## 6. 替代方案 为了避免安全风险和性能问题,可以考虑以下替代方案: - 使用`exec`系列函数(如`execl`, `execvp`)直接执行命令,避免使用shell解释器(这样可以防止命令注入,但需要自己处理参数分割)。 - 使用进程间通信(IPC)机制(如管道、套接字)来捕获命令的输出并处理输入。 - 使用标准库或第三方库(如`popen`)来执行命令并读取输出(但要注意`popen`也存在类似的安全问题)。 ## 7. 常见问题 - **命令中的空格和特殊字符**:在构造命令字符串时,需要确保参数中的空格和特殊字符(如`$`, `&`, `>`等)被正确转义。例如,文件名包含空格时,应该用引号括起来:`system("rm -f \"file with space.txt\"")`。 - **路径问题**:使用相对路径时,当前工作目录可能变化,建议使用绝对路径或者先设置工作目录。 ## 结论 `system`函数是一个简单但存在安全隐患的系统调用,适用于执行简单的、非频繁的shell命令。在需要处理用户输入或性能敏感的场合,应避免使用,转而使用更安全、高效的方法。 §§相关问题§§ 1. 如何在C语言中安全地执行外部命令并获取输出? 2. `system`函数和`exec`系列函数有什么区别? 3. 在多线程环境中使用`system`函数需要注意哪些问题? [^1]: [system()系统函数在编程过程中经常被调用...](引用1的URL) [^2]: [system函数的详细使用...](引用2的URL) </think>## 详细分析 `system(cmd)` 函数 `system(cmd)` 是 C/C++ 标准库中用于执行 shell 命令的函数,其功能和行为在不同操作系统上有显著差异。下面从多个维度进行详细分析: ### 1. 函数原型与基本用法 ```c #include <stdlib.h> int system(const char *command); ``` - **参数**:`command` - 要执行的 shell 命令字符串 - **返回值**: - 成功时返回命令的退出状态码 - 命令执行失败返回 `-1` - 在 Linux 中,若 `command` 为 `NULL` 则返回非零值(表示 shell 可用) ### 2. 工作原理(跨平台差异) #### Linux/Unix 系统 [^2] ```mermaid graph LR A[调用system函数] --> B[fork创建子进程] B --> C[子进程执行/bin/sh -c command] C --> D[执行shell命令] D --> E[退出状态传递] E --> F[父进程wait收集状态] ``` - 通过 `fork()` 创建子进程 - 子进程调用 `exec` 执行 `/bin/sh -c "command"` - 父进程调用 `wait()` 等待子进程结束 #### Windows 系统 - 直接调用 `cmd.exe /C "command"` - 没有进程复制开销 - 命令语法遵循 Windows CMD 规则 ### 3. 返回值解析 返回值由三部分组成(Linux示例): $$ \text{返回值} = (\text{退出状态} \times 256) + \text{终止信号} $$ | 场景 | 返回值示例 | 解析方法 | |-----------------------|------------|----------------------| | 命令成功退出 | 0 | `WEXITSTATUS(status)`| | 命令被信号终止 | 32512 | `WTERMSIG(status)` | | 无法启动shell | -1 | 检查 `errno` | | 参数为 `NULL` | ≠0 | 仅检测shell可用性 | ### 4. 安全风险与缺陷 1. **命令注入漏洞**(高危): ```c char input[100]; scanf("%s", input); system(input); // 若输入"; rm -rf /" 将导致灾难 ``` 2. **环境依赖问题**: - 路径解析依赖 `PATH` 环境变量 - 用户权限继承当前进程上下文 - 工作目录可能被意外修改 3. **性能开销**(Linux): - 每次调用需创建 3 个新进程(`fork()` + `exec()` + `sh`) - 不适合高频调用场景(如循环执行) ### 5. 最佳实践建议 1. **替换方案**: ```c // 使用 exec 系列函数避免 shell 解析 pid_t pid = fork(); if (pid == 0) { execl("/bin/ls", "ls", "-l", NULL); exit(EXIT_FAILURE); } waitpid(pid, &status, 0); ``` 2. **输入验证**: ```c // 白名单过滤命令参数 if (!is_valid_input(cmd_param)) { return ERROR; } snprintf(full_cmd, "safe_command %s", sanitized_param); ``` 3. **返回值处理**: ```c int status = system("ls -l"); if (status == -1) { perror("system failed"); } else if (WIFEXITED(status)) { printf("退出码:%d\n", WEXITSTATUS(status)); } ``` ### 6. 特殊场景注意事项 1. **信号处理**: - `system()` 会临时屏蔽 `SIGCHLD` 信号 - `SIGINT` 和 `SIGQUIT` 会被忽略 - 需在调用后恢复原有信号处理程序 2. **多线程环境**: - 非原子操作,需加锁避免资源竞争 - 子进程继承父进程内存快照(可能含脏数据) 3. **资源限制**: - 子进程继承打开的文件描述符 - 可能导致文件描述符泄漏(需提前关闭)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值