CSAPPShellLab
- 手机
- 2025-08-20 17:21:01

CSAPP Shell Lab
本实验的目的是更加熟悉过程控制和信号的概念。 您将通过编写一个支持作业控制的简单 Unix shell 程序来完成此操作。
我们要做的是填写tsh.c文件的空函数,完善其内容,使其达到我们预期的目的。详细的需要实现的功能请查看实验文档。根据文档的提示,我们可以根据测试文件tracei.txt来从易到难一步一步实现相应功能,通过make testi和make rtesti对比输出内容判断相应功能是否正确实现。做之前先看一遍已经给实现好的辅助函数一遍后续解题正确使用,并且要仔细看实验文档,里面有很多重要提示,最好仔细阅读过第八章内容。
关键点以下是程序中的一些要点,一些甚至关乎着程序的正确性:
实现信号处理程序时,务必将信号发送至整个进程组,即在kill函数中使用-pid而不是pid。使用sigprocmask来避免deletejob和addjob出现竞争情况。做法是父进程必须在派生子进程之前使用 sigprocmask 来阻塞SIGCHLD信号,在将子进程通过addjob添加到作业列表后再次使用 sigprocmask以取消阻塞这些信号。 由于子进程继承了父进程的阻塞向量,因此子进程必须在执行新程序之前取消阻塞 SIGCHLD 信号。使用setpgid(0,0)将fork的子进程创建新的进程组。确保在前台进程中只有一个进程,即我们的shell即tsh进程。这样做是为避免我们所执行的作业影响我们的shell,如果我们的shell创建了一个子进程,默认情况该子进程也是前台进程组,若此时键入ctrl-c将会向前台的进程组发送SIGINT,将导致shell退出,显然不对。waitpid的WUNTRACED和WNOHANG选项,可避免等待任何运行中的子进程,同时可以获知子进程的终止和停止状态。在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 lines if (!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); // 阻塞SIGCHLD if ((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 end waitfg(pid); } } } waitfg阻塞直到pid不再是前台进程。这里使用sigsuspend作为睡眠函数,得益于其本身的原子性,避免潜在的竞争,详细原因见8.5.7节。
void waitfg(pid_t pid) { sigset_t mask; Sigemptyset(&mask); while (pid == fgpid(jobs)) Sigsuspend(&mask); } sigtstp_handlerSIGTSTP信号的处理程序,捕获它并通过发送SIGTSTP挂起前台作业。
void sigtstp_handler(int sig) { int olderrno = errno; pid_t pid = fgpid(jobs); if (pid) Kill(-pid, sig); errno = olderrno; } sigint_handlerSIGINT信号的处理程序,捕获它并将其发送到前台作业。
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信号而返回0 while ((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] == '%') { // jid if (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 { // pid if (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进行比较,检查是否除了pid和ps 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屏蔽全部信号,因为我认为,及时指令序列被处理程序中断,也不会影响其后续的结果,关于这一问题,暂存疑,若有缺陷的地方,后续将会修改。
CSAPPShellLab由讯客互联手机栏目发布,感谢您对讯客互联的认可,以及对我们原创作品以及文章的青睐,非常欢迎各位朋友分享到个人网站或者朋友圈,但转载请说明文章出处“CSAPPShellLab”
上一篇
《L1-5分合集总结》