【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;
}
相关推荐
酥心糖小可爱1 天前
shell脚本案例
shell·脚本
China_Yanhy3 天前
[k8s]报错:Cannot connect to the Docker daemon at unix:///var/run/docker.sock
docker·kubernetes·unix
stevenjoo673 天前
UNIX简史
unix
桃酥4035 天前
GCC实用干货
linux·shell·gcc
vortex56 天前
文本处理工具: UNIX/Linux 的设计哲学
linux·服务器·unix
斐夷所非7 天前
Unix 传奇 | 谁写了 Linux | Unix birthmark
unix
SoftwareTeacher7 天前
Unix 和 Windows 的有趣比较
服务器·windows·unix
北亚数据恢复9 天前
UNIX数据恢复—UNIX系统常见故障问题和数据恢复方案
数据恢复·unix·服务器数据恢复·存储数据恢复·unix数据恢复
月光技术杂谈10 天前
5G模组AT命令脚本-命令发送及回显读取
linux·5g·shell·5g模组·5g终端·at命令
大乔乔布斯10 天前
sed 替换连续空格
linux·bash·unix