音无结弦之时,天使跃动之心。立于浮华之世,奏响天籁之音。.|

次林梦叶

园龄:3年3个月粉丝:22关注:3

CSAPP Shell Lab

sakura🌸

框架

实验框架

本实验目的是通过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程序执行,不用用forkexecve等。执行完后直接下一轮main中的while

然后builtin_cmd会返回是否是内置命令,不是内置命令然后就要forkexecve

如果是fg则调用waitfg函数,等到前台命令执行完成


ctrl+c ctrl+z


这个操作是让前台的进程停下

当我们按下ctrl+c ctrl+z,是Shell程序注册的signal捕捉到了,然后执行相应操作

sigint_handlersigtstp_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);

其中 str1str2 是要比较的两个 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
image

然而我却一遍又一遍地写着如下代码
image

检查已回收子进程的退出状态

如果说上面的错误只是使代码重复,臃肿那么我这条信息的错误导致了我的ctrl+z完全不能使用了。

首先要知道,当父进程向子进程发送信号SIGINT(ctrl+c)或SIGSTP(ctrl+z)时,子进程会停止或终止,这个时候子进程都会向父进程发送信号SIGCHLD

那么问题来了,父进程在sigchld_handler函数中应该如何去辨别子进程是因为SIGINT而发来信号,还是SIGSTP。毕竟这关乎到是要把子进程移除job列表还是简单地改变下状态

同时还要判断下这个子进程是否是正常终止的


好吧,当时我完全忘记了这点,只是将代码简单地写成了如下,造成的其他地方代码的影响也就不多说了
image


在书本上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是十分不友好的

我这里就有个错误使用方式:
image

在return之前如果上了锁记得解锁

可能每一个人实现的方式不同,但是下面我这样的
image
这些锁我经常忘记解开

既然封装了函数就要理解他

当我在测试的时候,发现我有些样例会与标准答案多好多测试

image

后来才发现是因为我封装了Execve函数
image

但是他出问题,也就是命令找不到时,我直接返回了,因为是在Fork后调用的Execve,导致子进程会运行我父进程的代码,真糟糕!

image

所以Execve要改下,如果出错,进程要退出

image

实验知识点

原则

  • 检查系统调用错误,并处理,在处理程序中保存并恢复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中给我们在waitfgsigchld_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

image

测试

毒瘤测试点

我的错误测试结果:
image

正确结果:
image

改动:
image

自动测评

image

写一个shell脚本,因为主要是进程号不同,所以用sed将进程号替换成1000,diff比较文件的不同

image

可以很清楚的看到差别

Shell Lab完结!

Code and Notebook

Here

本文作者:次林梦叶的小屋

本文链接:https://www.cnblogs.com/cilinmengye/p/18092929

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   次林梦叶  阅读(13)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起