CSAPP:shlab
该实验对应CSAPP第8章:异常控制流,可以先温习该章内容再做实验。
通读实验指南,了解实验目的:
shlab
简介
The purpose of this assignment is to become more familiar with the concepts of process control and signalling. You’ll do this by writing a simple Unix shell program that supports job control.
本实验的目的就是写一个简单的Unix shell,主要任务就是修改tsh.c文件,需修改以下7个函数。
1.void eval(char *cmdline); //解释命令[70 lines]
2.int builtin_cmd(char **argv);//识别命令是否为内置命令quit、jobs、fg、bg ,若是,则执行对应命令.[25
lines]
- quit命令会终止shell。
- jobs 命令列出所有后台工作。
bg 命令通过发送一个 SIGCONT 信号重新启动,然后在后台运行。参数可以是 PID 或 JID。- fg 命令通过发送一个 SIGCONT 信号重新启动,然后在前台运行。
3.void do_bgfg(char **argv);//执行bg,fg操作[50 lines]
4.void waitfg(pid_t pid);//等待前台操作完成 [20 lines]
5.void sigchld_handler(int sig);//抓住 SIGCHILD [信号80 lines]
6.void sigtstp_handler(int sig);//抓住 SIGINT (ctrl-c) 信号[15 lines]
7.void sigint_handler(int sig);//抓住 SIGTSTP (ctrl-z) 信号[15 lines]
关于你输入的命令:
如果第一个词是内置的命令,shell会在当前进程中立即执行该命令,否则,假定该词是可执行程序的路径名,在这种情况下,shell会fork一个子进程,然后在子进程的上下文中加载并运行程序。
如果命令行以"&"结尾,则作业在后台运行,这意味着shell在打印提示符和等待下一行命令前,不会等待job结束。否则,job在前台运行,这意味着shell在等待下一个命令行之前会等待job终止。因此,在任何时间点上,最多只能有一个job在前台运行。但是,可以有任意数量的job在后台运行。
下面一段说明很好地解释了main中的参数argc、argv代表什么意思(这里环境的参数被省略了):
Typing the command line
tsh> /bin/ls -l -d
runs the ls program in the foreground. By convention, the shell ensures that when the program begins executing its main routine
int main(int argc, char *argv[])
the argc and argv arguments have the following values:
• argc == 3,
• argv[0] == ‘‘/bin/ls’’,
• argv[1]== ‘‘-l’’,
• argv[2]== ‘‘-d’’.
argc指的是参数个数,argv是指针数组,每个指针指向一个参数字符串。
一些注意事项:
- 如果命令行以&结束,则tsh应该在后台运行job。 否则,它将在前台运行该job。
- 每个作业都可以由进程ID(PID)或作业ID(JID)标识,该ID是tsh分配的正整数。 JID应该在命令行上以前缀“%”表示。 例如,“%5”表示JID 5,“ 5”表示PID5。
- tsh应该管理回收(reap)所有的僵尸子进程。 如果任何job由于接收到未捕获到的信号而终止,则tsh应该识别此事件并打印一条带有该job的PID的消息以及对该问题的信号的描述。
- job Id 和 process id 是有区别的,前者需要以
%
为前缀,后者为一个数字。这一点在处理bg、fg指令时尤为重要 - 在
eval
中一定要 -
- 在fork子进程前用
sigprocmask
阻塞SIGCHLD
信号;fork之后解除该阻塞 - 在加子进程到joblist前调用
sigprocmask
阻塞全部信号 - 因为子进程会从父进程处继承阻塞信息,所以在
execve
其他二进制文件前,一定要解除信号阻塞 - 子进程按照建议应该在
sigchld_handler
被收割(reap),所以父进程需要在调用addjob
之前阻塞SIGCHLD
信号,以防止竞态条件
- 在fork子进程前用
- 当在标准unix shell中运行tsh程序时,从tsh程序fork出来的子程序会和tsh处于同一个process group;因此你需要使用setpgid来重置子进程process group id;如若不然,在ctrl-c & ctrl-z的处理上会有一些问题
- 用sigprocmask阻塞信号,因为有时候不希望在接到信号时就立即停止当前执行,去处理信号,同时也不希望忽略该信号,而是延时一段时间去调用信号处理函数。
检验结果用到的一些命令
先make
共16组测试数据,test结果要和rtest完全一样才算通过。
make test01
相当于 ./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
make rtest01
相当于 ./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
实验代码和注释
eval
eval解析输入的命令:
如果第一个词是内置的命令builtin_cmd(argv),shell会在当前进程中立即执行该命令。
否则,假定该词是可执行程序的路径名,在这种情况下,shell会fork一个子进程,然后在子进程的上下文中加载并运行程序。
注意:在创建子进程前要阻塞信号 ,为了避免父进程运行到addjob之前子进程就退出了,导致信号丢失,所以在fork子进程前阻塞sigchld信号,并在fork,addjob后解除
/*
* 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)
{
// static char array[MAXLINE]; /* holds local copy of command line */
// char *buf = array; /* ptr that traverses command line */
char *argv[MAXARGS]; //命令行参数
pid_t pid; //子进程PID
int bg; // 最后是否是&,即是否后台执行,
sigset_t mask_one, prev, mask_all;
// strcpy(buf, cmdline); //缓存命令行
bg = parseline(cmdline, argv);
if(argv[0] == NULL)
return;//忽略空行
if(!builtin_cmd(argv)){//如果不是shell的内嵌命令
sigemptyset(&mask_one);//初始化信号量集
sigaddset(&mask_one, SIGCHLD);//将SIGCHLD添加到信号量集中
sigfillset(&mask_all);// 设置全阻塞
sigprocmask(SIG_BLOCK, &mask_one, &prev);//阻塞信号 ,为了避免父进程运行到addjob之前子进程就退出了,所以在fork子进程前阻塞sigchld信号,addjob后解除
if((pid = fork()) == 0){
// 子进程继承了父进程的阻塞向量,也要解除阻塞,避免收不到它本身的子进程的信号
setpgid(0, 0); //把pid=0放到gpid=0的进程组
sigprocmask(SIG_SETMASK, &prev, NULL);//恢复被屏蔽的信号,防止遗漏
//容错
if(execve(argv[0], argv, environ) < 0){
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, pid, bg?BG:FG, cmdline);
sigprocmask(SIG_SETMASK, &prev, NULL);
if(bg){
//在后台解除屏蔽
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}else{
waitfg(pid);
}
}
return;
}
builtin_cmd
判断是否内置命令(1是,0否),并执行对应操作。
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0], "quit")){
exit(0);
}
if(!strcmp(argv[0], "jobs")){
listjobs(jobs);
return 1;
}
if(!strcmp(argv[0], "&")){
// ignore singleton '&'
return 1;
}
if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}
do_bgfg
在命令行中的第一个参数argv[0]可以用于判断输入的是bg还是fg命令。
PID和JID有不同的格式,分别用不同的方式读入sscanf(argv[1],"%d",&pid)和sscanf(argv[1],"%%%d",&jid),若都不是,则会输出错误提示。
- 每个作业都可以由进程ID(PID)或作业ID(JID)标识,该ID是tsh分配的正整数。 JID应该在命令行上以前缀“%”表示。 例如,“%5”表示JID 5,“ 5”表示PID5。
看process或者job是否存在,不存在则报错,若存在则修改状态,并重启。
若是fg前台则等待前一个进程结束,若是bg后台则输出相应字符串。
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{ int jid;
struct job_t *job;
pid_t pid;
sigset_t mask, prev;
//argv[0]判断bg还是fg
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return;
}
//首先确定是pid还是jid,然后将其转化为kill的参数
//读jid
if(sscanf(argv[1],"%%%d",&jid) > 0){
job = getjobjid(jobs, jid); //需要获得job,因为要修改job信息
if(job == NULL || job->state == UNDEF){
printf("%s: No such job\n", argv[1]);
return;
}
//读pid
}else if(sscanf(argv[1],"%d",&pid) > 0){
job = getjobpid(jobs, pid);
if(job == NULL || job->state == UNDEF){
printf("(%s): No such process\n", argv[1]);
return;
}
}else{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//修改job信息
sigfillset(&mask);
sigprocmask(SIG_BLOCK, &mask, &prev);
//更改状态
if(!strcmp(argv[0], "fg")){
job->state = FG;
}else{
job->state = BG;
}
sigprocmask(SIG_SETMASK, &prev, NULL);
pid = job->pid;
//发送SIGCONT重启
kill(-pid, SIGCONT);
if(!strcmp(argv[0], "fg")){
waitfg(pid);//若是前台则等待前一个进程结束
}else{
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);//若是后台则输出相应字符串
}
return;
}
waitfg
等待pid进程不再是前台进程
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs)){
sleep(1);
}
return;
}
sigchld_handler
//抓住 SIGCHILD 信号
内核在每当子进程终止(成为僵尸)或停止时,因为它收到SIGSTOP或SIGTSTP信号,该处理zombie进程,但不等待任何其他的。终止当前正在运行的子进程。
回收的子进程有三种状态:正常退出、信号退出和信号停止。
if(WIFEXITED(state)){ // 正常退出
deletejob(jobs, pid);
}else if(WIFSIGNALED(state)){ // 信号退出
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(state));
deletejob(jobs, pid);
}else if(WIFSTOPPED(state)){ // 信号停止
job = getjobpid(jobs, pid);
job->state = ST;//注意
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
}
/*****************
* Signal handlers
*****************/
/*
* 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 old_errno = errno;
pid_t pid;
sigset_t mask, prev;
int state;
struct job_t *job;
sigfillset(&mask);// 设置全阻塞
while((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0){
// WNOHANG | WUNTRACED 是立即返回
// 用WIFEXITED(status),WIFSIGNALED(status),WIFSTOPPED(status)等来补获终止或者被停止的子进程的退出状态。
sigprocmask(SIG_BLOCK, &mask, &prev);
if(WIFEXITED(state)){ // 正常退出
deletejob(jobs, pid);
}else if(WIFSIGNALED(state)){ // 信号退出
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(state));
deletejob(jobs, pid);
}else if(WIFSTOPPED(state)){ // 停止
job = getjobpid(jobs, pid);
job->state = ST;//注意
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}
errno = old_errno;
return;
}
sigint_handler
捕捉 SIGINT (ctrl-c) 信号
sigtstp_handler
捕捉 SIGTSTP (ctrl-z) 信号
两个函数写法看起来相似,但是在用sigchld_handler处理的时候有区别,对于 sigtstp_handler我们需要修改job的状态job->state = ST。
/*
* 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;
pid_t pid = fgpid(jobs);
if(pid!=0){
kill(-pid,sig);
}
errno = olderrno;
return;
}
/*
* 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;
pid_t pid = fgpid(jobs);
if(pid!=0){
kill(-pid,sig);
}
errno = olderrno;
return;
}
/*********************
* End signal handlers
*********************/
参考
README:http://csapp.cs.cmu.edu/3e/README-shlab
说明:http://csapp.cs.cmu.edu/3e/shlab.pdf
代码:http://csapp.cs.cmu.edu/3e/shlab-handout.tar
复习:http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/recitations/rec09.pdf
博客:
https://zhuanlan.zhihu.com/p/151050267
https://zhuanlan.zhihu.com/p/119034923
https://zhuanlan.zhihu.com/p/89224358