CSAPP之詳解ShellLab

實(shí)驗(yàn)之前

這個(gè)實(shí)驗(yàn)難度比較適中,當(dāng)然前提是你第八章認(rèn)真研究過(guò)了幾遍,在做這個(gè)實(shí)驗(yàn)之前,請(qǐng)必須閱讀以便官網(wǎng)的writeup文檔,否則你可能不明白這個(gè)實(shí)驗(yàn)要實(shí)現(xiàn)干點(diǎn)什么,然后下載官網(wǎng)的實(shí)驗(yàn)材料到你的電腦上:
Write文檔:
Writeup幫助文檔鏈接

實(shí)驗(yàn)材料:
實(shí)驗(yàn)材料下載鏈接

實(shí)驗(yàn)說(shuō)明

  1. 你只能修改tsh.c文件來(lái)完成其中的7個(gè)函數(shù):
    ?eval:解析和解釋命令行的主例程。 [大約70行]
    ?builtin_cmd:識(shí)別并解釋內(nèi)置命令:quit,fg,bg和job。 [大約25
    行]
    ?do_bgfg:實(shí)現(xiàn)bg和fg內(nèi)置命令。 [大約50行]
    ?waitfg:等待前臺(tái)作業(yè)完成。 [大約20行]
    ?sigchld處理程序:處理SIGCHILD信號(hào)。 [大約80行]
    ?sigint處理程序:處理SIGINT(ctrl-c)信號(hào)。 [大約15行]
    ?sigtstp處理程序:處理SIGTSTP(ctrl-z)信號(hào)。 [大約15行]

  2. 每次修改tsh.c文件后,你都需要鍵入單獨(dú)的一個(gè)make命令來(lái)完成一系列準(zhǔn)備工作,官方提供了16個(gè)測(cè)試來(lái)檢驗(yàn)?zāi)愕拇鸢?,你需要鍵入make testXX來(lái)輸出自己的答案,然后輸入make rtestXX來(lái)比對(duì)標(biāo)準(zhǔn)答案,其中XX = 01、02、03、......、16。你的答案必須和標(biāo)準(zhǔn)答案一樣才算成功(進(jìn)程號(hào)可以是不同的,因?yàn)樗鼈兪请S機(jī)的),例如:

make test03
這里會(huì)輸出你的答案

make rtest03
這里是標(biāo)準(zhǔn)答案

  1. 在tsh.c文件中,老師已經(jīng)幫我們實(shí)現(xiàn)了很多輔助函數(shù),在代碼中我會(huì)標(biāo)識(shí)它的作用(讀一讀是很有幫助的),并且已經(jīng)定義了一些全局變量,大大降低了實(shí)驗(yàn)難度,我們需要充分理解這些變量和函數(shù)。如下,這些是已經(jīng)準(zhǔn)備好的函數(shù)或者變量:
extern char **environ;      /* defined in libc 這個(gè)時(shí)環(huán)境變量,exec的參數(shù),老師已經(jīng)安排好了*/
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 */
/*進(jìn)程結(jié)構(gòu)體,保管著所有tsh所有子進(jìn)程的信息,必要的時(shí)候,我們必須要修改它*/
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);        /*解析命令行的函數(shù),和書(shū)上一樣*/ 
void sigquit_handler(int sig);                          /*quit信號(hào)處理*/
void clearjob(struct job_t *job);                       /*清理進(jìn)程鏈表,退出的時(shí)候用*/
void initjobs(struct job_t *jobs);                      /*初始化進(jìn)程鏈表*/
int maxjid(struct job_t *jobs);                         /*找到最大的進(jìn)程組號(hào)*/
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);    /*添加進(jìn)程,這個(gè)是需要我們手動(dòng)添加的*/
int deletejob(struct job_t *jobs, pid_t pid);           /*刪除進(jìn)程,依然需要我們手動(dòng)刪除*/
pid_t fgpid(struct job_t *jobs);                        /*如果有前臺(tái)工作進(jìn)程,返回1,否則返回0*/
struct job_t *getjobpid(struct job_t *jobs, pid_t pid); /*通過(guò)pid獲得對(duì)于的進(jìn)程結(jié)構(gòu)體指針*/ 
struct job_t *getjobjid(struct job_t *jobs, int jid);   /*通過(guò)jid獲得對(duì)于的進(jìn)程結(jié)構(gòu)體指針*/
int pid2jid(pid_t pid);                                 /*返回對(duì)于pid進(jìn)程的jid*/
void listjobs(struct job_t *jobs);                      /*打印進(jìn)程信息*/
void usage(void);                                       /*不用管他*/
void unix_error(char *msg);                             /*打印錯(cuò)誤信息*/
void app_error(char *msg);
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);
  1. 用戶(hù)鍵入的命令行應(yīng)由一個(gè)名稱(chēng)和零個(gè)或多個(gè)參數(shù)組成,所有參數(shù)以一個(gè)或多個(gè)空格分隔。如果name是內(nèi)置命令,則tsh應(yīng)該立即處理它并等待下一個(gè)命令行。否則,tsh應(yīng)該假定名稱(chēng)是可執(zhí)行文件,它會(huì)在初始子進(jìn)程的上下文中加載并運(yùn)行(在這種情況下,工作一詞是指此初始子流程。有幾個(gè)需要注意的地方:
    ?tsh不需要支持管道(|)或I / O重定向(<和>)。
    ?鍵入ctrl-c(ctrl-z)應(yīng)該會(huì)導(dǎo)致SIGINT(SIGTSTP)信號(hào)發(fā)送到當(dāng)前的前地面作業(yè)以及該作業(yè)的任何后代(例如,它派生的任何子進(jìn)程)。如果沒(méi)有前臺(tái)作業(yè),則該信號(hào)應(yīng)該沒(méi)有任何作用。
    ?如果命令行以&結(jié)束,則tsh應(yīng)該在后臺(tái)運(yùn)行作業(yè)。否則,它應(yīng)該在前臺(tái)運(yùn)行作業(yè)。
    ?每個(gè)作業(yè)都可以由進(jìn)程ID(PID)或作業(yè)ID(JID)標(biāo)識(shí),該ID是一個(gè)正整數(shù)
    由tsh分配。 JID應(yīng)該在命令行上以前綴“%”表示。例如,“%5” 表示JID 5,“ 5”表示PID5。(我們已為您提供了所需的所有例程處理工作清單。)
    ?tsh應(yīng)該支持以下內(nèi)置命令:
    – quit命令終止tsh程序。
    – jobs命令列出所有后臺(tái)作業(yè)。
    – bg <job>命令通過(guò)向其發(fā)送SIGCONT信號(hào)來(lái)重新啟動(dòng)<job>,然后在
    的背景。 <job>參數(shù)可以是PID或JID。
    – fg <job>命令通過(guò)向其發(fā)送SIGCONT信號(hào)來(lái)重新啟動(dòng)<job>,然后在
    前景。 <job>參數(shù)可以是PID或JID。

  2. 正如前面所說(shuō)的,我們的答案必須要和標(biāo)準(zhǔn)答案相同,所以在特定的位置我們需要輸出特定的語(yǔ)句。例如,在后臺(tái)工作運(yùn)行時(shí),我們要打印它的一系列參數(shù)等等等。

實(shí)驗(yàn)代碼

Fork函數(shù)

這個(gè)不是要求的,但我們?nèi)匀话b好它:

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


eval函數(shù)

這個(gè)函數(shù)的重點(diǎn)就是要小心同步并發(fā)流中的競(jìng)爭(zhēng),此外,setpgid也是很有必要的,它將子進(jìn)程組與tsh進(jìn)程組分開(kāi),避免tsh收到莫名的信號(hào)而停止。

/*利用書(shū)上已有的框架和543提到的避免并發(fā)競(jìng)爭(zhēng)發(fā)生*/
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和信號(hào)處理競(jìng)爭(zhēng)*/
        if ((pid = Fork()) == 0){
            /*setpgid將子進(jìn)程組和tsh進(jìn)程組分開(kāi)來(lái),避免停止子進(jìn)程組把tsh一起停止掉,同時(shí)子進(jìn)程組id就等于pid,發(fā)送消息很方便*/
            /*請(qǐng)注意進(jìn)程組id并不等于題目中的jid*/
            if (setpgid(0, 0) < 0){
                perror("setpgid error!");
                exit(0);
            }
            sigprocmask(SIG_SETMASK, &prev_one, NULL);/*子進(jìn)程中不需要堵住它,但父進(jìn)程需要*/
            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);/*鎖住一切信號(hào),避免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);/*打印全局變量,仍然需要加塞,防止途中被中斷,可能造成還未讀(寫(xiě))內(nèi)存而內(nèi)存的值卻被修改的情況*/
        }
    }
    //printf ("eve return\n");
    return;



builtin_cmd函數(shù)

這個(gè)函數(shù)我們按要求解析4個(gè)內(nèi)置命令就好了

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); /*這個(gè)是老師寫(xiě)好的輔助函數(shù),實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單*/
    else
        //printf ("cmd before return :%s \n",argv[0]);
        return 0;/*如果不是內(nèi)置命令,返回1*/
    return 1;     /* not a builtin command */
}


do_bgfg函數(shù)

這個(gè)函數(shù)發(fā)生繼續(xù)信號(hào)到指定進(jìn)程中,并指定是以fg模式還是bg模式運(yùn)行,這意味著原來(lái)的bg進(jìn)程可能會(huì)變成fg進(jìn)程,所以要注意修改信息。

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;
        }   
    }
    /*都失敗的話,打印錯(cuò)誤消息*/
    else{
        printf ("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }

    /*發(fā)送信號(hào),這里要求發(fā)送到進(jìn)程組,所以采用負(fù)數(shù)*/
    /*子進(jìn)程的進(jìn)程組id和pid是一致的,請(qǐng)不要將jid和進(jìn)程組id搞混了*/
    kill(-(job->pid), SIGCONT);
    if (strcmp(argv[0], "bg") == 0){
        job->state = BG;/*設(shè)置狀態(tài)*/
        printf ("[%d] (%d) %s", job->jid, job->pid, job->cmdline);      
    }
    else{
        job->state = FG;/*設(shè)置狀態(tài)*/
        waitfg(job->pid);
    }
    //printf ("bgfg return");
    return;
}


waitfg函數(shù)

等待前臺(tái)任務(wù)結(jié)束,可以利用老師給定函數(shù)判定,注意這里不能使用簡(jiǎn)單的pause,因?yàn)樾盘?hào)可能會(huì)在執(zhí)行pause前到來(lái)(恰好),這樣就會(huì)形成競(jìng)爭(zhēng)關(guān)系,如果程序這個(gè)時(shí)候執(zhí)行了pause的話,就會(huì)等待著一個(gè)永遠(yuǎn)不會(huì)到來(lái)的信號(hào)(可能永遠(yuǎn)不會(huì)醒過(guò)來(lái))。所以要使用sigsuspend函數(shù),它等價(jià)于三條語(yǔ)句(請(qǐng)翻書(shū)),第一條語(yǔ)句與pause是原子屬性的,它是不可中斷的,在此之前我們堵塞SIGCHLD信號(hào),然后在執(zhí)行sigsuspend第一條語(yǔ)句時(shí),我們短暫的解除堵塞,然后原子的立即執(zhí)行pause,如果在執(zhí)行sigsuspend之前SIGCHLD信號(hào)發(fā)送過(guò)來(lái),它會(huì)被堵塞,而當(dāng)sigsuspend執(zhí)行時(shí),它會(huì)等在pause執(zhí)行時(shí)被釋放,這會(huì)喚醒pause,同時(shí)陷入處理程序,設(shè)置while循環(huán)條件,結(jié)束循環(huán),如果沒(méi)有信號(hào)到來(lái),那么pause就會(huì)正確執(zhí)行。
當(dāng)然這里也可以簡(jiǎn)單的用sleep語(yǔ)句,但是書(shū)上545頁(yè)也陳述了它的缺點(diǎn),最好還是利用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函數(shù)

這個(gè)函數(shù)要處理三種情況,一是處理正常中止的情況,二是收到信號(hào)中止的情況,三是被信號(hào)所暫時(shí)停止的情況,前兩種情況都要顯示的將進(jìn)程從進(jìn)程表中刪除,第三種情況卻不用,但是卻要更改狀態(tài)。為了防止del程序被中斷,我們?nèi)匀恍枰尤?/p>

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函數(shù)和sigtstp_handler函數(shù)

最后兩個(gè)函數(shù)比較簡(jiǎn)單,我們只要負(fù)責(zé)將信號(hào)發(fā)送給前臺(tái)作業(yè)即可。因?yàn)檫@兩個(gè)信號(hào)都是有默認(rèn)行為的,不需要我們瞎操心。

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;
}




總結(jié)

雖然只需要我們實(shí)現(xiàn)7個(gè)函數(shù),但是閱讀老師給定幫助函數(shù)是非常有幫助的。做這個(gè)實(shí)驗(yàn)可能更多的要處理好并發(fā)造成的問(wèn)題吧,并發(fā)編程是非常容易出錯(cuò)的,我們必須小心小心再小心。
完整的tsh.c文件下載:
https://github.com/happysnaker/CSAPPLabs/blob/CSAPP/tsh.c

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 原文鏈接 目標(biāo) 補(bǔ)全tsh.c中剩余的代碼: void eval(char *cmdline):解析并執(zhí)行命令。 ...
    Coc0閱讀 1,133評(píng)論 0 0
  • 一步一步教你寫(xiě)SHELL 這個(gè)LAB 是上完CMU CSAPP的14-15 LECTURE之后,就可以做了。csa...
    西部小籠包閱讀 445評(píng)論 0 1
  • 一步一步教你寫(xiě)SHELL 這個(gè)LAB 是上完CMU CSAPP的14-15 LECTURE之后,就可以做了。csa...
    西部小籠包閱讀 9,622評(píng)論 1 5
  • shell lab 在嘗試完成這個(gè) shell lab 之前,先看看官方給了什么代碼吧,一個(gè)是書(shū)上有的 shlle...
    oo上海閱讀 3,498評(píng)論 1 1
  • 實(shí)驗(yàn)介紹 完成一個(gè)簡(jiǎn)單的shell程序,總體的框架和輔助代碼都已經(jīng)提供好了,我們需要完成的函數(shù)主要以下幾個(gè): ev...
    leon4ever閱讀 8,542評(píng)論 1 4

友情鏈接更多精彩內(nèi)容