博客中的文章均为meelo原创,请务必以连接形式注明本文地址html
Shell Lab是CMU计算机系统入门课程的一个实验。在这个实验里你须要实现一个shell,shell是用户与计算机的交互界面。普通意义上的shell就是能够接受用户输入命令的程序。它之因此被称做shell是由于它隐藏了操做系统低层的细节。完成Shell Lab你会对shell有更加深刻的认识,并熟悉Linux的多进程编程方法。linux
编程实现是一种绝佳的学习方式,然而就像这个实验同样,不少很好的课程做业都隐藏在互联网当中。大多数人难以经过这种方式来学习,这篇文章的目的接就是介绍给你这个绝佳地学习Linux编程的方式,让这个学习的过程变得稍微简单一点。git
项目实现的shellgithub
Shell介绍shell
Shell会打印出提示符,等待来自stdlin的输入,根据输入执行特定地操做,这样就产生了一种错觉,彷佛输入的文字(命令行)控制了程序的执行。编程
命令行是一串ASCII字符由空格分隔。字符串的第一个单词是一个可执行程序,或者是shell的内置命令。命令行的其他部分是命令的参数。若是第一个单词是内置命令,shell会当即在当前进程中执行。不然,shell会新建一个子进程,而后再子进程中执行程序。新建的子进程又叫作做业。一般,做业能够由Unix管道链接的多个子进程组成。bash
若是命令行以&符号结尾,那么做业将在后台运行,这意味着在打印提示符并等待下一个命令以前,shell不会等待做业终止。 不然,做业在前台运行,这意味着shell在做业终止前不会执行下一条命令行。 所以,在任什么时候候,最多能够在一个做业中运行在前台。 可是,任意数量的做业能够在后台运行。例如,键入命令行:网络
sh> jobsapp
会让shell运行内置命令jobs。键入命令行ide
sh> /bin/ls -l -d
会致使shell在前台运行ls程序。根据约定,shell会执行程序的main函数
int main(int argc, char *argv[])
argc和argv会接收到下面的值:
argc == 3,
argv[0] == ‘‘/bin/ls’’,
argv[1]== ‘‘-l’’,
argv[2]== ‘‘-d’’.
下面以&结尾的命令行会在后台执行ls程序
sh> /bin/ls -l -d &
Unix shell支持做业控制的概念,容许用户在前台和后台之间来回移动做业,并更改进程的状态(运行,中止或终止)。在做业运行时,键入ctrl-c会将SIGINT信号传递到前台做业中的每一个进程。SIGINT的默认动做是终止进程。相似地,键入ctrl-z会致使SIGTSTP信号传递给全部前台进程。 SIGTSTP的默认操做是中止进程,直到它被SIGCONT信号唤醒为止。Unix shell还提供支持做业控制的各类内置命令。例如:
jobs:列出运行和中止的后台做业。
bg <job>:将中止的后台做业更改成正在运行的后台做业。
fg <job>:将中止或运行的后台做业更改成在前台运行。
kill <job>:终止做业。
实验的流程
实验和配套的教材《深刻理解计算机系统》是紧密相关的,在网络上还能够找到CMU使用这本教材的教学视频。我没有阅读教材,只是把对应的视频看了一遍。
实验提供了初始文件,包括不少辅助函数,这样你就只须要实现shell最为核心的部分。
在作实验以前,须要阅读实验说明,对实验的总体有一个初步的认识。也就是说你须要了解你须要实现什么功能,大致会须要什么样的函数。
你须要编写的文件是tsh.c,所以须要把这个文件里的程序阅读一遍,了解提供了哪些辅助函数。
此外实验还提供了测试用例以及标准的shell实现,这样你就能够对比你的实现结果是否与标准的结果一致。这是一个绝佳的调试方法,也是攻破这个实验的一条路径,先解决第1个测试用例,而后第2个……这样你就不用担忧无从下手了。
测试函数调用了myint、myspin和mysplit程序,所以你也须要阅读一遍。
难点
编程须要遵循良好的编程规范,其中一个就是检查函数的返回值,一般系统函数会使用返回值0或-1表示执行错误。虽然大多数状况下都不会出现问题,可是一旦出错检查返回值可以让你快速发现错误的源头。在csapp.h头文件里,不少系统函数有一个头文件大写的函数,与原有的系统函数拥有一样的参数,可是合理地检查了返回值。
在fork新的进程时,有可能发生竞争条件。子进程很快结束了运行,发送SIGCHLD给主进程,进而回收子进程同时从做业列表中删除该做业。可是此时,主进程还没来得及将做业加入做业列表。解决方案是在主进程将做业加入做业列表以前屏蔽该信号,完成后再恢复该信号。须要注意的是子进程会继承屏蔽的信号,所以在子进程也须要恢复。
另外一个难点是SIGCHLD的信号处理函数,若是你没有正确处理,有可能会没法经过最后一个测试用例。
问题之一:有可能多个子进程结束,主进程却只接收到一次信号。主进程没法知道有多少个子进程结束了。
解决方案:将waitpid置于while循环中,并传入参数WNOHANG。参数WNOHANG表示,若是没有须要回收的进程了,会返回0,若是回收了子进程会返回子进程的pid,经过判断返回值就能够结束循环。
问题之二:waitpid默认只有当正常结束才会返回,若是是被其它进程kill或中止是不会返回的,这样shell就无从知晓子进程是否结束了。
解决方案:传入WUNTRACED参数给waitpid。这样子进程正常结束、被kill或者是中止都会返回。咱们就须要一种方式判断子进程究竟是由何种方式结束的,这个信息能够在waitpid的status参数中获得。status是一个整数,不一样的值表示不一样的返回状态,有一系列的宏能够判断是否status是某种状态。好比WIFEXITED(status)能够判断是否正常结束,WIFSIGNALED(status)能够判断是否被终止,WIFSTOPPED是否被中止。全部的信息均可以在man页面找到。
实验的说明里提到,前台进程与后台进程的惟一区别是shell会等待前台进程,所以前台进程只有一个。waitfg实现了这一等待的功能。最显而易见的选择是用waitpid等待前台进程的结束。那么你须要像SIGCHLD信号处理函数那样考虑各类复杂的进程结束条件,所以这不是最佳选择。最佳选择是使用sleep函数,只要前台进程仍然是须要等待的进程,主进程就sleep。那么sleep多长时间呢,sleep(0)是最佳的选择,0表示进程会让其它进程来执行,若是没有其它的进程在执行会继续执行。这样总会有进程再执行,而不会出现CPU空转的状况。
从实验说开去
从实验中咱们明确区分了两类命令:内置命令和可执行程序。内置命令直接执行,不须要进行做业管理,可执行程序须要建立一个可执行程序来执行。那么对于一个真实的shell来讲,有哪些内置命令。下面列出来bash的部份内置命令。shell内置命令大体能够分为4类,经过type命令能够显示命令的类型,type本身就是一个内置命令:
A.2.1 bash内置命令 .:执行当前进程环境中的程序。同source。 . file:dot命令从文件file中读取命令并执行。 : 空操做,返回退出状态0。 alias:显示和建立已有命令的别名。 bg:把做业放到后台。 bind:显示当前关键字与函数的绑定状况,或将关键字与readline函数或宏进行绑定。 break:从最内层循环跳出。 builtin [sh-builtin [args]]:运行一个内置Shell命令,并传送参数,返回退出状态0。当一个函数与一个内置命令同名时,该命令将颇有用。 cd [arg]:改变目录,若是不带参数,则回到主目录,带参数则切换到参数所指的目录。 command comand [arg]:即便有同名函数,仍然执行该命令。也就是说,跳过函数查找。 declare [var]:显示全部变量,或用可选属性声明变量。 dirs:显示当前记录的目录(pushd的结果)。 disown:从做业表中删除一个活动做业。 echo [args]:显示args并换行。 enable:启用或禁用Shell内置的命令。 eval [args]:把args读入Shell,并执行产生的命令。 exec command:运行命令,替换掉当前Shell。 exit [n]:以状态n退出Shell。 export [var]:使变量可被子Shell识别。 fc:历史的修改命令,用于编辑历史命令。 fg:把后台做业放到前台。 getopts:解析并处理命令行选项。 hash:控制用于加速命令查找的内部哈希表。 help [command]:显示关于内置命令的有用信息。若是指定了一个命令,则将显示该命令的详细信息。 history:显示带行号的命令历史列表。 jobs:显示放到后台的做业。 kill [-signal process]:向由PID号或做业号指定的进程发送信号。输入kill-l查看信号列表。 let:用来计算算术表达式的值,并把算术运算的结果赋给变量。 local:用在函数中,把变量的做用域限制在函数内部。 logout:退出登陆Shell。 popd:从目录栈中删除项。 pushd:向目录栈中增长项。 pwd:打印出当前的工做目录。 read [var]:从标准输入读取一行,保存到变量var中。 readonly [var]:将变量var设为只读,不容许重置该变量。 return [n]:从函数中退出,n是指定给return命令的退出状态值。 set:设置选项和位置参量。 shift [n]:将位置参量左移n次。 stop pid:暂停第pid号进程的运行。 suspend:终止当前Shell的运行(对登陆Shell无效)。 test:检查文件类型,并计算条件表达式。 times:显示由当前Shell启动的进程运行所累计用户时间和系统时间。 trap [arg] [n]:当Shell收到信号n(n为0、一、2或15)时,执行arg。 type [command]:显示命令的类型,例如:pwd是Shell的一个内置命令。 typeset:同declare。设置变量并赋予其属性。 ulimit:显示或设置进程可用资源的最大限额。 umask [八进制数字]:用户文件关于属主、属组和其余用户的建立模式掩码。 unalias:取消全部的命令别名设置。 unset [name]:取消指定变量的值或函数的定义。 wait [pid#n]:等待pid号为n的后台进程结束,并报告它的结束状态。
处理做业:bg fg jobs disown kill wait stop
文件系统:cd pwd dirs pushd popd
变量相关:let local readonly printf var declare
命令相关:history type alias help unalias hash
函数相关:return shift
用实现的shell执行程序,必须给出程序的完整路径,好比须要执行ls须要输入/bin/ls。那么bash是如何肯定该执行那个程序的呢?下面给出的两篇文章解释得很是清楚。shell会以必定的顺序搜索命令,若是找到了命令就执行,没找到会返回错误信息。
shell搜索变量的顺序
https://www.cyberciti.biz/tips/how-linux-or-unix-understand-which-program-to-run-part-i.html
https://www.cyberciti.biz/tips/an-example-how-shell-understand-which-program-to-run-part-ii.html