Tiny Shell Lab

In this lab, you will implement a shell terminal that supports forking processes and manages both foreground and background processes. A total of 32 tests are provided, with the first part focusing on implementing the functionality to pass the first 23 tests.

Part I

First, look at the initialization section of the main function.

char cmdline[MAXLINE_TSH]; defines a character array that can be written to.

init_job_list(); initializes a linked list.

void init_job_list(void) {
    init = true;
    for (jid_t jid = 1; jid <= MAXJOBS; jid++) {
        struct job_t *job = get_job(jid);
        clearjob(job);
        job->cmdline = NULL;
    }
    nextjid = 1;
}

nextjid is a static variable used for assigning new job IDs.

Next, signal handlers are set up for different signals by calling the Signal() function.

// Install the signal handlers
Signal(SIGINT, sigint_handler);   // Handles Ctrl-C
Signal(SIGTSTP, sigtstp_handler); // Handles Ctrl-Z
Signal(SIGCHLD, sigchld_handler); // Handles terminated or stopped child

Signal(SIGTTIN, SIG_IGN);
Signal(SIGTTOU, SIG_IGN);

Signal(SIGQUIT, sigquit_handler);

SIGINT typically represents a keyboard interrupt (i.e., the user presses Ctrl+C) and is a common way to terminate a program. A custom sigint_handler function is used here to catch and handle this signal.

SIGTSTP is the signal sent when the user presses Ctrl+Z, typically used to suspend (pause) a process. The custom handler sigtstp_handler will process this signal.

SIGCHLD is sent to the parent process when a child process terminates or is suspended, and is often used for the parent to clean up finished child processes (e.g., by calling wait() or waitpid() to handle zombie processes).

SIGTTIN is sent when a background process tries to read from the terminal. If not ignored, this signal will pause the background process.

SIGTTOU is sent when a background process tries to write to the terminal. Like SIGTTIN, ignoring this signal can prevent the background process from being paused.

SIGQUIT typically indicates the user presses Ctrl+\, and causes the program to generate a core dump and terminate. The sigquit_handler function alters its default behavior.

The sigint_handler, sigtstp_handler, sigchld_handler, and sigquit_handler functions will be introduced later.

After obtaining the cmdline, the next step is to parse it.

eval()

parse_result = parseline(cmdline, &token);

struct cmdline_tokens {
    int argc;               ///< Number of arguments passed
    char *argv[MAXARGS];    ///< The arguments list
    char *infile;           ///< The filename for input redirection, or NULL
    char *outfile;          ///< The filename for output redirection, or NULL
    builtin_state builtin;  ///< Indicates if argv[0] is a builtin command
    char _buf[MAXLINE_TSH]; ///< Internal backing buffer (do not use)
};

buildin_cmd()

There are two types of commands: built-in and others. Built-in commands include:

Other Commands

When it is not a buildin_cmd, a valid command is one that intends to run an executable program. For such a command, we fork a new process and execute it using execve().

    else if (!buildin_cmd(cmdline))
    {       
        pid_t pid;
        sigset_t mask, prev;
        sigemptyset(&mask);
        sigaddset(&mask, SIGCHLD);
        sigaddset(&mask, SIGINT);
        sigaddset(&mask, SIGTSTP);
        sigprocmask(SIG_BLOCK, &mask, &prev);
        pid = fork();
        if(pid == 0){       //child process
            sigprocmask(SIG_SETMASK, &prev, NULL);
            setpgid(pid, pid);
            if(execve(token.argv[0], token.argv, environ) < 0){
                perror("fialed to execve\n");
                exit(-1);
            }
        }else{
            if(parse_result == PARSELINE_FG){     // forground job
                fg_pid = pid;
                fg_running = 1;
                add_job(pid, FG, cmdline);
                while(fg_running == 1){     // while the fg job is still running
                    sigsuspend(&prev);
                }
                sigprocmask(SIG_SETMASK, &prev, NULL);
            }else if (parse_result == PARSELINE_BG)       // background job
            {
                add_job(pid, BG, cmdline);
                jid_t jid = job_from_pid(pid);
                printf("[%d] (%d) %s\n", jid, pid, cmdline);
                sigprocmask(SIG_SETMASK, &prev, NULL);
            }
        }
    }

If the command is to be executed in the foreground (PARSELINE_FG), the global variable fg_pid will be set to the pid of the current child process.

sigsuspend(&prev);

This temporarily replaces the process's signal mask with prev and suspends the parent process until a signal is received. Its purpose is to wait for the foreground process to finish (e.g., by receiving a child process termination signal via SIGCHLD), preventing the parent process from continuing execution before the foreground job ends. Changes in the fg_running state will be explained in the upcoming sigchld_handler().

If the command is to be executed in the background (PARSELINE_BG), a new jid and pid will be assigned, and a message indicating background execution will be printed. Signals will be unblocked, and the prompt will be ready for the next command.

sigchld_handler()

The sigchld_handler function handles the SIGCHLD signal, which is sent to the parent process when the state of a child process changes. This signal handler checks the status of the child process and updates the job status based on its exit status, termination signal, or stop status.

void sigchld_handler(int sig) {
    int olderrno = errno;
    sigset_t mask, prev;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTSTP);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    int wstate;
    pid_t pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
    while(pid > 0){
        if(WIFCONTINUED(wstate)){
            jid_t jid = job_from_pid(pid);
            job_state jstate = job_get_state(jid);      // get the state of the job
            if(jstate == ST){           // ST -> BG
                job_set_state(jid, BG);
            }
        }else{
            jid_t jid = job_from_pid(pid);
            if(WIFEXITED(wstate)){  // normally exit
                delete_job(jid);
                if(pid == fg_pid){  // if a forground job
                    fg_running = 0;     // stop running the job
                }
            }
            if(WIFSIGNALED(wstate)){    // SIGINT
                delete_job(jid);
                int signum = WTERMSIG(wstate);
                sio_printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, signum);
                if(pid == fg_pid){
                    fg_running = 0;
                }
            }
            if(WIFSTOPPED(wstate)){     // SIGTSTP
                fg_running = 0;
                job_set_state(jid, ST);
                int signum = WSTOPSIG(wstate);
                sio_printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, signum);
            }
        }
        pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);
    errno = olderrno;
    return;
}

Create an empty signal set mask. Add SIGINT and SIGTSTP to the signal set mask. Use sigprocmask to block these signals to prevent them from being received during processing; prev saves the old signal mask.

Use waitpid to wait for changes in the status of child processes. The -1 argument indicates waiting for any child process:

In the while loop, as long as there are child processes with status changes:

If the process is not stopped:

sigint_handler()

void sigint_handler(int sig) {
    int _errno = errno;
    if(fg_pid > 0){
        killpg(fg_pid, sig);
    }
    errno = _errno;
}

Note that killpg does not kill processes but rather sends signals. The function killpg(fg_pid, sig) is used to send a signal to all processes in the foreground process group. Here, sig represents the type of signal passed to sigint_handler (in this case, SIGINT). killpg is a higher-level function for sending signals to a process group, which is more efficient than sending signals individually. Blocking signals is not necessary when merely sending a signal.

sigtstp_handler()

void sigtstp_handler(int sig) {
    int _errno = errno;
    if(fg_pid > 0){
        killpg(fg_pid, sig);
    }
    errno = _errno;
}

It is similar to the code above.

Part II

After implementing the basic buildin_cmd commands and handling foreground (fg) and background (bg) operations, as well as managing signal blocking and the job list, the second part will involve implementing job state transitions (st), foreground (fg), and background (bg) transformations, as well as handling I/O redirection (fd).

First, we'll review convert_to_bg and convert_to_fg, which were not explained in Part I.

convert_to_bg()

static void convert_to_bg(struct cmdline_tokens *token){
    if(token->argc == 2){       // correct number to parse
        const char *id = token->argv[1];
        sigset_t mask, prev;
        sigemptyset(&mask);
        sigaddset(&mask, SIGCHLD);
        sigaddset(&mask, SIGINT);
        sigaddset(&mask, SIGTSTP);
        sigprocmask(SIG_BLOCK, &mask, &prev);       // block
        pid_t pid = 0;
        jid_t jid = 0;
        if(id == NULL || id[0] == '\0'){            // the id is nothing...
            printf("bg command requires PID or %%jobid argument\n");
        }else{
            if(id[0] == '%'){                               // jid
                jid = (jid_t)atoi(&id[1]);
                if(job_exists(jid) == false){
                    printf("%%%d: No such job\n", jid);
                    sigprocmask(SIG_SETMASK, &prev, NULL);      // unblock
                    return;
                }
                pid = job_get_pid(jid);
            }else if ('0' <= id[0] && '9' >= id[0])         // pid
            {
                pid = (pid_t)atoi(id);
                jid = job_from_pid(pid);
            }else{                                          // bg xxx, not pid or %jid
                printf("bg: argument must be a PID or %%jobid\n");
            }
        }
        if(pid != 0){
            const char *cmdline = job_get_cmdline(jid);
            printf("[%d] (%d) %s\n", jid, pid, cmdline);
            st_fgbg = 2;
            killpg(pid, SIGCONT);                           // send comtinue
        }
        sigprocmask(SIG_SETMASK, &prev, NULL);      // unblock

    }else{  // bg input error more than 2 argv...
        printf("bg command requires PID or %%jobid argument\n");
        return;
    }
}

st_fgbg is a global variable. A value of 0 represents transitioning from background to foreground, 1 represents transitioning from stopped to foreground (STfg), and 2 represents transitioning from stopped to background (STbg). There is no direct transition from foreground to background because, during foreground execution, only signals like Ctrl+C and Ctrl+Z can stop or end the foreground process.

killpg(pid, SIGCONT); will continue the execution of processes that are in the stopped state (ST).

This will trigger sigchld_handler. We need to modify sigchld_handler from Part I to handle the transition from stopped to background (STbg).

convert_to_fg()

static void convert_to_fg(struct cmdline_tokens *token){
    if(token->argc != 2){
        printf("fg command requires PID or %%jobid argument\n");
        return;
    }
    const char *id = token->argv[1];
    pid_t pid = 0;
    pid_t jid = 0;
    sigset_t mask, prev;
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTSTP);
    sigprocmask(SIG_BLOCK, &mask, &prev);       // block
    if(id == NULL || id[0] == '\0'){            // the id is nothing...
        printf("fg command requires PID or %%jobid argument\n");
    }else{
        if(id[0] == '%'){                               // jid
            jid = (jid_t)atoi(&id[1]);
            if(job_exists(jid) == false){
                printf("%%%d: No such job\n", jid);
                sigprocmask(SIG_SETMASK, &prev, NULL);      // unblock
                return;
            }
            pid = job_get_pid(jid);
        }else if ('0' <= id[0] && '9' >= id[0])         // pid
        {
            pid = (pid_t)atoi(id);
            jid = job_from_pid(pid);
        }else{                                          // bg xxx, not pid or %jid
            printf("fg: argument must be a PID or %%jobid\n");
        }
    }
    if(pid != 0){
        // different from background job things from here
        job_state jstate = job_get_state(jid);
        fg_pid = pid;
        fg_running = 1;
        if(jstate == ST){
            st_fgbg = 1;
            killpg(pid, SIGCONT);       // send continue
        }else if(jstate == BG){
            st_fgbg = 0;
            job_set_state(jid, FG);     // BG -> FG
        }
        while(fg_running == 1){
            sigsuspend(&prev);
        }
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);      // unblock
}

The only difference from bg is that after setting fg_running = 1;, you need to wait for the foreground process to finish.

4o mini

FG/BG/ST Status Convert

void sigchld_handler(int sig) {
    int olderrno = errno;
    sigset_t mask, prev;
    ...
    while(pid > 0){
        if(WIFCONTINUED(wstate)){
            jid_t jid = job_from_pid(pid);
            job_state jstate = job_get_state(jid);      // get the state of the job
            if(jstate == ST){           // ST -> BG
                if(st_fgbg == 1){
                    job_set_state(jid, FG);
                }else if(st_fgbg == 2){
                    job_set_state(jid, BG);
                }
            }
        }else{
            ...
        }
        pid = waitpid(-1, &wstate, WUNTRACED | WNOHANG | WCONTINUED);
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);
    errno = olderrno;
    return;
}

The modification is as follows: if a SIGCONT signal is received, use WIFCONTINUED(wstate) to check if the child process has resumed execution. If it has, and the current job status is stopped (ST), update the job status to foreground (FG) or background (BG) based on the value of st_fgbg.

File Redirection

In eval(), modifications are also needed to change the input and output I/O by altering stdin and stdout.

void eval(const char *cmdline) {
    parseline_return parse_result;
    ...
    else if (!buildin_cmd(cmdline))
    {
        ...
        if(pid == 0){       //child process
            sigprocmask(SIG_SETMASK, &prev, NULL);
            // pid = getpid(pid, pid);
            setpgid(pid, pid);
            if(token.infile != NULL){              // '<' ST_INFILE
                int fd = open(token.infile, O_RDONLY, 0664);
                if (fd == -1) {
                    fprintf(stderr, "%s: No such file or directory\n", token.infile);
                    return;
                }    
                if (dup2(fd, STDIN_FILENO) == -1) {
                    fprintf(stderr, "%s: No such file or directory\n", token.infile);
                    close(fd);
                    return;
                }            
            }
            if (token.outfile != NULL)       // '>' ST_OUTFILE
            {
                int fd = open(token.outfile, O_WRONLY | O_CREAT | O_TRUNC, 0664);
                if (fd == -1) {
                    fprintf(stderr, "%s: No such file or directory\n", token.outfile);
                    return;
                }
                if (dup2(fd, STDOUT_FILENO) == -1) {
                    fprintf(stderr, "%s: No such file or directory\n", token.outfile);
                    close(fd);
                    return;
                }
            }
            int exe_fd = open(token.argv[0], O_RDONLY, 0664);
            if(exe_fd == -1){
                if (errno == ENOENT) {
                    fprintf(stderr, "%s: No such file or directory\n", token.argv[0]);
                } else if (errno == EACCES) {
                    fprintf(stderr, "%s: Permission denied\n", token.argv[0]);
                } else {
                    fprintf(stderr, "Error opening file: %s\n", strerror(errno));
                }            
                exit(-1);
            }
            ...
}

Input Redirection:

Output Redirection:

Executable File Check:

With this, the tshlab is complete.