操作系统课程设计——Shell编程(用c编写一个Linux的外壳Shell)_os系统shell课设-程序员宅基地

技术标签: c语言  shell  linux  课程设计  

前言

最近操作系统结课了,不过还有一个课程设计需要完成。可供选择的题目并不少: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上,供大家学习和参考。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43326014/article/details/107215174

智能推荐

while循环&CPU占用率高问题深入分析与解决方案_main函数使用while(1)循环cpu占用99-程序员宅基地

文章浏览阅读3.8k次,点赞9次,收藏28次。直接上一个工作中碰到的问题,另外一个系统开启多线程调用我这边的接口,然后我这边会开启多线程批量查询第三方接口并且返回给调用方。使用的是两三年前别人遗留下来的方法,放到线上后发现确实是可以正常取到结果,但是一旦调用,CPU占用就直接100%(部署环境是win server服务器)。因此查看了下相关的老代码并使用JProfiler查看发现是在某个while循环的时候有问题。具体项目代码就不贴了,类似于下面这段代码。​​​​​​while(flag) {//your code;}这里的flag._main函数使用while(1)循环cpu占用99

【无标题】jetbrains idea shift f6不生效_idea shift +f6快捷键不生效-程序员宅基地

文章浏览阅读347次。idea shift f6 快捷键无效_idea shift +f6快捷键不生效

node.js学习笔记之Node中的核心模块_node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是-程序员宅基地

文章浏览阅读135次。Ecmacript 中没有DOM 和 BOM核心模块Node为JavaScript提供了很多服务器级别,这些API绝大多数都被包装到了一个具名和核心模块中了,例如文件操作的 fs 核心模块 ,http服务构建的http 模块 path 路径操作模块 os 操作系统信息模块// 用来获取机器信息的var os = require('os')// 用来操作路径的var path = require('path')// 获取当前机器的 CPU 信息console.log(os.cpus._node模块中有很多核心模块,以下不属于核心模块,使用时需下载的是

数学建模【SPSS 下载-安装、方差分析与回归分析的SPSS实现(软件概述、方差分析、回归分析)】_化工数学模型数据回归软件-程序员宅基地

文章浏览阅读10w+次,点赞435次,收藏3.4k次。SPSS 22 下载安装过程7.6 方差分析与回归分析的SPSS实现7.6.1 SPSS软件概述1 SPSS版本与安装2 SPSS界面3 SPSS特点4 SPSS数据7.6.2 SPSS与方差分析1 单因素方差分析2 双因素方差分析7.6.3 SPSS与回归分析SPSS回归分析过程牙膏价格问题的回归分析_化工数学模型数据回归软件

利用hutool实现邮件发送功能_hutool发送邮件-程序员宅基地

文章浏览阅读7.5k次。如何利用hutool工具包实现邮件发送功能呢?1、首先引入hutool依赖<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.19</version></dependency>2、编写邮件发送工具类package com.pc.c..._hutool发送邮件

docker安装elasticsearch,elasticsearch-head,kibana,ik分词器_docker安装kibana连接elasticsearch并且elasticsearch有密码-程序员宅基地

文章浏览阅读867次,点赞2次,收藏2次。docker安装elasticsearch,elasticsearch-head,kibana,ik分词器安装方式基本有两种,一种是pull的方式,一种是Dockerfile的方式,由于pull的方式pull下来后还需配置许多东西且不便于复用,个人比较喜欢使用Dockerfile的方式所有docker支持的镜像基本都在https://hub.docker.com/docker的官网上能找到合..._docker安装kibana连接elasticsearch并且elasticsearch有密码

随便推点

Python 攻克移动开发失败!_beeware-程序员宅基地

文章浏览阅读1.3w次,点赞57次,收藏92次。整理 | 郑丽媛出品 | CSDN(ID:CSDNnews)近年来,随着机器学习的兴起,有一门编程语言逐渐变得火热——Python。得益于其针对机器学习提供了大量开源框架和第三方模块,内置..._beeware

Swift4.0_Timer 的基本使用_swift timer 暂停-程序员宅基地

文章浏览阅读7.9k次。//// ViewController.swift// Day_10_Timer//// Created by dongqiangfei on 2018/10/15.// Copyright 2018年 飞飞. All rights reserved.//import UIKitclass ViewController: UIViewController { ..._swift timer 暂停

元素三大等待-程序员宅基地

文章浏览阅读986次,点赞2次,收藏2次。1.硬性等待让当前线程暂停执行,应用场景:代码执行速度太快了,但是UI元素没有立马加载出来,造成两者不同步,这时候就可以让代码等待一下,再去执行找元素的动作线程休眠,强制等待 Thread.sleep(long mills)package com.example.demo;import org.junit.jupiter.api.Test;import org.openqa.selenium.By;import org.openqa.selenium.firefox.Firefox.._元素三大等待

Java软件工程师职位分析_java岗位分析-程序员宅基地

文章浏览阅读3k次,点赞4次,收藏14次。Java软件工程师职位分析_java岗位分析

Java:Unreachable code的解决方法_java unreachable code-程序员宅基地

文章浏览阅读2k次。Java:Unreachable code的解决方法_java unreachable code

标签data-*自定义属性值和根据data属性值查找对应标签_如何根据data-*属性获取对应的标签对象-程序员宅基地

文章浏览阅读1w次。1、html中设置标签data-*的值 标题 11111 222222、点击获取当前标签的data-url的值$('dd').on('click', function() { var urlVal = $(this).data('ur_如何根据data-*属性获取对应的标签对象

推荐文章

热门文章

相关标签