该任务将通过实现一个小的shell(我们称为143A shell)来教您如何使用Unix系统调用接口和shell。
您可以在任何支持Unix API的操作系统(Linux Openlab计算机,运行Linux或Linux VM的笔记本电脑,甚至MacOS等)上进行此分配。
我们将在以下部分中构建shell:1)读取和解析命令,2)执行程序,3)实现对I / O重定向的支持,以及4)实现对管道的支持。
正如我们将看到的,shell是一个实质上等待用户输入,执行命令并重复的程序。我们将通过仅调用一个函数sh_loop来无限期地循环,读取,解释和执行命令,从而使外壳保持简单。通常,shell会做更多的事情(与初始化,配置,终止,关闭等有关的步骤)。如果将以上代码片段放入文件sh.c中,则可以使用C编译器(例如gcc)对其进行编译。
Shell的主要工作是执行命令。一种分解方法是:
- 从标准输入读取命令。
- 通过将命令字符串分成程序字符串和参数字符串来解析命令字符串。
- 执行程序,并将适当的参数传递给它。
它在循环中运行,每次循环执行时都会向用户提供提示:#
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
int pid=-1; //当前的子进程
int exeNum=0; //当前等待执行的程序数目
char tmp[100]; //输入的字符串
typedef struct //存储一条指令
{
char name[100]; //目标文件
char infile[100]; //重定向输入文件名
char outfile[100]; //重定向输出文件名
char args[20][100]; //输入变量
int ein; //是否应该从文件重定向输入
int eout; //是否应该从文件重定向输入
int epipe; //是否重定向输出到管道
int ipipe; //是否从管道重定向输入
int argNum; //变量数目
} node;
void initNode(node *n){ //初始化结点
n->ein=0;n->eout=0;n->ipipe=0;
n->epipe=0;n->argNum=0;
}
typedef void (*sighandler_t) (int); //中断捕捉
void sigcat() {
if(pid >0 ) kill(pid, SIGKILL);//杀死子进程
}
int main(int argc,char *argv[]) {
signal(SIGINT, (sighandler_t)sigcat); //注册一个本进程处理中断信号的函数
printf("# ");
while(1){
node exe[20]; //初始化
exeNum=0;pid=-1;
int readName=1;
int echoHello=1;
//获取命令阶段
while(1) { //循环读取命令到数组中
if(scanf("%s", tmp)==EOF){ //输入结束,退出
return 0;
}
if(echoHello){ //输出提示符
printf("# ");
echoHello=0;
}
if (readName) { //首次读取
initNode(&exe[exeNum]);
strcpy(exe[exeNum].name, tmp); //读取执行文件名
strcpy(exe[exeNum].args[0], tmp); //复制存储参数
readName = 0;
if(exeNum>0){ //发生了管道
exe[exeNum].ipipe=1;
}
}
else if (strcmp(tmp, ">")==0) { //重定向输出
scanf("%s", exe[exeNum].outfile);
exe[exeNum].eout=1;
}
else if (strcmp(tmp, "<")==0) { //重定向输入
scanf("%s", exe[exeNum].infile);
exe[exeNum].ein=1;
}
else if (strcmp(tmp, "|")==0) { //遇到管道
exe[exeNum].epipe=1;
++exeNum; readName=1;
}
else {
strcpy(exe[exeNum].args[++(exe[exeNum].argNum)], tmp); //参数的存入
}
char ch;
ch = getchar();
if(ch=='\n') break;
}
//命令解析执行阶段
int pin[2];
int pout[2] ;
pipe(pin);
pipe(pout);
for(int id=0;id<=exeNum;id++){
pid=fork();
if(pid==0){
char* argList[20]={0}; //构建参数列表
for(int i=0;i<=exe[id].argNum;i++){
argList[i]=&exe[id].args[i][0];
}
// stdin、stdout、stderr ⽂件描述符分别为 0 、1、2
if(exe[id].ein){ //从文件重定向输入
int fd = open(exe[id].infile,O_RDONLY);
dup2(fd,0);
}
if(exe[id].eout){ //从文件重定向输出
int fd = open(exe[id].outfile,O_CREAT|O_WRONLY|O_TRUNC,0664);
dup2(fd,1);
}
if(exe[id].ipipe){ //从管道重定向输入
close(pin[1]); //关闭输入管道的输入端
dup2(pin[0],0); //替换掉标准输入
close(pin[0]); //关闭输入管道的输出端
}
if(exe[id].epipe){ //从管道重定向输出
dup2(pout[1],1); //替换掉标准输出
close(pout[1]); //关闭输出管道的输出端
}
execvp(exe[id].name, argList);//execvp() 会从 PATH 环境变量所指的⽬录中查找符合参数 file 的⽂件名,找到后便执⾏该⽂件,然后将第⼆个参数 argv 传给该欲执⾏的⽂件。如果执⾏成功则函数不会返回,执⾏失败则直接返回 -1,失败原因存于 errno 中。
perror("Fail to execute: ");
exit(EXIT_FAILURE);
}
else{ //父进程
close(pin[1]);
wait(NULL);
close(pout[1]);
pipe(pin);
char ch;
while(read(pout[0],&ch,1)){
write(pin[1],&ch,1);
}
pipe(pout);
}
}
}
}