简介
CSAPP实验介绍
学生实现他们自己的带有作业控制的Unix Shell程序,包括Ctrl + C和Ctrl + Z按键,fg,bg,和 jobs命令。这是学生第一次接触并发,并且让他们对Unix的进程控制、信号和信号处理有清晰的了解。
什么是Shell?
Shell就是用户与操作系统内核之间的接口,起着协调用户与系统的一致性和在用户与系统之间进行交互的作用。
Shell最重要的功能是命令解释,从这种意义上说,Shell是一个命令解释器。
Linux系统上的所有可执行文件都可以作为Shell命令来执行。当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。在查找该命令时分为两种情况:
(1)用户给出了命令的路径,Shell就沿着用户给出的路径进行查找,若找到则调入内存,若没找到则输出提示信息;
(2)用户没有给出命令的路径,Shell就在环境变量Path所制定的路径中依次进行查找,若找到则调入内存,若没找到则输出提示信息。
关于本次实验
本次实验需要我们熟读CSAPP第八章异常控制流。
需要设计和实现的函数:
- eval 函数:解析命令行。
Evaluate the command line that the user has just typed in.
- builtin_cmd:判断是否为内置 shell 命令
If the user has typed a built-in command then execute it immediately.
- do_bgfg:实现内置命令bg,fg。
Execute the builtin bg and fg commands.
- waitfg:等待前台作业完成。
Block until process pid is no longer the foreground process
- sigchld_handler:捕获SIGCHLD信号。
- sigint_handler:捕获SIGINT信号。
- sigtstp_handler:捕获SIGTSTP信号。
TinyShell辅助函数:
/* Here are helper routines that we've provided for you */ int parseline(const char *cmdline, char **argv); //解析命令行参数 void sigquit_handler(int sig);//退出的处理函数 /*jobs是全局变量,存储每一个进程的信息。*/ /*jid为job编号ID,pid为进程ID*/ void clearjob(struct job_t *job);//清除所有工作 void initjobs(struct job_t *jobs);//初始化工作结构体 int maxjid(struct job_t *jobs); //返回jobs中jid的最大值 int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//添加job int deletejob(struct job_t *jobs, pid_t pid); //删除job pid_t fgpid(struct job_t *jobs);//返回前台运行job的pid struct job_t *getjobpid(struct job_t *jobs, pid_t pid);//返回对应pid的job struct job_t *getjobjid(struct job_t *jobs, int jid); //返回jid对应的job int pid2jid(pid_t pid); //pid转jid void listjobs(struct job_t *jobs);//遍历 void usage(void);//帮助信息 void unix_error(char *msg);//报错unix-style error routine void app_error(char *msg);//报错application-style error routine typedef void handler_t(int); handler_t *Signal(int signum, handler_t *handler);//信号设置
实验要求
-
tsh的提示符:tsh>
-
用户输入的命令行应该包括一个名字、0或多个参数,并用一个或多个空格分隔。
-
如果名字是内置命令,tsh立即处理并等待用户输入下一个命令行。否则,假定这个名字是一个可执行文件的路径,tsh在初始子进程的上下文中加载和运行它。
-
tsh不需要支持管(|)或I/O重定向(<和>)。
-
键入ctrl-c(ctrl-z)应该导致SIGINT(SIGTSTP)信号被发送到当前的前台作业,及其该作业的子孙作业(例如,它创建的任何子进程)。如果没有前台工作,那么信号应该没有效果。
-
如果命令行以&结尾,则tsh在后台运行该作业;否则,在前台运行该作业
-
可以用进程ID(PID)或tsh赋予的正整数作业ID(job ID,JID)标识一个作业。JID用前缀%,例如%5标识作业ID为5的作业,5表示PID为5的作业。
-
已经提供了处理作业列表所需的所有函数
-
tsh支持以下内置命令:
- quit:终止tsh程序
- jobs:列出所有后台job
- bg:后台运行程序
- fg:前台运行程序
回顾
fork
pid_t fork(void)
在函数调用处创建子进程。
父进程函数返回子进程的PID。
子进程函数返回0。
waitpid
一个进程可以通过waitpid函数来等待它的子进程终止或者停止。
pid_t waitpid(pid_t pid, int *statusp, int options);
pid:判定等待集合的成员
- 当pid > 0时,waitpid等待进程ID为pid的进程;
- 当pid = -1时,waitpid等待所有它的子进程。
options:修改默认行为
options中有如下选项:
- WNOHANG:若当前没有等待集合中的子进程终止,则立即返回0
- WUNTRACED:等待直到某个等待集合中的子进程停止或返回,并返回这个子进程的pid。
- WCONTINUED:等待直到某个等待集合中的子进程重新开始执行或终止。
- 组合WNOHANG | WUNTRACED:立即返回,如果等待集合中的子进程都没有被停止或终止,则返回0。如果有,则返回PID。
statusp:检查已回收子进程的退出状态
如果statusp参数非空,那么waitpid就会在status中放入关于导致返回的子进程的状态信息,status是statusp指向的值。
- WIFEXITED(status):如果子进程通过调用exit或者返回(return)正常终止,就返回真。
- ········
kill函数
int kill(pid_t pid, int signo);
- pid > 0,信号发送给pid进程;
- pid == 0,把信号发送给本进程(自己)所在的进程组中所有进程,不包括系统进程;
- pid < 0,把信号发送给组id 为 -pid 的进程组中所有进程;
- pid == -1,把信号发送给所有进程,除系统进程外(有些进程不接受9和19号信号)
安全的信号处理
目的
让信号处理程序和主程序它们可以安全地,无错误地,按照我们预期地并发地运行。
方法
-
处理程序尽可能简单。
-
在处理程序只调用异步信号安全的函数。
- 可重入的(只访问局部变量)。
- 不能被信号处理程序中断。
-
保存和恢复errno。避免干扰其他依赖于errno的部分。解决方法是用局部变量存储,再恢复。
void Example(int sig) { int olderrno = errno; /* this is your code */ errno = olderrno; }
- 阻塞所有信号,保护对共享全局变量数据结构的访问。
- 用volatile声明全局变量。
- 用sig_atomic_t声明标志。
例:在添加job时,阻塞信号,因为jobs是全局变量。
This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP signals until we can add the job to the job list. This eliminates some nasty races between adding a job to the job list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals.
注意
- 不可以用信号来对其他进制中发生的事件计数。
- 使用原子(atomic)函数如sigsuspend函数消除潜在的竞争并提高效率。
实验
eval
要点分析:
-
创建子进程前需要阻塞信号,防止竞争。
-
将子进程加入到jobs后,需要恢复,即解除阻塞。
-
创建子进程时,为子进程创建一个新的进程组。
/* * eval - Evaluate the command line that the user has just typed in * * If the user has requested a built-in command (quit, jobs, bg or fg) * then execute it immediately. Otherwise, fork a child process and * run the job in the context of the child. If the job is running in * the foreground, wait for it to terminate and then return. Note: * each child process must have a unique process group ID so that our * background children don't receive SIGINT (SIGTSTP) from the kernel * when we type ctrl-c (ctrl-z) at the keyboard. */ void eval(char *cmdline) { /* $begin handout */ char *argv[MAXARGS]; /* argv for execve() */ int bg; /* should the job run in bg or fg? */ pid_t pid; /* process id */ sigset_t mask; /* signal mask */ /* Parse command line */ bg = parseline(cmdline, argv); if (argv[0] == NULL) return; /* ignore empty lines */ if (!builtin_cmd(argv)) { /* * This is a little tricky. Block SIGCHLD, SIGINT, and SIGTSTP * signals until we can add the job to the job list. This * eliminates some nasty races between adding a job to the job * list and the arrival of SIGCHLD, SIGINT, and SIGTSTP signals. */ if (sigemptyset(&mask) < 0) unix_error("sigemptyset error"); if (sigaddset(&mask, SIGCHLD)) unix_error("sigaddset error"); if (sigaddset(&mask, SIGINT)) unix_error("sigaddset error"); if (sigaddset(&mask, SIGTSTP)) unix_error("sigaddset error"); if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0) unix_error("sigprocmask error"); /* Create a child process */ if ((pid = fork()) < 0) unix_error("fork error"); /* * Child process */ if (pid == 0) { /* Child unblocks signals */ sigprocmask(SIG_UNBLOCK, &mask, NULL); /* Each new job must get a new process group ID so that the kernel doesn't send ctrl-c and ctrl-z signals to all of the shell's jobs */ if (setpgid(0, 0) < 0) unix_error("setpgid error"); /* Now load and run the program in the new job */ if (execve(argv[0], argv, environ) < 0) { printf("%s: Command not foundn", argv[0]); exit(0); } } /* * Parent process */ /* Parent adds the job, and then unblocks signals so that the signals handlers can run again */ addjob(jobs, pid, (bg == 1 ? BG : FG), cmdline); sigprocmask(SIG_UNBLOCK, &mask, NULL); if (!bg) waitfg(pid); else printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); } /* $end handout */ return; }
builtin_cmd
要点分析:
调用listjobs时,属于访问全局变量,需要阻塞和解除阻塞。
/* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */ int builtin_cmd(char **argv) { if(*argv == NULL) { return 0; } sigset_t mask, prev_mask; if(sigfillset(&mask)) { unix_error("sigfillset error!"); } if(! strcmp(argv[0], "quit")) { if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量需要阻塞 { unix_error("sigprocmask error!"); } int i; for(i = 0; i < MAXJOBS; i ++ )//退出时终止所有所有的子进程 { if(jobs[i].pid) { kill(- jobs[i].pid, SIGINT); } } if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } exit(0);//Shell exit }else if(! strcmp(argv[0], "jobs")) { if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//同理,访问全局变量 { unix_error("sigprocmask error!"); } listjobs(jobs);//遍历 if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } return 1; }else if(! strcmp(argv[0], "&")) { return 1;// &也是内置命令,需要返回1 }else if(! strcmp(argv[0], "fg") || ! strcmp(argv[0], "bg")) { do_bgfg(argv); return 1; } return 0; /* not a builtin command */ }
do_bgfg
要点分析:
-
需要保证参数正确,即将不正确的情况排除。
-
区别jid和pid。
/* * do_bgfg - Execute the builtin bg and fg commands */ void do_bgfg(char **argv) { /* $begin handout */ struct job_t *jobp = NULL; /* Ignore command if no argument */ if (argv[1] == NULL) { printf("%s command requires PID or %%jobid argumentn", argv[0]); return; } /* Parse the required PID or %JID arg */ if (isdigit(argv[1][0])) { pid_t pid = atoi(argv[1]); if (!(jobp = getjobpid(jobs, pid))) { printf("(%d): No such processn", pid); return; } } else if (argv[1][0] == '%') { int jid = atoi(&argv[1][1]); if (!(jobp = getjobjid(jobs, jid))) { printf("%s: No such jobn", argv[1]); return; } } else { printf("%s: argument must be a PID or %%jobidn", argv[0]); return; } /* bg command */ if (!strcmp(argv[0], "bg")) { if (kill(-(jobp->pid), SIGCONT) < 0) unix_error("kill (bg) error"); jobp->state = BG; printf("[%d] (%d) %s", jobp->jid, jobp->pid, jobp->cmdline); } /* fg command */ else if (!strcmp(argv[0], "fg")) { if (kill(-(jobp->pid), SIGCONT) < 0) unix_error("kill (fg) error"); jobp->state = FG; waitfg(jobp->pid); } else { printf("do_bgfg: Internal errorn"); exit(0); } /* $end handout */ return; }
waitfg
要点分析:
-
在等待的循环不使用可能会无限休眠的pause,也不使用太慢的sleep。
-
在等待的循环中使用sigsuspend函数,因为它是原子的。
-
在等待前,需阻塞chld信号。
/* * waitfg - Block until process pid is no longer the foreground process */ void waitfg(pid_t pid) { sigset_t mask, prev_mask; if(sigemptyset(&mask)) { unix_error("sigempty error!"); } if(sigaddset(&mask, SIGCHLD)) { unix_error("sigaddset error!"); } if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问jobs先阻塞chld信号 { unix_error("sigprocmask error!"); } while(fgpid(jobs) == pid) { sigsuspend(&prev_mask);//消除竞争 } // if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } return; }
sigchld_handler
要点分析:
-
删除作业信息时,属于访问全局变量,需要阻塞全部信号。
-
保存恢复errno。
/* * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever * a child job terminates (becomes a zombie), or stops because it * received a SIGSTOP or SIGTSTP signal. The handler reaps all * available zombie children, but doesn't wait for any other * currently running children to terminate. */ void sigchld_handler(int sig) { int olderrno = errno; pid_t pid; int status; sigset_t mask, prev_mask; if(sigfillset(&mask)) { unix_error("sigfillset error!"); } while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { if(sigprocmask(SIG_SETMASK, &mask, &prev_mask))//访问全局变量前阻塞所有信号 { unix_error("sigprocmask error!"); } struct job_t *temp = getjobpid(jobs, pid); if(WIFEXITED(status))//正常结束 { deletejob(jobs, pid); }else if(WIFSIGNALED(status))//被未捕获的信号终止 { int jid = pid2jid(pid); printf("Job [%d] (%d) terminated by signal %dn", jid, pid, WTERMSIG(status)); deletejob(jobs, pid); }else if(WIFSTOPPED(status))//停止的信号 { temp->state = ST; int jid = pid2jid(pid); printf("Job [%d] (%d) stopped by signal %dn", jid, pid, WSTOPSIG(status)); } fflush(stdout);//之前printf输出,所以刷新 if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } } errno = olderrno; return; }
sigint_handler
/* * sigint_handler - The kernel sends a SIGINT to the shell whenver the * user types ctrl-c at the keyboard. Catch it and send it along * to the foreground job. */ void sigint_handler(int sig) { int olderrno = errno;//保存和恢复errno sigset_t mask, prev_mask; if(sigfillset(&mask)) { unix_error("sigfillset error!");//阻塞所有信号 } if(sigprocmask(SIG_SETMASK, &mask, &prev_mask)) { unix_error("sigprocmask error!"); } pid_t pid = fgpid(jobs); if(pid != 0)//对进程组发送SIGINT kill(-pid, sig); if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } errno = olderrno; return; }
sigtstp_handler
/* * sigtstp_handler - The kernel sends a SIGTSTP to the shell whenever * the user types ctrl-z at the keyboard. Catch it and suspend the * foreground job by sending it a SIGTSTP. */ void sigtstp_handler(int sig) { int olderrno = errno; sigset_t mask, prev_mask; if(sigfillset(&mask)) { unix_error("sigfillset error!");//阻塞所有信号来访问全局变量 } if(sigprocmask(SIG_SETMASK, &mask, &prev_mask)) { unix_error("sigprocmask error!"); } pid_t pid = fgpid(jobs); if(pid != 0)//向进程组发送SIGTSTP kill(-pid, sig); if(sigprocmask(SIG_SETMASK, &prev_mask, NULL)) { unix_error("sigprocmask error!"); } errno = olderrno; return; }
测试
对比tsh和参考shell程序tshref,测试了16组例子。