[CS:APP-ch8] Shell Lab

[CS:APP-ch8] Shell Lab

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

http://condor.depaul.edu/glancast/374class/hw/shlab-readme.html

https://github.com/zhoudiqiu/Shell-lab/blob/master/lab5/tsh.c


Target

implement

  • eval, builtin_cmd, do_bgfg, waitfg
  • sigchld_handler, sigint_handler, sigtstp_handler

0. Preparation

tshref: 正确的参考shell,要求自己实现后的shell和它的结果一样

make test01用trace01.txt来验证结果,可以参考make rtest01的结果

gdb makefile去掉参数O2,加上-g


1. void eval ( char*cmdline )

原型:p755

  • If command is a built-in command, the shell program handles it immediately.
  • Otherwise, the shell creates a child process to load and execute the program for command.

Hint

  • In eval, the parent must use sigprocmaskto block SIGCHLD signals before it forks the child, then unblock these signals, again using sigprocmask after it adds the child to the job list by calling addjob.
  • Since children inherit the blocked vectors of their parents, the child must be sure to then unblock SIGCHLD signals before it execs the new program.
  • The parent needs to block the SIGCHLD signals in this way in order to avoid the race condition where the child is reaped by sigchld_handler (and thus removed from the job list) before the parent calls addjob.
  • After the fork, but before the execve, the child process should call setpgid(0, 0), which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group. When you type ctrl-c, the shell should catch the resulting SIGINT and then forward it to the appropriate foreground job (or more precisely, the process group that contains the foreground job).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
 * eval - Evaluate the command line that the user has just typed in
 *
 * If the user has requested a built-in command (quit, jobs, bg or fg)
 * then execute it immediately. Otherwise, fork a child process and
 * run the job in the context of the child. If the job is running in
 * the foreground, wait for it to terminate and then return.  Note:
 * each child process must have a unique process group ID so that our
 * background children don't receive SIGINT (SIGTSTP) from the kernel
 * when we type ctrl-c (ctrl-z) at the keyboard.
 */
void eval ( char *cmdline ) {

    pid_t pid;
    sigset_t mask;
    char *argv[ MAXARGS ];
    //Parse the command line and build the argv array
    int bg = parseline ( cmdline, argv );

    if ( !builtin_cmd ( argv ) ) {

        // blocking SIGCHLD, race
        sigemptyset ( &mask );
        sigaddset ( &mask, SIGCHLD );
        sigprocmask ( SIG_BLOCK, &mask, NULL );

        // fork new process
        if ( ( pid = fork () ) < 0 )
            unix_error ( "fork error" );

        // child process
        else if ( pid == 0 ) {
            // unblock SIGCHLD in child process
            sigprocmask ( SIG_UNBLOCK, &mask, NULL );
            // ensures that there will be only one process
            setpgid ( 0, 0 );
            // execute command
            if ( execvp ( argv[ 0 ], argv ) < 0 ) {
                printf ( "%s: Command not found\n", argv[ 0 ] );
                exit ( 1 );
            }

        // parent
        } else {

            addjob ( jobs, pid, bg ? BG : FG, cmdline );
            // get SIGCHLD back after add job
            sigprocmask ( SIG_UNBLOCK, &mask, NULL );

            if ( !bg ) {
                // reap when job terminated ( send signal SIGCHILD )
                waitfg ( pid );
            } else {
                //[1] (6325) ./myspin 1 &
                printf ( "[%d] (%d) %s", pid2jid ( pid ), pid, cmdline );
            }
        }
    }
    return;
}

2.int builtin_cmd( char **argv)

  • Four commands are to be built-in in the shell
  • quit: exit the shell.
  • jobs: List the running and stopped background jobs.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/*
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.
 */ 
int builtin_cmd ( char **argv ) {
    if ( strcmp ( argv[ 0 ], "quit" ) == 0 ) {
        exit ( 0 );
    } else if ( strcmp ( argv[ 0 ], "jobs" ) == 0 ) {
        listjobs ( jobs );
        return 1;
    } else if ( !strcmp ( argv[ 0 ], "fg" ) || !strcmp ( argv[ 0 ], "bg" ) ) {
        do_bgfg ( argv );
        return 1;
    }
    return 0; /* not a builtin command */
}

3. void waitfg( pid_t pid )

  • shell should wait for foreground process
  • This function should wait by sleeping for 1 second repeatedly until the specified process is no longer the foreground process (state = FG)

Hints

  • In waitfg, use a busy loop around the sleep function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg ( pid_t pid ) {
    struct job_t *job = getjobpid ( jobs, pid );
    if ( pid == 0 )
        return;

    if ( job != NULL ) {
        // sleep
        while ( pid == fgpid ( jobs ) ) {
        }
    }
    return;
}

4. void do_bgfg( char **argv )

  • bg <job>: Change a stopped background job to a running background job.

  • fg <job>: Change a stopped or running background job to a running in the foreground.

  • The bg <job> command restarts <job> by sending it a SIGCONTsignal, and then runs it in the background. The <job> argument can be either a PID or a JID.

  • The fg <job>command restarts <job> by sending it a SIGCONTsignal, and then runs it in the foreground. The <job> argument can be either a PID or a JID.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg ( char **argv ) {
    struct job_t *job;
    char *tmp;
    int jid;
    pid_t pid;

    tmp = argv[ 1 ];

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

    // jid
    if ( tmp[ 0 ] == '%' ) {
        // string to integer
        jid = atoi ( &tmp[ 1 ] );
        job = getjobjid ( jobs, jid );

        // unvalid jid
        if ( job == NULL ) {
            //%2: No such job
            printf ( "%s: No such job\n", tmp );
            return;
        } else {
            pid = job->pid;
        }
    }

    // pid
    else if ( isdigit ( tmp[ 0 ] ) ) {
        pid = atoi ( tmp );
        job = getjobpid ( jobs, pid );

        // unvalid pid
        if ( job == NULL ) {
            //(2): No such process
            printf ( "(%d): No such process\n", pid );
            return;
        }
    } else {
        printf ( "%s: argument must be a PID or %%jobid\n", argv[ 0 ] );
        return;
    }
    // awakened by the receipt of a SIGCONT signal.
    kill ( -pid, SIGCONT );

    if ( !strcmp ( "fg", argv[ 0 ] ) ) {
        // foreground, shell wait for it
        job->state = FG;
        waitfg ( job->pid );
    } else {
        job->state = BG;
        // print
        printf ( "[%d] (%d) %s", job->jid, job->pid, job->cmdline );
    }

    return;
}

5.void sigchld_handler( int sig )

This is the shell’s handler for theSIGCHLD signal.

Hints

  • In sigchld_handler, use exactly one call to waitpid
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

/*
 * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever
 *     a child job terminates (becomes a zombie), or stops because it
 *     received a SIGSTOP or SIGTSTP signal. The handler reaps all
 *     available zombie children, but doesn't wait for any other
 *     currently running children to terminate.
 */
void sigchld_handler ( int sig ) {
    pid_t pid;
    int status;

    // wait for all child to end
    while ( ( pid = waitpid ( -1, &status, WNOHANG | WUNTRACED ) ) > 0 ) {
        // child currently stopped
        if ( WIFSTOPPED ( status ) ) {
            // change state
            struct job_t *job = getjobpid ( jobs, pid );
            job->state = ST;

            int jid = pid2jid ( pid );
            // Job [2] (14171) stopped by signal 20
            printf ( "Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG ( status ) );

        }
        // child terminated because of a signal
        else if ( WIFSIGNALED ( status ) ) {
            int jid = pid2jid ( pid );
            // INT
            // Job [1] (13253) terminated by signal 2
            printf ( "Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG ( status ) );
            deletejob ( jobs, pid );
        }
        // child terminated normally
        else if ( WIFEXITED ( status ) ) {
            deletejob ( jobs, pid );
        }
    }
    return;
}

6. void sigint_handler ( int sig )

Hints

  • When you implement your signal handlers, be sure to send SIGINT and SIGTSTP signals to the entire foreground process group, using -pid instead of pid in the argument to the kill function.
    The sdriver.pl program tests for this error.
1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * 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 ) {
    pid_t pid = fgpid ( jobs );

    if ( pid != 0 ) {
        kill ( -pid, sig );
    }
    return;
}

7.void sigtstp_handler ( int sig )

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
 * 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 ) {
    pid_t pid = fgpid ( jobs );

    if ( pid != 0 ) {
        kill ( -pid, sig );
    }
    return;
}