《深入理解計算機系統》配套實驗:Shell Lab

和前面幾個實驗不同,Shell Lab的內容和書本上的內容有很大關係,一定要認真看完CSAPP 第八章後(指的是書上每一段代碼都自己敲過,運行過),才能開始這次實驗。

本次實驗我大概用時10小時,2小時研究實驗指導書和框架代碼,剩下時間用來編程和debug,個人認為實驗內容還是很有挑戰性的。

實驗準備

  • CSAPP 官網 第八章的總結PPT,在看完書、做實驗前看一遍,把第八章內容整理的很好:

http://www.cs.cmu.edu/afs/cs/academic/class/15213-f15/www/recitations/rec09.pdfwww.cs.cmu.edu

  • 實驗的實驗指導書WriteUp,從頭到尾好好讀兩遍,每句話都是重點。
  • 把給的框架代碼好好的看一遍。

實驗要求

本實驗要求寫一個自己的shell,對16個測試用例,輸出情況和給的參考shell輸出一樣。

給了一個shell的框架代碼,包括主函數、parser、作業控制邏輯,需要我們實現的是信號處理程序、shell內置命令等函數。

我在實驗中使用了CSAPP書中的包裝函數,具體實現可以去谷歌csapp.c csapp.h 查看這些包裝函數的具體實現。

注意點

  • eval()函數書上給了一個簡單版本的,可以照著書上的邏輯修改
  • eval()函數中注意屏蔽信號,具體見書
  • 對於「Command not found」的情況,把它當作一個前台作業,等待它瞬間結束即可。
  • 實驗指導書中專門提到,在eval()函數中,fork()之後,我們需要修改子進程的group id,方便實現ctrl + C ctrl + Z 的邏輯。setpgrp()函數把本進程的gid 修改為pid。
  • 對於屏蔽的信號,在fork()之後,別忘了在子進程中解除屏蔽。fork()會複製父進程的block vector。
  • 在eval()函數中別忘了調用addjob()!
  • parseline()函數寫的很好,建議好好讀一下。了解對於沒有參數的情況,是怎麼處理argv的。
  • do_bgfg()函數需要考慮各種情況,一開始不知道怎麼寫沒關係,後面會有測試樣例告訴你該輸出什麼。
  • bg %5 和bg 5是不一樣的!一個是對一個作業操作,另一個是對進程操作,而作業代表了一個進程組。
  • Signal handler 全部別忘了save-restore eerno。
  • 同樣的,bg %5 和bg 5 錯誤的輸出也是不一樣的,仔細看測試樣例的輸出。
  • 注意getjobpid() getjobjid() 如果找不到會返回NULL的,注意特判。
  • 如何讓暫停的程序恢復執行?用kill()發SIGCONT命令。
  • waitfg()用sigsuspend()實現。
  • (非常關鍵)假設你按下ctrl + C, shell 程序捕獲了這個信號,然後發給子進程,但是這個時候,你不應該在sigint_handler里輸出子進程中止的信息,因為子進程有可能屏蔽了SIGINT信號,或者有自己的處理函數。正確方法是在sigchld_handler()里處理各種情況。
  • 所以sigchld_handler()函數是這個lab最難的,需要處理進程正常結束,被信號stopped,被信號terminated。好好看書,要用的東西書上都有。另外別忘了修改對應進程的job狀態,該deletejob()的,改狀態的,都得處理。
  • 最後幾個測試樣例因為調用了ps命令,所以輸出的東西很多,可以重定向到文件,然後用diff命令對比。注意關掉chrome,這個進程名字非常長。

代碼實現

本代碼有缺陷,主要是在各種信號的handler里沒有用可重入版本。

/* * tsh - A tiny shell program with job control * * Written by fanesemyk, as a CSAPP self-study Lab */#include <stdio.h>#include <stdlib.h>#include <unistd.h>#include <string.h>#include <ctype.h>#include <signal.h>#include <sys/types.h>#include <sys/wait.h>#include <errno.h>/* Misc manifest constants */#define MAXLINE 1024 /* max line size */#define MAXARGS 128 /* max args on a command line */#define MAXJOBS 16 /* max jobs at any point in time */#define MAXJID 1<<16 /* max job ID *//* Job states */#define UNDEF 0 /* undefined ps:WTF is undefined? */#define FG 1 /* running in foreground */#define BG 2 /* running in background */#define ST 3 /* stopped *//* * Jobs states: FG (foreground), BG (background), ST (stopped) * Job state transitions and enabling actions: * FG -> ST : ctrl-z * ST -> FG : fg command * ST -> BG : bg command * BG -> FG : fg command * At most 1 job can be in the FG state. *//* Global variables */extern char **environ; /* defined in libc */char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */int verbose = 0; /* if true, print additional output PS:WTF?*/int nextjid = 1; /* next job ID to allocate */char sbuf[MAXLINE]; /* for composing sprintf messages */volatile sig_atomic_t flag;typedef struct job_t { /* The job struct */ pid_t pid; /* job PID */ int jid; /* job ID [1, 2, ...] */ int state; /* UNDEF, BG, FG, or ST */ char cmdline[MAXLINE]; /* command line */}job_t;struct job_t jobs[MAXJOBS]; /* The job list *//* End global variables *//* Function prototypes *//* Here are the functions that you will implement */void eval(char *cmdline);int builtin_cmd(char **argv);void do_bgfg(char **argv);void waitfg(pid_t pid);void Sigfillset(sigset_t* set);void sigchld_handler(int sig);void sigtstp_handler(int sig);void sigint_handler(int sig);pid_t Fork(void);void Sigemptyset(sigset_t* set);void Sigaddset(sigset_t* set, int signum);void Sigprocmask(int, const sigset_t*, sigset_t*);void Execve(const char* filename, char* const argv[], char* const envp[]);/* Here are helper routines that weve provided for you */int parseline(const char *cmdline, char **argv); void sigquit_handler(int sig);void clearjob(struct job_t *job);void initjobs(struct job_t *jobs);int maxjid(struct job_t *jobs); int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);//maybe jobs is a list?int deletejob(struct job_t *jobs, pid_t pid); pid_t fgpid(struct job_t *jobs);//I think this function returns the pid of the only fg processstruct job_t *getjobpid(struct job_t *jobs, pid_t pid);//find a job according to pidstruct job_t *getjobjid(struct job_t *jobs, int jid); //find a job according to jidint pid2jid(pid_t pid); void listjobs(struct job_t *jobs);void usage(void);void unix_error(char *msg);void app_error(char *msg);typedef void handler_t(int);handler_t *Signal(int signum, handler_t *handler);/* * main - The shells main routine */int main(int argc, char **argv) { char c; char cmdline[MAXLINE]; int emit_prompt = 1; /* emit prompt (default) */ /* Redirect stderr to stdout (so that driver will get all output * on the pipe connected to stdout) */ dup2(1, 2); /* Parse the command line */ while ((c = getopt(argc, argv, "hvp")) != EOF) { switch (c) { case h: /* print help message */ usage(); break; case v: /* emit additional diagnostic info */ verbose = 1; break; case p: /* dont print a prompt */ emit_prompt = 0; /* handy for automatic testing */ break; default: usage(); } } /* Install the signal handlers */ /* These are the ones you will need to implement */ Signal(SIGINT, sigint_handler); /* ctrl-c */ Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */ Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */ /* This one provides a clean way to kill the shell */ Signal(SIGQUIT, sigquit_handler); /* Initialize the job list */ initjobs(jobs); /* Execute the shells read/eval loop */ while (1) { /* Read command line */ if (emit_prompt) { printf("%s", prompt); fflush(stdout); } if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin)) app_error("fgets error"); if (feof(stdin)) { /* End of file (ctrl-d) */ fflush(stdout); exit(0); } /* Evaluate the command line */ eval(cmdline); fflush(stdout); } exit(0); /* control never reaches here */} /* * 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 dont receive SIGINT (SIGTSTP) from the kernel * when we type ctrl-c (ctrl-z) at the keyboard. */void eval(char *cmdline) { char* argv[MAXARGS];//Argument list execve() char buf[MAXLINE];//Holds modifiled command line int fg, jid;//Should the job run in bg or fg? pid_t pid;//Process id sigset_t mask_all, mask_one, prev_one; Sigfillset(&mask_all);//mask_all contains all kinds of signals Sigemptyset(&mask_one); Sigaddset(&mask_one, SIGCHLD);//mask_one blocks signal SIGCHLD strcpy(buf, cmdline); fg = !parseline(buf, argv); if(argv[0] == NULL) return;//Ignore empty lines if(!builtin_cmd(argv))//If it is a builtin command, then the builtin_cmd() will execute it { Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);//block SIGCHLD to avoid race if((pid = Fork()) == 0)//Child runs user job { setpgrp();//set the processs groupid to its pid //why? see Write-Up for more details Sigprocmask(SIG_SETMASK, &prev_one, NULL);//restore the initial mask vector if(execve(argv[0], argv, environ) < 0) { printf("%s: Command not found
", argv[0]); exit(-1); } } Sigprocmask(SIG_BLOCK, &mask_all, NULL);//block all signals, do the addjob() //Parent waits for foreground job to terminate if(fg) { flag = 0;//set the global flag varible, indicate whether the forground job has finished addjob(jobs, pid, FG, buf); //Attention! The signals are still being blocked waitfg(pid); } else//background { jid = addjob(jobs, pid, BG, buf); Sigprocmask(SIG_SETMASK, &prev_one, NULL); //restore intial block vector printf("[%d] (%d) %s", jid, pid, buf); } } return;}/* * parseline - Parse the command line and build the argv array. * * Characters enclosed in single quotes are treated as a single * argument. Return true if the user has requested a BG job, false if * the user has requested a FG job. */int parseline(const char *cmdline, char **argv) { static char array[MAXLINE]; /* holds local copy of command line */ char *buf = array; /* ptr that traverses command line */ char *delim; /* points to first space delimiter */ int argc; /* number of args */ int bg; /* background job? */ strcpy(buf, cmdline); buf[strlen(buf)-1] = ; /* replace trailing
with space */ while (*buf && (*buf == )) /* ignore leading spaces */ buf++; /* Build the argv list */ argc = 0; if (*buf == ) { buf++; delim = strchr(buf, ); } else { delim = strchr(buf, ); } while (delim) { argv[argc++] = buf; *delim = ; buf = delim + 1; while (*buf && (*buf == )) /* ignore spaces */ buf++; if (*buf == ) { buf++; delim = strchr(buf, ); } else { delim = strchr(buf, ); } } argv[argc] = NULL; if (argc == 0) /* ignore blank line */ return 1; /* should the job run in the background? */ if ((bg = (*argv[argc-1] == &)) != 0) { argv[--argc] = NULL; } return bg;}/* * 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 - Execute the builtin bg and fg commands */void do_bgfg(char **argv) { if(argv[1] == NULL) { printf("%s command requires PID or %%jobid argument
", argv[0]); return; } int bg = 0;//whether the command is bg if(!strcmp(argv[0], "bg")) bg = 1; job_t* job_ptr = NULL; int pid, jid; int isjob; if(sscanf(argv[1], "%d", &pid) > 0)//There is no %, so the number is process PID { isjob = 0; job_ptr = getjobpid(jobs, pid); if(job_ptr == NULL || (job_ptr -> state == UNDEF)) { printf("(%d): No such process
", pid); return; } jid = job_ptr -> jid; } else if(sscanf(argv[1], "%%%d", &jid) > 0)//The number is job jid { isjob = 1; job_ptr = getjobjid(jobs, jid); if(job_ptr == NULL || (job_ptr -> state == UNDEF)) { printf("%%%d: No such job
", jid); return; } pid = job_ptr -> pid; } else { printf("%s: argument must be a PID or %%jobid
", argv[0]); return; } if(bg)//The command is bg { if(job_ptr -> state == BG)//do nothing, just print the message { printf("[%d] (%d) %s", jid, pid, job_ptr -> cmdline); return; } if(job_ptr -> state == ST)//send SITCONT to the job/process { if(isjob)//Situation differs between jid and pid! a jid refers to a process group kill(-pid, SIGCONT);//send signal to the whole group else kill(pid, SIGCONT); job_ptr -> state = BG; printf("[%d] (%d) %s", jid, pid, job_ptr -> cmdline); } } else//The fg command { sigset_t mask_full; Sigfillset(&mask_full); Sigprocmask(SIG_BLOCK, &mask_full, NULL); flag = 0; if(job_ptr -> state == ST)//send SIGCONT to the process/process group { if(isjob) kill(-pid, SIGCONT); else kill(pid, SIGCONT); } job_ptr -> state = FG; waitfg(pid); return; }}/* * waitfg - Block until process pid is no longer the foreground process */void waitfg(pid_t pid)//use global varible flag, which indicates whether the fg work has been finished/stopped{ sigset_t mask_empty; Sigemptyset(&mask_empty); while(!flag)//use while, because we could possibly recieve the SIGCHLD of bg jobs/ or other signals { sigsuspend(&mask_empty); } Sigprocmask(SIG_SETMASK, &mask_empty, NULL);//restore the block vector return;}/***************** * 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. (use option of waitpid!) * The handler reaps all * available zombie children, but doesnt wait for any other * currently running children to terminate. */void sigchld_handler(int sig)//Both process stopped/ terminated will send SIGCHLD to its parent//So we should deal with these conditions in this handler(because the child process could block the SIGTSTP so ctrl + Z will not //stop the child process{ //printf("Sigchild handler!
"); int olderrno = errno; int status; struct job_t* job_ptr = NULL; pid_t child_pid; pid_t fg_pid = fgpid(jobs); while((child_pid = waitpid(-1, &status, WUNTRACED | WNOHANG )) > 0)//return immediately //the option means two things: if there exists child process running, return immediately with 0 //if there are child processes stopped or terminated, return the pid { //printf("!
"); if(child_pid == fg_pid) flag = 1; if(WIFEXITED(status))//if the child process exit normally { deletejob(jobs, child_pid);//delete the job from job list } else if(WIFSIGNALED(status))//if the child process is terminated because of a signal(Ctrl + C) { job_ptr = getjobpid(jobs, child_pid); printf("Job [%d] (%d) terminated by signal %d
", job_ptr -> jid, fg_pid, WTERMSIG(status)); deletejob(jobs, child_pid);//delete the job from job list } else//the child process is stopped { job_ptr = getjobpid(jobs, child_pid); job_ptr -> state = ST;//The process is stopped printf("Job [%d] (%d) stopped by signal %d
", job_ptr -> jid, child_pid, WSTOPSIG(status)); } } errno = olderrno;}/* * 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. Attention, you need to send it to the whole * group! */void sigint_handler(int sig) { int olderrno = errno; pid_t fg_pid = fgpid(jobs); if(fg_pid == 0)//no foreground group return; kill(-fg_pid, SIGINT);//send SIGINT to the whole foreground group errno = olderrno;}/* * 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. The same as sigint_handler. */void sigtstp_handler(int sig) { int olderrno = errno; pid_t fg_pid = fgpid(jobs); if(fg_pid == 0)//no foreground group return; kill(-fg_pid, SIGTSTP);//send SIGTSTP to the whole foreground group errno = olderrno;}/********************* * End signal handlers *********************//*********************************************** * Helper routines that manipulate the job list **********************************************//* clearjob - Clear the entries in a job struct */void clearjob(struct job_t *job) { job->pid = 0; job->jid = 0; job->state = UNDEF; job->cmdline[0] = ;}/* initjobs - Initialize the job list */void initjobs(struct job_t *jobs) { int i; for (i = 0; i < MAXJOBS; i++) clearjob(&jobs[i]);}/* maxjid - Returns largest allocated job ID */int maxjid(struct job_t *jobs) { int i, max=0; for (i = 0; i < MAXJOBS; i++) if (jobs[i].jid > max) max = jobs[i].jid; return max;}/* I think there is a bug about nextjid *//* addjob - Add a job to the job list */int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline) { int i; if (pid < 1) return 0; for (i = 0; i < MAXJOBS; i++) { if (jobs[i].pid == 0) { jobs[i].pid = pid; jobs[i].state = state; jobs[i].jid = nextjid++; if (nextjid > MAXJOBS) nextjid = 1; strcpy(jobs[i].cmdline, cmdline); if(verbose){ printf("Added job [%d] %d %s
", jobs[i].jid, jobs[i].pid, jobs[i].cmdline); } return jobs[i].jid; } } printf("Tried to create too many jobs
"); return 0;}/* deletejob - Delete a job whose PID=pid from the job list */int deletejob(struct job_t *jobs, pid_t pid) { int i; if (pid < 1) return 0; for (i = 0; i < MAXJOBS; i++) { if (jobs[i].pid == pid) { clearjob(&jobs[i]); nextjid = maxjid(jobs)+1; return 1; } } return 0;}/* fgpid - Return PID of current foreground job, 0 if no such job */pid_t fgpid(struct job_t *jobs) { int i; for (i = 0; i < MAXJOBS; i++) if (jobs[i].state == FG) return jobs[i].pid; return 0;}/* getjobpid - Find a job (by PID) on the job list */struct job_t *getjobpid(struct job_t *jobs, pid_t pid) { int i; if (pid < 1) return NULL; for (i = 0; i < MAXJOBS; i++) if (jobs[i].pid == pid) return &jobs[i]; return NULL;}/* getjobjid - Find a job (by JID) on the job list */struct job_t *getjobjid(struct job_t *jobs, int jid) { int i; if (jid < 1) return NULL; for (i = 0; i < MAXJOBS; i++) if (jobs[i].jid == jid) return &jobs[i]; return NULL;}/* pid2jid - Map process ID to job ID */int pid2jid(pid_t pid) { int i; if (pid < 1) return 0; for (i = 0; i < MAXJOBS; i++) if (jobs[i].pid == pid) { return jobs[i].jid; } return 0;}/* listjobs - Print the job list */void listjobs(struct job_t *jobs) { int i; for (i = 0; i < MAXJOBS; i++) { if (jobs[i].pid != 0) { printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid); switch (jobs[i].state) { case BG: printf("Running "); break; case FG: printf("Foreground "); break; case ST: printf("Stopped "); break; default: printf("listjobs: Internal error: job[%d].state=%d ", i, jobs[i].state); } printf("%s", jobs[i].cmdline); } }}/****************************** * end job list helper routines ******************************//*********************** * Other helper routines ***********************//* * usage - print a help message */void usage(void) { printf("Usage: shell [-hvp]
"); printf(" -h print this message
"); printf(" -v print additional diagnostic information
"); printf(" -p do not emit a command prompt
"); exit(1);}/* * unix_error - unix-style error routine */void unix_error(char *msg){ fprintf(stdout, "%s: %s
", msg, strerror(errno)); exit(1);}/* * app_error - application-style error routine */void app_error(char *msg){ fprintf(stdout, "%s
", msg); exit(1);}/* * Signal - wrapper for the sigaction function */handler_t *Signal(int signum, handler_t *handler) { struct sigaction action, old_action; action.sa_handler = handler; sigemptyset(&action.sa_mask); /* block sigs of type being handled */ action.sa_flags = SA_RESTART; /* restart syscalls if possible */ if (sigaction(signum, &action, &old_action) < 0) unix_error("Signal error"); return (old_action.sa_handler);}/* * sigquit_handler - The driver program can gracefully terminate the * child shell by sending it a SIGQUIT signal. */void sigquit_handler(int sig) { printf("Terminating after receipt of SIGQUIT signal
"); exit(1);}pid_t Fork(void){ pid_t pid; if((pid = fork()) < 0) unix_error("Fork error"); return pid;}void Sigprocmask(int how, const sigset_t* set, sigset_t* oldset){ if(sigprocmask(how, set, oldset) < 0) unix_error("Sigprocmask error"); return;}void Execve(const char* filename, char* const argv[], char* const envp[]){ if(execve(filename, argv, envp) < 0) unix_error("Execve error");}void Sigfillset(sigset_t* set){ if(sigfillset(set) < 0) unix_error("Sigfillset error"); return;}void Sigemptyset(sigset_t* set){ if(sigemptyset(set) < 0) unix_error("Sigemptyset error"); return ;}void Sigaddset(sigset_t* set, int signum){ if(sigaddset(set, signum) < 0) unix_error("Sigaddset error"); return;}

推薦閱讀:

TAG:深入理解計算機系統書籍 | 操作系統 | Linux |