前言

视频现已刷完好几天了,lab才打完这个,不得不说shell lab也是规划的非常交心,注意事项简直都能在文档找到。

参阅资料:

课程视频链接:2015 CMU 15-213 CSAPP 深入了解计算机体系 课程视频

试验文档:shlab.dvi (cmu.edu)

一、试验须知

试验文件现已在main函数中为咱们完结了指令行参数的读取、给信号绑定handler等主体部分,并且供给了一系列有用好用的函数,咱们需求做的只有在tsh.c中完结以下函数:

  1. eval: 解析每条指令行的类型并履行
  2. builtin_cmd: 辨认和处理四条内置指令: quit, fg, bg 和 jobs.
  3. do_bgfg: 完结内置指令fg 和 bg
  4. waitfg: 等候前台作业完结
  5. sigchld_handler: 接纳SIGCHLD信号
  6. sigint_handler: 接纳SIGINT(Ctrl C)信号
  7. sigtstp_handler: 接纳SIGINT(Ctrl Z)信号

试验文件还供给了参阅的tshref,sdriver.pl以及许多tracexx.txt文件供咱们对比检验自己的tsh是否符合要求,运用方法如下:

root@Andrew:/usr/coding/shlab-handout# ./sdriver.pl -a "-p" -t trace01.txt -s ./tsh
#
# trace01.txt - Properly terminate on EOF.
#
root@Andrew:/usr/coding/shlab-handout# ./sdriver.pl -a "-p" -t trace01.txt -s ./tshref
#
# trace01.txt - Properly terminate on EOF.
#
root@Andrew:/usr/coding/shlab-handout#

-a “-p”表示输入不会有剩余的prompt,观感更佳
-t 指定tracexx.txt,即测验点,共有16个
-s 指定shell,一次指定参阅文件tshref,一次指定自己的tsh,查看两次的输出是否一致即可

二、编写代码

依照把trace01到trace16一个一个攻关的思路,就能一点点把shell造全了。

由于进程实在有些弯曲,并且也有部分失败的代码被抛弃了,就懒得展现每一步的进程了。直接上每个函数的代码然后说明。

1. eval

eval的参数cmdline是从指令行输入的一串字符,结束是换行符n,所以在输出cmdline是自带换行

试验供给了函数parseline,用于解析cmdline中的每个参数,回来一个int值bg代表后台作业(结束是&符号的时候)

随后利用builtin_cmd判别是否为内置指令,是的话在其间履行,回来1;不是的话回来0,之后再fork execve调用可履行文件。注意到需求对不存在的可履行文件做异常处理,打印Command not found,并且指定子进程的进程组id为本身setgpid(0, 0),然后确保前台进程组只有咱们的shell。

After the fork, but before the execve, the child process should call setpgid(0, 0), which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group

由于咱们保护一个全局的job_t类型数组jobs,并在每次创立作业后调用addjob保存作业的信息到jobs当中。

可是由于咱们将要在sigchld_handler中对jobs数组做某些操作,比如运用deletejob。为了确保addjob和deletejob的顺序,咱们必须堵塞SIGCHLD 从fork进程之完结了addjob之后,才允许进程接纳SIGCHLD

完好代码如下:

void eval(char *cmdline)
{   
    char *argv[MAXARGS];
    int bg = parseline(cmdline, argv);
    if(argv[0] == NULL)
        return;
    pid_t pid;
    if(!builtin_cmd(argv)){
        // 堵塞SIGCHLD的接纳,确保addjob和deletejob的顺序
        sigset_t mask;
        sigemptyset(&mask);
        sigaddset(&mask, SIGCHLD);
        sigprocmask(SIG_BLOCK, &mask, NULL);
        if((pid = fork()) < 0){
            // Fork失败
            unix_error("Fork error");
        }else if(pid == 0){
            // 子进程,免除SIGCHLD的堵塞,然后execve运转程序
            sigprocmask(SIG_UNBLOCK, &mask, NULL);
            setpgid(0, 0);
            if(execve(argv[0], argv, environ) < 0){
                printf("%s:Command not found.n",argv[0]);
                exit(-1);
            }
        }else{
            // 主进程
            // 添加job后免除堵塞,确保add和delete的顺序
            // 前台运转时,调用waitfg做自旋(spin)
            addjob(jobs, pid, bg ? BG : FG, cmdline);
            sigprocmask(SIG_UNBLOCK, &mask, NULL);
            if(bg){
                printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
            }else{
                waitfg(pid);
            }
        }
    }  
    return;
}

2. builtin_cmd

内置指令包括四个:

  1. quit:遇到quit时,直接中止shell地点进程就行了,简略!
  2. jobs:也简略!试验现已给咱们写好了listjobs,直接调就好了
  3. bg和fg:辨认出来后就丢进do_bgfg里边,别的再处理
int builtin_cmd(char **argv)
{   
    if(!strcmp(argv[0], "quit")){
        exit(0);
    }
    if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")){
        do_bgfg(argv);
        return 1;
    }
    if(!strcmp(argv[0], "jobs")){
        listjobs(jobs);
        return 1;
    }  
    return 0;     /* not a builtin command */
}

3. sigint_handler 和 sigtstp_handler

经过函数fgpid拿到前台作业的pid,然后将信号发送给进程组中每个进程

int kill(pid_t pid, int sig)
假如 pid 大于零,那么 kill 函数发送信号号码 sig 给进程 pid。假如 pid 小于零,那么 kill 发送信号sig给进程组 abs(pid)中的每个进程。

void sigint_handler(int sig)
{   
    pid_t pid = fgpid(jobs);
    // printf("Job [%d] (%d) terminated by signal 2n", pid2jid(pid), pid);
    kill(-pid, SIGINT);
    return;
}
void sigtstp_handler(int sig)
{   
    pid_t pid = fgpid(jobs);
    // printf("Job [%d] (%d) stopped by signal 20n", pid2jid(pid), pid);
    kill(-pid, SIGTSTP);
    return;
}

这儿不打印前台作业被中止或中止的信息,是由于进程被中止或中止时会发送SIGCHLD信号,咱们能够在sigchld_handler中处理。

而非要在sigchld_handler处理的原因是:sigchld_handler不只能够监控前台作业,还能够监控后台作业的中止和中止(为了经过trace16),并且能够利用status获得中止或中止的原因

4. do_bgfg

bg 和 fg的处理思路高度相似,先解析参数有没有%,然后决定是运用getjobjid仍是getjobpid确认作业是否存在,然后做一系列error handling(trace13要求)

最后,向作业的pid所标识进程组发送信号SIGCONT,假如是bg就打印后台作业的信息,假如是fg就调用waitfg自旋

完好代码如下:

void do_bgfg(char **argv)
{   
    // parse argument 
    int jid = 0;
    pid_t pid = 0;
    int bg = !strcmp(argv[0], "bg");
    struct job_t *job;
    if(argv[1] == NULL){
        printf("%s command requires PID or %%jobid argumentn", argv[0]);
        return;
    }
    if(argv[1][0] == '%'){
        jid = atoi(argv[1]   1);
    }else{
        pid = atoi(argv[1]);
    }
    if(jid){
        job = getjobjid(jobs, jid);
        if(job == NULL){
            printf("%%%d: No such jobn", jid);
            return ;
        }
        pid = job->pid;
    }else if(pid){
        job = getjobpid(jobs, pid);
        if(job == NULL){
            printf("(%d): No such processn", pid);
            return ;
        }
    }else{
        printf("%s: argument must be a PID or %%jobidn", argv[0]);
        return ;
    }
    kill(-pid, SIGCONT);
    if(bg){
        job->state = BG;
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
    }else{
        job->state = FG;
        waitfg(pid);
    }
    return;
}

5. waitfg

前面说了几次调用waitfg自旋(在eval和do_bgfg中),为啥要自旋而不是waitpid呢?

由于假如咱们在waitfg中调用waitpid收回前台作业的话,当该作业中止或中止,它会发送信号SIGCHLD,这就造成了sigchld_handler和waitfg的竞争联系,那就不如让sigchld_handler收回就好了。

并且文档中也清晰主张了waitfg中仅运用一个循环 sleep:

One of the tricky parts of the assignment is deciding on the allocation of work between the waitfg and sigchld_handler functions. We recommend the following approach:

– In waitfg, use a busy loop around the sleep function.

– In sigchld_handler, use exactly one call to waitpid.

While other solutions are possible, such as calling waitpid in both waitfg and sigchld_handler, these can be very confusing. It is simpler to do all reaping in the handler.

代码如下:

void waitfg(pid_t pid)
{   
    while(fgpid(jobs) == pid){
        sleep(1);
    }
    return;
}

6. sigchld_handler

最重量级的来了,前面说到:当子进程中止或中止时,都会给父进程发送一个SIGHCLD信号,告诉父进程该调用waitpid去收回它了

当然,waitpid也有很多花活:

pid_t waitpid(pidt pid,int *status, int options);

假如 pid>0,那么等候调集便是一个独自的子进程,它的进程ID等于 pid。
假如 pid=-1,那么等候调集便是由父进程一切的子进程组成的。

所以咱们这儿应该运用pid = -1等候任一子进程

能够经过用常量 WNOHANG 和WUNTRACED的不同组合来设置 options,修正默认行为:
WNOHANG: 假如没有等候调集中的任何子进程中止,那么就当即回来(回来值为0)。
WUNTRACED: 挂起调用进程的履行,直到等候调集中的一个进程变成中止的或许被暂停。回来的 PID为导致回来的中止或暂停子进程的PID。
WNOHANG | WUNTRACED: 当即回来,假如没有等候调集中的任何子进程中止或中止,那么回来值为0,或许回来值等于那个被中止或许中止子进程的PD。

咱们需求sigchld_handler在后台静静地处理中止和中止的子进程,而不会影响主进程的履行,所以options应该挑选WNOHANG | WUNTRACED

然后便是根据几个解释status的宏,处理每种发送SIGCHLD的状况

WIFEXITED(status): 假如子进程正常中止就回来真,也便是经过调用exit 或许一个回来(return)
WIFSIGNALED(status): 假如是由于一个未被捕获的信号造成了子进程的中止,那么就回来真
WIFSTOPPED(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);
        }else if(WIFSIGNALED(status)){
            // 收到信号中止(为了能监控其他进程)
            printf("Job [%d] (%d) terminated by signal 2n", pid2jid(pid), pid);
            deletejob(jobs, pid);
        }else if(WIFSTOPPED(status)){
            // 假如进程中止
            printf("Job [%d] (%d) stopped by signal 20n", pid2jid(pid), pid);
            struct job_t *job = getjobpid(jobs, pid);
            job->state = ST;
        }
    }
    return;
}