1.1 引言
1.1.1 选题的背景
本次课程设计是在学习完了《现代操作系统》课程之后做的,本人选择了编写命令解释器模拟shell功能问题题目来实现。
1.1.2设计思路和预期目标
通过对《现代操作系统》的学习,我预计将用命令分割得到命令和参数,通过fork()函数创建子进程调用exec系统函数,并自己编写无法用exec系统函数执行的cd命令函数,依次完成打开提示符,获取用户输入的指令可解析指令、可寻找命令文件、可执行基本的命令的功能。
1.2 课程设计目的与意义
通过学习Linux下的系统调用、进程和进程间的通信,我基本了解内核的外围部分是如何合理的分配各种资源来使各类应用程序正常工作的。通过编写shell命令解释器,可以练习熟练使用系统调用和各类IO库函数。进行shell编程,对于一个嵌入式工程师来说做shell编程是一个基本工,是做好嵌入式开发的前提。
1.3 课程设计内容与要求
可以打开提示符,获取用户输入的指令可解析指令、可寻找命令文件、可执行基本的命令的功能。
1.4 课程设计地点及设计环境
课程设计地点:图书馆五楼软件实验室
课程设计环境:Red hat Linux系统
开发环境:Linux 开发平台
第2章 系统设计
2.1 系统框架设计
2.1.1 系统主结构图
图1 函数调用结构图
execvp()
图2 进程调度结构图
2.1.2 函数流程图
本程序除系统函数外还有三个重要函数main()、CD()、str_cut(char *p,char *buf[])。其中main()函数流程图如图3,CD()函数流程图如图4,str_cut(char *p,char *buf[])函数流程图如图5。
图3 主函数流程图
图4 命令裁剪函数流程图
图5 CD函数流程图
2.2 系统模块功能说明
2.2.1 主模块说明
主函数完成读命令,调用各其它函数,创建子进程,在子进程调用exec执行,提示错误等功能。
首先,申请一个buff的字符串数组,用来存放命令,另外申请一个pid_t型的变量来存放新生成的子进程的IP号。pid_t是Linux里的一个宏定义,通常可以当作一个整形看待,但为了便于移植程序,因为不同类型的计算机对整形长度的定义是不同的,在嵌入式里,不同的嵌入式处理器的运算位数不同,数据类型的定义也不同。
然后,进入一个死循环,打印出提示符当前路径。完成这一功能依赖的是char *get_current_dir_name()这一系统函数调用。要注意的是这个函数虽然加了头文件,但在使用前要声明。或者定义#define _GNU_SOURCE。因为unsigned long 和 char* 都是16位 把返回的char * 当成了unsigned long 需要用_GNU_SOURCE 来声明一下,这个宏是用来声明ISO POSIX 之类的标准,就可以区分返回值是char * 和 unsigned long了。
接下来,读取命令,这里我用的是fgets函数,之所以不用scanf函数来读字符串,是因为它以空格为结束标志,这样参数就无法读入了。
之后是if句子是用来做退出用的,如果敲入的是“exit”那么就退出循环,结束整个程序。
然后还有一个if语句是用来判断是不是cd命令,而决定是否调用cd函数。这两个if语句的位置很重要,在编程初期,我将它放在了下面创建的子进程中,出现了一些问题。比如,当cd()命令执行时地址制定错误后,提示语"Change dir error!\\n"会出现两次。原因是当执行“exit”“cd”时子进程中不需要执行exec函数,也就是说子进程没有被替代,它一直与父进程保持一致。所以出现了以上代码会执行两次的现象。所以这两个if语句的语句必须放在子进程外。
下面就是创建了一个子进程,子进程的IP号放到pid里,其中父进程得到了子进程的IP号,子进程里得到0;这样,子进程执行if里的代码,父进程执行else里的代码;
特别要指出的是子进程里,execlp使buff里的命令执行,这个子进程的代码都被命令文件的代码替换了,子进程在exec结束时已经结束,所以成功执行时后面的错误提示代码是不被执行的。当这个命令不存在时,不会将剩余代码替换,他会继续执行下面的代码,即打印一句提升:“command not found!”不让程序限于阻塞。父进程里,通过得到的子进程的IP号,来使用wait函数等待子进程的退出,接着进入下一次循环,打印提示符,等待命令的再次输入。
2.2.2子模块说明
1、切割函数
剪切的方式取决于调用的exec函数所需要的参数形式。execl(执行文件)
相关函数 fork,execle,execlp,execv,execve,execvp调用方式分别为:
main()
{execl(“/bin/ls”,”ls”,”-al”,”/etc/passwd”,(char * )0);}
main()
{execlp(“ls”,”ls”,”-al”,”/etc/passwd”,(char *)0);}
main()
{char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char*) }};
execv(“/bin/ls”,argv);}
main()
{char * argv[ ]={“ls”,”-al”,”/etc/passwd”,(char *)0};
char * envp[ ]={“PATH=/bin”,0}
execve(“/bin/ls”,argv,envp);}
main()
{char * argv[ ] ={ “ls”,”-al”,”/etc/passwd”,0};
execvp(“ls”,argv);}
由上可知execvp()函数调用最简单易用,且切割方便。第一个参数为命令文件名,第二个参数为一个指针数组,这样就可以把命令和参数全部丢给执行函数。为此我们也为切割函数设置一个是原命令字符串指针buf1,一个是存储字符串指针的字符串指针buf2。切割函数将完成以下功能。
将buf1中的空格全部置换为“\\0”,它有很多作用。其一,在把buf1作为execvp()第一个参数时,读取命令后会自动停止,因为命令与参数之间的空格已经变为结束符。其二,在把buf2作为第二个参数时,按指针寻址后,避免了一次读取到多个参数的现象。
最后一位的’\\n’换成’\0’,即buf1和buf2最后一个单元置空,以符合参数形式要求。
2、CD函数
命令分为两种,一种为内部命令,一种为外部命令。
用户输入的命令是执行存储在文件系统下中的可执行程序,我们称之为外部命令或外部程序。外部命令的形式是一系列分隔的字符串。第一个字符串可以是可执行程序的名字,其它的是传递给这个外部程序的参数。如果第一个字符串所声名的可执行文件并不存在或者不可执行,则认为这个命令是错误的。
以上其它命令就是外部命令,因而可以用exec系统文件函数完成执行。
但是cd命令和exit命令是内部命令,不能用exec函数执行的,它的出现时shell特有的功能。所以在这里,cd函数也是我自己编写的。
它的功能有两个,其一是判断路径的正确性,其二是执行变换路径函chdir()。
第3章 源程序代码设计
#include #include #include #include #include char *get_current_dir_name(); void str_cut(char *p,char *buf[]) { int i=0; //i即为buf数组下标 int u=0; //u是一个标志位,u=0,表示前一字符为空格或无字符 //即刚开始;u=1,表示前一字符为字母 while(*p) //p为原字符串指针,*p不为'\\0'则继续循环 { if(*p==' ') { *p='\\0'; //把空格替换为结束符 p++; u=0; //标志位u清零 } else { if(u==0) { buf[i++]=p++; //单词的首地址赋给buf u=1; //标志位u置1 } else p++; //什么也不做,p指向下一个 } } buf[i]=NULL; //buf里最后一个地址是空便于execvp使用 } void CD(char argv1[]) { int res=0; if(argv1!=NULL) res=chdir(argv1); if(res<0) printf("Change dir error!\\n"); } int main(void) { char buf1[50]; char *buf2[10]; pid_t pid; char *p; while(1) { p=get_current_dir_name(); printf("[%s]#",p); fgets(buf1,sizeof(buf1),stdin); buf1[strlen(buf1)-1]='\\0'; str_cut(buf1,buf2); //调用上面的切割函数,原字符串仍存在 // buf1里,其分割后地址存在buf2里 if(strncmp(buf1,"exit",4)==0) break; if(strncmp(buf1,"cd",2)==0) { CD(buf2[1]); continue; } if((pid=fork())==0) { execvp(buf2[0],buf2); printf("command not found!\\n"); //命令不可执行,则向下走 //打印无此命令 exit(1); } else waitpid(pid,NULL,0); } exit(0); } 第4章 系统的调试和运行 本系统可以执行目录命令、文件命令,带参数和不带参数命令均可正确执行。并且有检错功能,伴有各类错误提示。 “ls”“pwd”“mkdir”“rmdir”“cd”“mv”等不带参数的目录操作命令可以正常执行,结果如图6。 “ls –l -a”“mkdir –p 电路/数电”“rmdir –p 电路/数电”“cd /root”等带参数甚至多参数目录命令可以正常执行,结果如图7。 “cat”“more”“cp”“rm”“vi”“gcc”等命令无论有无参数也均可正常执行,结果如图8。其中vi编辑情况如图9。 本程序有比较好的健壮性,不会阻塞,且有各类错误提示,如图10。 内部命令“exit”执行结果如图10。 图6 无参数目录命令执行图 图7 有参数目录命令执行图 图8 文件操作命令执行图 图9 vi插入数据图 图10 错误提示及退出执行图 通过各类数据的测试包括不同命令、不同参数、不同参数数量以及错误命令的测试,可以证明,本程序属于一个功能完善,健壮性强的作品。 结 论 本次操作系统课程设计我选择的命令解析器模拟shell的问题是通过Linux操作系统实现,使我对Linux系统有了一些了解,并在课程设计过程中得到进一步深化,特别是对Linux下各种命令及其系统文件调用方式方法。巩固了我的操作系统的理论,提高了我使用linux C的能力。 本程序有很多创新点,它是一步步完善,一步步升级的。 在第一阶段,它只有输入不带参数的简单命令(不包括cd命令),使用的是scanf()读取字符,以空格结束命令。用execlp()调用系统文件函数。 第二阶段增加创新点,用fgets ()获取命令字符串,它以回车为结束标志,使得空空格分隔命令和参数时也可以整条命令存入字符串。用execvp()调用系统文件函数,使得可以传入多条参数,且参数数量可以不固定。并按其参数要求设计切割函数切割方式。 第三阶段在发现了cd命令执行报错后,查看相关资料发现cd不属于外部命令,不能用exec函数调用,之后我自己编写了CD()函数。 第四阶段在全面功能测试时我发现cd命令输入错误路径后的错误提示输出了两遍。在仔细分析研究之后我发现了错误。在子进程没有调用exec函数前,父进程与子进程是一模一样,调用后子进程才被替代,即调用前所有代码执行两遍,造成了提示的重复。改正方法,充分考虑代码运行对子进程的依赖程度,没有依赖性的调出子进程,只在父进程中运行。 通过这个从简到繁,逐步扩展的过程基本程序才得以完成,通过发现问题解决问题的过程,程序才得以逐步完善。里面融合这我每一点的创新观念。 经过测试已经证明本程序有很强的功能和很好的性能,但是仔细观察模拟shell和真实shell的区别还是可以发现。 比如说文字变色功能,在shell中,浏览目录时文本和文件夹名称显示颜色不同,vi编辑器中,输入C语言的关键字等会自动变色提示。而在模拟shell中,所有输出均为黑色。 又如在真实shell中,存在很多快捷键,像向上向下键可以重复已经输入过的命令,向左向右键可以移动光标,而在模拟shell中,摁这些键则会认定为是录入字符。 这些证明真实的shell功能是更复杂更完善的,我的程序还有很多需要改进的地方,以后要继续学习,继续完善。 为了完成本次试验,我参看了很多文献,我学到了很多新的东西,比如调用系统文件的函数种类和用法,比如用户可以调用内核文件的原因、范围和途径,比如头文件、宏定义、函数声明的配合使用,再比如创建子进程的目的,需要通过子进程完成的操作,父进程与子进程之间的关系,并且在学习之后把它们运用的了实践之中。源代码虽然很少但搜寻、学习、尝试应用相关知识的过程却是非常复杂。我也从中发下了自己不懂得专业知识实在太多,有一种井底之蛙的感觉。我以后一定要拓宽自己的知识面。 我在此还要特别的感谢那些在网上分享自己的学习心得经验的人,我是在解读了各个版本的作品后形成的自己的思路。我要感谢那网上讲解各种疑难问题的人,在他们讲解下,我解决了很多从没遇到过的问题。我还要感谢给予我指导和帮助的同学和老师。 程序使用说明书 1.显示工作目录 命令名称:pwd - print name of current/working directory 命令格式:pwd [--help][--version] 功能说明:执行pwd指令可显示当前所在的工作目录的绝对路径名称。 命令参数: --help 在线帮助。 --version 显示版本信息。 范例: 显示当前工作目录。 [lly@localhost ~]$ pwd /home/lly [lly@localhost ~]$ 2.改变当前目录 命令名称:cd - change diretory 命令格式:cd [-L|-P] [dir] 功能说明:改变当前目录为dir指定的目录。其中dir可为绝对路径或相对路径。若目录名称省略,则变换至用户的 home directory (也就是刚 login 时所在的目录)。 另外,"~" 也表示为 home directory 的意思,"." 则是表示目前所在的目录,".." 则表示目前目录位置的上一层目录。 范例: (1)进入/home目录。 [lly@localhost ~]$ cd /home [lly@localhost home]$ (2)进入根目录。 [lly@localhost ~]$ cd / [lly@localhost /]$ (3)进入用户lly的主目录。 [lly@localhost /]$ cd ~ [lly@localhost ~]$ 或者: [lly@localhost /]$ cd /home/lly [lly@localhost ~]$ (4)进入当前目录的上一级目录。 [lly@localhost ~]$ cd .. [lly@localhost home]$ 3.显示目录内容 命令名称:ls - list directory contents 命令格式:ls [OPTION]... [FILE]... 功能说明:显示指定工作目录下的内容(列出当前工作目录所包含的子目录名称或文件名称)。 命令参数: -a 显示所有文件及目录 (包括隐藏文件,隐藏文件以“.”开头) -l 除文件名称外,亦将文件型态、权限、拥有者、文件大小等信息详细列出 -r 将文件以相反次序显示(原定依英文字母次序) -t 将文件依建立时间先后次序列出 -A 同 -a ,但不列出 "." (目前目录) 及 ".." (父目录) -F 在列出的文件名称后加一符号;例如可执行文件则加 "*", 目录则加 "/" -R 若目录下有文件,则其下的文件也按次序列出 范例: (1)列出目前工作目录下的内容。 [lly@localhost ~]$ ls Desktop Documents Download Music Pictures Public Templates Videos [lly@localhost ~]$ (2)列出当前目录的详细内容。 [lly@localhost ~]$ ls -l total drwxr-xr-x 2 lly lly 4096 2009-05-02 17:43 Desktop drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Documents drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Download drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Music drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Pictures drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Public drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Templates drwxr-xr-x 2 lly lly 4096 2009-05-02 17:18 Videos [lly@localhost ~]$ (3)列出目前工作目录下的内容,包括隐藏文件。 [lly@localhost ~]$ ls -a . .esd_auth .ICEauthority .thumbnails .. .fontconfig .kde .tomboy .bash_history .gconf .local .tomboy.log .bash_logout .gconfd .metacity .Trash .bash_profile .gimp-2.4 Music Videos .bashrc .gnome .nautilus .wapi .config .gnome2 Pictures .xsession-errors Desktop .gnome2_private Public .zshrc .dmrc .gstreamer-0.10 .pulse-cookie Documents .gtk-bookmarks .recently-used.xbel Download .gtkrc-1.1-gnome2 Templates [lly@localhost ~]$ 4.创建目录 命令名称:mkdir - make directories 命令格式:mkdir [OPTION] DIRECTORY... 功能说明:建立名称为DIRECTORY.的子目录。 命令参数: -p 确保目录名称存在,不存在的就建一个。 范例: (1)在当前目录下建立一个名为Game的子目录。 [lly@localhost ~]$ mkdir Game [lly@localhost ~]$ ls Desktop Documents Download Game Music Pictures Public Templates Videos [lly@localhost ~]$ (2)把Cartoon子目录建立在当前目录的Movie子目录下,且当前Movie子目录不存在。 若用一条命令完成可进行如下操作: [lly@localhost ~]$ ls Desktop Documents Download Game Music Pictures Public Templates Videos [lly@localhost ~]$ mkdir -p Movie/Cartoon [lly@localhost ~]$ ls Desktop Download Movie Pictures Templates Documents Game Music Public Videos [lly@localhost ~]$ ls Movie Cartoon [lly@localhost ~]$ 5. 删除目录 命令名称:rmdir - remove empty directories 命令格式:rmdir [OPTION]... DIRECTORY... 功能说明:删除空的目录。 命令参数: -p 是当子目录被删除后使它也成为空目录的话,则顺便一并删除。 范例: (1)删除之前建立的Game的子目录。 [lly@localhost ~]$ ls Desktop Download Movie Pictures Templates Documents Game Music Public Videos [lly@localhost ~]$ rmdir Game [lly@localhost ~]$ ls Desktop Documents Download Movie Music Pictures Public Templates Videos [lly@localhost ~]$ (2)删除之前建立的Cartoon子目录。 [lly@localhost ~]$ ls Desktop Documents Download Movie Music Pictures Public Templates Videos [lly@localhost ~]$ ls Movie/ Cartoon [lly@localhost ~]$ rmdir -p Movie/Cartoon/ [lly@localhost ~]$ ls Desktop Documents Download Music Pictures Public Templates Videos [lly@localhost ~]$ 6.复制文件 命令名称:cp - copy files and directories 命令格式: cp [OPTION]... [-T] SOURCE DEST cp [OPTION]... SOURCE... DIRECTORY cp [OPTION]... -t DIRECTORY SOURCE... 功能说明:复制文件。 命令参数: -a 尽可能将档案状态、权限等资料都照原状予以复制。 -r 若 source 中含有目录名,则将目录下之档案亦皆依序拷贝至目的地。 -f 若目的地已经有相同档名的档案存在,则在复制前先予以删除再行复制。 范例: 复制/etc/passwd文件到当前目录下,同时更名为mypasswd。 [lly@localhost ~]$ cp /etc/passwd ~/mypasswd [lly@localhost ~]$ ls Desktop Download mypasswd Public Videos Documents Music Pictures Templates [lly@localhost ~]$ 7.显示文件 显示文件的命令可以用cat、more。下面分别介绍。 (1)cat 命令名称:cat - concatenate files and print on the standard output 命令格式:cat [OPTION] [FILE]... 功能说明:显示文件内容。 命令参数: -n 或 --number 由 1 开始对所有输出的行数编号 -b 或 --number-nonblank 和 -n 相似,只不过对于空白行不编号 -s 或 --squeeze-blank 当遇到有连续两行以上的空白行,就代换为一行的空白行 -v 或 --show-nonprinting 范例: 显示之前复制得来的passwd文件内容。 [lly@localhost ~]$ cat mypasswd (2)more 命令名称:more - file perusal filter for crt viewing 命令格式:more [-dlfpcsu] [-num] [+/ pattern] [+ linenum] [file ...].. 功能说明:类似cat ,不过会以一页一页的显示方便使用者逐页阅读,而最基本的指令就是按空白键(space)就往下一页显示,按 b 键就会往回(back)一页显示,而且还有搜寻字串的功能(与 vi 相似),使用中的说明文件,请按 h 。 命令参数: -num 一次显示的行数 -d 提示使用者,在画面下方显示 [Press space to continue, 'q' to quit.] ,如果使用者按错键,则会显示 [Press 'h' for instructions.] 而不是 '哔' 声 -l 取消遇见特殊字元 ^L(送纸字元)时会暂停的功能 -f 计算行数时,以实际上的行数,而非自动换行过后的行数(有些单行字数太长的会被扩展为两行或两行以上) -p 不以卷动的方式显示每一页,而是先清除萤幕后再显示内容 -c 跟 -p 相似,不同的是先显示内容再清除其他旧资料 -s 当遇到有连续两行以上的空白行,就代换为一行的空白行 -u 不显示下引号 (根据环境变数 TERM 指定的 terminal 而有所不同) +/ 在每个档案显示前搜寻该字串(pattern),然后从该字串之后开始显示 +num 从第 num 行开始显示 fileNames 欲显示内容的档案,可为复数个数 范例: 分屏显示之前复制得来的passwd文件内容。 [lly@localhost ~]$ more mypasswd 8.删除文件 命令名称:rm - remove files or directories 命令格式:rm [OPTION]... FILE... 功能说明:删除空的目录。 命令参数: -i 删除前逐一询问确认。 -f 即使原档案属性设为唯读,亦直接删除,无需逐一确认。 -r 将目录及以下之档案亦逐一删除。 范例: 删除mypasswd文件 [lly@localhost ~]$ rm mypasswd 二、vi编辑器的使用 vi或vim是Linux最基本的文本编辑工具,vi或vim虽然没有图形界面编辑器那样点鼠标的简单操作,但vi编辑器在系统管理、服务器管理中,永远不是图形界面的编辑器能比的。当没有安装X-windows桌面环境或桌面环境崩溃时,我们仍需要字符模式下的编辑器vi,vi或vim 编辑器在创建和编辑简单文档是最高效的工具。 1.vi编辑器的三种工作模式 vi有三种工作模式:命令模式、插入模式和末行模式 1命令模式 在shell环境中启动vi时,初始就是进入命令模式。在该模式下,用户可以输入命令,用于管理自己的文档,包括控制屏幕光标的移动,字符、字或行的删除、移动、复制等。此时从键盘输入的任何字符都作为编辑命令来解释。若输入的是合法的vi命令,则vi在接受用户命令之后完成相应的动作;若输入的是不合法的命令,vi会响铃报警。需要注意的是,所输入的命令在屏幕上不显示。不管用户处于何种模式,只要用户按一下 2插入模式 只有在插入模式下才可以进行文字输入。在命令模式下输入命令i、附加命令啊、打开命令o、修改命令c、取代命令r或替换命令s都可以进入插入模式。在该模式下,用户输入的任何字符都被vi当做文件内容保存起来,并将其显示在屏幕上。在文本输入过程中,若想回到命令模式,按 3末行模式 在命令模式下,用户按<:>键即可进入末行模式,此时vi会在显示窗口的最后一行显示一个“:”作为末行模式的提示符,等待用户输入命令。多数文件管理命令都是在此模式下执行的,如保存文件或退出vi、寻找字符串、列出行号等。末行命令执行完后,vi自动回到命令模式。 2.vi的进入与退出 vi是在Linux终端张运行的程序,它的所有操作必须通过键入相应的命令完成。本节介绍如何启动vi编辑啟、如何保存编辑的文件以及如何退出vi。 1进入vi 在终端shell提示符后键入vi和想要编辑或新建的文件名,便可进入vi。图2-4为输入命令vi hello.c后的vi窗口。 进入vi之后,首先进入命令模式。光标停在屏幕第一行第一列上,其余各行首均有一个“~”符号,表示该行为空行。最后一行称为状态行,显示当前正在编辑的文件名及其状态。本例中“hello.c[NewFile],hello.c是一个新建文件。如果该文件已经存在,输入上述命令后,则会显示出该文件的内容。 2保存文件和退出vi 当编辑完文件,准备退出vi返回到shell时,可以使用以下几种方法之一: ●在命令模式下 连按两次大写字母 ●在末行模式下 用以下命令进行保存文件: w vi保存当前编辑的文件而不退出vi,继续等待用户输入命令。 w w! 使用下面方法可以退出vi: q 不保存文件退出vi。若文件修改过,则提示:no write since last chang(use!to overrides,即提示使用“!”放弃保存。 q! 放弃对文件所做的修改,直接退出vi返回到shell。 wq vi先保存文件,然后退出vi返回到shell 3.其他命令 前面介绍了vi的几个基本操作,表2-2和表2-3列出了vi的一些常用操作。这些操作都是在命令模式下使用的。 表2-2 删除操作 1.打开vi编辑器 2.编写程序hello.c #includes void main() { printf(“Hello World!\\n”); } 3.编译并运行该程序 ●gcc –o hello hello.c ●.\\hello ●注:如源文件有错误,请重新用vi进行修改,重新进入方法:vi [filename]。
表2-3 改变与替换操作命令 作用 x 删除光标所在的字符 dw 删除光标所在的单词 d$ 删除光标至行尾的所有字符 D 同d$ dd 删除当前行
三、程序的编写、编译和运行命令 作用 r 替换光标所在的字符 R 替换字符序列 cw 替换一个单词 ce 同cw cb 替换光标所在的前一字符 c$ 替换自光标位置至行尾的所有字符 C 同c$ cc 替换当前行