1. 如何在程序中执行 Shell 命令
1.1 背景与目标
- 在
bash环境下,可以直接运行命令(如ls,cd)或程序。 - 目标:在自己的 C 语言程序中,实现类似
bash的功能,即执行任意的shell命令。
1.2 system 函数接口
- 功能:执行一条
shell命令。 - 头文件:
stdlib.h。 - 基本用法:
- 类似于在
bash中操作,通过printf打印命令提示符。 - 获取用户输入数据。
- 将获取到的字符串传递给
system函数执行。
- 类似于在
1.3 输入获取方式的选择
scanf的问题:scanf遇到空格会停止读取,认为空格是分隔符。- 例如输入
cat system.c,scanf可能只读取cat,导致命令执行不完整。
- 解决方案
fgets:- 函数原型:
fgets(buffer, size, stdin)。 - 参数说明:
- 缓冲区地址。
- 大小(如 1024)。
- 文件流(标准输入
stdin)。
- 优势:可以读取包含空格的一整行命令。
- 函数原型:
1.4 初步实现逻辑
- 进入死循环(
while(1)),持续等待命令。 - 打印提示符(
printf)。 - 使用
fgets获取用户输入到buffer。 - 调用
system(buffer)执行命令。 - 注意:
system执行完毕后通常会刷新缓冲区,无需手动fflush。
1.5 命令执行效果测试
ls:正常显示文件列表。cd:- 在
bash中cd会切换当前目录。 - 在
system调用的子shell中,cd只会改变子进程目录,父进程(当前程序)目录不变。
- 在
cat system.c:- 若使用
scanf,可能无法正确显示文件内容。 - 若使用
fgets,可以正确显示文件内容。
- 若使用
- 进程终止测试:
- 按下
Ctrl+C终止的是当前执行的命令(如cat),而不是主程序。 - 再次按下
Ctrl+C才会终止主程序。
- 按下
2. system 函数的底层逻辑与安全风险
2.1 底层执行机制
- 进程创建:
system函数底层会启动一个/bin/sh进程。 - 验证方法:
- 运行包含
system("sleep 20")的程序。 - 在另一个终端使用
ps -ef | grep sleep查看进程。 - 观察到进程树:主进程 ->
sh -c sleep 20->sleep 20。
- 运行包含
- 结论:
system实际上是 fork 了一个子进程,然后在子进程中运行shell解释器来执行命令。
2.2 安全风险(命令注入)
- 风险来源:
system支持shell语法,包括命令连接符(如;,|,&)。 - 示例代码:
char cmd[1024]; // 假设用户输入文件名 fgets(cmd, sizeof(cmd), stdin); // 拼接命令 char final_cmd[1024]; sprintf(final_cmd, "cat %s", cmd); system(final_cmd); - 攻击场景:
- 正常输入:
system.c-> 执行cat system.c。 - 恶意输入:
system.c; rm -rf /*。 - 实际执行:
cat system.c; rm -rf /*。 - 后果:不仅显示了文件,还执行了删除命令,造成严重破坏。
- 正常输入:
- 结论:
system函数存在命令注入风险,不适合用于执行不可信输入。若需执行特定程序,应使用exec函数族。
3. exec 函数族详解
3.1 exec 函数族概述
- 目的:替换当前进程映像为一个新的程序。
- 特点:
- 直接执行指定程序,不经过
shell解析,安全性更高。 - 不支持
shell语法(如通配符、管道),只能执行单一程序。 - 一旦执行成功,当前进程代码被替换,后续代码不再执行(除非出错)。
- 直接执行指定程序,不经过
- 头文件:
unistd.h。
3.2 函数命名规则与参数差异
exec 族函数通过后缀字母区分参数传递方式和特性:
l(list):- 参数逐个列出,以
NULL结尾。 - 示例:
execl(path, arg0, arg1, ..., NULL)。
- 参数逐个列出,以
v(vector):- 参数通过字符串数组传递。
- 示例:
execv(path, argv[])。
p(path):- 自动在
PATH环境变量中查找可执行文件。 - 参数只需提供文件名,无需完整路径。
- 示例:
execlp("ls", "ls", "-l", NULL)。
- 自动在
e(environment):- 允许显式传递环境变量数组。
- 示例:
execle(path, arg0, ..., NULL, envp[])。

3.3 参数细节
- 参数列表:
- 第一个参数通常是程序路径(
p后缀除外)。 - 第二个参数通常是程序名(约定俗成,对应
argv[0])。 - 后续为实际参数。
- 最后必须以
NULL(空指针)结尾。
- 第一个参数通常是程序路径(
- 环境变量
envp:- 格式:字符串数组,每个元素为
KEY=VALUE。 - 例如:
"USER=linux", "SHELL=/bin/bash", NULL。 - 若使用带
e的函数,新进程将使用指定的环境变量,不再继承父进程环境变量。
- 格式:字符串数组,每个元素为
3.4 返回值与执行流
- 成功:不返回,当前进程被新程序完全替换(进程 ID 不变,代码段、数据段等变为新程序)。
- 失败:返回
-1,并设置errno。 - 代码逻辑:
exec(...); perror("exec failed"); // 只有执行失败才会运行到这行
3.5 环境变量继承
- 默认行为:
exec族函数(不带e)默认继承父进程的环境变量。 - 进程关系:
- 父进程(如
bash)创建子进程。 - 子进程继承父进程的环境变量(如
PATH,USER,SHELL)。 - 使用
exec替换后,新进程依然保留这些继承的环境变量。
- 父进程(如
- 查看环境变量:
- 命令:
printenv或env。 - 代码中可通过
extern char **environ;访问。
- 命令:
3.6 示例代码对比
-
execl:execl("/bin/pwd", "pwd", NULL);需指定完整路径
/bin/pwd。 -
execlp:execlp("ls", "ls", "-l", NULL);无需指定路径,自动查找
PATH。 -
execv:char *args[] = {"ls", "-l", NULL}; execv("/bin/ls", args);使用数组传递参数。
4. 综合案例:自定义实现 system 函数
4.1 实现思路
要模仿标准库的 system 函数,需要组合使用多进程编程的三个核心接口:
fork:创建子进程。exec:在子进程中替换映像,执行shell命令。wait/waitpid:父进程等待子进程结束,并获取退出状态。
4.2 逻辑步骤
- 定义函数:
int my_system(const char *cmd)。 - 创建子进程:
- 调用
fork()。 - 若返回
-1,创建失败,打印错误并返回。 - 若返回
0,进入子进程逻辑。 - 若返回
>0,进入父进程逻辑。
- 调用
- 子进程逻辑:
- 调用
execl("/bin/sh", "sh", "-c", cmd, NULL)。 - 解释:启动
sh,使用-c参数执行传入的字符串命令。 - 若
execl返回,说明执行失败,调用perror并退出子进程。
- 调用
- 父进程逻辑:
- 调用
waitpid()等待子进程结束。 - 获取子进程的退出状态。
- 处理状态码(正常退出或信号终止)。
- 将状态返回给调用者。
- 调用
4.3 代码实现细节
- 头文件依赖:
stdlib.h(system, exit)stdio.h(printf, fgets)unistd.h(fork, execl)sys/types.h,sys/wait.h(waitpid, status macros)
- 状态处理宏:
WIFEXITED(status):判断是否正常退出。WEXITSTATUS(status):获取退出码。WIFSIGNALED(status):判断是否被信号终止。WTERMSIG(status):获取终止信号号。
4.4 验证测试
- 编译代码:
gcc my_system.c -o my_system。 - 运行测试:
- 输入
ls:应显示文件列表。 - 输入
pwd:应显示当前路径。 - 输入复杂命令:验证
sh -c是否正确解析。
- 输入
- 结果:自定义的
my_system功能与标准system基本一致。
4.5 注意事项
- 头文件包含:某些类型定义(如
pid_t)可能隐含在其他头文件中,但建议显式包含sys/types.h以确保兼容性。 - 错误处理:
fork或exec失败时需谨慎处理,避免进程状态异常。 - 返回值:
waitpid的返回值和状态码需正确传递,以便上层调用者判断命令执行结果。
5. 总结
system函数:方便但存在安全风险(命令注入),底层通过sh -c执行。exec函数族:更安全,直接执行程序,支持多种参数传递方式(l,v,p,e)。- 进程替换:
exec成功则不返回,当前进程映像被替换。 - 环境变量:默认继承父进程,可通过
exec...e系列函数自定义。 - 组合应用:通过
fork+exec+wait可以实现灵活的进程控制和命令执行逻辑,是理解 Linux 多进程编程的核心案例。