CS:APP LAB 자료 링크: http://csapp.cs.cmu.edu/3e/labs.html
혹시 잘못된 내용이 있다면 메일이나 댓글로 알려주시면 정말 감사하겠습니다

trace별로 해결 과정을 순차적으로 제시할까 했지만 가독성이 너무 떨어질 것 같아 완성된 답안을 토대로 구현해야 하는 함수들을 설명하는 방식으로 포스팅하겠습니다.
스켈레톤에 이미 구현되어 있고 수정할 필요가 없는 함수들은 포함하지 않겠습니다.
참고로 Waitpid Fork Sigemptyset 등 함수의 첫 글자가 대문자 처리되어 있는 함수들은 각각 원래 함수들의 wrapper function 으로, 원래 함수 (예를 들어, Waitpid라면 waitpid 함수)를 실행하고 에러가 발생했는지 확인하는 역할을 합니다.
비슷하게 sio_puts sio_putl 등 함수는 주어진 argument를 async-signal-safe 하게 출력하는 wrapper function 입니다.
모두 저자들이 제공하는 csapp.h, csapp.c 에 구현되어 있고, 이 파일들은 구글링하면 나옵니다!

eval

void eval(char *cmdline) 
{
    int bg;
    int pid;
    sigset_t mask;
    char *argv[MAXARGS];

    bg = parseline(cmdline, argv);

    if (argv[0] == NULL) { //terminate on EOF
        return;
    }

    if (builtin_cmd(argv)) { //if it is a built-in-command: execute it and return 1. else return 0.
        return;
    }

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

    Sigprocmask(SIG_BLOCK, &mask, NULL); //mask SIGCHLD 

    if ((pid = Fork()) == 0) {
        // Child's behavior
        setpgid(0, 0);
        Sigprocmask(SIG_UNBLOCK, &mask, NULL);
        
        if (Execve(argv[0], argv, environ) < 0) {
            printf("%s: Command not found.\n", argv[0]);
            exit(0);
        }
    }

    // Parent's behavior
    if (!bg) {
        addjob(jobs, pid, FG, cmdline);
        Sigprocmask(SIG_UNBLOCK, &mask, NULL);
        waitfg(pid); /* wait for the foreground job to finish */
    } else {
        addjob(jobs, pid, BG, cmdline); 
        Sigprocmask(SIG_UNBLOCK, &mask, NULL);
        printf("[%d] (%d) %s", pid2jid(pid), (int)pid, cmdline); /* print out log and execute in background */
    }

    return;
}

Shell의 메인이 되는 함수입니다. 이 함수에서 built-in command를 실행하거나 child를 fork 해서 execve로 다른 프로그램을 실행합니다.
Parent가 addjob 을 실행하기 전에 child의 작동이 끝나서 SIGCHLD 신호를 받으면 안 되기 때문에 신호를 막아주는 것과 parseline 함수의 결과에 따라 background 작업인지 foreground 작업인지 나누어 처리하는 것에 유의하면 됩니다.

builtin_cmd

int builtin_cmd(char **argv) 
{
    char* command = argv[0];

    if (!strcmp("quit", command)) {
        exit(0);
    } else if (!strcmp("jobs", command)) {
        listjobs(jobs);
    } else if (!strcmp("bg", command) || !strcmp("fg", command)) {
        do_bgfg(argv);
    } else {
        return 0;     /* not a builtin command */
    }

    return 1;
}

Built-in command를 실행하는 함수입니다.

do_bgfg

void do_bgfg(char **argv) 
{
    struct job_t *job;
    int is_bg = !strcmp("bg", argv[0]);
    int is_pid;

    if (argv[1] == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }

    if (argv[1][0] == '%') { /* if it's jid */
        is_pid = 1;
        job = getjobjid(jobs, my_atoi(&argv[1][1]));
    } else { /* if it's pid */
        is_pid = 0;
        job = getjobpid(jobs, my_atoi(argv[1]));
    }

    if (errno == EINVAL) { //my_atoi error (wrong argument)
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }

    if (job == NULL) { //can't find job
        if (is_pid) {
            printf("%s: No such job\n", argv[1]);
        } else {
            printf("(%s): No such process\n", argv[1]);
        }
    } else {
        Kill(-job->pid, SIGCONT); /* send SIGCONT signal to every process under process group */

        if (is_bg) { // command: bg
            job->state = BG;
            printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        } else { // command: fg
            job->state = FG;
            waitfg(job->pid);
        }
    }

    return;
}

Built-in command 중 하나인 bgfg를 처리하는 함수입니다.
스펙에 있는대로 job id나 process id를 받아서 멈춰있던 함수에 SIGCONT 신호를 보내줍니다. bg가 호출되었다면 background에서 마저 실행하고, fg가 호출되었다면 foreground에서 실행하면 됩니다.
이때, 해당 job들의 state도 변경해줘야 합니다.
예외 처리에 my_atoi라는 함수가 사용된 것을 보실 수 있는데, string을 int로 파싱해주기 위해 제가 만든 함수입니다. 만약 argument를 int로 바꾸는 게 불가능하다면 global variable인 errno의 값을 EINVAL로 바꿔주고 0을 반환합니다.
구현은 아래와 같습니다.

int my_atoi(char* start) {
    if (start == NULL) {
        errno = EINVAL;
        return 0;
    }

    int result = 0;
    char curr;

    while ((curr = *start) != '\0') {
        if ('0' <= curr && curr <= '9') {
            result = result*10 + curr - '0';
            ++start;
        } else {
            errno = EINVAL;
            return 0;
        }
    }

    return result;
}

waitfg

void waitfg(pid_t pid)
{
    /* according to the Hint on assignment handout, use budy loop around sleep function
     * and do all reaping in the signal handler
     */

    while(1) {
        if (fgpid(jobs) != pid) { /* when given foreground job terminated */
            break;
        } else {
            Sleep(1);
        }
    }

    return;
}

주석에 적혀 있듯, CS:APP의 저자들이 제공하는 handout에 waitfg 함수의 경우 sleep 함수를 사용해 foreground 함수가 끝날때까지 기다리라고 instruction이 있습니다.
물론 좋은 구현 방법은 아닌 것 같지만.. 일단 연습하는 입장이므로 간단하게 구현하기 위해 instruction을 따르겠습니다.

sigchld_handler

void sigchld_handler(int sig) 
{
    pid_t pid;
    int status;
    char log_message_buff[1024];
    struct job_t *job;
    int prev_errno = errno;

    /* WNOHANG: return immediately if none of the child processes in the wait set has terminated yet.
       WUNTRACED: return pid of the terminated or "stopped" child */
    while((pid = Waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        job = getjobpid(jobs, pid);

        if (WIFSIGNALED(status)) {
            /* when the process terminated */
            snprintf(log_message_buff, 1024, "Job [%d] (%d) terminated by signal %d\n", job->jid, (int)pid, WTERMSIG(status));
            sio_puts(log_message_buff);
            deletejob(jobs, pid);
        } else if (WIFSTOPPED(status)) {
            /* when the process stopped */
            snprintf(log_message_buff, 1024, "Job [%d] (%d) stopped by signal %d\n", job->jid, (int)pid, WSTOPSIG(status));
            sio_puts(log_message_buff);
            job->state = ST;
        } else if (WIFEXITED(status)) {
            /* delete finished job from the job list
               without this, you get to send signal to wrong pid at sigint/sigtstp handler. */
            deletejob(jobs, pid);
        }
    }
    
    errno = prev_errno;

    return;
}

SIGCHLD 신호를 받았을 때 작동할 signal handler 입니다.
현재 종료된 child 들을 모두 reap 해야 하기 때문에 while 문을 사용하고 waitpid 함수의 첫번째 parameter로 -1을 넘겨주었습니다. 또, 모든 child들이 끝나길 기다려서는 안 되고(WNOHANG), terminate 된 상태뿐 아니라 stop된 상태의 child도 다뤄야하기 때문에(WUNTRACED), 세번째 parameter로는 WNOHANG | WUNTRACED를 넘겨주었습니다.
printf는 async-signal-safe 하지 못합니다. 프로그램의 여러 곳에서 printf를 호출해주고 있기 때문에, printf가 호출되고 있는 상황에 신호가 들어와서 signal-handler가 호출되고 printf가 그 안에서 호출되면 데드락 상황이 발생합니다.
따라서 printf를 사용하는 대신 넘칠 일 없을만큼 충분히 큰 (여기서 출력하고자 하는 것은 로그 메시지이므로 길이의 상한을 이미 알고 있습니다) 버퍼 log_message_buff를 정적으로 할당해주고, snprintf 함수와 sio_puts 함수로 출력해주었습니다.
이 함수를 구현하며 제가 애를 먹었던 부분이 if - else if 문의 마지막 분기인

else if (WIFEXITED(status)) {
    deletejob(jobs, pid); //***** 중요! 이미 제대로 끝난 작업들은 job list 에서 삭제해줘야 함. 안 그러면 잘못된 pid로 signal 보내게 됨. at sigint/sigtstp handler
}

이 부분이었습니다.
자꾸 kill에서 해당 process group을 찾을 수 없다는 에러가 나길래 뭔가 했더니.. WIFSIGNALED(status)는 catch하지 못한 signal로 종료된 작업들만 잡아낸다는 게 문제였습니다.
정상적으로 종료된 child 들의 경우도 deletejob을 통해 job list에서 제거해줘야 하는걸 간과했습니다.
위에 있듯 WIFEXITED(status)를 써서 잡아낼 수 있습니다.

sigint_handler

void sigint_handler(int sig) 
{
    /* preserve previous errno */
    int prev_errno = errno;

    pid_t foreground_pid = fgpid(jobs);

    if (foreground_pid != 0) {
        /* if there is a foreground job, send SIGINT */
        Kill(-foreground_pid, sig);
    }

    errno = prev_errno;
    return;
}

ctrl-c가 입력되었을 때 Shell은 여전히 작동하면서 foreground의 process는 중지시키도록 해주는 signal handler입니다.
현재 foreground에 실행되고 있는 process의 process id를 fgpid 함수로 얻어오고 그 process group에 SIGINT 신호를 보냅니다. eval 함수에서 setpgid(0, 0); 식으로 child의 process group id를 설정해줬기 때문에 kill의 첫번째 parameter로 -foreground_pid를 제공해주면 그 process group 전체를 terminate 할 수 있게 됩니다.
앞의 my_atoi 함수에서 errno를 활용해서 로직을 처리하기 때문에 이 signal handler가 errno를 임의로 변경하면 안 됩니다. 따라서 함수를 시작하면서 기존 errno를 저장해주고 함수가 끝날 때 다시 복원합니다.

sigtstp_handler

void sigtstp_handler(int sig) 
{
    int prev_errno = errno;

    pid_t foreground_pid = fgpid(jobs);

    if (foreground_pid != 0) {
        /* if there is a foreground job, send SIGTSTP */
        Kill(-foreground_pid, sig);
    }

    errno = prev_errno;

    return;
}

sigint_handler 와 같습니다.

Comments