c语⾔实现shell,以及多级管道的实现
思想
Shell简介
Shell是系统的⽤户界⾯,提供⽤户与内核进⾏交互操作的⼀种接⼝,它接收⽤户输⼊的命令并把它送⼊内核去执⾏。⽽这个“送”的实现就是通过系统调⽤execvp()函数。
实际上Shell是⼀个命令解释器,它解释由⽤户输⼊的命令并且把它们送到内核。这些命令包
括“echo”,“ls”,“wc”,“grep”,“cat”…以及⼀些重定向和管道的格式。可以发现有的命令必须输⼊⼀些选项,⽐如wc、cat、grep,必须输⼊对应⽂件,⽽有的命令却不需要从输⼊流获取,⽐如echo和ls等。
Shell解释的过程可以⼤致分为读取命令,解析命令和执⾏命令三个步骤。本次的实验也按照这三个步骤进⾏。
shell设计
1. 基本命令
⽤c语⾔实现在linux系统下的简单shell,⾸先需要想到shell是需要不断对输⼊命令进⾏分析,因此作为⼀个主进程是不能退出的,⽽同时我们还需要不断地调⽤execvp()去执⾏命令。为实现这个⽬的,可以利⽤fork()语句创建⼦进程,让⼦进程去调⽤execvp()系统调⽤执⾏命令,这样⽗进程就可以根据输⼊的命令不断的创建⼦进程,同时⽗进程不必退出,实现shell的功能。当然,在这之前,我们需要进⾏预处理。这是因为我们输⼊的是⼀个字符串,需要对字符串按照空格进⾏切割,划分成⼀个个⼩的字符串,以此作为命令参数,然后执⾏。
2. 重定向
以上过程只考虑到了最简单的没有重定向也没有管道时的情况,接下来进⼀步考虑有重定向时的解决⽅法。在这之前,⾸先了解为什么要重定向,⽐如在linux系统中,对于echo语句,功能就是输出参数内容,并不需要从标准输⼊流中读取数据,因此如果⼀条语句是“echo < ”,⽆论⽂件中是什么内容,它都只会输出⼀个换⾏。这也说明,对echo语句进⾏输⼊重定向是没有意义的。因此,输⼊重定向只针对需要从标准输⼊流中获取数据的命令有意义;当然,输出重定向貌似对任何命令都有意义,因为它会将命令执⾏后本应该输出到屏幕上的结果,重定向到⽂件中。了解此之后,介绍输⼊重定向的处理⽅式:
在将输⼊的字符串切割成⼩的字符串后,可以遍历输⼊字符串识别是否有“<”或“>”字符串的出现,这分
别表⽰输⼊重定向和输出重定向。当遇到输⼊重定向时,我们的标准输⼊不再是键盘,⽽是重定向符后⾯的⽂件。为实现这⼀功能,我们可以利⽤linux下⽂件的管理⽅式的特点:每个进程维护⼀个⽂件描述符表,其中0,1,2分别表⽰stdin,stdout,stderr,并且使⽤open()函数在创建⽂件时,会⾃动返回⽂件描述符表中空余的最⼩的⽂件描述符。因此,输⼊重定向的解决⽅法便很简单了,⾸先close(0),然后open(“”),这样便将0号⽂件描述符分配给,之后再从标准输⼊中读取数据时便会从⽂件读取。同理,输出重定向也可以先close(1),然后打开对应的⽂件。这样就解决了重定向问题。
3. 单个管道
尽管有输⼊输出重定向,但本质上上⾯内容都只执⾏⼀条命令,假如想要实现“查进程名字对应的进程信息”的功能,通常⽤命令“ps -ef | grep xxx”来实现,这⾥使⽤ps和grep组合命令的使⽤来实现这个功能。过程就是先⽤ps命令列出当前所有的进程的进程信息,然后⽤grep命令查这些进程中包含关键词“xxx”的进程。中间的竖线就是管道,这也是shell中的⼀个很重要的功能。通俗来讲,管道就是将前⼀个命令的输出作为后⼀个命令的输⼊;具体来讲,管道是linux中进程之间的⼀种通信机制,其思想是在内存中创建⼀个共享⽂件,从⽽使通信双⽅利⽤这个共享⽂件来传递信息。管道有匿名管道和有名管道两种,其中匿名管道主要⽤于⽗⼦进程之间通信。这⾥我们利⽤匿名管道,将两个命令作为⼦进程和⽗进程来实现通信(暂时只考虑⼀个管道的情况)。由于我们之前已经创建了⼀个⼦进程,
利⽤其来执⾏输⼊的命令,此时可以在这个进程(记为1)中再fork⼀个⼦进程2,让⼦进程2执⾏管道前的命令,⽽⽗进程1执⾏管道后的命令,这样处理的原因是⽗进程1必须先等待⼦进程2执⾏结束以后,才可以从管道中获取数据,并执⾏命令。那么,问题来了,如何将管道前的命令的执⾏结果输出到管道中呢?这⾥依然使⽤重定向的思想,因为管道的本质就是⽂件,因此将⼦进程2的输出重定向到管道中即可,只不过管道的绑定函数不是像⽂件打开那样,⽽是利⽤dup(pipe[1])系统调⽤,将管道写端绑定在空余的最⼩的⽂件描述符,当然,在此之前,需要先close(1)。同理,⽗进程1在利⽤waitpid()系统调⽤等待⼦进程以后,也可以通过dup(pipe[0])系统调⽤,将标准输⼊重定向在管道读端上,然后再执⾏对应的命令。在此过程中,需要判断是否有⽆重定向符号,有的话则需要进⾏输⼊输出重定向。
4. 多级管道
上述内容只能解决单管道问题,当我们需要解决依次执⾏多条命令的问题时,就需要⽤到多级管道,即command1 | command2 | command3 | … | commandN的形式。⼀个有效的做法是递归实现,即先fork递归到最底层执⾏command1,然后依次出栈,执⾏command2…直到⽗进程执⾏commandN。
但此次实验中并没有使⽤这个⽅法,⽽是利⽤管道属于“共享⽂件”的本质,选择⼀个更简单的⽅法:⽗进程维护⼀个共享⽂件,⽤⽗进程去创建⼦进程1,将⼦进程的输出重定向到共享⽂件中;⽗进程回收
进程1后,fork下⼀个⼦进程2,并将此进程的输⼊重定向到该共享⽂件,从⽂件中读取数据后,清空共享⽂件,并将该进程的输出重定向到共享⽂件;⽗进程回收⼦进程2…直到最终⽗进程回收⼦进程N-1,然后输⼊重定向到共享⽂件,并执⾏最后⼀条命令N。实现共享⽂件的⽅法也很简单,由于进程1,2…N-1都属于⽗进程创建的⼦进程,会继承⽗进程的全局变量,因此将⽂件名的字符串表⽰定义为全局变量即可。
这样,就实现了多级管道。
5. 键盘中断信号的处理
在实际的shell中,如果某个命令正在执⾏,那么可以通过ctrl-c发送SIGINT信号中断该进程,但是shell本⾝并不会退出。所以,我们实现的shell也必须满⾜这个条件。由于我们的命令都是通过创建⼦进程执⾏的,因此收到SIGINT时主进程应该依然存在,⽽⼦进程退出。如果根据默认的信号处理函数,收到SIGINT信号时主进程,即shell会退出,显然不满⾜我们的要求。因此,需要注册⼀个信号处理函数,在接收到SIGINT信号时,需要给当前的⼦进程发送⼀个kill。为此,只需要将pid定义为全局变量,并且满⾜pid=fork()即可。在收到中断信号后给这个pid发送kill,由于⽗进程的返回值是⼦进程的进程号,因此便可以杀掉⼦进程。使shell当前运⾏的命令停⽌。
代码
#include<stdio.h>
//close
#include<unistd.h>
//open
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<stdlib.h>
//#include <pthread.h>
#include<wait.h>
#include<string.h>
#include<signal.h>
int arg=0;//命令个数
char buf[1024];//读⼊字符串
char*command[100];//切分后的字符串数组
int pid;//设为全局变量,⽅便获得⼦进程的进程号
char*f="";//共享⽂件
void sigcat(){
kill(pid,SIGINT);
}
//read
void read_command(){
char*temp=strtok(buf," ");
int i=0;
while(temp){
command[i++]=temp;
temp=strtok(NULL," ");
}
arg=i;
command[i]=0;//命令形式的字符串数组最后⼀位必须是NULL
}
int flag[100][2];//管道的输⼊输出重定向标记
char*file[100][2]={0};//对应两个重定向的⽂件
char*argv[100][100];//参数
int ar=0;//管道个数
//解析命令
void analazy_command(){
ar=0;
for(int i=0;i<100;i++){
flag[i][0]=flag[i][1]=0;
file[i][0]=file[i][1]=0;
for(int j=0;j<100;j++){
argv[i][j]=0;
}
}
for(int i=0;i<arg;i++) argv[0][i]=command[i];//初始化第⼀个参数
argv[0][arg]=NULL;
int a=0;//当前命令参数的序号
for(int i=0;i<arg;i++){
//判断是否存在管道
if(strcmp(command[i],"|")==0){//c语⾔中字符串⽐较只能⽤strcmp函数//printf("遇到 | 符号\n");
argv[ar][a++]=NULL;
ar++;
a=0;
}
else if(strcmp(command[i],"<")==0){//存在输⼊重定向
flag[ar][0]=1;
file[ar][0]=command[i+1];
argv[ar][a++]=NULL;
}
else if(strcmp(command[i],">")==0){//没有管道时的输出重定向
flag[ar][1]=1;
file[ar][1]=command[i+1];
argv[ar][a++]=NULL;//考虑有咩有输⼊重定向的情况
}
else argv[ar][a++]=command[i];
}
}
//创建⼦进程,执⾏命令
int do_command(){
//printf("seccesee||\n");
pid=fork();//创建的⼦进程
if(pid<0){
perror("fork error\n");
exit(0);
}
/
/先判断是否存在管道,如果有管道,则需要⽤多个命令参数,并且创建新的⼦进程。否则⼀个命令即可else if(pid==0){
if(!ar){//没有管道
if(flag[0][0]){//判断有⽆输⼊重定向
close(0);
int fd=open(file[0][0],O_RDONLY);
}
if(flag[0][1]){//判断有⽆输出重定向
close(1);
int fd2=open(file[0][1],O_WRONLY|O_CREAT|O_TRUNC,0666);
}
execvp(argv[0][0],argv[0]);
}
else{//有管道
int tt;//记录当前遍历到第⼏个命令
for(tt=0;tt<ar;tt++){
int pid2=fork();
if(pid2<0){
perror("fork error\n");
exit(0);
}
else if(pid2==0){
if(tt){//如果不是第⼀个命令,则需要从共享⽂件读取数据
close(0);
int fd=open(f,O_RDONLY);//输⼊重定向
}
if(flag[tt][0]){
close(0);
int fd=open(file[tt][0],O_RDONLY);
}
if(flag[tt][1]){
close(1);
int fd=open(file[tt][1],O_WRONLY|O_CREAT|O_TRUNC,0666);
}
close(1);
c语言如何创建字符串数组remove(f);//由于当前f⽂件正在open中,会等到解引⽤后才删除⽂件
int fd=open(f,O_WRONLY|O_CREAT|O_TRUNC,0666);
if(execvp(argv[tt][0],argv[tt])==-1){
perror("execvp error!\n");
exit(0);
}
}
else{//管道后的命令需要使⽤管道前命令的结果,因此需要等待
waitpid(pid2,NULL,0);
}
}
//接下来需要执⾏管道的最后⼀条命令
close(0);
int fd=open(f,O_RDONLY);//输⼊重定向
if(flag[tt][1]){
close(1);
int fd=open(file[tt][1],O_WRONLY|O_CREAT|O_TRUNC,0666);
int fd=open(file[tt][1],O_WRONLY|O_CREAT|O_TRUNC,0666);
}
execvp(argv[tt][0],argv[tt]);
}
}
//father
else{
waitpid(pid,NULL,0);
}
return1;
}
int main(int argc,char*argv[]){
signal(SIGINT,&sigcat);//注册信号响应函数
while(gets(buf)){//读⼊⼀⾏字符串
//初始化
for(int i=0;i<100;i++) command[i]=0;
arg=0;//初始化参数个数
read_command();//将字符串拆分成命令形式的字符串数组
analazy_command();//分析字符串数组中的各种格式,如管道或者重定向
do_command();//创建⼦进程执⾏命令
}
return0;
}
反思
1. Shell中的命令类型并不是完全相同的,有的命令只包括命令和参数,⽽对于有的命令,除命令和参数之外,还包括输⼊的⽂件或者其
他内容。⽽能通过管道传递的都是输⼊的内容,并不是命令参数。
2. 管道的本质依然是⽂件,只不过是多进程可以共享的⽂件,因此多进程可以通过这个⽂件来进⾏数据传递;同时每次只能⼀个进程往
⽂件中写数据,⽽另⼀个进程从⽂件中读数据,因此管道是单⽅向的。
3. 根据管道本质是共享⽂件,因此管道是基于⽂件操作实现的,那么可以直接跳过建⽴管道这⼀步,⽽直接通过共享⽂件来实现多管道
的通信,在这过程中需要涉及到很多进程输⼊重定向在共享⽂件中,输出也重定向在共享⽂件中,为了及时清空共享⽂件内容,可在这两者之间加⼀个remove()函数,并且经过官⽅⽂档的查询,发现当⽂件处于open状态时,remove()函数并不会⽴刻执⾏,⽽是等到open结束之后。这样就避免了还没有从共享⽂件中读取数据⽂件便被删除的问题。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。