操作系统课程设计——Shell编程(用c编写一个Linux的外壳Shell)

前言

最近操作系统结课了,不过还有一个课程设计需要完成。可供选择的题目并不少:Shell编程、系统调用、文件系统等等。当时觉得Shell编程看起来很有挑战性,实现后应该会很炫酷,而且课程设计指导上对Shell编程介绍的篇幅也是最大的。老师或许会给这个课题最高的分数? 于是没有太多犹豫就确定了下来。
本以为自己写一个Shell不简单,等到多看了几篇博客理解了其中原理后,才发现要实现一个简单的Shell其实并不困难。它就是对之前操作系统实验的综合运用。
关于如何用c来写一个Shell,网上已经有很多很棒的博客啦。比如山城过雨这位大神写的文章——《myShell:Linux Shell 的简单实现》,写的非常详细,只需要认真阅读一遍就能对整体流程了解地差不多了。我写这篇博客,不会去过多描述相同的内容,而是会记录一些自己做课程设计中的一些心得。

功能与展示

要做什么,标题已经说得很清楚了。那么再展示一下实现的功能吧,是不是你所想要的,一眼便能看明白。

功能列表

  1. 编写一个C语言程序作为Linux内核的shell命令行解释程序,所执行的结果需和系统命令行方式保持一致。
  2. 增加后台运行功能。即用户可以使用”&”作为一个命令结束,以启动下一个命令。
  3. 增加I/O重定向功能。即用户可以使用”<”和”>”符号改变程序/文件的输入和输出。
  4. 增加管道功能。即支持以“|”进行进程间通信操作。
  5. 增加退出功能。输入“exit”命令或者Ctrl +D退出。
  6. 增加文件名替换功能。(课设要求上是这么写的,我理解为mv命令的使用)
  7. 增加命令补全功能。即按下tab键可以补全命令。
  8. 增加查阅历史记录的功能。可以查看历史命令。
  9. 支持目录检索功能。即文件不存在,继续打印提示符。
  10. 支持一定的错误输入处理。例如:多于空格的出现,输入命令不存在,空输入等等。

主要功能就这些,常用的外部命令,ls、cat、cp等,我就不再列出了。其实在课设中还有一个可选实现的功能:alias别名功能。但由于时间有限(一周内要完成两个课设和两门期末考试,承担的还是主要代码工作),所以没能深入研究下去。不过在文末,我会给出自己关于alias功能实现的一些思路以及参考链接。以后有空了再回头研究。

功能展示

简单放几张展示图,分别为帮助功能、查看历史命令功能和I/O重定向功能。Shell提示符模仿的是命令提示符的样式,并在头部加入了红色的Myshell字样用于区分。
查看帮助
查看历史命令
I/O重定向

依赖库安装

在Shell的编写过程中,需要用到readline库,借此实现tab键代码补全、查看历史命令等功能。当然,readline库的功能远不止于此,如果想深入了解并运用,可以阅读官方文档
我的虚拟机上安装的系统是Ubuntu 16.04.6 LTS。
redhat系列下这个软件包叫readline-devel,ubuntu下叫readline-dev,细分为libreadline5-dev和libreadline6-dev。但是apt安装时发现,libreadline5-dev已经不存在了,不过libreadline6-dev还是在的,于是可以通过以下命令进行安装:

sudo apt-get install libreadline6-dev

此外,readline库还依赖于一个底层输入输出的库——ncurses,安装命令行如下:

sudo apt-get install libncurses5-dev

到此为止,依赖库的安装配置已经完成。

具体实现

Shell工作流程

Shell的工作流程如下图所示:
Shell工作流程
其实实现原理很简单,使用while循环持续接收用户命令,根据读入的字符串判断命令类型并进行相应的处理。
接下来,我会以流程图与代码结合的说明工作流程。

外部命令工作流程

外部命令工作流程如下图所示:
在这里插入图片描述
以“mv t1 t2”命令为例,首先程序读入用户输入的字符串,存入字符串数组command中。调用analysis_command()函数进行分析:

int i = 1;
char *p;
//分割依据,这里的分割依据为空格
char delims[] = " ";
argc = 1;
//将command字符串中第一个空格以前的字符串存入argv[0],这里是"mv"
strcpy(argv[0],strtok(command,delims));
//继续对剩余字符串进行切分,并将切分结果存入argv数组中
while(p = strtok(NULL,delims)){
	strcpy(argv[i++],p);
	argc++;
}
//判断输入的指令是否为内置命令
if(!(strcmp(argv[0],"exit"))||!(strcmp(argv[0],"help"))|| !(strcmp(argv[0],"cd"))||!(strcmp(argv[0],"history"))){
	BUILTIN_COMMAND = 1;	
}
//判断输入的指令是否含有管道功能的符号
int pipe_location;
for(int j = 0;j < argc;j++){
	if(strcmp(argv[j],"|") == 0){
		PIPE_COMMAND = 1;
		pipe_location = j;				
		break;
	}	
}
...
//中间省略了其他指令的判定及处理
if(PIPE_COMMAND){
	...
}
...
else{
	//argvtmp1存储分割后的指令
	argvtmp1 = malloc(sizeof(char *)*argc+1);
	int i;	
	for(i = 0;i < argc + 1;i++){
		argvtmp1[i] = malloc(sizeof(char)*100);
		if(i < argc)
			strcpy(argvtmp1[i],argv[i]);	
	}
	//execvp函数中,参数列表的最后一项必须是 NULL
	argvtmp1[argc] = NULL;
}

得到以空格分割后的指令数组argvtmp1后,调用do_command()函数执行指令:

if(PIPE_COMMAND){
	...
}
...
else{
	//调用fork()函数创建子进程,在子进程中执行指令
	pid_t pid = fork();	
	if(pid == -1){
		printf("fork failed !\n");		
	}
	else if(pid == 0){
		//调用execvp()函数执行程序,第一个参数是要运行的程序名,第二个参数是命令行参数列表
		//若失败则返回值为-1,输出"command not found"的提示
		if(execvp(argvtmp1[0],argvtmp1) < 0){
			printf("%s:command not found\n",argvtmp1[0]);			
		}
	}
	else{
		int pidReturn = wait(NULL);	
	}
}
//释放malloc( )分配的内存
free(argvtmp1);

外部命令执行结束,回到主程序并打印提示符。

内置命令工作流程

内置命令的工作流程如下图所示:
在这里插入图片描述
内置命令的工作流程与外部命令的工作流程大同小异,只是多了一步执行对应函数的操作。这个对应函数可以由自己编写,比如help命令:

void builtin_command(){
...
	//判断输入指令是否为"help"
	if(strcmp(argv[0],"help") == 0){
		help();
	}
	...
}

void help(){
	//打印用户名和内置命令列表
	struct passwd* pwp;
	pwp = getpwuid(getuid());
	printf("Hi, %s !\n",pwp->pw_name);
	printf("Here are the built-in commands:\n\n");
	printf("1. cd\n");
	printf("2. history\n");
	printf("3. help\n");
	printf("4. exit\n");
}

也可以调用已有的函数,比如要实现cd指令,就需要用到chdir()函数。

管道功能与I/O重定向的实现

管道功能与I/O重定向的实现,与上面内置命令与外部命令的工作流程没有多大区别,前人之述备矣。读过之后我在Shell中加入了简单的输入重定向,没有遇到太大的问题,就不再赘述了。

alias功能的一些思考

alias不是外部命令,只能调用函数或者自己实现。在网上找了好久有关alias的工作原理,不知是描述不准确还是别的原因,我并没看到描述alias工作原理的相关文章,也没找到某个库是附带alias功能函数的。不过看的多了,也就有了一点自己的想法,先挖个坑记录下来,等以后有时间了再埋。
目前的思路是,在用户主目录下的.bashrc文件中记录存放alias命令的文件名(比如.aliases)。然后于Shell内置命令中添加alias和unalias的指令判定,添加的别名都存入文件.aliases中。(感觉alias的实现原理大致就是如此,但在没有看到某篇文章明确说之前,也不敢下结论)
网上找到的一些博客,或许对你有些帮助:
bash中的alias命令实现(自己实现)
在文件保存alias命令并生效

Shell的编译与运行

readline是动态链接库,gcc时需要加上-lreadline。ncurses作为readline依赖的底层输入输出库,需要放在readline的后面。

gcc MyShell.c -o MyShell -lreadline -lncurses

输入./MyShell即可进入Shell。
编译与运行

源码

Shell源码已经放在Github上,供大家学习和参考。

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值