shell.docx
《shell.docx》由会员分享,可在线阅读,更多相关《shell.docx(11页珍藏版)》请在冰豆网上搜索。
shell
在前面的学习中,我们认识了Linux下的一些命令,能够使用命令在Linux下完成任务。
我们输入命令,操作系统执行命令并输出结果。
通过命令,我们控制计算机完成一件又一件任务。
那么Linux是如何解析命令、执行命令并返回结果的呢?
接下来我们将学习相关知识并会接触到一个如雷贯耳的名字——Shell。
什么是Shell
在解释什么是Shell之前,我们先来重新检视一下用户与计算机系统的关系。
我们知道计算机的运作不能离开硬件,在计算机刚出来时用户通过命令直接控制硬件完成任务。
但是这样不安全也特麻烦,于是就在硬件的上面加了一层,用户就不需要直接跟硬件打交道了。
这一层就叫操作系统也就是所谓的Kernel,比如Linux、Windows、FreeBSD等等。
但是Kernel是非常脆弱的,如果用户输入某个无效或无害的命令导致Kernel不能运行,那么计算机也就无法工作了。
于是人们在Kernel上又加了一层,叫做Shell。
如图1所示。
Shell翻译过来就是壳的意思,一个是核,一个是壳,壳包裹着核,Shell把Kernel包裹在里面。
图1、硬件、内核和用户的关系
Shell就是一个命令解释器,一个程序,一个介于用户和操作系统内核间的接口。
从图1可以看出,用户和Linux操作系统内核之间的对话必须通过Shell。
那么这个关系是怎么建立起来的呢?
当我们启动一台基于Linux的计算机时,Linux的内核就会被调入计算机的内存里,一旦内核调入内存,它就准备执行用户的请求。
然而用户首先得登录,然后才能发出请求。
内核也必须知道用户是谁以及如何和他通话。
为了做到这一点,内核调用两个特殊的程序:
getty和login。
getty显示一个登录提示,接着等待用户名和密码的输入。
当getty取得用户输入时,它便调用login程序。
login程序建立用户身份并验证用户密码,如果密码不正确则返回getty。
如果正确,login调用password文件里用户条目中所记录的程序并把控制交给它。
到此为止,计算机就准备好为用户服务了。
下图是登陆Linux后,屏幕上出现的提示符:
小提示:
在/etc/passwd文件中,每个用户对应的最后一项,就是用户登陆之后,要执行的程序。
Shell的分类
Shell基本上可以分为两大类:
图形界面Shell和命令行式Shell。
图形界面Shell应用最广泛的就是WindowsExplorer,Linux下图形界面Shell有GNOME、Xwindows和CDE等。
传统意义上的Shell指的是命令行式Shell,以后如不特别指明,Shell是指命令行式Shell。
在Unix环境下,使用最广泛的Shell是BourneShell、CShell、KornShell和BashShell。
BourneShell也称sh,是Version7Unix默认的UnixShell。
CShell是由柏克莱大学的BillJoy设计依附于BSD中的Shell,由于这个Shell的语法类似于C语言,所以得名CShell,简称csh。
KornShell是由贝尔实验室的DavidKorn早期编写,它完全向上兼容BourneShell并包含了许多CShell的特性,简称ksh。
BashShell是Linux默认的Shell,是与sh兼容并且根据一些使用者的需求而加强的Shell。
作为GNU计划中重要的程序之一,BashShell具有以下特点:
命令记忆功能、命令补全功能、命令别名设定功能、工作控制,背景前景控制和程序脚本。
Shell的功能
Shell的种类这么多,我们到底选择哪种Shell好呢?
其实你选择任何一种Shell都可以,因为Shell都有一个共同的目标:
为用户提供一个界面。
为了达到这个目标,所有的Shell都具有以下的基本功能:
命令行解释
运行程序
输入输出重定向
管道连接
变量维护
环境控制
Shell编程
接下来我们就会学习这些基本功能并且自己动手实现部分基本功能,自己动手做一个简单的Shell。
通过前面的学习我们知道了什么是Shell以及它的分类和功能,现在我们将一起学习Shell是怎么执行程序的,也就是Shell的第一个功能。
在这里我们首先理解Shell怎么执行程序,学习相关知识(什么是进程、如何创建进程、如何运行程序),然后实现这个功能,这样一个简单的Shell就诞生了(我把这个Shell叫AShell,简称ash)。
什么是进程
Linux是如何运行程序的?
这看起来很容易:
首先登录,然后Shell打印提示符,输入命令并按回车键,程序立即就开始运行了。
当程序结束后,Shell打印一个新的提示符。
但这些是如何实现的呢?
Shell做了些什么?
内核又做了些什么?
程序是什么?
运行一个程序意味着什么?
一个程序是存储在文件中的机器指令序列。
运行一个程序意味着将这个机器指令序列载入内存然后让处理器逐条执行这些指令。
在Linux术语中,一个可执行程序是一个机器指令及其数据的序列。
一个进程是程序运行时的内存空间和设置,它由进程控制块、程序段和数据段三部分组成。
简单的说,进程就是运行中的程序。
我们可以使用ps命令查看用户空间的内容,这个命令会列出当前的进程。
这里有两个进程在运行:
bash(Shell)和ps命令。
每个进程都有一个可以唯一标识它的数字,称为进程ID。
一般简称PID。
ps有很多选项,和ls命令一样,ps支持-a、-l选项。
-a选项列出所有进程,但是不包括Shell;-l选项用来打印更多细节。
[
名为S的一列表示各个进程的状态。
S列的值为R说明该进程正在运行,值为S说明该进程处于睡眠状态。
每个进程都属于由UID指明的用户,每个进程都有一个进程ID,同时也有一个父进程ID(PPID)。
标记为PRI和NI的列分别是进程的优先级和niceness级别。
标记为TTY的列表示与进程相连的终端,值为?
就表示该进程为系统进程。
Shell是如何运行程序的
Shell打印提示符,用户输入命令,Shell就执行这个命令,然后Shell再次打印提示符——如此反复。
那么Shell到底是怎么运行程序的呢?
一个Shell的主循环执行下面的4步:
1、接受命令
2、建立一个新的进程来运行这个命令
3、将程序从磁盘载入
4、程序在它的进程中运行直到结束
例如我们依次输入ls和ps命令,那么下图就表示事件发生的次序。
Shell从用户那读入字符串ls。
Shell建立一个新的进程,接着在新进程中运行ls程序并等待新进程结束。
然后Shell读入新的一行输入,建立一个新进程,在这个进程中运行程序并等待这个进程结束。
从图2可以看出,要实现这个流程,我们就需要解决三个问题:
如何建立新进程,父进程如何等待子进程结束以及如何在一个程序中运行另一个程序。
如何建立新进程
我们可以使用fork()系统调用来创建进程。
由fork创建的新进程被称为子进程。
fork成功调用后,就会存在两个进程,一个是父进程,另一个是子进程。
子进程是新进程,是父进程的副本,简单的说父进程有的东西子进程也都复制了一份。
下图显示了进程调用fork前后发生了什么。
从图上可以看出,内核通过复制父进程来创建子进程,它将父进程的代码和当前运行到的位置都复制给子进程。
其中当前运行的位置是由随着代码向下移动的箭头表示的。
子进程从fork返回的地方开始运行。
fork返回后,父子进程有相同的代码,运行到同一行有相同的数据和进程属性。
这时我们通过fork的返回值来分辨父子进程。
不同的进程,fork的返回值是不同的。
在子进程中fork返回0,在父进程中fork返回子进程的pid。
父进程如何等待子进程结束
进程调用wait可以等待子进程结束,使用方法是:
pid=wait(&status);这里系统调用wait做两件事。
首先,wait暂停调用它的进程直到子进程结束。
然后,wait取得子进程结束时exit的值。
下图显示了wait是如何工作的。
当子进程调用exit,内核唤醒父进程同时将传递exit的参数。
图中从exit的括号到父进程的箭头表示唤醒和传递exit值的动作。
这样wait执行两个操作:
通知和通信。
通知就是告诉父进程子进程已经结束了,wait的返回值是调用exit的子进程的PID,因此父进程总是可以找到是哪个子进程终止了。
通信就是告诉父进程子进程是以何种方式结束的,终止进程的终止状态通过wait的参数返回。
wait系统调用的参数status是一个整形指针,如果status不是一个空指针,则终止进程的终止状态就存放在它所指向的单元内。
一个进程有3种结束方式:
1、顺利完成它的任务。
在Linux中,成功的程序调用exit(0)或者从main函数中return0。
2、进程失败。
程序遇到问题而要调用exit退出时,程序需要传给exit一个非零的值。
这个值由程序员分配。
3、程序被一个信号杀死。
通常情况下,一个既没有被忽略又没有被捕获的信号会杀死进程。
如何在一个程序中运行另一个程序
在Linux系统中一个函数族可以解决这个问题——exec函数族(下面简称exec)。
exec一共包含六个成员函数,每个函数都通过系统调用execve来调用内核服务。
当进程调用exec执行一个程序时,exec系统调用从当前进程中把当前程序的机器指令清除,然后在空的进程中载入调用时指定的程序代码,最后运行这个新的程序。
也就说exec就像换脑,原来的进程被将要执行的程序替换。
下面是这个函数族的一些成员简介:
intexecl(char*pathname,char*arg0,...,argn,(char*)0)
intexecv(char*pathname,char*argv[])
intexecle(char*pathname,char*arg0,...,argn,(char*)0,envp)
intexecve(char*pathname,char*argv,char*constenvp[])
intexecvp(char*filename,char*argv[])
intexcelp(char*filename,char*arg0,…,argn,(char*)0)
我们这里主要使用的是execvp函数,execvp有两个参数:
要运行的程序名和那个程序的命令行参数数组。
当程序运行时命令行参数以argv[]传给程序。
注意:
将数组的第一个元素置为程序的名称,最后一个元素必须是null。
execlp不像execvp那样用一个参数数组。
execlp和execvp中的p代表路径(path),这两个函数在环境变量PATH中列出的路径中查找由第一个参数指定的程序。
除了不在PATH中查找程序文件外,execv和execvp非常相似。
这6个exec函数的参数很难记忆,我们可以根据函数名中的字符来记忆。
字母p表示该函数取filename作为参数,并且用PATH环境变量寻找可执行文件。
字母l表示该函数取一个参数表,v表示该函数取一个argv[]数组。
最后,字母e表示该函数取envp[]数组,而不是当前环境。
到此,我们就可以写出一个简单Shell了。
这个简单Shell实现了Shell三大功能中的运行程序的功能,我们这里把它编译成ash。
ash接受命令名称、参数列表、运行命令、报告结果,然后再重新接受和运行其他程序。
这里ash由于没有实现管理输入输出的功能,因此用户还不能在一行中输入所有参数。
下面是相关源码:
1.#include
2.#include
3.#include
4.#include
5.#define MAXARGS 20
6.#define ARGLEN 100
7.main()
8.{
9. char *arglist[MAXARGS+1];
10. int numargs;
11. char argbuf[ARGLEN];
12. char *makestring();
13. numargs = 0;
14. while
(1)
15. {
16. printf('Arg[%d]?
', numargs);
17. if ( fgets(argbuf, ARGLEN, stdin) && *argbuf !
= '/n' )
18. arglist[numargs++] = makestring(argbuf);
19. else
20. {
21. if ( numargs > 0 )
22. {
23. arglist[numargs]=NULL;
24. execute( arglist );
25. numargs = 0;
26. }
27. }
28. }
29. return 0;
30.}
31.execute( char *arglist[] )/* * 使用 fork、execvp和wait实现 */
32.{
33. int pid,exitstatus;
34. pid = fork();
35. if(pid < 0) //创建进程失败
36. {
37. perror('fork failed');
38. exit
(1);
39. }else if(pid == 0) //子进程执行代码
40. {
41. execvp(arglist[0], arglist);
42. perror('execvp failed');
43. exit
(1);
44. }else //父进程执行代码
45. {
46. while( wait(&exitstatus) !
= pid )
47. ;
48. printf('child exited with status %d,%d/n',exitstatus>>8, exitstatus&0377);
49. }
50.}
51.char *makestring( char *buf )/* * 去掉换行符并且为字符串申请存储空间 */
52.{
53. char *cp;
54. buf[strlen(buf)-1] = '/ 0';
55. cp = (char *)malloc( strlen(buf)+1 );
56. if ( cp == NULL )
57. {
58. fprintf(stderr,'no memory/n');
59. exit
(1);
60. }
61. strcpy(cp, buf);
62. return cp;
63.}
前面我们写了一个简单的Shell,这个Shell能够接收用户的命令并运行命令。
不过这个Shell有个不足就是用户输入命令的时候不能一次输入全部的命令,只能把命令和参数分开输入。
这样子对用户极不友好,下面我们就来解决这个问题,方法就是添加命令行解析的功能。
命令行解析功能说白了就是解析字符串,把字符串中包含的命令和参数分开,放入字符数组中,作为execlp系统调用的参数。
为了实现这个功能,我们需要一个接受用户输入的命令行参数的函数,一个将命令行分解成参数数组的函数。
下面是程序主函数:
viewplain
1.int main()
2.{
3. char *cmdline, *prompt, **arglist;
4. int result;
5. signal(SIGINT,SIG_IGN);
6. signal(SIGQUIT,SIG_IGN);
7. while((cmdline = next_cmd(prompt,stdin)) !
= NULL)
8. {
9. if((arglist = spitline(cmdline)) !
= NULL)
10. {
11. result = execute(arglist);
12. freelist(arglist);
13. }
14. free(cmdline);
15. }
16. return 0;
17.}
下面就分别来解说这三个函数:
1、next_cmd()函数从输入流中读入下一个命令,它调用malloc来分配内存以接受任意长度的命令行。
碰到文件结束符,它返回NULL。
2、splitline()函数将一个字符串分解为字符串数组,并返回这个数组。
它调用malloc来分配内存以接受任意个数参数的命令行。
这个数组由NULL标记结束,是execute()函数的输入。
3、execute()函数使用fork、execvp和wait来运行一个命令,返回命令的结束状态。
Shell中的流程控制
前面我们实现了Shell的两个主要功能:
运行命令和处理命令行。
现在我们来简单实现Shell的第三个功能——可编程。
任何一种编程语言都需要对流程进行控制,Shell也不例外,这里就简单实现在Shell中如何提供if控制语句。
Shell中的if语句的作用与其他语言的if语句相同:
条件检测。
如果条件为正值,则有一部分代码被执行。
不过这里有一点与其他语言不同,在Shell中,if语句有以下特点:
1、条件是一个命令,返回正值意味着命令运行成功。
2、exit(0)代表成功。
3、如果if后的条件是一系列的命令,那么最后一个命令的exit值被用作这个语句块的条件值,并由此来决定条件是否成立。
在Shell中if的工作流程主要如下:
1、Shell运行if之后的命令
2、Shell检查命令的exit状态
3、exit的状态为0意味着成功,非0意味着失败
4、如果成功,Shell执行then部分的代码
5、如果失败,Shell执行else部分的代码
6、关键字fi标识if块的结束
在Shell中增加if
现在已经知道if控制语句做什么,也知道它是如何工作的。
那么如何在Shell中增加if语句呢?
在前面的版本中,Shell的控制流从splitline直接到fork,每个命令都被直接传给exec。
新增if语句后命令处理变得复杂,我们这里用process函数来处理。
process将脚本看作一个接一个的代码区域。
第一个区域是then代码块,第2个区域是else代码块,第3个是在if语句之外的代码块。
如下图所
114.jpg
对于不同的区域,Shell的处理方法是不同的。
1、if语句之外的区域,称为中立区。
对于这类区域的代码,简单地度一条,分析一条,执行一条。
2、if和then之间的区域。
这个区域中,Shell每执行一条命令就记录下它的退出状态
3、then到fi或else之间的区域。
这个区域中,Shell如果遇到if语句就重复第2个处理方法。
4、else到fi之间的区域。
在fi后又回到中立区。
Shell记录当前区域类型,还必须记录在WAIT_THEN区域中执行命令的结果。
不同区域的处理方法不同,特定的区域与程序的特定状态联系在一起。
proces通过3个函数来处理区域问题。
is_control_command返回一个布尔变量告诉process这条命令是脚本语言的一部分还是一条可执行的命令。
do_control_command处理关键字if、then和fi,每个关键字都是区域的界标。
这个函数更新状态变量并执行必要的操作。
ok_to_execute根据当前的状态和条件命令的结果返回一个布尔值,说明能否执行当前命令。