CSAPP Shell Lab

框架
实验框架
本实验目的是通过Linux信号函数实现简单的Shell
main:
在main程序中,主要是使用`signal`注册当内核向Shell程序发送信号时,应该调用的处理函数
然后使用while循环,每一次循环打印shell提示符tsh>
,以及等待用户输入,用户输入完成后调用eval进行解析用户命令
while循环在执行quit内置命令时,直接执行exit(0),停止Shell程序。
eval:
eval函数中解析命令并调用函数执行命令,调用`parseline`函数解析命令,并返回执行是在bg还是在fg
调用命令builtin_cmd
判断是否内置命令,如quit
,bg
,fg
,jobs
.如果是则在builtin_cmd
函数中调用相应实现函数直接在Shell程序执行,不用用fork
,execve
等。执行完后直接下一轮main中的while
然后builtin_cmd
会返回是否是内置命令,不是内置命令然后就要fork
并execve
。
如果是fg则调用waitfg
函数,等到前台命令执行完成
ctrl+c ctrl+z
这个操作是让前台的进程停下
当我们按下ctrl+c ctrl+z,是Shell程序注册的signal捕捉到了,然后执行相应操作
sigint_handler
和 sigtstp_handler
都使用kill
向前台进程发送信息。
前台进程因为是Shell程序的子进程,所以当其停止或终止时都会向Shell程序发送SIGCHLD
信号,这个信号会被Shell程序中通过signal
注册的sigchld_handler
处理函数捕获
在sigchld_handler
处理函数中,我会根据导致子进程发送SIGCHLD
信号的原因而进行相应处理
知识框架
异常是控制流的突变(如执行完第Ii条指令后,本应该执行第Ii+1条,然而突然跳到执行第Ij条指令)
事件是状态变化
系统为了能够使对系统状态的变化做出反应,则使控制流发生突变来应对这些情况
这些突变,我们称为异常控制流
异常可以归类为4种
- 中断
受到外部设备的影响,而导致的状态改变,如I/O设备的信号 - 陷阱
程序发生要执行系统调用的请求 - 故障
潜在可恢复的错误 - 终止
不可恢复的错误
每一种异常都有对应的异常处理程序,当异常发生时,通过异常号+异常表(地址在CPU的异常表基址寄存器中)可以得到异常处理程序的代码 P503
异常处理程序运行在内核中
以上是硬件层与操作系统层之间协作对异常的处理
在操作系统层和应用层之间也有对异常的处理
这里的异常主要是进程接受到了来自内核的信号,并对此进行处理
C语言知识点
strcmp
strcmp
是 C 语言标准库中的一个函数,用于比较两个字符串。它通常在 <string.h>
头文件中声明。strcmp
函数的原型如下:
int strcmp(const char *str1, const char *str2);
其中 str1
和 str2
是要比较的两个 C 字符串。
strcmp
函数会按照字典顺序逐个比较两个字符串中对应位置的字符,直到找到不同的字符或者遇到字符串结束符(\0'
)。它会返回一个整数值来表示比较结果:
- 如果
str1
小于str2
,则返回一个负整数(通常是 -1)。 - 如果
str1
等于str2
,则返回0。 - 如果
str1
大于str2
,则返回一个正整数(通常是 1)。
不过要注意的是在C语言中,字符串只能够通过
双引号
包裹,否则会报错,如strcmp(argv[0], 'bg')
是错误的
sprintf
sprintf
函数用于将格式化的数据写入字符串缓冲区中,其用法与 printf
类似,不同之处在于 printf
将格式化的结果输出到标准输出流(通常是屏幕),而 sprintf
则将结果写入指定的字符串缓冲区中。
下面是 sprintf
函数的基本用法:
#include <stdio.h> int sprintf(char *str, const char *format, ...);
str
:指向用于存储结果的字符串缓冲区的指针。format
:格式化字符串,包含了要写入到str
中的文本以及可选的格式化说明符。...
:可变参数,根据format
字符串中的说明符来提供要格式化的值。
需要注意的是,在sprintf中如果想要转义
%
符号,需要用%%
,如sprintf(str,"%s command requires PID or %%jobid argument", argv[0]);
那么就会输出%jobid
这个字符串了
sscanf
在 C 语言中,sscanf()
函数是用于从字符串中读取格式化输入的函数,它的工作方式类似于 scanf()
,但是从字符串而不是标准输入中读取数据。
对于 sscanf(argv[1], "%d", &jid);
这行代码,它会尝试从字符串 argv[1]
中按照 %d
的格式读取整数,并将读取到的整数赋值给变量 jid
。
如果 argv[1]
的值是 "abc",则 sscanf()
函数会失败,因为 "abc" 无法被解释为一个整数。在这种情况下,sscanf()
函数会返回 0,表示没有成功匹配到任何项目,并且变量 jid
的值将保持不变,即不会被赋予任何有效的整数值。
致命错误点
没有看清楚并用上框架已经实现的代码,导致代码臃肿
比如官方明明已经实现了,获取当前状态是FG
的pid
然而我却一遍又一遍地写着如下代码
检查已回收子进程的退出状态
如果说上面的错误只是使代码重复,臃肿那么我这条信息的错误导致了我的ctrl+z
完全不能使用了。
首先要知道,当父进程向子进程发送信号SIGINT
(ctrl+c)或SIGSTP
(ctrl+z)时,子进程会停止或终止,这个时候子进程都会向父进程发送信号SIGCHLD
那么问题来了,父进程在sigchld_handler
函数中应该如何去辨别子进程是因为SIGINT
而发来信号,还是SIGSTP
。毕竟这关乎到是要把子进程移除job列表还是简单地改变下状态
同时还要判断下这个子进程是否是正常终止的
好吧,当时我完全忘记了这点,只是将代码简单地写成了如下,造成的其他地方代码的影响也就不多说了
在书本上P517
pid_t waitpid(pid_t pid, int *statusp, int options); 如果statusp非空,那么waitup就会在status中放上关于导致返回的子进程的状态信息 WIFEXITED(status): 子进程通过调用exit或者return正常终止就返回真 WIFSIGNALED(status): 如果一个子进程是因为一个未被捕获的信号而终止的,那么就返回真。(可以用来判断子进程是否是因为接受了SIGINT信号而终止的) WIFSTOPPED(status): 如果引起返回的子进程当前是停止的,就返回真(可以用来判断子进程是否是因为接受了SIGISTP信号而终止的)
unix_error的使用
在程序运行中使用unix_error
会导致程序整个退出,在本实验中基本上在shell程序是不要用到unix_error的,因为如果出现一点错误就退出shell是十分不友好的
我这里就有个错误使用方式:
在return之前如果上了锁记得解锁
可能每一个人实现的方式不同,但是下面我这样的
这些锁我经常忘记解开
既然封装了函数就要理解他
当我在测试的时候,发现我有些样例会与标准答案多好多测试
后来才发现是因为我封装了Execve
函数
但是他出问题,也就是命令找不到时,我直接返回了,因为是在Fork后调用的Execve,导致子进程会运行我父进程的代码,真糟糕!
所以Execve要改下,如果出错,进程要退出
实验知识点
原则
-
检查系统调用错误,并处理,在处理程序中保存并恢复errno (P512,P536)
当Unix系统级函数遇到错误时,它们总是会返回-1
并设置全局整数变量errno来表示什么出错了
fprintf(stderr,"%s\n",strerror(errno))
像书本上的示例代码,其中如
Fork
,Execve
,Kill
,Sleep
,Waitpid
,Sigprocmask
,Sigemptyset
,Sigfillset
,Sigaddset
,Sigdelset
等,开头字母大写表示他们是对原函数进行错误处理分装后的函数
有个特殊的函数是sighandler_t signal(int signum, sighandler_t handler);
它是出错则返回SIG_ERR,而不设置errno P531,但是这不影响我们可以分装原函数对其进行错误检查 P532注意在处理函数(即signal函数中写的信号函数)中,保存旧errno,返回时恢复旧errno(P536,P537)
-
访问共享资源时 或 在并发时需要按某种顺序执行时,使用
Sigprocmask
进行上锁
书上P541的一个惨痛案例,是addjob与deletejob函数之间的竞争,我们希望是在新fork一个进程后,对于这个新的进程,addjob应该在deletejob之前执行 -
正确的信号处理,如果存在一个未处理的信号就表明至少一个信号到达了
在书本上P537上的一个错误案例:
if (waitpid(-1,NULL,0) < 0){...}
, 这样会导致信号不会排队(P536)的问题,而导致信号丢失。
于是一个正确的写法是:while(waitpid(-1,NULL,0) > 0){...}
,同时可以利用errno来判断是否出错。P517,P539 -
以及写在P535,P536的一堆原则
显示地等待信号 sigsuspend
想想在writeup中给我们在waitfg
和sigchld_handler
之前,哪个用waitpid
的提示
One of the tricky parts of the assignment is deciding on the allocation of work between the waitfg
and sigchld handler functions. We recommend the following approach:
– In waitfg, use a busy loop around the sleep function.
– In sigchld handler, use exactly one call to waitpid.
然而waitfg有着更好的写法
在书P545上,描写着使用sleep
等待太慢,pause
会被信号中断的缺陷,于是有了sigsuspend
测试
毒瘤测试点
我的错误测试结果:
正确结果:
改动:
自动测评
写一个shell脚本,因为主要是进程号不同,所以用sed将进程号替换成1000,diff比较文件的不同
可以很清楚的看到差别
Shell Lab完结!
Code and Notebook
本文作者:次林梦叶的小屋
本文链接:https://www.cnblogs.com/cilinmengye/p/18092929
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步