CSAPP Shell Lab
创始人
2024-05-10 22:48:43
0

CSAPP Shell Lab

本实验的目的是更加熟悉过程控制和信号的概念。 您将通过编写一个支持作业控制的简单 Unix shell 程序来完成此操作。

我们要做的是填写tsh.c文件的空函数,完善其内容,使其达到我们预期的目的。详细的需要实现的功能请查看实验文档。根据文档的提示,我们可以根据测试文件tracei.txt来从易到难一步一步实现相应功能,通过make testimake rtesti对比输出内容判断相应功能是否正确实现。做之前先看一遍已经给实现好的辅助函数一遍后续解题正确使用,并且要仔细看实验文档,里面有很多重要提示,最好仔细阅读过第八章内容。

关键点

以下是程序中的一些要点,一些甚至关乎着程序的正确性:

  • 实现信号处理程序时,务必将信号发送至整个进程组,即在kill函数中使用-pid而不是pid
  • 使用sigprocmask来避免deletejobaddjob出现竞争情况。做法是父进程必须在派生子进程之前使用 sigprocmask 来阻塞SIGCHLD信号,在将子进程通过addjob添加到作业列表后再次使用 sigprocmask以取消阻塞这些信号。 由于子进程继承了父进程的阻塞向量,因此子进程必须在执行新程序之前取消阻塞 SIGCHLD 信号。
  • 使用setpgid(0,0)fork的子进程创建新的进程组。确保在前台进程中只有一个进程,即我们的shelltsh进程。这样做是为避免我们所执行的作业影响我们的shell,如果我们的shell创建了一个子进程,默认情况该子进程也是前台进程组,若此时键入ctrl-c将会向前台的进程组发送SIGINT,将导致shell退出,显然不对。
  • waitpidWUNTRACEDWNOHANG选项,可避免等待任何运行中的子进程,同时可以获知子进程的终止和停止状态。
  • waitfg中使用一个围绕睡眠函数的繁忙循环。
  • 只要子进程的状态发生改变就会向父进程发送SIGCHLD信号。
  • 在使用printf等有输出缓冲区的函数往标准输出上输出后,及时使用fflush刷新缓冲区,避免出现令人意想不到的结果。
  • 阻塞所有的信号,保护对共享全局数据结构的访问。
  • 保护和恢复errno

准备工作

我是预先添加了错误处理函数,简化程序结构,如下。参考csapp官网。

// 额外增加错误处理包装函数--begin
pid_t Fork(void) {pid_t pid;if ((pid = fork()) < 0) unix_error("Fork error");return pid;
}pid_t Waitpid(pid_t pid, int *iptr, int options) {pid_t retpid;if ((retpid = waitpid(pid, iptr, options)) < 0) unix_error("Waitpid error");return (retpid);
}
void Kill(pid_t pid, int signum) {int rc;if ((rc = kill(pid, signum)) < 0) unix_error("Kill error");
}void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset) {if (sigprocmask(how, set, oldset) < 0) unix_error("Sigprocmask error");return;
}void Sigemptyset(sigset_t *set) {if (sigemptyset(set) < 0) unix_error("Sigemptyset error");return;
}void Sigfillset(sigset_t *set) {if (sigfillset(set) < 0) unix_error("Sigfillset error");return;
}void Sigaddset(sigset_t *set, int signum) {if (sigaddset(set, signum) < 0) unix_error("Sigaddset error");return;
}int Sigsuspend(const sigset_t *set) {int rc = sigsuspend(set); /* always returns -1 */if (errno != EINTR) unix_error("Sigsuspend error");return rc;
}
void Setpgid(pid_t pid, pid_t pgid) {int rc;if ((rc = setpgid(pid, pgid)) < 0) unix_error("Setpgid error");return;
}
// 额外增加错误处理包装函数--end

builtin_cmd

最简单是先完成builtin_cmd,判断是否是内置命令,若是执行。

int builtin_cmd(char **argv) {if (!strcmp(argv[0], "quit")) exit(EXIT_SUCCESS);if (!strcmp(argv[0], "jobs")) {listjobs(jobs);return 1;}if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {do_bgfg(argv);return 1;}return 0; /* not a builtin command */
}

eval

再仿照书上完成eval的内容,需要额外注意的是,如果是运行在前台的,要调用waitfg函数等待其不再执行。

void eval(char *cmdline) {char *agrv[MAXARGS];char buf[MAXLINE];int bg;pid_t pid;strcpy(buf, cmdline);bg = parseline(buf, agrv);if (agrv[0] == NULL) return;  // Ignore empty linesif (!builtin_cmd(agrv)) {sigset_t mask_all, mask_one, prev_one;Sigfillset(&mask_all);Sigemptyset(&mask_one);Sigaddset(&mask_one, SIGCHLD);// 阻塞SIGCHLD信号,消除竞争,避免父进程运行到addjob之前子进程就退出// 在调用fork之前,阻塞SIGCHLD信号,然后再调用addjob之后取消阻塞这些信号,保证了在子进程被添加到作业列表之后回收该子进程Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);  // 阻塞SIGCHLDif ((pid = Fork()) == 0) {                     // Child runs user job// 创建一个新的进程组,和tsh进程组分离,避免后续操作影响到tsh进程Setpgid(0, 0);Sigprocmask(SIG_SETMASK, &prev_one, NULL);  // 子进程继承父进程状态,需要恢复上面阻塞SIGCHLD信号之前的状态if (execve(agrv[0], agrv, environ) < 0) {printf("%s: Command not found\n", agrv[0]);exit(EXIT_SUCCESS);}}int status = bg ? BG : FG;Sigprocmask(SIG_BLOCK, &mask_all, NULL);  // 阻塞全部信号addjob(jobs, pid, status, cmdline);// bg需要打印信息,也会使用全局数据,为保证安全,也需要在Sigprocmask之间,屏蔽所有信号if (bg) printf("[%d] (%d) %s", getjobpid(jobs, pid)->jid, pid, cmdline);Sigprocmask(SIG_SETMASK, &prev_one, NULL);  // 恢复阻塞SIGCHLD信号之前的状态if (!bg) {  // No background, waiting for child process to endwaitfg(pid);}}
}

waitfg

阻塞直到pid不再是前台进程。这里使用sigsuspend作为睡眠函数,得益于其本身的原子性,避免潜在的竞争,详细原因见8.5.7节。

void waitfg(pid_t pid) {sigset_t mask;Sigemptyset(&mask);while (pid == fgpid(jobs)) Sigsuspend(&mask);
}

sigtstp_handler

SIGTSTP信号的处理程序,捕获它并通过发送SIGTSTP挂起前台作业。

void sigtstp_handler(int sig) {int olderrno = errno;pid_t pid = fgpid(jobs);if (pid) Kill(-pid, sig);errno = olderrno;
}

sigint_handler

SIGINT信号的处理程序,捕获它并将其发送到前台作业。

void sigint_handler(int sig) {int olderrno = errno;pid_t pid = fgpid(jobs);if (pid) Kill(-pid, sig);errno = olderrno;
}

sigchld_handler

只要子进程的状态发生改变就会向父进程发送SIGCHLD信号。如终止,停止,继续,只要导致这三种状态的出现,都会触发SIGCHLD信号。下面的处理程序,只处理终止和停止的状态,继续执行的这种状态专由do_bgfg处理。检查子进程的状态由waitpid函数的statusp记录,详见8.4.3节。

void sigchld_handler(int sig) {pid_t pid;int status;sigset_t mask_all, prev_all;Sigfillset(&mask_all);  // 设置全阻塞Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);// 这里要判断>0,因为WNOHANG | WUNTRACED表示若等待集合中的子进程都没有停止或终止,则返回0,// 若有任一个终止或停止,返回该子进程的pid// 由于我们的SIGCONT信号的处理是放在do_bgfg()中的,所以有可能是SIGCONT信号导致的SIGCHLD信号而返回0while ((pid = Waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {if (WIFEXITED(status)) {  // 正常终止deletejob(jobs, pid);} else if (WIFSIGNALED(status)) {  // 信号导致终止printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));fflush(stdout);deletejob(jobs, pid);} else if (WIFSTOPPED(status)) {  // 信号导致停止struct job_t *job = getjobpid(jobs, pid);job->state = ST;printf("Job [%d] (%d) stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status));fflush(stdout);}}Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}

do_bgfg

此函数则是用于处理内置命令bg fg的,程序相当一部分是关于参数判断和错误的处理。需要注意的是进程状态的改变也要相应的在jobs中改变,如果是调到前台运行的,要调用相应的waitfg函数。

void do_bgfg(char **argv) {// argv错误处理if (argv[1] == NULL) {printf("%s command requires PID or %%jobid argument\n", argv[0]);fflush(stdout);return;}sigset_t mask_all, prev_all;Sigfillset(&mask_all);struct job_t *job;if (argv[1][0] == '%') {                                             // jidif (strspn(argv[1] + 1, "0123456789") != strlen(argv[1] + 1)) {  // 如果%后面的不全是数字,错误printf("%s: argument must be a PID or %%jobid\n", argv[0]);fflush(stdout);return;}int jid = atoi(argv[1] + 1);job = getjobjid(jobs, jid);if (job == NULL) {printf("%s: No such job\n", argv[1]);fflush(stdout);return;}} else {                                                     // pidif (strspn(argv[1], "0123456789") != strlen(argv[1])) {  // 如果不全是数字,错误printf("%s: argument must be a PID or %%jobid\n", argv[0]);fflush(stdout);return;}pid_t pid = atoi(argv[1]);job = getjobpid(jobs, pid);if (job == NULL) {printf("(%s): No such process\n", argv[1]);fflush(stdout);return;}}Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);  // 需要访问jobs全局变量,先屏蔽全部信号if (!strcmp(argv[0], "fg")) {                  // fg,将一个停止或运行的后台作业改变为在前台运行if (job->state == ST) {                    // 如果是停止的,发送SIGCONT信号重新启动Kill(-(job->pid), SIGCONT);}job->state = FG;Sigprocmask(SIG_SETMASK, &prev_all, NULL);waitfg(job->pid);  // 等待前台作业运行} else {               // bg,将一个后台停止作业改变为后台运行printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);Kill(-(job->pid), SIGCONT);job->state = BG;Sigprocmask(SIG_SETMASK, &prev_all, NULL);}
}

总结

可以通过以下命令将测试的输出内容重定向到新建的文件tsh.out,随后通过文本比较和tshref.out进行比较,检查是否除了pidps a不同,其他都和tshref.out相同,经测试,以上所编写的代码和tshref.out比较无误:

./sdriver.pl -t trace01.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace02.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace03.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace04.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace05.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace06.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace07.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace08.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace09.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace10.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace11.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace12.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace13.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace14.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace15.txt -s ./tsh -a "-p" >> tsh.out 
./sdriver.pl -t trace16.txt -s ./tsh -a "-p" >> tsh.out 

本实验难度适中,代码量也不大,若是认真阅读过第八章内容以及实验文档,编写起来还是比较顺利的。通过此实验,较清晰的了解了shell的工作原理,以及信号的处理,如何避免出现竞争,并发的编程规范,但以上程序中,对于getjobpid此类从job获取信息但不对其进行修改我并没有使用sigprocmask屏蔽全部信号,因为我认为,及时指令序列被处理程序中断,也不会影响其后续的结果,关于这一问题,暂存疑,若有缺陷的地方,后续将会修改。

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...