【CMU 15-213 CSAPP】详解shell lab——编写自己的shell

前言

视频已经刷完好几天了,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是否符合要求,使用方法如下:

shell 复制代码
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

完整代码如下:

c 复制代码
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里边,另外再处理
c 复制代码
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)中的每个进程。

c 复制代码
void sigint_handler(int sig) 
{   
    pid_t pid = fgpid(jobs);
    // printf("Job [%d] (%d) terminated by signal 2\n", pid2jid(pid), pid);
    kill(-pid, SIGINT);
    return;
}
c 复制代码
void sigtstp_handler(int sig) 
{   
    pid_t pid = fgpid(jobs);
    // printf("Job [%d] (%d) stopped by signal 20\n", 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自旋

完整代码如下:

c 复制代码
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 argument\n", 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 job\n", jid);
            return ;
        }
        pid = job->pid;
    }else if(pid){
        job = getjobpid(jobs, pid);
        if(job == NULL){
            printf("(%d): No such process\n", pid);
            return ;
        }
    }else{
        printf("%s: argument must be a PID or %%jobid\n", 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.

代码如下:

c 复制代码
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): 如果引起返回的子进程当前是暂停的,那么就返回真。

完整代码:

c 复制代码
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 2\n", pid2jid(pid), pid);
            deletejob(jobs, pid);
        }else if(WIFSTOPPED(status)){
            // 如果进程停止
            printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);
            struct job_t *job = getjobpid(jobs, pid);
            job->state = ST;
        }
    }
    return;
}
相关推荐
病树前头2 天前
一些以前使用的linux及shell命令,gnuplot脚本
linux·shell
网络研究院3 天前
影响 Linux、Unix 系统的 CUPS 漏洞可导致 RCE
linux·运维·安全·unix·系统·漏洞
标标大人3 天前
unix中的exec族函数介绍
嵌入式硬件·unix
promise5247 天前
MySQL实现按分秒统计数据量
linux·运维·数据库·sql·mysql·shell
DoubleCui8 天前
记一次Mac 匪夷所思终端常用网络命令恢复记录
网络·macos·shell·terminal
飞的肖10 天前
在Linux实时监控某个应用是否运行,未运行,执行运行命令
linux·运维·shell
Jinterest10 天前
知乎:从零开始做自动驾驶定位; 注释详解(一)
人工智能·自动驾驶·unix
MonkeyKing_sunyuhua11 天前
dial unix /var/run/docker.sock: connect: permission denied
docker·eureka·unix
标标大人11 天前
unix中如何查询和修改进程的资源限制
服务器·unix
标标大人11 天前
什么是unix中的fork函数?
linux·运维·unix