Tiny Shell Lab

在这个lab中,实现一个可以fork进程的shell终端,维护前台后台的进程。一共提供32个测试,第一部分先介绍如何实现来通过前23个测试。

Part I

优先看主函数main 初始化部分。

char cmdline[MAXLINE_TSH]; 规定了一个可以写入的char数组。

init_job_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 是一个static变量,在分配新的job时使用。

接下来通过调用 Signal() 函数为不同的信号安装处理函数

// 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 通常表示键盘中断(即用户按下 Ctrl+C),这是用来终止程序的常见方式。这里使用了自定义的 sigint_handler 函数来捕获并处理该信号。

SIGTSTP 是用户按下 Ctrl+Z 时发送的信号,通常用于将进程挂起(暂停执行)。自定义的处理函数 sigtstp_handler 将处理此信号。

SIGCHLD 信号在子进程终止或暂停时发送给父进程,常用于父进程清理已结束的子进程(如调用 wait()waitpid() 处理僵尸进程)。

SIGTTIN 信号在后台进程尝试读取终端输入时发送。如果不忽略这个信号,后台进程会被暂停。

SIGTTOU 信号在后台进程尝试写入终端输出时发送。和 SIGTTIN 类似,忽略这个信号可以避免后台进程被暂停。

SIGQUIT 信号通常表示用户按下 Ctrl+\,这个信号会导致程序生成核心转储(core dump)并终止进程。通过 sigquit_handler 改变其默认行为。

稍后介绍sigint_handlersigtstp_handlersigchld_handlersigquit_handler

在获取了cmdline之后,解析cmdline

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()

一共有两种commend,buildin和其他。buildin cmd 包括

其他cmd

当不是buildin_cmd的时候,合法的指令是想要运行一个可执行程序。我们为这个指令fork一个新的进程并执行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);
            }
        }
    }

如果是foreground执行(PARSELINE_FG)那么将全局变量fg_pid 赋值为当前子进程pid

sigsuspend(&prev);

会临时替换进程的信号掩码为 prev,并挂起父进程,直到接收到信号。它的作用是等待前台进程结束(如通过 SIGCHLD 接收子进程结束信号),避免父进程在前台作业结束前继续执行。fg_runing 状态改变会在接下来 sigchld_handler() 中说明。

入宫是background执行(PARSELINE_BG)那么会简单的将分配一个新的jidpid之后打印后台执行,恢复signal unblock,等待下一个prompt

sigchld_handler()

sigchld_handler 函数处理 SIGCHLD 信号,这个信号在子进程的状态发生变化时发送给父进程。此信号的处理函数会检查子进程的状态,并根据其退出状态、被终止的信号或停止状态来更新作业的状态。

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

创建一个空的信号集 mask。将 SIGINTSIGTSTP 添加到信号集 mask 中。使用 sigprocmask 将这些信号阻塞,以防在处理过程中它们被接收,prev 保存了旧的信号掩码

使用 waitpid 等待子进程状态的变化。-1 表示等待任意子进程,

在while loop中,只要有状态变化的子进程,那么使用 WIFCONTINUED 检查子进程是否恢复执行,如果是且原状态是停止(ST),则将状态改为后台运行(BG)。

如果不是停止

sigint_handler()

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

注意,killpg并不是杀死进程,而是传递信号。killpg(fg_pid, sig) 函数用于向前台进程中的所有进程发送信号。这里的 sig 是传递给 sigint_handler 的信号类型(在这种情况下是 SIGINT)。killpg 是一个更高层次的信号发送函数,用于向一个进程组发送信号,这比逐个发送信号更高效。不需要block信号因为只是发送一个信号。

sigtstp_handler()

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

原理同上类似。

Part II

在实现了基本的buildin_cmd, 和其他fg, bg操作并处理好block信号维护job list之后,在第二部分,我们将实现st, fg, bg对转换,以及io变换fd。

首先,回收Part I中没有解释的convert_to_bg , convert_to_fg

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 是全局变量。0代表bg→fg, 1代表ST→fg, 2代表ST→bg。并不存在fg→bg因为在fg执行的时候,只有Ctrl+C, Ctrl+Z这种能让fg停止或者结束。

killpg(pid, SIGCONT); 将ST的进程继续执行。

之后会触发sigchld_handler 我们需要在Part I 的基础上修改sigchld_handler 实现ST→bg的转换。

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
}

和bg唯一的区别是将fg_running = 1; 之后等待fg结束。

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

修改的地方是,如果接受到一个SIGCONTWIFCONTINUED(wstate) 检查子进程是否被继续执行。如果是,并且当前作业状态是停止状态(ST),根据 st_fgbg 的值将作业状态更新为前台(FG)或后台(BG)。

File Redirection

eval()中还需要改动stdin, stdout 改变函数的输入输出io。

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 redirect:

output redirect:

执行文件检查:

至此,tshlab结束