CSAPP Shell Lab 詳細解答


Shell Lab的任務為實現一個帶有作業控制的簡單Shell,需要對異常控制流特別是信號有比較好的理解才能完成。需要詳細閱讀CS:APP第八章異常控制流並理解所有例程。

Slides下載:https://www.cs.cmu.edu/afs/cs/academic/class/15213-f21/www/schedule.html

Lab主頁:http://csapp.cs.cmu.edu/3e/labs.html

完整源碼:https://github.com/zhangyi1357/CSAPP-Labs/blob/main/shlab-handout/tsh.c

示例程序分析

首先可以參考課本上給出的不帶作業控制的Shell的代碼。

/* $begin shellmain */
#include "csapp.h"
#define MAXARGS   128

/* Function prototypes */
void eval(char* cmdline);
int parseline(char* buf, char** argv); // implementation omitted
int builtin_command(char** argv);

int main()
{
    char cmdline[MAXLINE]; /* Command line */

    while (1) {
        /* Read */
        printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
            exit(0);

        /* Evaluate */
        eval(cmdline);
    }
}
/* $end shellmain */

/* $begin eval */
/* eval - Evaluate a command line */
void eval(char* cmdline)
{
    char* argv[MAXARGS]; /* Argument list execve() */
    char buf[MAXLINE];   /* Holds modified command line */
    int bg;              /* Should the job run in bg or fg? */
    pid_t pid;           /* Process id */

    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL)
        return;   /* Ignore empty lines */

    if (!builtin_command(argv)) {
        if ((pid = Fork()) == 0) {   /* Child runs user job */
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        /* Parent waits for foreground job to terminate */
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
        else
            printf("%d %s", pid, cmdline);
    }
    return;
}

/* If first arg is a builtin command, run it and return true */
int builtin_command(char** argv)
{
    if (!strcmp(argv[0], "quit")) /* quit command */
        exit(0);
    if (!strcmp(argv[0], "&"))    /* Ignore singleton & */
        return 1;
    return 0;                     /* Not a builtin command */
}
/* $end eval */

main函數中負責讀入cmdline發送給eval函數進行處理,如果發現讀入EOF則退出程序。

eval函數的主要流程為使用parseline函數將cmdline解析為argv數組,然后發送到builtin_command函數進行處理,如果內置命令則在此函數內直接處理並返回1,反之則不處理返回0交還控制權到eval函數。

接下來eval函數運用fork-execve慣用法執行cmdline,父進程根據cmdline為前台或后台程序做不同處理,前台程序則等待其子進程執行完畢,后台程序則直接輸出子進程PID和命令,而后返回控制權給main函數繼續讀入新的cmdline。

Shell示例程序流程簡化圖解
Shell示例程序流程簡化圖解

作業控制實現思路

作業控制實際上就是維護一個jobs數組,新建一個任務時將其加入到數組之中,任務執行完畢由父進程的中斷處理程序將該任務刪除。另外還需要在適當的時候將任務的狀態進行調整,中斷處理程序。

具體到本Lab,需要做的就是在eval函數中添加任務,然后在sigchld_handler處理程序中回收子進程並刪除相應任務,還有sigint_handler和sigstop_handler中改變任務的狀態。

值得注意的是,為了避免race,需要在fork之前阻塞SIGCHLD信號,然后完成fork,在父進程中添加該任務之后再解除SIGCHLD信號的阻塞,以免發生刪除任務發生在添加任務之前的情況。另外,由於子進程會繼承父進程的阻塞,所以在execve之前需要取消對SIGCHLD信號的阻塞。

本Lab對於jobs數組的各種操作的實現都已經提供,只需要調用相應api即可,無需自己實現。

Lab 實現

本Lab建議以trace[n].txt文件為指導,逐步實現其功能。

trace01 EOF

trace01要求在讀取EOF信號時退出Shell,在初始代碼中該功能已經實現。

        if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
            app_error("fgets error");
        if (feof(stdin)) { /* End of file (ctrl-d) */
            fflush(stdout);
            exit(0);
        }

trace02 quit

trace02則測試內置的quit命令,課本示例中也已經進行實現。

    // quit command
    if (!strcmp(argv[0], "quit"))
        exit(0);

trace03~04 前后台程序+作業控制

trace03為測試前台運行quit,trace04為測試后台運行myspin程序。

主要需要解析命令行末尾的&,並針對前后台運行進行不同的處理。其中parseline函數已經幫助解析了命令行末尾&,所以只需要對前后台程序進行不同處理即可。

如前所述,前台則需等待執行完畢,后台則只需要將其添加到jobs即可。

首先在eval函數中實現添加作業的代碼以及前后台程序處理。特別注意這里對SIGCHLD信號在適當的地方進行了阻塞和解除阻塞。另外進行阻塞所使用的函數是包裹了錯誤處理的系統調用。具體實現參考源代碼。

    Sigemptyset(&mask);
    Sigaddset(&mask, SIGCHLD);

    if (!builtin_cmd(argv)) {
        Sigprocmask(SIG_BLOCK, &mask, &prev);  // block SIGCHLD

        if ((pid = fork()) == 0) {   /* Child runs user job */
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        addjob(jobs, pid, bg ? BG : FG, cmdline);

        Sigprocmask(SIG_SETMASK, &prev, NULL);  // unblock SIGCHLD

對於后台程序按照給出的對照程序(tshref)輸出其相應的任務號,PID以及命令行。

對於前台程序處理則依賴於sigchld_handler信號處理程序,接收到其終止信號時將其移出jobs數組。於是可以通過判斷fgpid函數返回當前前台程序PID是否等於子進程的PID來判斷是否運行完畢。

// code in evalvoid sigchld_handler(int sig)
{
    int old_errno = errno;

    pid_t pid;
    int status;

    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
    }

    if (errno != ECHILD)
        unix_error("waitpid_error");

    errno = old_errno;
    return;
}
				/* Parent waits for foreground job to terminate */
        if (!bg)  // foreground
            waitfg(pid);
        else      // background
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);

// waitfg function
void waitfg(pid_t pid)
{
    while (pid == fgpid(jobs))
        sleep(0);
    return;
}

具體到SIGCHLD的處理,需要在其中使用waitpid回收所有的終止的子進程。其中WNOHANG | WUNTRACED代表立即返回,如果有子進程停止或終止則返回其PID,用while循環包起來確保一次盡可能將所有已經終止或停止的子進程回收。

void sigchld_handler(int sig)
{
    int old_errno = errno;

    pid_t pid;
    int status;

    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
    }

    errno = old_errno;
    return;
}

trace05 jobs

trace05為實現jobs功能,在完成了前面的基本的作業控制后非常簡單,只需要在builtin_cmd中調用起始代碼已經提供了的listjobs函數即可

    // jobs command
    if (!strcmp(argv[0], "jobs")) {
        listjobs(jobs);
        return 1;
    }

trace06~08 SIGINT和SIGSTOP

這三個trace是測試SIGINT和SIGSTOP能否被正確處理,值得注意的是,前台程序收到這兩個信號都應該將其發送給其所在組的所有程序,而不是本身。

具體發送於是sigint和sigstop的任務非常簡單,即收到信號后轉手給所在的整個組發一下信號,給整個組發信號只需要給kill的pid為負數即可。

void sigint_handler(int sig)
{
    int olderrno = errno;

    // get the foreground job pid
    pid_t fg_pid;
    fg_pid = fgpid(jobs);

    // send the signal to the group in the foreground
    kill(-fg_pid, sig);

    errno = olderrno;
    return;
}
void sigtstp_handler(int sig)
{
    int olderrno = errno;

    // get the foreground job pid
    pid_t fg_pid;
    fg_pid = fgpid(jobs);

    // send the signal to the group in the foreground
    kill(-fg_pid, sig);

    errno = olderrno;
    return;
}

具體處理這兩個的信號在sigchld_hanlder里,sigchld_handler里收到子進程終止或停止的消息后給出對應的輸出然后改變其狀態,對於終止的進程就在jobs里將其刪除,對於停止的進程則設置其state為ST。值得注意的是在信號處理程序里不可以使用異步信號不安全的printf,我這里使用的是csapp.h里給出的Sio包。

    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }
        if (WIFSIGNALED(status)) { // terminated by ctrl-c
            Sio_puts("Job [");
            Sio_putl(pid2jid(pid));
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") terminated by signal ");
            Sio_putl(WTERMSIG(status));
            Sio_puts("\n");
            deletejob(jobs, pid);
        }
        if (WIFSTOPPED(status)) { // stopped by ctrl-z
            Sio_puts("Job [");
            Sio_putl(pid2jid(pid));
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") stopped by signal ");
            Sio_putl(WSTOPSIG(status));
            Sio_puts("\n");
            getjobpid(jobs, pid)->state = ST;
        }
    }

此外還有非常重要的一點就是,我們的shell程序本身是所有子進程的父進程,那么就會分配在同一個組里,終止子進程所在組會導致shell程序本身也被終止,這里的解決辦法是給子進程設置一個單獨的組,只需要添加在fork和exec之間。

        if ((pid = fork()) == 0) {   /* Child runs user job */
            setpgid(0, 0);
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

trace09~10 bg 和 fg

trace09是關於內置命令bg和fg的,其使用方法為

$ fg/bg <job>

其中 為響應任務的PID或JID,如果為JID則需%作為前綴。fg和bg都是發送SIGCONT信號來將相應任務重啟。

首先在builtin_cmd函數中判斷是否為bg或fg,如果是則執行相應的操作。

    // bg or fg command
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
        do_bgfg(argv);
        return 1;
    }

具體的do_bgfg函數首先根據有無%判斷是PID還是JID,然后取得該job指針,然后給其所在進程組發送SIGCONT,最后根據其是fg還是bg來做出與eval中類似的行為。

void do_bgfg(char** argv)
{
    struct job_t* job;
    char* id = argv[1];
    if (id[0] == '%') { // jid
        job = getjobjid(jobs, atoi(id + 1));
    }
    else {              // pid
        job = getjobpid(jobs, atoi(id));
    }

    kill(-(job->pid), SIGCONT);

    if (!strcmp(argv[0], "fg")) {  // fg command
        job->state = FG;
        // wait for the job to terminate
        waitfg(job->pid);
    }
    else {                         // bg command
        job->state = BG;
        printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
    }

    return;
}

trace11~13 Tests for SIGSTOP & SIGINT & fg/bg

trace11.txt - Forward SIGINT to every process in foreground process group

trace12.txt - Forward SIGTSTP to every process in foreground process group

trace13.txt - Restart every stopped process in process group

這三個traces主要測試前面是否正確實現了SIGSTOP和SIGINT的處理程序,以及fg/bg的實現,如果沒有將進程組中的所有程序一並處理這里可能會出現錯誤,前面的實現中已經處理了這些情況,這里不再贅述。

trace14 Error handling

這個測試需要對fg和bg的輸入參數進行一些錯誤處理,例如沒有參數或參數非數值或所選任務或進程不存在等。在do_bgfg函數中進行相應處理即可。

void do_bgfg(char** argv)
{
    struct job_t* job;
    char* id = argv[1];

    // no argument for bg/fg
    if (id == NULL)
    {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }

    if (id[0] == '%') { // jid
        if (!checkNum(id + 1)) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);
            return;
        }
        int jid = atoi(id + 1);
        job = getjobjid(jobs, jid);
        if (job == NULL) {
            printf("%%%d: No such job\n", jid);
            return;
        }
    }
    else {              // pid
        if (!checkNum(id)) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);
            return;
        }
        int pid = atoi(id);
        job = getjobpid(jobs, pid);
        if (job == NULL) {
            printf("(%d): No such process\n", pid);
            return;
        }
    }

    kill(-(job->pid), SIGCONT);

    if (!strcmp(argv[0], "fg")) {  // fg command
        job->state = FG;
        // wait for the job to terminate
        waitfg(job->pid);
    }
    else {                         // bg command
        job->state = BG;
        printf("[%d] (%d) %s", pid2jid(job->pid), job->pid, job->cmdline);
    }

    return;
}

trace15~16

trace15.txt - Putting it all together

trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT signals that come from other processes instead of the terminal.

對前面的程序進行的一些綜合性測試,已經通過。

exit fix

參考exit與_exit的區別,可以知道在fork出的child中要用_exit來退出,否則exit會調用用atexit注冊的函數並刷新父進程的緩沖區。一般來說在一個main函數中只調用一次exit或return。

        if ((pid = fork()) == 0) {   /* Child runs user job */
            setpgid(0, 0);
            Sigprocmask(SIG_UNBLOCK, &prev, NULL);  // unblock SIGCHLD
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                _exit(1);
            }
        }

原文鏈接:https://www.cnblogs.com/zhangyi1357/p/16005508.html
轉載請注明出處!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM