shlab这节是要求写个支持任务(job)功能的简易shell,主要考察了linux信号机制的相关内容。难度上如果熟读了《CSAPP》的“异常控制流”一章,应该是可以不算困难的写出来。但如果读书不仔细,或者实践的时候忘记了部分细节,那就可能完全不知道怎么下手,或者得改bug改到吐了。我自己写了大概八个小时,其中仅一半的时间都在处理收到SIGTSTP后莫名卡死的问题,最后才发现是课本没看仔细,子进程停止后也会向父进程发送SIGCHLD

在实验中我们需要实现job、fg、bg、kill四个内建命令和对执行本地程序的支持,并且还要处理好SIGCHLDSIGINTSIGTSTP这几个信号。关键要点都在课本的534页有说过了:

  • 处理程序尽可能简单

  • 处理程序中只用异步信号安全的函数

  • 保存恢复errno

  • 访问共享全局变量时阻塞所有信号

  • volatile声明全局变量

  • sig_atiomic_t声明标志

验收标准这一块因为是在实际操作系统上跑的,不能保证进程号相同,但要保证处理进程号意外所有指令的顺序和信息都要与参考程序的输出完全相同。这点可以用linux上的各种diff工具进行结果比较。

eval

eval函数在课本P525页有一个缺陷版,我们要做的就是以此为蓝本加上点信号处理。

void eval(char* cmdline)
{
char* argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid; strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL) {
return;
}
if (!builtin_cmd(argv)) {
sigset_t mask_chld, prev_mask, mask_all;
sigemptyset(&mask_chld);
sigaddset(&mask_chld, SIGCHLD);
sigfillset(&mask_all); /*因为子进程可能在addjob前就结束并调用deleltejob,所以我们要先阻塞掉SIGCHLD,
保证addjob操作成功*/
sigprocmask(SIG_BLOCK, &mask_chld, &prev_mask); if ((pid = fork()) == 0) {
//子进程默认继承父进程的mask,所以这里要恢复一下
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
setpgid(0, 0); //令进程组号等于进程号
if (execve(argv[0], argv, environ) <= 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
} // addjob涉及到全局变量的操作,需要保证操作的原子性,故这里阻塞掉所有信号
sigprocmask(SIG_SETMASK, &mask_all, NULL);
addjob(jobs, pid, bg?BG:FG, cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 在线程终止前需要打印些相关信息,所以addjob完还要阻塞一会儿SIGCHLD
sigprocmask(SIG_BLOCK, &mask_chld, NULL); if (!bg) {
waitfg(pid);
} else {
// 同上,操作全局变量时阻塞
sigprocmask(SIG_SETMASK, &mask_all, NULL);
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
// 操作结束后解除阻塞
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
} return;
}

waitfg

waitfg负责等待前台进程结束。每次都调用fgpid有点低效了,我们直接用一个全局标志fg_child_flag代表前台进程是否异常,默认为0,如果切换到停止或退出状态就置1。

void waitfg(pid_t pid)
{
sigset_t mask_empty;
sigemptyset(&mask_empty);
fg_child_flag = 0;
while(!fg_child_flag){
// 参考课本545页,挂起进程直到任意信号到达
sigsuspend(&mask_empty);
}
return;
}

sigchld_handler

要注意子进程终止或停止都可能触发SIGCHLD,所以我们得分类讨论。

void sigchld_handler(int sig)
{ int olderrno=errno;
sigset_t mask_all, prev_mask;
pid_t pid;
int status; sigfillset(&mask_all);
// 这里一定要设置成WUNTRACED,否则在子进程处于停止状态时会卡死
if((pid=waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
// 涉及到对全局变量jobs的访问,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
struct job_t *job = getjobpid(jobs, pid);
if(job->state == FG){ // 子进程为前台进程,打开标志
fg_child_flag=1;
} if(WIFEXITED(status)){ // 正常退出,删除任务即可
deletejob(jobs, pid);
}
else if(WIFSIGNALED(status)){ // 收到信号非正常退出,打印消息后删除任务
printf("Job [%d] (%d) terminated by signal %d\n", job->jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(status)){ // 子进程处于停止状态,切换对应的任务状态
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid ,pid, WSTOPSIG(status));
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL); }
errno=olderrno;
return;
}

sigint_handler

因为进程在终止时会自动向父进程发送SIGCHLD信号,所以部分逻辑放在了sigchld_handler,这里只要对子进程发出SIGINT信号就行

void sigint_handler(int sig)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all); // 访问全局变量,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
int pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL); if(pid > 0){
kill(-pid, SIGINT); // 对子进程及其后代发送,故加负号
} return;
}

sigtstp_handler

设计思路同上

void sigtstp_handler(int sig)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all); // 访问全局变量,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
int pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL); if(pid > 0){
kill(-pid, SIGTSTP); // 对子进程及其后代发送,故加负号
} return;
}

builtin_cmd

仍旧参考课本525页,挨个命令strcmp就行

int builtin_cmd(char** argv)
{
if (!strcmp(argv[0], "quit")) {
exit(0);
}
if (!strcmp(argv[0], "fg") || !strcmp(argv[0], "bg")) {
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "jobs")) {
//访问全局变量,阻塞所有信号
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask);
listjobs(jobs);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
return 1;
}
if(!strcmp(argv[0], "kill")){
do_bgfg(argv);
return 1;
}
if(!strcmp(argv[0], "&")){
return 1;
}
return 0; /* not a builtin command */
}

do_bgfg

这个也没啥难度,对着参考输出慢慢地添加判断细节就行

void do_bgfg(char** argv)
{
sigset_t mask_all, prev_mask;
sigfillset(&mask_all);
// 访问全局变量jobs,阻塞所有信号
sigprocmask(SIG_SETMASK, &mask_all, &prev_mask); struct job_t *job;
int pid;
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
else if(argv[1][0] == '%'){
int jid = atoi(argv[1] + 1);
job = getjobjid(jobs, jid);
if(job == NULL) {
printf("%%%d: No such job\n", jid);
return;
}
pid = job->pid;
}
else {
pid = atoi(argv[1]);
if(pid <= 0){
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
job = getjobpid(jobs, pid);
if(job == NULL){
printf("(%d): No such process\n", pid);
return;
}
}
if(!strcmp(argv[0], "bg")){
job->state = BG;
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
return;
}
else if(!strcmp(argv[0], "fg")){
job->state = FG;
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid, SIGCONT); // 对子进程及其后代发送,故加负号
waitfg(pid); // 子进程切换到了前台,故要等待它执行完
return;
}
else if(!strcmp(argv[0], "kill")){
sigprocmask(SIG_SETMASK, &prev_mask, NULL);
kill(-pid,SIGQUIT); // 对子进程及其后代发送,故加负号
return;
}
return;
}

到这所有的实现都捋完一遍了。做这个实验的缘由是在看数据库网课,讲到缓存管理的时候老师说这一块儿知识和你们学操作系统文件系统管理的知识一个样,只不过我们为了效率得另写一套。然后我发现这块知识快忘光了,得补补操作系统,刚好CSAPP还剩下几个实验当初不屑做,干脆一块搞了吧。

【CSAPP】Shell Lab 实验笔记的更多相关文章

  1. CSAPP shell Lab 详细解答

    Shell Lab的任务为实现一个带有作业控制的简单Shell,需要对异常控制流特别是信号有比较好的理解才能完成.需要详细阅读CS:APP第八章异常控制流并理解所有例程. Slides下载:https ...

  2. 【CSAPP】Cache Lab 实验笔记

    cachelab这节先让你实现个高速缓存模拟器,再在此基础上对矩阵转置函数进行优化,降低高速缓存不命中次数.我的感受如上一节,实在是不想研究这些犄角旮旯的优化策略了. 前期准备 我实验的时候用到了va ...

  3. 【CSAPP】Attack Lab实验笔记

    attacklab这节玩的是利用一个字符串进行缓冲区溢出漏洞攻击,就小时候想象中黑客干的事儿. 做题的时候好几次感叹这些人的脑洞,"这都可以攻击?还能这么注入?这还可能借力打力?" ...

  4. 【CSAPP】Performance Lab 实验笔记

    perflab这节的任务是利用书中知识,来对图像处理中的Rotate和Smooth操作函数进行优化.这次没对上电波,觉得学了一堆屠龙之技.于我个人理解,现在计算机配置比以前高多了,连SWAP分区都几近 ...

  5. 【CSAPP】Architecture Lab 实验笔记

    archlab属于第四章的内容.这章讲了处理器体系结构,就CPU是怎样构成的.看到时候跃跃欲试,以为最后实验是真要去造个CPU,配套资料也是一如既往的豪华,合计四十多页的参考手册,一大包的源码和测试程 ...

  6. 【CSAPP】Bomb Lab实验笔记

    bomblab这节搞的是二进制拆弹,可以通俗理解为利用反汇编知识找出程序的六个解锁密码. 早就听闻BOMBLAB的大名,再加上我一直觉得反汇编是个很艰难的工作,开工前我做好了打BOSS心理准备.实际上 ...

  7. 【CSAPP】Data Lab实验笔记

    前天讲到要刚CSAPP,这一刚就是两天半.CSAPP果然够爽,自带完整的说明文档,评判程序,辅助程序.样例直接百万组走起,管饱! datalab讲的是整数和浮点数怎么用二进制表示的,考验的是用基本只用 ...

  8. ChCore Lab3 用户进程和异常处理 实验笔记

    本文为上海交大 ipads 研究所陈海波老师等人所著的<现代操作系统:原理与实现>的课程实验(LAB)的学习笔记的第三篇:用户进程与异常处理.所有章节的笔记可在此处查看:chcore | ...

  9. 深入理解计算机系统项目之 Shell Lab

    博客中的文章均为meelo原创,请务必以链接形式注明本文地址 Shell Lab是CMU计算机系统入门课程的一个实验.在这个实验里你需要实现一个shell,shell是用户与计算机的交互界面.普通意义 ...

随机推荐

  1. Postman请求报错:Error:getaddrinfo ENOENT 50.88.88.88

    一.问题来源 今天发布一个新开发的项目到通州现场,内容是开放几个接口给第三方调用,需要现场部署的同事使用postman调用测试一下,现场同事使用postman调用后反馈有如下错误: 二.解决方法 发现 ...

  2. Http请求的Get和Post的区别?

    1. get从地址栏以明文的方式提交请求信息内容?username=admin&password=123,用户可见, 而post从请求正文提交请求信息内容,用户不可见. 2. get提交因为是 ...

  3. java中会存在内存泄漏吗,请简单描述?

    所谓内存泄露就是指一个不再被程序使用的对象或变量一直被占据在内存中.java中有垃圾回收机制,它可以保证一对象不再被引用的时候,即对象编程了孤儿的时候,对象将自动被垃圾回收器从内存中清除掉.由于Jav ...

  4. java-等待唤醒机制(线程中的通信)-线程池

    为什么需要线程间的通信 多个线程并发执行时,在默认情况下CPU时随机切换线程的,当我们需要多个线程共同完成一件任务,并且 希望他们有规律的执行,那么多线程之间需要一些协调通信,以此来帮我们达到多线程共 ...

  5. eclipse更换工作空间后,需要修改哪些常用配置

    一.对于配置不太了解,第一次配置. 常用 (ps:配置我们在导航栏的 Windows --> preference 里进行配置) 1.首先,我们配置编译环境:Java --> Instal ...

  6. ArrayList跟LinkedList的区别

    ArrayList和LinkedList都是实现list接口,它们不同如下: ArrayList是基于索引的数据接口,底层是数组.它可以以O(1)时间复杂度对元素进行随机访问.与此相对,linkedL ...

  7. java集合类框架的基本接口有哪些

    集合类接口指定了一组叫做元素的对象.集合类接口的每一种具体的实现类都可以以他自己的方式对元素进行保存和排序.有的集合允许重复的键,有些不允许. java集合类里面最基本 的接口: Collection ...

  8. Apollo代码学习(七)—MPC与LQR比较

    前言 Apollo中用到了PID.MPC和LQR三种控制器,其中,MPC和LQR控制器在状态方程的形式.状态变量的形式.目标函数的形式等有诸多相似之处,因此结合自己目前了解到的信息,将两者进行一定的比 ...

  9. C#和TS/JS的对比学习02:函数与方法

    程序本质上,就是由数据和处理数据的方法构成.函数和方法,这两个名词虽然字面不同,但意义上其实没有区别.只是因为它们出现的地方有异,给予了不同的名称,比如在全局环境中,叫函数,在对象或类中,叫方法.而C ...

  10. Java中读取 .properties 和 .xml 文件

    配置文件内容获取 总结内容 1. Java中为什么要使用配置文件 2. Java中常用的配置文件类型有哪些以及它们的特点 Properties配置文件 XML配置文件 总结 总结内容 1. Java中 ...