CMU15-213 Shell Lab实验记录

前言

The purpose of this assignment is to become more familiar with the concepts of process control and signaling. You’ll do this by writing a simple Unix shell program that supports job control.

这个实验使你更加熟悉进程控制和信号的知识。你将通过编写一个简单的,能够完成作业控制的Unix Shell来掌握它们。

下载资料

这里

异常控制流

在这里插入图片描述这是文档中的第一个提示:认真阅读CSAPP的第八章,因为我还没读过,所以在实验之前先把从头到尾把这一章读一遍。

异常

我的理解:
首先有一个控制流的概念,流就是序列,那么控制流就是一串控制指令的序列。而异常指的是控制流的突变,也就是在平滑的顺序执行过程中,由异常跳转到异常处理程序中执行。

异常的基本流程

我的理解:
由外部或内部出现异常,处理器进入异常处理程序中(具体的实现依赖于异常表和异常号,比如中断中的向量表和中断处理函数),通过栈进行地址、寄存器的保存,处理完之后从栈恢复现场,继续执行。

异常的类别

我的理解:
要么是外部的,要么是内部的,可以按照触发的原因分成三大类:

  • 中断:外部I/O的请求,异步,一般是处理器处理着一半突然进中断,然后做完之后有继续处理中断发生之前的进程。
  • 陷阱:陷阱反而会误导人,更好的说法应该是系统调用,同步,进程执行时需要请求一些可能会危害操作系统的操作,这时候需要从用户模式过渡到内核模式,内核模式处理完系统调用之后返回到之前的进程。
  • 故障 & 终止:进程运行时发生了错误,根据错误的情况,如果能处理好就是故障返回,不行就直接终止掉该进程。

Linux系统上的异常

亿些细节。

进程

什么是进程?运行程序的实例化;
什么是逻辑控制流?不同进程的PC值(程序计数器的数值,指向进程的指令)的序列。流就是序列。
什么是并发?不同进程在时间维度上的重叠。
进程之所以牛逼,我觉得关键在于虚拟化,让每个进程以为自己独占了CPU,独占了内存。

系统调用错误处理

通过包装错误处理过程,可以有效地减少对系统调用的代码量。

进程控制

fork()

父子进程指向同一片地址空间,但是它们的变量却是独立的,说明在该地址空间中又被划分出两个不同进程对于的私有地址空间。书上的文字和代码不太一致,正确的应该是pid = 0是子进程,而不为0是父进程,那么子进程的子进程pid是多少呢?

wait()

父进程等待子进程的函数,更复杂的有waitpid(),但是应该不大会用得上。

unsigned int sleep(unsigned int secs)

让进程挂起一段时间,并返回还需要挂起的时间

int pause(void)

将进程挂起,直到该进程收到一个信号

int execve(const char* filename, const char* argv, const char* envp[])

execve用于调用一个新的程序,与fork不同,它不会拷贝自身生成一个新的进程,而是在自身重新加载一个新的进程,之后会跳转到新的main(),从这个意义上说,fork()返回两次,而execve()不会返回。而envp[]指的是指向环境字符串的指针,字符串形如"name=value"。在这个基础上,用getenv()查询环境字符串,用setenv()改环境字符串,用unsetenv()删环境字符串。

信号

异常是低层次的,而信号是高层次的,即软件形式的异常。和异常类似,具体的实现依赖于一张信号列表的约定来表示相应时间。

信号术语

信号其实和异常,尤其是系统调用的逻辑很像,只是接收方和发送方调转了下。

发送信号

进程组:进程的集合,或者说序列,同样有个pgid标识,可以通过setpgid()来同时设置进程和进程组的PID。
/bin/kill:向某进程发送某信号

/bin/kill -9 15215	// 向15215进程发送9号信号

如果PID为负,那么认为该PID是PGID,并发送到该进程组的所有进程中去。

int kill(pid_t pid, int sig);	// 向pid进程发送sig信号,如果pid为负,则看作是一个进程组
unsigned int alarm(unsigned int secs);	// 在secs秒后发送SIGALRM信号,同样会终止进程

接收信号

sighandler_t signal(int signum, sighandler_t handler);	// signum是信号,handler是处理类型
// 如果是SIG_IGN,忽略
// 如果是SIG_DFL,默认处理
// 如果是用户定义的函数的地址,那么就是信号处理程序,跳转到该程序执行,这被称为捕获信号

阻塞信号

阻塞包括显式和隐式,隐式即内核默认阻塞任何当前进程正在处理的待处理信号,换句话说,处理过一次,不会再发送;显式即进程自己设置阻塞的信号集合,包括增删查改等。

编写信号处理程序:安全

  1. 处理程序尽可能地简单
  2. 在处理程序中只调用异步信号安全的函数。什么意思?就是只调用那些可重入、且不能被信号处理程序中断的函数
  3. 保存和恢复errorno。因为如果出错会修改errorno,所以要记得保存,就和栈保存寄存器一样的。
  4. 阻塞所有的信号,保护对共享数据结构的访问。此时共享的数据结构就像互斥的临界区,我们需要给它加锁
  5. 用volatile声明全局变量。同样的道理,全局变量也是临界区,保护的机制是每次不缓存修改后的全局变量
  6. 用sig_atomic_t声明标志,使得对标志位的读写是原子性的

编写信号处理程序:正确

关键问题是:如果有一个未处理的信号,并不代表只到达了一个未处理的信号,而是说,至少有一未处理的信号到达了,还可能有被丢弃的。
那么,为了解决这个问题,在响应信号处理时,我们不能将信号和事件个数一一对应,而是每次收到信号,就应该尽可能地处理。具体可以看原书8-36和8-37的例子,说得非常清楚。

编写信号处理程序:可移植

不同的系统对于signal的语义不尽相同,有的需要重复设置siganl,有的在信号处理程序返回后,如果程序被中断了,就无法继续执行。选择包装好的Signal函数就没啥问题了。

同步流

什么是同步流?其实就是找到某种方式,使得程序在并发过程中随我心意地并发执行,比如addjob()和deletejob(),我们为了控制add在delete之前,需要阻塞可以触发delete动作的信号。

显示等待信号

比如在写shell的过程中,我们需要等待一个前台作业完成,才能继续下一步,这时候,我们就要进入一个循环等待,这会非常消耗CPU,为此我们利用以下函数暂时挂起该进程:

int sigsuspend(const sigset_t* mask);

它通过用mask暂时代替现在的阻塞信号集合,当信号处理程序返回时,我们用设置回原来的信号集合。

非本地跳转

C语言级别的catch和throw:

int setjump(jmp_buf env);	// catch
int longjump(jmp_buf env, int retval);	// throw

Hand Out Instructions

Start by copying the file shlab-handout.tar to the protected directory (the lab directory) in which
you plan to do your work. Then do the following:
• Type the command tar xvf shlab-handout.tar to expand the tarfile.
• Type the command make to compile and link some test routines.
• Type your team member names and Andrew IDs in the header comment at the top of tsh.c.

首先下载文件,然后输入:

tar xvf shlab-handout.tar

来压缩文件,使用:

make

来完成编译和链接,在tsh.c文件上写下你的大名。

Looking at the tsh.c (tiny shell) file, you will see that it contains a functional skeleton of a simple Unix shell. To help you get started, we have already implemented the less interesting functions. Your assignment is to complete the remaining empty functions listed below. As a sanity check for you, we’ve listed the
approximate number of lines of code for each of these functions in our reference solution (which includes
lots of comments)

打开tsh.c文件,你会看到它包含了一个简易的shell框架,为了让更好地起步,已经提供了一些比较不那么有趣的函数。你的任务是完成如下列出的函数列表,同时还有完成各个函数大致需要的代码行数。

// 处理和解释命令行的主要函数
eval: Main routine that parses and interprets the command line. [70 lines]
// 处理内置的命令
builtin cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. [25
lines]
// 实现内置的bg和fg命令,bg将一个暂停的后台程序继续执行,fg则是将其调至前台继续执行
do_bgfg: Implements the bg and fg built-in commands. [50 lines]
// 等待一个前台作业完成
waitfg: Waits for a foreground job to complete. [20 lines]
// 捕获SIGCHLD(子进程终止或结束)的信号
sigchld handler: Catches SIGCHILD signals. 80 lines]
// 捕获SIGINT(来自键盘的中断)的信号
sigint handler: Catches SIGINT (ctrl-c) signals. [15 lines]
// 捕获SIGSTP(停止直到下一个SIGCONT)到来
sigtstp handler: Catches SIGTSTP (ctrl-z) signals. [15 lines]

General Overview of Unix Shells

A shell is an interactive command-line interpreter that runs programs on behalf of the user. A shell repeatedly prints a prompt, waits for a command line on stdin, and then carries out some action, as directed by the contents of the command line.

shell是一个通过命令行交互的解释器,基本原理就是等待输入(stdin),然后执行指令。

The command line is a sequence of ASCII text words delimited by whitespace. The first word in the
command line is either the name of a built-in command or the pathname of an executable file. The remaining words are command-line arguments. If the first word is a built-in command, the shell immediately executes the command in the current process. Otherwise, the word is assumed to be the pathname of an executable program. In this case, the shell forks a child process, then loads and runs the program in the context of the child. The child processes created as a result of interpreting a single command line are known collectively as a job. In general, a job can consist of multiple child processes connected by Unix pipes.

shell会检查输入的命令是否为内置的命令行,如果不是就默认是一个可执行的文件进行处理。在第二种情况下,shell会fork出一个子进程,然后加载程序,子进程也被称为作业。作业可以看做是许多子进程在Unix管道连接下形成的集合。

If the command line ends with an ampersand ”&”, then the job runs in the background, which means that the shell does not wait for the job to terminate before printing the prompt and awaiting the next command line. Otherwise, the job runs in the foreground, which means that the shell waits for the job to terminate before awaiting the next command line. Thus, at any point in time, at most one job can be running in the foreground. However, an arbitrary number of jobs can run in the background. For example, typing the command line

如果命令行以&结尾,那么作业在后台运行,否则在前台运行,也就是shell要等待该进程终止才能继续读入下一串命令行。因此,前台最多有一个进程运行,而后台则可以有无数个。
一些例子如下:

tsh> jobs

shell执行jobs内置命令

tsh> bin/ls -l -d

在前台运行ls,如果后面加&就变为后台运行,shell应该将参数处理成这样:

argc == 3
argv[0] == "bin/ls"
argv[1] == "-l"
argv[2] == "-d"

Unix shells support the notion of job control, which allows users to move jobs back and forth between background and foreground, and to change the process state (running, stopped, or terminated) of the processes in a job. Typing ctrl-c causes a SIGINT signal to be delivered to each process in the foreground job. The default action for SIGINT is to terminate the process. Similarly, typing ctrl-z causes a SIGTSTP signal to be delivered to each process in the foreground job. The default action for SIGTSTP is to place a process in the stopped state, where it remains until it is awakened by the receipt of a SIGCONT signal. Unix shells also provide various built-in commands that support job control.

这一段就是介绍了SIGINT和SIGCONT,和前面所说的一样:SIGINT默认终止进程,SIGSTP暂停进程,直到再接收到一个SIGCONT信号。

具体实现过程

trace01

默认就是一样的……

trace02

参考书525页,先处理命令行,然后跳到builtin_cmd查看是否存在内置命令,如果是quit那就直接exit就好
在这里插入图片描述

trace03

运行前台作业,在判断不是内置命令的情况下,写一个execve就好了:
在这里插入图片描述

trace04

要完成后台作业,就需要区分bg和fg进行相应的处理(见后面源代码),然后参考P543添加sigchld的handler:
在这里插入图片描述

trace05

在这里我注意到一个问题,handler里面的waitpid不能像书上写成while类型,要么就会出现后台代码像前台一样的情况。
第五个trace需要在内置命令函数调用一个listjobs即可。
在这里插入图片描述

trace06 & trace07

在sigint_handler函数中用kill()杀掉前台进程,完成第六个和第七个trace。
在这里插入图片描述
在这里插入图片描述

trace08

不管你向子进程发送什么终止还是停止信号,子进程都会回复一个SIGCHLD!,所以记得在SIGCHLD作出新的判断,否则会出错。
在这里插入图片描述

trace09

这里的关键是处理字符串,用sscanf读取argv[1],如果是pid就直接匹配,如果是jid需要加%%来匹配:
在这里插入图片描述

trace10

这一题的问题在于,判断子进程回收不能写fgpid = 当前pid,因为回收之后二者还是相等!
在这里插入图片描述

trace11

没有需要额外添加的内容
在这里插入图片描述

trace12-16

没有添加额外的内容

代码实现链接

我的代码写得不够规范,如果需要修正建议看这一篇


总结

磕磕绊绊,终于算是完成了整个shell的工作,虽然不敢说一定能百分之百地不出错,但是起码shell里面的各种实现逻辑和原理算是把握清楚了……继续加油!

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值