内容回顾
上一节我们探讨了父子进程间通过匿名管道进行通信的原理。管道作为特殊的内存级文件,仅支持单向读写。为避免数据混乱,通常由一个进程写入,另一个进程读取,双方需正确关闭不用的读写端。
Linux 的匿名管道指令
Linux 中常见的 cmd1 | cmd2 语法本质就是匿名管道。Shell 解释器通过 fork 创建子进程,并在 fork 前调用 pipe 创建管道。子进程继承文件描述表后,标准输出被重定向至管道写端,标准输入被重定向至读端,从而实现进程间的数据流传递。
完善 Shell 外壳程序
整体框架
实现支持管道的 Shell 主要包含四个模块:获取用户输入、解析字符串、判断指令类型、执行指令。
1. 获取用户输入的字符串
由于输入可能包含空格和管道符,scanf 会在空格处截断,因此必须使用 fgets 读取整行。
char temp[MAX_SIZE];
if(fgets(temp,sizeof(temp),stdin)==NULL){
perror("fgets");
continue;
}
2. 解析字符串
我们需要统计基本指令个数,并分割指令部分、参数部分以及重定向信息。为此定义了一个 order 类来封装指令属性。
class order{
public:
char* _argv[MAX_SIZE];
int _argc=0;
int _check_redir=0;
char* filename=nullptr;
};
解析流程分为两步:先用 strtok 以 | 分割出所有基本指令存入临时数组,再逐个解析每个指令的参数。这里有一个关键陷阱:不能边分割边解析。因为 strtok 内部维护静态指针,若在 pipe_string 中调用 strtok 后,立即在 get_string 中再次调用 strtok,会覆盖之前的指针位置,导致后续指令丢失。
int pipe_string(std::vector<order>& order_array,char temp[]){
int cmdnum=0;
char* tmp[MAX_SIZE];
char* token=strtok(temp,"|");
while(token!=NULL&&cmdnum<MAX_SIZE){
tmp[cmdnum]=token;
cmdnum++;
token=strtok(NULL,"|");
}
for(int i=0;i<cmdnum;i++){
get_string(order_array[i],tmp[i]);
}
return cmdnum;
}
在 get_string 中,以空格分割参数,并检查末尾是否有重定向符号(>, >>, <)。注意重定向符号及其后的文件名不属于有效指令参数,需置为 NULL,但需先保存文件名到 filename 成员。
3. 判断指令个数
若指令数为 0,提示重新输入;若为 1,需判断是否为内置指令(如 cd, pwd)。内置指令修改的是当前 Shell 环境(如工作目录),必须在父进程中执行,否则子进程的修改对父进程无效。
const char* in_cmd[]={"cd","pwd",NULL};
bool check_order(order& a){
for(int i=0;in_cmd[i]!=NULL;i++){
if(strcmp(a._argv[0],in_cmd[i])==0){return true;}
}
return false;
}
4. 指令的执行
当指令数大于 1 时,涉及管道通信。需创建 n-1 个管道,并启动 n 个子进程。
- 起始进程:只写管道,不读。
- 结束进程:只读管道,不写。
- 中间进程:读上一个管道的读端,写下一个管道的写端。
- 重定向优先级:文件重定向优先级高于管道。例如
cmd1 >> log.txt | cmd2,cmd1的输出应写入log.txt而非管道。
父进程负责关闭所有未使用的管道端,并等待子进程结束。
void pipe_order_complete(int cmdnum, std::vector<order>& order_array, std::vector<_pipe>& pipe_array){
// 1. 创建所有管道
for(int i =0; i < cmdnum -1; i++){
int n=pipe(pipe_array[i].pipe_fd);
if(n<0){perror("pipe");exit(EXIT_FAILURE);}
}
// 2. 创建子进程处理每个命令
for(int i =0; i < cmdnum; i++){
int pid =fork();
if(pid <0){perror("fork");exit(EXIT_FAILURE);}
if(pid ==0){
// 子进程逻辑:处理重定向或管道连接
execvp(order_array[i]._argv[0], order_array[i]._argv);
}
}
// 3. 父进程关闭管道并等待
for(int i =0; i < cmdnum -1; i++){
close(pipe_array[i].pipe_fd[0]);
close(pipe_array[i].pipe_fd[1]);
}
for(int i =0; i < cmdnum; i++){
wait(NULL);
}
}
完整实现
主要头文件 myshell.h 声明了数据结构与函数接口。myshell.cpp 中实现了主循环,不断读取用户输入并分发处理。
结语
通过上述步骤,我们实现了一个支持管道和基本重定向的简易 Shell。这加深了对进程间通信、文件描述符管理及系统调用的理解。实际开发中还需考虑更多边界情况,如信号处理、异步 IO 等。

