CSAPP深入理解计算机——shellLab(2018)

今天,我们能够跟随大佬的思路,亲自搭建自己的shell程序,虽然是阉割版,但是也能够非常激动。花了三个晚上终于完成了。

再一次感谢csapp这本书,以及老师的习题和lab。废话不多说,进入正题:


这次的任务十分简单清晰,直接。在做题之前请务必认真的读了实验lab的指导书,每个单词都很重要。

http://csapp.cs.cmu.edu/3e/shlab.pdf

总结下,我觉得其中最终的就是两点:

1. 请务必把异常控制流的每个字都认真读一遍。(不认真读,绝对会走很多弯路,而且最后你最后还是得乖乖回来看的)。

2. 请珍惜认真使用老师编写的测试程序,一个一个test做,一个个功能晚上做。不要一起写完了,再妄想一次通过,对于我这种萌新来说是不可能的,所以请踏踏实实的一个个来。

其他的细节,见实验指导书。具体说:这次的目标我们需要完成6个函数。


int builtin_cmd(char **argv, int argc);
void do_bgfg(char **argv, int argc);
void waitfg(pid_t pid);

void sigchld_handler(int sig);

void sigtstp_handler(int sig);

void sigint_handler(int sig);

注意,这里我修改了原来的函数原型,添加了在builtin_cmd函数和do_bgfg函数的参数列表中添加了argv,表示输入指令的数目,同时修改了parseline函数,使得它能够它argv数目通过argcp指针正确传递回来。为的是,正确处理一些错误输入。

----------------------分割线-------------------

全局变量:

//用于void sigchld_handler中,判断是否是当前引起停止信号的是否是前台进程,
//这种标志的作法,借鉴了书中P546页的做法
volatile sig_atomic_t fg_stop_or_exit;

下面一个个函数的说:

首先是eval函数,算是最需要好好考虑的函数之一:

思路和步骤:

step 1 初始化各变量以及设置信号阻塞合集

step 2 解析命令行,得到是否是后台命令,置位state(前后台标志)

step 3 判断是否时内置命令,如果是立即执行。否则fork子进程。

            子进程调用执行具体的命令文件。然后exit

             父进程执行step4.

step 4  将子进程job加入到job list中。

step 5 判断是否是bg,fg调用waifg函数等待前台运行完成,bg打印消息即可

整个过程中,需要仔细考虑显式阻塞的安排和设计,什么地方需要阻塞哪些信号。

原则1:在访问全局变量(jobs)必须阻塞所有信号,包括调用老师给的那些函数。由于这些函数基本都是使用for循环遍历完成功能的,所以,务必保证函数执行中不能被中断。

原则2:在一些函数或者指令有必须的前后执行顺序时,请阻塞,保证前一个函数调用完成后(比如必须先addjob,再deletejob)

细节见注释。


void eval(char *cmdline)
{
    //step 1 初始化各变量以及信号阻塞合集
    char* argv[MAXARGS];
    char buf[MAXLINE];
    int state;
    int argc;
    pid_t curr_pid;//储存当前前台pid
    sigset_t mask_all, mask_one, mask_prev;

    //设置阻塞集合
    sigemptyset(&mask_one);
    sigaddset(&mask_one, SIGCHLD);
    sigfillset(&mask_all);

    //step 2 解析命令行,得到是否是后台命令,置位state
    strcpy(buf, cmdline);
    state = parseline(buf, argv, &argc)? BG : FG;

    //step 3 判断是否时内置命令
    if(!builtin_cmd(argv, argc)){
        //不是内置命令,阻塞SIGCHLD,防止子进程在父进程之间结束,也就是addjob和deletejob之间,必须保证这个拓扑顺序
        sigprocmask(SIG_BLOCK, &mask_one, &mask_prev);
        if((curr_pid = fork()) == 0){
            //子进程,先解除对SIGCHLD阻塞
            sigprocmask(SIG_SETMASK, &mask_prev, NULL);
            //改进进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件。
            setpgid(0, 0);
            if(execve(argv[0], argv, environ) < 0){
                //没找到相关可执行文件的情况下,打印消息,直接退出
                printf("%s: Command not found.\n", argv[0]);
            }
            //这里务必加上exit(0),否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
            exit(0);
        }
        //step 4
        //创建完成子进程后,父进程addjob,整个函数执行期间,必须保证不能被中断。尤其是玩意在for循环过程中中断了不堪设想
        //因此,阻塞所有信号,天塌下来也要让我先执行完
        sigprocmask(SIG_BLOCK, &mask_all, NULL);
        addjob(jobs, curr_pid, state, cmdline);
        //再次阻塞SIGCHLD
        sigprocmask(SIG_SETMASK, &mask_one, NULL);

        //step 5 判断是否是bg,fg调用waifg函数等待前台运行完成,bg打印消息即可
        //还有一个问题是,如果在前台任务,如果我使用默认的waitpid由于该函数是linux定义的原子性函数,无法被信号中断,那么前台
        //函数在执行的过程中,无法相应SIGINT和SIGSTO信号,这里我使用sigsuspend函数加上while判断fg_stop_or_exit标志的方法。具体见waitfg函数
        if(state == FG){
            waitfg(curr_pid);
        }
        else{
            //输出后台进程的信息
            //读取全局变量,阻塞所有的信号防止被打断
            sigprocmask(SIG_BLOCK , &mask_all, NULL);
            struct job_t* curr_bgmask = getjobpid(jobs, curr_pid);
            printf("[%d] (%d) %s", curr_bgmask->jid, curr_bgmask->pid, curr_bgmask->cmdline);
        }
        //解除所有的阻塞
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    }
    return;
}


然后是buildin_cmd函数,相对来说逻辑很简单,就是判断是哪个内置命令转到相应的函数,然后return 1;否则,就不是内置命令,啥也不做,返回0。

上面两条原则需要时刻注意!

int builtin_cmd(char **argv, int argc)
{
    //初始化变量以及阻塞集合
    char* cmd = argv[0];
    sigset_t mask_all, mask_prev;

    sigfillset(&mask_all);

    if(!strcmp(cmd, "quit")){
        //直接退出,这里可以考虑更加完善些,比如如果当前任务列表中还有没运行完成的,给列表中所有的进程组,发送9信号,kill掉。
        exit(0);
    }
    else if(strcmp(cmd, "fg") == 0 || strcmp(argv[0], "bg") == 0){
        do_bgfg(argv, argc);
        return 1;
    }
    else if(!strcmp(cmd, "jobs")){
        //访问全局变量,需要阻塞全部信号
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        listjobs(jobs);
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
        return 1;
    }
    return 0;     /* not a builtin command */
}

接着,是do_bgfg函数,就是实现bg和fg两条内置命令。

这个函数的核心就两点:

1. 区分bg和fg命令,以及传入pid或者jid参数对应的进程的状态。前者if,后者switch就可以包括所用的情况

2. 注意用户输入错误处理,比如参数数量不够或者参数传入错误的情况

上面两条原则需要时刻注意!说三遍!

/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv, int argc)
{
    //其实这里应该加上错误判断,比如使用输入了三个参数
    if(argc != 2){
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        fflush(stdout);
        return;
    }
    //初始化变量
    char* cmd = argv[0];
    char* para = argv[1];
    struct job_t* curr_job;
    sigset_t mask_all, mask_prev;
    int curr_jid;
    //判断传入的pid还是jid,并且获取对应的job结构体
    sigfillset(&mask_all);
    if(para[0] == '%'){
        curr_jid = atoi(&(para[1]));
        //错误处理2,如果传入的参数不是规定的格式,报错返回
        if(curr_jid == 0){
            printf("%s:argument must be a PID or %%jobid\n", cmd);
            fflush(stdout);
            return;
        }
    }
    else{
        curr_jid = atoi(para);
        if(curr_jid == 0){
            printf("%s:argument must be a PID or %%jobid\n", cmd);
            fflush(stdout);
            return;
        }
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        curr_jid = pid2jid(curr_jid);
    }

    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    curr_job = getjobjid(jobs, curr_jid);
    //
    if(curr_job == NULL){
        printf("(%s): No such process\n", para);
        fflush(stdout);
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
        return;
    }
    //区分bg还是fg                        
    if(!strcmp(cmd, "bg")){
        //区分job的状态
        switch(curr_job->state){
        case ST:
            //bg命令,改变该任务的运行状态,ST->BG,同时发送信号给对应的子进程
            curr_job->state = BG;
            kill(-(curr_job->pid), SIGCONT);
            printf("[%d] (%d) %s", curr_job->jid, curr_job->pid, curr_job->cmdline);
            break;
        case BG:
            //如果该任务已经是后台运行了,就啥也不干
            break;
        //如果bg前台或者unfef,那么肯定哪里出错了
        case UNDEF:
        case FG:
            unix_error("bg 出现undef或者FG的进程\n");
            break;
        }
    }
    else{
        //fg 命令
        switch(curr_job->state){
        //如果fg挂起的进程,那么重启它,并且挂起主进程等待它回收终止。
        case ST:
            curr_job->state = FG;
            kill(-(curr_job->pid), SIGCONT);
            waitfg(curr_job->pid);
            break;
        //如果fg后台进程,那么将它的状态转为前台进程,然后等待它终止
        case BG:
            curr_job->state = FG;
            waitfg(curr_job->pid);
            break;
        //如果本省就是前台进程,那么出错了 。
        case FG:
        case UNDEF:
            unix_error("fg 出现undef或者FG的进程\n");
            break;
        }
    }
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);

    return;
}

再是waitfg函数,即eval函数调用的那个:

这里的精髓,书中讲的很详细了,不赘述了。

void waitfg(pid_t pid)
{
    //注意到,进来之间阻塞了SIGCHLD信号
    sigset_t mask;
    sigemptyset(&mask);
    //前台进程的pid和挂起标志
    //FGPID = 0;
    fg_stop_or_exit = 0;
    //让SIGCHLD信号处理程序处理任何子进程传回来的SIGCHLD信号,注意子进程挂起或者终止都会返回这个信号,所以信号处理程序需要区分,处理不同的情况
    //只有发出这个信号的子进程是前台进程才设置fg_stop_or_exit标志。
    while(!fg_stop_or_exit){
        sigsuspend(&mask);
    }
    return;
}

接着是信号处理程序部分,共计三个:

首先,当然是万众瞩目的:

这里的细节,其实书上也讲得很清楚了,我们要做的就是学以致用,把多种情况结合起来使用:

这里的思想步骤,我后续再更,快12点了,赶紧弄完了,回去。

void sigchld_handler(int sig)
{
    int olderrno = errno;
    sigset_t mask_all, mask_prev;
    pid_t gc_pid;
    struct job_t* gc_job;
    int status;

    sigfillset(&mask_all);
    //尽可能的回收子进程,同时使用WNOHANG选项使得如果当前进程都没有终止时,直接返回,而不是挂起该回收进程。这样可能会阻碍无法两个短时间结束的后台进程
    //即trace05.txt
    while((gc_pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        gc_job = getjobpid(jobs, gc_pid);
        if(gc_pid == fgpid(jobs)){
            fg_stop_or_exit = 1;
        }
        if(WIFSTOPPED(status)){
            //子进程停止引起的waitpid函数返回,再判断该进程是否是前台进程
            //struct job_t* stop_job = getjobpid(jobs, gc_pid);
            gc_job->state = ST;
            printf("Job [%d] (%d) terminated by signal %d\n", gc_job->jid, gc_job->pid, WSTOPSIG(status));
        }
        else{
            //子进程终止引起的返回,判断是否是前台进程
            //并且判断该信号是否是未捕获的信号
            if(WIFSIGNALED(status)){
                //struct job_t* gc_job = getjobpid(jobs, gc_pid);
                printf("Job [%d] (%d) terminated by signal %d\n", gc_job->jid, gc_job->pid, WTERMSIG(status));
            }
            //终止的进程直接回收
            deletejob(jobs, gc_pid);
        }
        fflush(stdout);
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    }
    errno = olderrno;
   

后面两个信号处理程序,思路简单相似:

1. 获取前台进程,判断当前是否有前台进程。如果没有直接返回。有则步骤2

2. 使用kill函数,发送SIGINT/SIGTSTP信号给前台进程组。

/*
 * 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.
 */
void sigint_handler(int sig)
{
    int olderrno = errno;
    sigset_t mask_all, mask_prev;
    pid_t curr_fg_pid;

    sigfillset(&mask_all);
    //访问全局结构体数组,阻塞信号
    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    curr_fg_pid = fgpid(jobs);
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);

    if(curr_fg_pid != 0){
        kill(-curr_fg_pid, SIGINT);
    }

    errno = olderrno;
    return;
}

/*
 * 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.
 */
void sigtstp_handler(int sig)
{
    int olderrno = errno;
    sigset_t mask_all, mask_prev;
    pid_t curr_fg_pid;

    sigfillset(&mask_all);

    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    curr_fg_pid = fgpid(jobs);
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);

    if(curr_fg_pid != 0){
        /* 臃肿的代码,保留了我调试的过程。
        fg_stop_or_exit = 1;
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        struct job_t* stop_fgjob = getjobpid(jobs, curr_fg_pid);
        printf("Job [%d] (%d) stopped by signal 20\n", stop_fgjob->jid, stop_fgjob->pid);
        stop_fgjob->state = ST;
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
        */
        kill(-curr_fg_pid, SIGTSTP);
    }

    errno = olderrno;
    return;
}
------------------------------分割线-------------------

最后补充测试的部分样图,左侧为测试自己的程序,右侧为老师提供的样本程序。我们需要的就是保证自己程序的行为完全跟它一样(除了pid)

最后,感谢网上的一些参考资料,在我看题时,让我更快的理解题意:

http://wdxtub.com/2016/04/16/thick-csapp-lab-5/


总结:

1. 以后我也要向老师这种模式学习,边测试边添加,这样心里有底,bug更少。python写测试可以学习下。哈哈

2. 这次学习让我又一次认识到,编程永远是,先思考,先画图,一个流程图肯定要画个大概的,起码要想清楚60%以上再开始,所以一定要保证一个函数的健壮性,然后测试通过,我完全相信它提供的服务和功能。微小的一个个函数堆积起来就是复杂庞大的系统。


  • 39
    点赞
  • 114
    收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页
评论 4

打赏作者

xiaolian_hust

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值