CSAPP Lab:Shell Lab——理解进程控制的秘密
本次实验目的是完成一个简单的shell程序,解析命令行参数,理解并使用(fork,execve,waitpid)常见的多进程函数,了解linux进程组,以及前台进程和后台进程的相关概念,理解linux的信号机制(包括发送信号,接受信号,阻塞信号等)。实验提示以及详情请阅读CMU的实验指导:http://csapp.cs.cmu.edu/public/labs.html 。
我们要完成的shell并不是从0开始,实验本身已经帮你完成了一部分内容,并且提供一些工具函数,我们要做的是实现一下这几个核心函数:
eval: Main routine that parses and interprets the command line. [70 lines]
builtin_cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. [25 lines]
dobgfg: Implements the bg and fg built-in commands. [50 lines]
waitfg: Waits for a foreground job to complete. [20 lines]
sigchld handler: Catches SIGCHILD signals. [80 lines]
sigint handler: Catches SIGINT (ctrl-c) signals. [15 lines]
sigtstp handler: Catches SIGTSTP (ctrl-z) signals. [15 lines]
eval
这是shell程序的核心函数,我们需要在这里完成以下几个事情:
1.调用parseline,生成argv以及判断是否是后台进程。
2.调用builtin_cmd判断是否是内建命令,如果是则已经在该方法中执行,shell直接返回,否则创建进程执行。
3.fork之前要注意屏蔽SIGHLD信号,否则有可能在addjob之前就调用deletejob造成竞争。
4.需要在fork后解锁SIGHLD的信号。
/* * 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) { char *argv[MAXARGS]; char buf[MAXLINE]; int isBg; pid_t pid; sigset_t mask; strcpy(buf,cmdline); isBg=parseline(buf,argv); if(argv[0]==NULL){ return; } if(!builtin_cmd(argv)){ //init mask sigemptyset(&mask); sigaddset(&mask,SIGCHLD); sigprocmask(SIG_BLOCK,&mask,NULL); //block SIGCHLD if((pid=fork())==0){ sigprocmask(SIG_UNBLOCK,&mask,NULL); //unblock SIGCHLD if(execve(argv[0],argv,environ)<0){ printf("%s:Command not found.\n",argv[0]); exit(0); } //set own pid as group pid setpgid(0,0); } if(!isBg){ addjob(jobs,pid,FG,cmdline); waitfg(pid); } else{ addjob(jobs,pid,BG,cmdline); printf("%d %s",pid,cmdline); } sigprocmask(SIG_UNBLOCK,&mask,NULL); //unblock SIGCHLD } return; }
builtin_cmd
builtin_cmd做的事情比较简单,判断是否是内建命令,如果是,则直接执行并返回true,否则返回false。
/* * 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")||!strcmp(argv[0],"q")){ exit(0); } 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; }
dobgfg
这个方法还是比较复杂的,主要完成了bg和fg命令的操作,需要注意如下几点:
1.需要区分输入的是pid还是jid来调用不同函数。
2.通过发送SIGCONT来重启进程,发送对象需要为进程组。
3.不要忘记将后台进程改为前台进程后需要等待前台进程完成(调用waitfg)。
/* * do_bgfg - Execute the builtin bg and fg commands */ void do_bgfg(char **argv) { struct job_t *job; char *id=argv[1]; pid_t pid; if(id==NULL){ printf("%s command requireds pid or %%jobid argument\n",argv[0]); return; } //process by jobid if(id[0]=='%') { int jid = atoi(&id[1]); job=getjobjid(jobs,jid); if(job==NULL) { printf("%s:No such job\n",id); return; } } //process by pid else if(isdigit(id[0])){ int pid = atoi(&id[1]); job = getjobpid(jobs,pid); if(job==NULL) { printf("%s:No such job\n",id); return; } } else{ printf("%s: argument must be a PID or %%jobid\n", argv[0]); return; } //send SIGCONT to restart kill(-(job->pid),SIGCONT); //set job status if(!strcmp(argv[0],"bg")){ job->state = BG; printf("[%d] (%d) %s", job->jid, job->pid,job->cmdline); }else{ job->state = FG; waitfg(job->pid); } return; }
waitfg
没什么好说的,根据实验指导,这里直接使用忙等待来实现。
/* * waitfg - Block until process pid is no longer the foreground process */ void waitfg(pid_t pid) { while(pid == fgpid(jobs)){ sleep(0); } }
sigchld handler
回收僵尸进程的关键函数,需要注意如下几点:
1.理解waitpid的用法,这里使用WNOHANG|WUNTRACED的组合会更合适,表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则返回进程的pid。
2.检验status的值来执行不同操作,status的含义有如下枚举:
- WIFEXITED(status):
如果进程是正常返回即为true,什么是正常返回呢?就是通过调用exit()或者return返回的 - WIFSIGNALED(status):
如果进程因为捕获一个信号而终止的,则返回true - WIFSTOPPED(status):
如果返回的进程当前是被停止,则为true
所以三种情况都是需要delete job的,当进程为停止状态同时需要设置job的status。
void sigchld_handler(int sig) { pid_t pid; int status; while((pid = waitpid(-1,&status,WNOHANG|WUNTRACED))>0){ if(WIFEXITED(status)){ deletejob(jobs,pid); } if(WIFSIGNALED(status)){ deletejob(jobs,pid); } if(WIFSTOPPED(status)){ struct job_t *job = getjobpid(jobs,pid); if(job !=NULL ){ job->state = ST; } } } if(errno != ECHILD) unix_error("waitpid error"); return; }
sigint handler
ctrl-c的响应函数,直接调用kill函数给相关进程。
void sigint_handler(int sig) { pid_t pid = fgpid(jobs); if(pid!=0){ kill(-pid,sig); } return; }
sigtstp handler
ctrl-z的响应函数,直接调用kill函数给相关进程,需要注意kill前判断状态,不要重复发送信号。
/* * 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) { pid_t pid = fgpid(jobs); if(pid!=0 ){ struct job_t *job = getjobpid(jobs,pid); if(job->state == ST){ return; }else{ Kill(-pid,SIGTSTP); } } return; }