CSAPP之详解ShellLab

实验之前

这个实验难度比较适中,当然前提是你第八章认真研究过了几遍,在做这个实验之前,请必须阅读以便官网的writeup文档,否则你可能不明白这个实验要实现干点什么,然后下载官网的实验材料到你的电脑上:
Write文档:
Writeup帮助文档链接

实验材料:
实验材料下载链接

实验说明

  1. 你只能修改tsh.c文件来完成其中的7个函数:
    •eval:解析和解释命令行的主例程。 [大约70行]
    •builtin_cmd:识别并解释内置命令:quit,fg,bg和job。 [大约25
    行]
    •do_bgfg:实现bg和fg内置命令。 [大约50行]
    •waitfg:等待前台作业完成。 [大约20行]
    •sigchld处理程序:处理SIGCHILD信号。 [大约80行]
    •sigint处理程序:处理SIGINT(ctrl-c)信号。 [大约15行]
    •sigtstp处理程序:处理SIGTSTP(ctrl-z)信号。 [大约15行]

  2. 每次修改tsh.c文件后,你都需要键入单独的一个make命令来完成一系列准备工作,官方提供了16个测试来检验你的答案,你需要键入make testXX来输出自己的答案,然后输入make rtestXX来比对标准答案,其中XX = 01、02、03、......、16。你的答案必须和标准答案一样才算成功(进程号可以是不同的,因为它们是随机的),例如:

make test03
这里会输出你的答案

make rtest03
这里是标准答案

  1. 在tsh.c文件中,老师已经帮我们实现了很多辅助函数,在代码中我会标识它的作用(读一读是很有帮助的),并且已经定义了一些全局变量,大大降低了实验难度,我们需要充分理解这些变量和函数。如下,这些是已经准备好的函数或者变量:
extern char **environ;      /* defined in libc 这个时环境变量,exec的参数,老师已经安排好了*/
char prompt[] = "tsh> ";    /* command line prompt (DO NOT CHANGE) */
int verbose = 0;            /* if true, print additional output */
int nextjid = 1;            /* next job ID to allocate */
char sbuf[MAXLINE];         /* for composing sprintf messages */
/*进程结构体,保管着所有tsh所有子进程的信息,必要的时候,我们必须要修改它*/
struct job_t {              /* The job struct */
    pid_t pid;              /* job PID */
    int jid;                /* job ID [1, 2, ...] */
    int state;              /* UNDEF, BG, FG, or ST */
    char cmdline[MAXLINE];  /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */

int parseline(const char *cmdline, char **argv);        /*解析命令行的函数,和书上一样*/ 
void sigquit_handler(int sig);                          /*quit信号处理*/
void clearjob(struct job_t *job);                       /*清理进程链表,退出的时候用*/
void initjobs(struct job_t *jobs);                      /*初始化进程链表*/
int maxjid(struct job_t *jobs);                         /*找到最大的进程组号*/
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);    /*添加进程,这个是需要我们手动添加的*/
int deletejob(struct job_t *jobs, pid_t pid);           /*删除进程,依然需要我们手动删除*/
pid_t fgpid(struct job_t *jobs);                        /*如果有前台工作进程,返回1,否则返回0*/
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); /*通过pid获得对于的进程结构体指针*/ 
struct job_t *getjobjid(struct job_t *jobs, int jid);   /*通过jid获得对于的进程结构体指针*/
int pid2jid(pid_t pid);                                 /*返回对于pid进程的jid*/
void listjobs(struct job_t *jobs);                      /*打印进程信息*/
void usage(void);                                       /*不用管他*/
void unix_error(char *msg);                             /*打印错误信息*/
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
  1. 用户键入的命令行应由一个名称和零个或多个参数组成,所有参数以一个或多个空格分隔。如果name是内置命令,则tsh应该立即处理它并等待下一个命令行。否则,tsh应该假定名称是可执行文件,它会在初始子进程的上下文中加载并运行(在这种情况下,工作一词是指此初始子流程。有几个需要注意的地方:
    •tsh不需要支持管道(|)或I / O重定向(<和>)。
    •键入ctrl-c(ctrl-z)应该会导致SIGINT(SIGTSTP)信号发送到当前的前地面作业以及该作业的任何后代(例如,它派生的任何子进程)。如果没有前台作业,则该信号应该没有任何作用。
    •如果命令行以&结束,则tsh应该在后台运行作业。否则,它应该在前台运行作业。
    •每个作业都可以由进程ID(PID)或作业ID(JID)标识,该ID是一个正整数
    由tsh分配。 JID应该在命令行上以前缀“%”表示。例如,“%5” 表示JID 5,“ 5”表示PID5。(我们已为您提供了所需的所有例程处理工作清单。)
    •tsh应该支持以下内置命令:
    – quit命令终止tsh程序。
    – jobs命令列出所有后台作业。
    – bg <job>命令通过向其发送SIGCONT信号来重新启动<job>,然后在
    的背景。 <job>参数可以是PID或JID。
    – fg <job>命令通过向其发送SIGCONT信号来重新启动<job>,然后在
    前景。 <job>参数可以是PID或JID。

  2. 正如前面所说的,我们的答案必须要和标准答案相同,所以在特定的位置我们需要输出特定的语句。例如,在后台工作运行时,我们要打印它的一系列参数等等等。

实验代码

Fork函数

这个不是要求的,但我们仍然包装好它:

/*自定义安全的Fork*/
pid_t Fork(void){
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("fork error!");
    return pid;
}


eval函数

这个函数的重点就是要小心同步并发流中的竞争,此外,setpgid也是很有必要的,它将子进程组与tsh进程组分开,避免tsh收到莫名的信号而停止。

/*利用书上已有的框架和543提到的避免并发竞争发生*/
void eval(char *cmdline) 
{
    char* argv[MAXARGS];
    char* buf[MAXLINE];
    int bg;
    pid_t pid;
    sigset_t mask_all, mask_one, prev_one;
    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;
    
    sigfillset(&mask_all);
    sigemptyset(&mask_one);
    sigaddset(&mask_one, SIGCHLD);

    if (!builtin_cmd(argv)){
        sigprocmask(SIG_BLOCK, &mask_one, &prev_one);/*必须要锁住,防止addjob和信号处理竞争*/
        if ((pid = Fork()) == 0){
            /*setpgid将子进程组和tsh进程组分开来,避免停止子进程组把tsh一起停止掉,同时子进程组id就等于pid,发送消息很方便*/
            /*请注意进程组id并不等于题目中的jid*/
            if (setpgid(0, 0) < 0){
                perror("setpgid error!");
                exit(0);
            }
            sigprocmask(SIG_SETMASK, &prev_one, NULL);/*子进程中不需要堵住它,但父进程需要*/
            if (execve(argv[0], argv, environ) < 0){
                printf ("%s: Command not found\n", argv[0]);
                exit(0);
            }
        }
        if (!bg){
            sigprocmask(SIG_BLOCK, &mask_all, NULL);/*锁住一切信号,避免addjob处理程序中断*/
            addjob(jobs, pid, FG, cmdline);
            sigprocmask(SIG_SETMASK, &prev_one, NULL);
            waitfg(pid);
        }
        else{
            sigprocmask(SIG_BLOCK, &mask_all, NULL);
            addjob(jobs, pid, BG, cmdline);
            printf ("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
            sigprocmask(SIG_SETMASK, &prev_one, NULL);/*打印全局变量,仍然需要加塞,防止途中被中断,可能造成还未读(写)内存而内存的值却被修改的情况*/
        }
    }
    //printf ("eve return\n");
    return;



builtin_cmd函数

这个函数我们按要求解析4个内置命令就好了

int builtin_cmd(char **argv) 
{
    //printf ("cmd\n");
    if (strcmp(argv[0], "quit") == 0)
        exit(0);    
    else if (strcmp(argv[0], "&") == 0)
        return 1;
    else if (strcmp(argv[0], "fg") == 0)
        do_bgfg(argv);
    else if (strcmp(argv[0], "bg") == 0)
        do_bgfg(argv);
    else if (strcmp(argv[0], "jobs") == 0)
        listjobs(jobs); /*这个是老师写好的辅助函数,实现起来也很简单*/
    else
        //printf ("cmd before return :%s \n",argv[0]);
        return 0;/*如果不是内置命令,返回1*/
    return 1;     /* not a builtin command */
}


do_bgfg函数

这个函数发生继续信号到指定进程中,并指定是以fg模式还是bg模式运行,这意味着原来的bg进程可能会变成fg进程,所以要注意修改信息。

void do_bgfg(char **argv) 
{
    //printf ("bgfg\n");
    struct job_t* job = NULL;
    if (argv[1] == NULL){
        printf ("%s command requires PID or %jobid argument\n", argv[0]);
        return;
    }
    int idex;
    /*解析pid*/
    if (sscanf(argv[1], "%d", &idex) > 0){
        if ((job = getjobpid(jobs, idex)) == NULL){
            printf ("%s: No such process\n", argv[1]);
            return;
        }
    }
    /*解析jid*/
    else if (sscanf(argv[1], "%%%d", &idex) > 0){
        if ((job = getjobjid(jobs, idex)) == NULL){
            printf ("%s: No such process\n", argv[1]);
            return;
        }   
    }
    /*都失败的话,打印错误消息*/
    else{
        printf ("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }

    /*发送信号,这里要求发送到进程组,所以采用负数*/
    /*子进程的进程组id和pid是一致的,请不要将jid和进程组id搞混了*/
    kill(-(job->pid), SIGCONT);
    if (strcmp(argv[0], "bg") == 0){
        job->state = BG;/*设置状态*/
        printf ("[%d] (%d) %s", job->jid, job->pid, job->cmdline);      
    }
    else{
        job->state = FG;/*设置状态*/
        waitfg(job->pid);
    }
    //printf ("bgfg return");
    return;
}


waitfg函数

等待前台任务结束,可以利用老师给定函数判定,注意这里不能使用简单的pause,因为信号可能会在执行pause前到来(恰好),这样就会形成竞争关系,如果程序这个时候执行了pause的话,就会等待着一个永远不会到来的信号(可能永远不会醒过来)。所以要使用sigsuspend函数,它等价于三条语句(请翻书),第一条语句与pause是原子属性的,它是不可中断的,在此之前我们堵塞SIGCHLD信号,然后在执行sigsuspend第一条语句时,我们短暂的解除堵塞,然后原子的立即执行pause,如果在执行sigsuspend之前SIGCHLD信号发送过来,它会被堵塞,而当sigsuspend执行时,它会等在pause执行时被释放,这会唤醒pause,同时陷入处理程序,设置while循环条件,结束循环,如果没有信号到来,那么pause就会正确执行。
当然这里也可以简单的用sleep语句,但是书上545页也陈述了它的缺点,最好还是利用sigsuspend指令。

void waitfg(pid_t pid)
{
    sigset_t mask, prev;
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    while (fgpid(jobs) != 0){
        sigsuspend(&mask);
        //printf ("wait here\n");
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);
    return;
}


sigchld_handler函数

这个函数要处理三种情况,一是处理正常中止的情况,二是收到信号中止的情况,三是被信号所暂时停止的情况,前两种情况都要显示的将进程从进程表中删除,第三种情况却不用,但是却要更改状态。为了防止del程序被中断,我们仍然需要加塞。

void sigchld_handler(int sig) 
{
    int olderrno = errno;
    int status;
    pid_t pid;
    struct job_t *job;
    sigset_t mask, prev;
    sigfillset(&mask);
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
        sigprocmask(SIG_BLOCK, &mask, &prev);
        if (WIFEXITED(status)){
            deletejob(jobs, pid);
        }
        else if (WIFSIGNALED(status)){
            printf ("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
            deletejob(jobs, pid);
        }
        else if (WIFSTOPPED(status)){
            printf ("Job [%d] (%d) stoped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
            job = getjobpid(jobs,pid);
            job->state = ST;
        }
        sigprocmask(SIG_SETMASK, &prev, NULL);          
    }
    errno = olderrno;
    //printf ("chldreturn\n");
    return;
}


sigint_handler函数和sigtstp_handler函数

最后两个函数比较简单,我们只要负责将信号发送给前台作业即可。因为这两个信号都是有默认行为的,不需要我们瞎操心。

void sigint_handler(int sig) 
{
    pid_t pid;
    if ((pid = fgpid(jobs)) > 0)
        kill(-pid, sig);
    return;
}
void sigtstp_handler(int sig) 
{
    pid_t pid;
    if ((pid = fgpid(jobs)) > 0)
        kill(-pid, sig);
    return;
}




总结

虽然只需要我们实现7个函数,但是阅读老师给定帮助函数是非常有帮助的。做这个实验可能更多的要处理好并发造成的问题吧,并发编程是非常容易出错的,我们必须小心小心再小心。
完整的tsh.c文件下载:
https://github.com/happysnaker/CSAPPLabs/blob/CSAPP/tsh.c

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • 原文链接 目标 补全tsh.c中剩余的代码: void eval(char *cmdline):解析并执行命令。 ...
    Coc0阅读 978评论 0 0
  • 一步一步教你写SHELL 这个LAB 是上完CMU CSAPP的14-15 LECTURE之后,就可以做了。csa...
    西部小笼包阅读 415评论 0 1
  • 一步一步教你写SHELL 这个LAB 是上完CMU CSAPP的14-15 LECTURE之后,就可以做了。csa...
    西部小笼包阅读 9,344评论 1 5
  • shell lab 在尝试完成这个 shell lab 之前,先看看官方给了什么代码吧,一个是书上有的 shlle...
    oo上海阅读 3,355评论 1 1
  • 实验介绍 完成一个简单的shell程序,总体的框架和辅助代码都已经提供好了,我们需要完成的函数主要以下几个: ev...
    leon4ever阅读 8,387评论 1 4