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 < xxx.txt”,无论xxx.txt文件中是什么内容,它都只会输出一个换行。这也说明,对echo语句进行输入重定向是没有意义的。因此,输入重定向只针对需要从标准输入流中获取数据的命令有意义;当然,输出重定向貌似对任何命令都有意义,因为它会将命令执行后本应该输出到屏幕上的结果,重定向到文件中。了解此之后,介绍输入重定向的处理方式:
    在将输入的字符串切割成小的字符串后,可以遍历输入字符串识别是否有“<”或“>”字符串的出现,这分别表示输入重定向和输出重定向。当遇到输入重定向时,我们的标准输入不再是键盘,而是重定向符后面的文件。为实现这一功能,我们可以利用linux下文件的管理方式的特点:每个进程维护一个文件描述符表,其中0,1,2分别表示stdin,stdout,stderr,并且使用open()函数在创建文件时,会自动返回文件描述符表中空余的最小的文件描述符。因此,输入重定向的解决方法便很简单了,首先close(0),然后open(“xxx.txt”),这样便将0号文件描述符分配给xxx.txt,之后再从标准输入中读取数据时便会从xxx.txt文件读取。同理,输出重定向也可以先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="temp.txt";//共享文件
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);
                    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);
			}
			execvp(argv[tt][0],argv[tt]);
		}
	}
	//father
	else {
		waitpid(pid,NULL,0);
	}
	return 1;
}
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();//创建子进程执行命令
	}
	return 0;
} 

反思

  1. Shell中的命令类型并不是完全相同的,有的命令只包括命令和参数,而对于有的命令,除命令和参数之外,还包括输入的文件或者其他内容。而能通过管道传递的都是输入的内容,并不是命令参数。
  2. 管道的本质依然是文件,只不过是多进程可以共享的文件,因此多进程可以通过这个文件来进行数据传递;同时每次只能一个进程往文件中写数据,而另一个进程从文件中读数据,因此管道是单方向的。
  3. 根据管道本质是共享文件,因此管道是基于文件操作实现的,那么可以直接跳过建立管道这一步,而直接通过共享文件来实现多管道的通信,在这过程中需要涉及到很多进程输入重定向在共享文件中,输出也重定向在共享文件中,为了及时清空共享文件内容,可在这两者之间加一个remove()函数,并且经过官方文档的查询,发现当文件处于open状态时,remove()函数并不会立刻执行,而是等到open结束之后。这样就避免了还没有从共享文件中读取数据文件便被删除的问题。
  • 9
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值