实验题目:shell-lab |
实验目的: 在本次实验中,我们需要构建一个简单的类Unix/Linux Shell。基于已经提供的“微Shell”框架tsh.c,完成部分函数和信号处理函数的编写工作。使用sdriver.pl可以评估你所完成的shell的相关功能。 |
实验环境: Ubuntu12.04 |
实验内容及操作步骤:
在本次实验中,我们需要做的就是填充如下几个函数的内容:
我们的tsh-shell应该有以下功能:
如何检查程序: 每一次修改好代码后都需要用make来重新编译更新我们的系统。 文件提供了16个跟踪文件(trace01-16.txt),通过这些文件结合shell驱动程序,我们来测试shell的正确性。编号较低的文件执行非常简单的测试,编号较高的测试执行更复杂的测试。 一些tips:
父级需要以这种方式阻止 SIGCHLD 信号,以避免在父级调用 addjob 之前子级被 sigchld 处理程序收割(并因此从作业列表中删除)的竞争条件。
解决方法如下:在fork之后,但在execve之前,子进程应该调用setpgid(0,0),这会将子进程放入一个新的进程组中,其组ID与子进程的PID相同。这将确保前台进程组中只有一个进程,即您的shell。当您键入ctrl-c时,shell应该捕获生成的SIGINT,然后将其转发到相应的前台作业(或者更准确地说,是包含前台作业的进程组)。 准备工作: 先用tar xvf shlab-handout.tar解压压缩包;在tsh.c文件中补充代码;利用make和16个test.txt文件进行测试。
1、void eval(char *cmdline)函数 函数功能: 用于评估用户刚输入的命令行。如果用户请求了内置命令(quit,jobs,bg或fg),则立即执行。 否则,fork一个子进程并在子进程的上下文中运行该作业。如果作业在前台运行,等待它终止然后返回。 参数: 形参是用户输入的命令行cmdline 。实参是argv , mask, SIG_UNBLOCK , argv[0] , environ。 代码: 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)//block mask集合置空 unix_error("sigemptyset error"); if (sigaddset(&mask, SIGCHLD)) //添加SIGCHLD进mask集合 unix_error("sigaddset error"); if (sigaddset(&mask, SIGINT)) //添加SIGINT进集合 unix_error("sigaddset error"); if (sigaddset(&mask, SIGTSTP)) //添加SIGTSTP(来自终端的停止信号)进集合 unix_error("sigaddset error"); if (sigprocmask(SIG_BLOCK, &mask, NULL) < 0)//用mask更新BLOCK 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);//用mask删除SIG_BLOCK
/* 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) //将创建新的进程组,进程组ID为调用进程ID 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 found\n", 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);//还原Block
if (!bg) waitfg(pid);//前台进行 else printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);//后台进行 } /* $end handout */ return; } 处理流程: 第一步:定义各个变量。使用parseline()函数函数解析命令行,得到命令行参数。如果argv[0]=NULL,则无命令。 第二步:使用builtin_cmd()函数判断命令是否为内置命令,如果不是内置命令,则继续执行。 第三步:设置阻塞集合。先初始化mask为空集合,再将SIGCHLD , SIGINT ,SIGTSTP 信号加入阻塞集合。然后创建子进程。 第四步:阻塞SIGCHLD,防止子进程在父进程之前结束,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。 第五步:子进程中,先解除对SIG_CHLD的阻塞,再使用setpgid(0,0)创建一个虚拟的进程组,使子进程拥有自己唯一的进程组ID,目的是为了在键盘上键入Ctrl+C (Ctrl+Z)时,我们的后台子进程就不会从内核接收SIGINT (SIGTSTP)信号。该进程组ID表明其不和tsh进程在一个进程组。然后调用execve函数,执行相应的文件。 第六步:将job添加到job list,解除SIG_CHLD阻塞信号。判断进程是否为前台进程,如果是前台进程,调用waitfg()函数,等待前台进程,如果是后台进程,则打印出进程信息。 要点分析: 1.在键盘上键入Ctrl+C (Ctrl+Z)时,我们的后台子进程就不会从内核接收SIGINT (SIGTSTP)信号。 2.在执行addjob之前需要阻塞信号,防止addjob()函数错误地把(不存在的)子进程添加到作业列表中。 2. int builtin_cmd(char **argv)函数 函数功能: 如果用户键入了内置命令,则立即执行。 参数: 形参:传入的参数数组argv 。实参:argv[0] ,“quit” ,"&" ,“jobs” ,“bg” ,“fg”, argv,SIG_BLOCK ,mask ,prev ,NULL。 代码: int builtin_cmd(char **argv) { sigset_t mask, prev; sigfillset(&mask);//阻塞全部信号,为下面lishjobs调用全局变量jobs准备 char *cmd = argv[0];//第一个字符 if(cmd == NULL) //若为空,就直接返回,返回1表示不用进行if语句内包含的内容因为如果仅仅按下回车会使得strcmp失效 return 1; if(!strcmp(cmd, "&"))//若只单单有一个&字符,同上 return 1; if(!strcmp(cmd, "quit"))//若退出,则选择退出,并选用安全的退出函数 _exit(0); if(!strcmp(cmd, "jobs")){//jobs列出当前的进程 sigprocmask(SIG_BLOCK, &mask, &prev);//调用全局变量前需要阻塞全部信号 listjobs(jobs); sigprocmask(SIG_SETMASK, &prev, NULL);//恢复原来信号 return 1; } if(!strcmp(cmd, "bg") || !strcmp(cmd, "fg")){//交由do_bgfg函数处理 do_bgfg(argv); return 1; } return 0; /* not a builtin command */ } 处理流程: 函数需要根据传入的参数数组,判断用户键入的是否为内置命令,采取的办法就是比较argv[0]和内置命令,如果是内置命令,则跳到相应的函数,并且返回1,如果不是,则什么也不做,并且返回0。 其中,如果命令为quit,则直接退出;如果命令是内置的jobs命令,则调用listjobs()函数,打印job列表;如果是fg或是bg两条内置命令,则调用do_bgfg()函数来处理即可。 要点分析: 1.要注意如果用户仅仅按下回车键,那么在解析后argv的第一个变量将是一个空指针。如果用这个空指针去调用strcmp函数会引发segment fault。 2.因为jobs是全局变量,为了防止其被修改,需要阻塞全部信号,过程大致为(后面函数阻塞全部信号的做法与此基本一致): 3. void do_bgfg(char **argv) 函数 函数功能: 执行内置bg和fg命令。 参数: 形参是传入的参数数组argv 。实参是argv[1] , argv[1][0] , jobs , pid , argv[1] , argv[0]。 代码: 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 argument\n", 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 process\n", pid); return; } } else if (argv[1][0] == '%') { int jid = atoi(&argv[1][1]); if (!(jobp = getjobjid(jobs, jid))) { printf("%s: No such job\n", argv[1]); return; } } else { printf("%s: argument must be a PID or %%jobid\n", 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 error\n"); exit(0); } /* $end handout */ return; } 处理流程: 第一步:先判断fg或bg后是否有参数,如果没有,则忽略命令。 第二步:如果fg或bg后面只是数字,说明取的是进程号,获取该进程号后,使用getjobpid(jobs, pid)得到job;如果fg或bg后面是%加上数字的形式,说明%后面是任务号(第几个任务),此时获取jid后,可以使用getjobjid(jobs, jid)得到job。 第三步:比较区分argv[0]是“bg”还是“fg”。如果是后台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为BG,打印任务的jid,pid和命令行;如果是前台进程,则发送SIGCONT信号给进程组PID的每个进程,并且设置任务的状态为FG,调用waitfg(jobp->pid),等待前台进程结束。 要点分析: 1.函数主要是先判断fg后面是%+数字还是只有数字的形式,从而根据进程号pid或是工作组号jid来获取结构体job;然后在根据前台和后台进程的不同,执行相应的操作。 2. isdigit()函数判断是否为数字,不是数字返回0。 3. atoi()函数把字符串转化为整型数。 4. SIGCONT信号对应事件为:继续进程如果该进程停止。 4. void waitfg(pid_t pid) 函数 函数功能: 阻止直到进程pid不再是前台进程。 参数: 形参是前台进程pid。实参是mask,jobs。 代码: void waitfg(pid_t pid) { sigset_t mask; sigemptyset(&mask);//在挂起进程的过程中清空block以便信号能够响应 while(pid == fgpid(jobs)) {//前台工作的进程值比较 sigsuspend(&mask);//挂起进程 } return; } 处理流程: 函数主体是while循环语句,判断传入的pid是否为一个前台进程的pid,如果是,则一直循环,如果不是,则跳出循环。其中while循环内部使用sigsuspend()函数,暂时用mask替换当前的阻塞集合,然后挂起该进程,直到收到一个信号,选择运行一个处理程序或者终止该进程。 要点分析: 1.在while内部,如果使用的只是pause()函数,那么程序必须等待相当长的一段时间才会再次检查循环的终止条件,如果使用向nanosleep这样的高精度休眠函数也是不可接受的,因为没有很好的办法来确定休眠的间隔。但是PPT上示例代码却给出sleep(1),我:???。 2.在while循环语句之前,初始化mask结合为空,在while内部用SIG_SETMASK使block=mask,这样sigsuspend()才不会因为收不到SIGCHLD信号而永远睡眠。 5. void sigchld_handler(int sig) 函数 函数功能: 每当一个子进程终止(成为僵死进程),或者因为接收到SIGSTOP或SIGTSTP信号而停止时,内核就向shell发送一个SIGCHLD。处理程序获取所有可用的僵死子进程,但不等待当前正在运行的任何其他子进程终止。 参数: 形参是sig,实参是status , WNOHANG | WUNTRACED , SIG_BLOCK , mask , prev, SIG_SETMASK。 代码: void sigchld_handler(int sig) { int olderrno = errno, status;//status 用于检查回收子进程的退出状态 sigset_t mask, prev;//用于阻塞全局共享数据结构 struct job_t *jobfirst;//方便后续删除 sigfillset(&mask); pid_t pid;//记录停止的子进程ID while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){//立即返回,如果等待集合中的子进程都没有被停止或终止,则返回值为0;如果有一个停止或终止,则返回值为该子进程的PID sigprocmask(SIG_BLOCK, &mask, &prev);//删除进程时需要阻塞所有信号——引用了全局共享数据结构 jobfirst = getjobpid(jobs, pid);//根据PID寻找进程 if(WIFEXITED(status)) {//引起返回的子进程当前是正常停止的 deletejob(jobs, pid); } else if(WIFSIGNALED(status)){//因一个未被捕获的信号终止 printf("Job [%d] (%d) terminated by signal %d\n", jobfirst->jid, jobfirst->pid, WTERMSIG(status)); deletejob(jobs, pid); } else if(WIFSTOPPED(status)){//因为子进程停止而停止 jobfirst->state = ST; printf("Job [%d] (%d) stopped by signal %d\n", jobfirst->jid, jobfirst->pid, WSTOPSIG(status)); } fflush(stdout); sigprocmask(SIG_SETMASK, &prev, NULL);//恢复Block } errno = olderrno;//还原errno return; } 处理流程: 第一步:把每个信号都添加到mask阻塞集合中,设置olderrno = errno 。 第二步:在while循环中使用waitpid(-1, &status, WNOHANG | WUNTRA CED)),其中,目的是尽可能回收子进程,其中WNOHANG | WUNTRACED表示立即返回,如果等待集合中没有进程被中止或停止返回0,否则孩子返回进程的pid。 第三步:在循环中阻塞信号,并且使用getjobpid()函数,通过pid找到job 。 第四步:通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。如果引起返回的子进程当前是停止的,那么WIFSTOPPED(status)就返回真,此时只需要将pid找到的job的状态改为ST,并且按照示例程序输出的信息,将job的jid,pid以及导致子进程停止的信号的编号输出即可。如果子进程是因为一个未被捕获的信号终止的,那么WIFSIGNALED(status)就返回真,此时同样按照示例程序输出的信息,将job的jid,pid以及导致子进程终止的信息的编号输出即可,因为此时进程是中止的的进程,所以还需要deletejob()将发出SIGCHLD信号的将其直接回收。 第五步:清空缓冲区,解除阻塞,恢复errno。 要点分析: 1.while循环来避免信号阻塞的问题,循环中使用waitpid()函数,以尽可能多的回收僵尸进程。但是使用while可能会让waitpid等待后台还在进行的进程结束,但如果使用一次if可能会导致信号累加的问题,例如多个后台程序同时结束的情况。然后PPT上建议使用一次waitpid,示例代码却又将其用在了while循环判断条件。考虑到函数的目的是要获取所有可用的僵死进程,故而采用while循环。 2.调用deletejob()函数时,因为jobs是全局变量,因此需要阻塞信号。 3.通过waitpid在status中放上的返回子进程的状态信息,判断子进程的退出状态。WIFSIGNALED判断子进程是否因为一个未被捕获的信号中止的,WIFSTOPPED判断引起返回地子进程当前是否为停止的。WIFEXITED判断是否是正常返回。 6. void sigint_handler(int sig) 函数和void sigchld_handler(int sig) 函数 函数功能: 前者:当用户在键盘上键入Ctrl+C时,内核向shell发送一个SIGINT。捕获它并将其发送到前台作业。 后者:每当用户在键盘上键入Ctrl+Z时,内核都会向shell发送一个SIGTSTP。捕获它并通过发送一个SIGTSTP来挂起前台作业。 参数: 形参是sig,实参是SIG_BLOCK , mask , prev, SIG_SETMASK。 代码: void sigint_handler(int sig) { pid_t pid; sigset_t mask, prev; int olderrno = errno; sigfillset(&mask); sigprocmask(SIG_SETMASK, &mask, &prev);//日常…… pid = fgpid(jobs);//获取前台进程组 sigprocmask(SIG_SETMASK, &prev, NULL); if(pid != 0) kill(-pid, SIGINT);//发给前台进程组信号 errno = olderrno; return; } void sigtstp_handler(int sig) { pid_t pid; sigset_t mask, prev; int olderrno = errno; sigfillset(&mask); sigprocmask(SIG_SETMASK, &mask, &prev);//日常…… pid = fgpid(jobs);//获取前台进程组 sigprocmask(SIG_SETMASK, &prev, NULL); if(pid != 0) kill(-pid, SIGTSTP);//发给前台进程组信号 errno = olderrno; return; } 处理流程: 第一步:将每个信号添加至mask阻塞集合,设置olderrno = errno。 要点分析: jobs为全局共享数据结构,需要进行信号阻塞后对它进行调用,然后需还原信号阻塞。发送信号的时候需要对整个进程组进行。
tsh和tshref进行对比,相同则说明结果正确。
——————————————————————————————————————————
——————————————————————————————————————————
——————————————————————————————————————————
——————————————————————————————————————————
——————————————————————————————————————————
——————————————————————————————————————————
—————————————————————————————————————————— 测试结果显示正确。 四、收获与体会 在本次实验中,我直到了shell接口的一些用法,对于信号处理相关的知识也更熟练了。 |
05-16
313

11-22
1089

08-20
1144

07-16
2万+
