一、 实验内容
Linux系统中,shell是用户和系统内核沟通的中介,它为用户使用操作系统的服务提供了一个命令界面。用户在命令提示符($或是#)下输入的每一个命令都由shell解释,然后传给内核执行。
本实验是实现一个简单的shell,能在虚拟shell界面下响应一些简单的shell命令,不考虑输入/输出重定向以及管道。
二、 简单shell命令解释器的分析
命令解释执行的过程为:1.初始化虚拟shell界面;2.获取用户指令(命令和参数);3.解析指令;4.寻找命令文件;5.执行命令。
三、 shell命令解析器的设计
(1)在虚拟的shell界面上出现命令提示符($或#);
(2)获取用户指令:获取用户在命令提示符后面输入的命令及其参数,并注意命令输入的最大长度;
(3)解析指令:对用户输入的命令进行解析,解析出命令名和参数;
(4)寻找命令文件:每个命令的执行都必须依靠对应的可执行文件,这些文件的存放路径存放在用户的PATH环境变量里;
(5)执行命令:可通过fork( )系统调用创建一个进程来完成执行命令的任务,具体的命令执行用execv( )函数。
注:为了简化程序设计的难度,对涉及输入/输出重定向以及管道的命令不予考虑。
四、 shell命令解析器实现的详细步骤
1.在虚拟的shell界面上打印出命令提示符“#”或者“$”。
通常在Linux命名提示符前会有路径或者机器名,我们可以先获取到当前执行的路径,由此调用一个外部函数get_current_dir_path(),返回值是一个指向当前目录绝对路径的字符串指针,然后连接到命令提示符之前即可。这样就得到了虚拟shell的命令提示行。因为用户不是只执行一个指令就结束了,还需要等待用户继续输入,所以用一个while死循环来循环执行该解析器。代码如下:
char *path;
while (1)
{
path = get_current_dir_name();
printf("%s>fangxu$",path);
}
2.获取用户输入的指令及其参数
通过getchar()或者gets()方法来获取用户输入的指令,设置一个缓冲区buffer来存储用户的输入,并定义缓冲区大小,如果用户输入的指令太长超出缓冲区大小,则提示用户输入指令太长并重新输入。定义一个临时变量inputLen来记录用户输入字符串的长度,如果超出缓冲区大小BUFFERSIZE,则发出提示并重复执行该程序允许用户再次重新输入指令;若没有超出,则把缓冲区的内容复制到input中,以此来存储用户输入的指令。代码如下(接上段代码printf之后):
//开始获取输入
inputLen = 0;
in_char = getchar();
while (in_char !='\n')
{
if(inputLen < BUFFERSIZE)
buffer[inputLen++] = in_char;
in_char = getchar();
}
/*命令超长处理*/
if (inputLen >= BUFFERSIZE)
{
printf("Your command is too long! Please re-enter your command!\n");
inputLen = 0;
continue;
}
else
buffer[inputLen] = '\0';/*加上串结束符号,形成字串*/
/*将命令从缓存拷贝到input中*/
input = (char *) malloc(sizeof(char) * (inputLen+1));
strcpy(input,buffer);
3. 解析指令:对用户输入的命令进行解析,解析出命令和参数
在完成了指令的输入后自然是要解析该指令,以获取命令和参数了。在这个过程中我们需要区分命令和参数,通过for循环,找到用户输入字符串中的空格或者\t,以此来区分开命令名和参数,并保存在指针数组char *arg[]中,其中arg[0]保存的为命令名,其后参数从arg[1]开始依次保存。最后,获取到命令名和参数后再释放input空间。
/* 获取命令和参数并保存在arg中*/
for (i = 0,j = 0,k = 0;i <= inputLen;i++)
{
//把输入的命令分开为各条命令和参数
if (input[i] == ' ' || input[i] =='\t' || input[i] == '\0')
{
if (j == 0) /*这个条件可以略去连在一起的多个空格或者tab*/
continue;
else
{
buffer[j++] = '\0';
arg[k] = (char *) malloc(sizeof(char)*j);
/*将指令或参数从缓存拷贝到arg中*/
strcpy(arg[k],buffer);
j = 0; /*准备取下一个参数*/
k++;
}
}
else
{
/*如果字串最后是‘&',则置后台运行标记为1*/
if (input[i] == '&' && input[i+1] == '\0')
{
isBk = 1;
continue;
}
buffer[j++] = input[i];
}
}
free(input);/*释放空间*/
4. 寻找命令文件
接下来我们就该完成寻找命令文件的工作了。那么,到哪里去找用户输入的命令呢?我们知道shell为每个用户提供了一组环境变量。这些变量定义在用户的.login文件中。其中路径变量是一组绝对路径的列表,表明shell将如何搜索命令文件。也就是说,只要我们获取了路径变量PATH,然后依次搜索各路径,就可以确定用户输入的命令文件的位置了。此外,我们还需要在用户输入退出指令的时候能退出我们的shell回到Linux的shell下。在这里,我们设定了exit指令来完成这一功能。其中的函数is fileexist就是用来判断输入的指令是否存在。存在返回0,并将指令的路径保存在buffer中,否则返回一1。程序如下:
//如果输入的指令是exit则退出while,即退出程序
if (strcmp(arg[0],"exit") == 0 )
{
printf("Bye bye\n");
break;
}
if (isRd == 0)
{ //非管道、重定向指令
//在使用exec执行命令的时候,最后的参数必须是NULL指针,
//所以将最后一个参数置成空值
arg[k] = (char *) 0;
//判断指令arg[0]是否存在
if (is_fileexist(arg[0]) == -1 ){
printf("This command is not found!\n");
for(i=0;i<k;i++)
free(arg[i]);
continue;
}
/*释放申请的空间*/
for (i=0;i<k;i++)
free(arg[i]);
}
}
}
int is_fileexist(char *comm)
{
char *path,*p;
int i;
i = 0;
/*使用getenv函数来获取系统环境变量,用参数PATH表示获取路径*/
path = getenv("PATH");
p = path;
while (*p != '\0'){
/*路径列表使用‘:’来分隔路径*/
if (*p != ':')
buffer[i++] = *p;
else{
buffer[i++] = '/';
buffer[i] = '\0';
/*将指令和路径合成,形成pathname,并使用access函数来判断该文件是否存在*/
strcat(buffer,comm);
if (access(buffer,F_OK) == 0) /*文件被找到*/
return 0;
else
/*继续寻找其它路径*/
i = 0;
}
p++;
}
/*搜索完所有路径,依然没有找到则返回-1*/
return -1;
}
5. 执行命令
当我们得到了命令的路径,就可以执行命令了。通过调用fork()创建一个子进程,在子进程中执行命令。fork()会创建一个新的子进程,其子进程会复制父进程的数据与堆栈空间并继承父进程的用户代码、组代码、环境变量、已打开的文件代码、工作目录和资源限制等。Linux使用COW (copy onwrite)技术,只有当其中一个进程试图修改欲复制的空间时才会做真正的复制动作。由于这些继承的信息是复制而来,并非指相同的内存空间,因此子进程对这些变量的修改和父进程不会同步。此外,子进程不会继承父进程的文件锁定和未处理的信号。需要注意的是,Linux不保证子进程会比父进程先执行或晚执行,因此编写程序时需注意死锁和竞争的发生。如果fork成功,则父进程会返回新建子进程的ID (PID,而在新建子进程中返回0,如果失败则返回-1。执行指令是通过调用exec函数。exec有一系列的变型函数:execl(), execle(), execlp(), execv(), execve()和execvp()。这里用到execv()函数,execv有两个参数,分别是指向命令路径的字符串指针和存放命令参数的数组指针。execv如果成功则不会返回,失败则返回-1。
程序如下:
if ((pid = fork()) ==0) /*子进程*/
execv(buffer,arg);
else /*父进程*/
if (isBk == 0) /*并非后台执行指令*/
waitpid(pid,&status,0);
/*释放申请的空间*/
for (i=0;i<k;i++)
free(arg[i]);
后台执行指令和普通指令之间的区别就在于父进程是否执行waitpid( )。也就是说如果是后台执行指令,那么父进程不等到子进程结束就继续执行下去了,否则父进程一直要等到子进程结束才继续执行。一个执行命令的正常过程是,父进程创建子进程,并在执行命令的时候等待,一直等到子进程终结。如果在命令行的末尾添加“&”,那么shell将会创建一个子进程,并且启动它执行指定的命令,但是父进程将不会等到子进程结束。也就是说,父进程和子进程将会同时执行。当子进程执行命令的时候,父进程将会在标准输出设备(stdout)上打出另一个提示符并等待用户输入其他命令。
至此,在不考虑管道以及输入输出重定向的情况下,shell命令解释器的功能已经完成。
五、 运行界面及结果分析
1.编译并执行shell.c文件,执行结果如下:
2.执行ls -l列举当前文件夹中的文件:
3.用mkdir命令创建文件夹files:
4.用rmdir命令删除文件夹abc:
5.用date命令显示系统日期时间(没有调整虚拟机Linux系统的时间,显示不太正确哈):
6.用ps –e&命令显示前台和后天所有进程(命令后加&使之运行为后台进程):
7.输入非Linux系统命令(提示找不到该命令):
8.输入exit自定义命令退出程序进入Linux系统的shell:
六、 总结
本次实验完成了简单的对shell命令的解析器。由于对于linux环境下的C语言编程的不熟悉,导致完成过程颇为曲折。但同时,也使自己对于Linux操作系统有了更为深入的了解。自己之前用的编程语言基本上都是完全面向对象的Java语言,偶尔使用C#语言,很少很少用C来写程序了。虽然照着书上敲了C代码,但为了让其正确运行,依旧费了不少力气。上网查资料,翻书等等,最终还是修改成功并使其运行了。在对C更为熟悉的同时,也使自己对于Liunx的一些基本命令更加了解。
学习Linux,不能只通过看书本上的知识,还要多上机编写程序,才会有深刻的体会,同时自己对应Linux各种机制才会有更好的理解,才能更好的运用编程,同时这样也能使自己的编程能力得到较大的提高。
学习不能仅仅局限于书本以及老师的讲解,还有更多的在于自己的动手查找以及实践的过程中所学到的知识。
附源码:
#include <linux/unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#define BUFFERSIZE 50
extern char *get_current_dir_name(void);
extern char *getenv(const char *name);
extern pid_t waitpid(pid_t pid, int *status, int options);
char buffer[BUFFERSIZE+1];
main()
{
char *path, *arg[10], *input;
int inputLen, isRd, isBk, i, j, k, pid, status;
char in_char;
while (1)
{
isRd = 0; //是否重定向标志
isBk = 0; //是否后台运行标志
path = get_current_dir_name();
printf("%s>fangxu>$",path);
//开始获取输入
inputLen = 0;
in_char = getchar();
while (in_char !='\n')
{
if(inputLen < BUFFERSIZE)
buffer[inputLen++] = in_char;
in_char = getchar();
}
/*命令超长处理*/
if (inputLen >= BUFFERSIZE)
{
printf("Your command is too long! Please re-enter your command!\n");
inputLen = 0;
continue;
}
else
buffer[inputLen] = '\0';/*加上串结束符号,形成字串*/
/*将命令从缓存拷贝到input中*/
input = (char *) malloc(sizeof(char) * (inputLen+1));
strcpy(input,buffer);
/* 获取命令和参数并保存在arg中*/
for (i = 0,j = 0,k = 0;i <= inputLen;i++)
{
//把输入的命令分开为各条命令和参数
if (input[i] == ' ' || input[i] =='\t' || input[i] == '\0')
{
if (j == 0) /*这个条件可以略去连在一起的多个空格或者tab*/
continue;
else
{
buffer[j++] = '\0';
arg[k] = (char *) malloc(sizeof(char)*j);
/*将指令或参数从缓存拷贝到arg中*/
strcpy(arg[k],buffer);
j = 0; /*准备取下一个参数*/
k++;
}
}
else
{
/*如果字串最后是‘&',则置后台运行标记为1*/
if (input[i] == '&' && input[i+1] == '\0')
{
isBk = 1;
continue;
}
buffer[j++] = input[i];
}
}
free(input);/*释放空间*/
//如果输入的指令是exit则退出while,即退出程序
if (strcmp(arg[0],"exit") == 0 )
{
printf("Bye bye\n");
break;
}
if (isRd == 0)
{ //非管道、重定向指令
//在使用exec执行命令的时候,最后的参数必须是NULL指针,
//所以将最后一个参数置成空值
arg[k] = (char *) 0;
//判断指令arg[0]是否存在
if (is_fileexist(arg[0]) == -1 ){
printf("This command is not found!\n");
for(i=0;i<k;i++)
free(arg[i]);
continue;
}
if ((pid = fork()) ==0) /*子进程*/
execv(buffer,arg);
else /*父进程*/
if (isBk == 0) /*并非后台执行指令*/
waitpid(pid,&status,0);
/*释放申请的空间*/
for (i=0;i<k;i++)
free(arg[i]);
}
}
}
int is_fileexist(char *comm)
{
char *path,*p;
int i;
i = 0;
/*使用getenv函数来获取系统环境变量,用参数PATH表示获取路径*/
path = getenv("PATH");
p = path;
while (*p != '\0'){
/*路径列表使用‘:’来分隔路径*/
if (*p != ':')
buffer[i++] = *p;
else{
buffer[i++] = '/';
buffer[i] = '\0';
/*将指令和路径合成,形成pathname,并使用access函数来判断该文件是否存在*/
strcat(buffer,comm);
if (access(buffer,F_OK) == 0) /*文件被找到*/
return 0;
else
/*继续寻找其它路径*/
i = 0;
}
p++;
}
/*搜索完所有路径,依然没有找到则返回-1*/
return -1;
}
实验二Linux常用命令使用一、实验目的1.掌握Linux一般命令格式。2.掌握有关文件和目录操作的常用命令。3.掌握有关进程操作…
Linux操作系统实验报告实验编号实验编号实验名称实验名称实验1Linux安装实验2掌握虚拟机的使用实验目的1熟练掌握Linux系…
实验项目名称Linux基础操作实验项目编号一学号组号上机实践日期20xx919上机实践时间2学时一目的本次实验所涉及并要求掌握的知…
专业计算机科学与技术学号姓名Linux操作系统报告单名称系统常用命令任课教师专业计算机科学与技术班级姓名学号完成日期成绩12345…
实验一Linux的基本操作命令一实验目的了解Linux的基本命令实现Linux的文件系统操作二实验内容1在Linux字符环境下练习…
Linux考试实验1修改运行级别,级别切换:(1)vi/etc/inittab将5改为3(2)reboot2修改root密码:进入…
LINUX实验报告专业班级学号姓名实验一实验名称Linux基本命令的使用实验时间2学时实验目的熟练使用Linux字符界面窗口系统的…
实验项目名称Linux基础操作实验项目编号一学号组号上机实践日期20xx919上机实践时间2学时一目的本次实验所涉及并要求掌握的知…
实验二Linux常用命令使用一、实验目的1.掌握Linux一般命令格式。2.掌握有关文件和目录操作的常用命令。3.掌握有关进程操作…
课程编号B080103040Linux操作系统实验报告东北大学软件学院实验一熟悉Linux环境实验内容一练习常用的Shell命令当…