本次實驗目的是完成一個簡單的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; }