要么改变世界,要么适应世界

CS:APP-Shell Lab

2023-11-14 09:40:12
161
目录

总体要求

实现一个小小的shell程序,涉及进程调度,信号处理,并发处理的知识,需要掌握教材第8章。

话不多说,打开电脑,带上键盘,开启实验!

一些说明

源文件已经帮我们实现了一部分函数,需要我们完成剩余的函数:

  • eval:主要用于解析和解释命令行(70行左右)

  • builtin_cmd:识别并解释内置命令:quit、fg、bg 和 jobs(25行左右)

  • do_bgfg: 实现内置命令bgfg. [50 行左右]

  • waitfg: 等待前台任务完成. [20 行左右]

  • sigchld_handler: 捕捉SIGCHILD 信号. 80 行左右]

  • sigint_handler: 捕捉SIGINT (ctrl-c) 信号. [15 行左右]

  • sigtstp_handler: 捕捉SIGTSTP (ctrl-z) 信号. [15 行左右]

对于输入到命令行中的字符串,第一个单词要么是内置命令,要么是可执行文件路径。

如果是内置命令,则在当前进程中执行;

如果是可执行文件路径,则forks一个子进程,并在子进程中执行相应程序。

我们称解释该字符串过程创造的子进程称为作业job,通常一个作业由多个子进程组成。

如果字符串以&结束,则该作业以后台方式运行,意味着shell程序不等待作业结束,而直接打印提示符等待输入下一条命令。

否则,该作业以前台方式运行,等待该命令执行完毕,再打印提示符等待输入下一条命令。

Unix shell 支持作业控制概念,允许我们将作业在前台后台之间切换,改变进程状态(运行,停止,终止)。

输入CTRL-C将发送SIGINT信号到每一个前台作业中,默认情况下,收到SIGINT信号的进程会被终止。

输入CTRL-Z将发送SIGTSTP信号到每一个前台作业中,默认情况下,收到SIGTSTP信号的进程会被停止,停止状态下,可以接收来自其他进程的SIGCONT信号,从而继续运行。

内置命令:

  • jobs:列出正在运行和停止的后台作业
  • bg :将一个停止状态的后台作业变为运行的后台作业
  • fg :将一个停止状态或者运行的后台作业变为运行的前台作业
  • kill :终止作业

tsh

  • 提示符为"tsh> "

  • 当输入的第一个单词是内置命令时,tsh应该立即执行并等待下一个命令输入,否则把第一个单词作为可执行文件路径,并在初始化后的子进程中加载并运行可执行文件

  • tsh不必支持管道(|)或者I/O(>和<)

  • 输入CTRL-C(CTRL-Z)时应发送SIGINT(SIGTSTP)信号到当前前台作业以及该作业的所有子进程(例如它fork出来的子进程),如果没有前台作业,则不会造成任何影响。

  • 如果字符串以&结束,则该作业以后台方式运行,否则,在前台运行

  • 每个作业,可以被进程ID或者作业ID识别。“%5”表示作业ID,“5”表示进程ID

  • tsh需要支持以下命令:

    • quit:退出shell
    • jobs:列出所有后台作业
    • bg :发送SIGCONT信号,重启并在后台运行作业,可以是PID(进程ID)或者JID(作业ID)
    • fg :发送SIGCONT信号,重启并在前台运行作业,可以是PID(进程ID)或者JID(作业ID)
  • tsh应该回收所有僵尸子进程,如果有作业因为接收到一个它没有捕捉到的信号而终止,那么 tsh 就应该识别这一事件,并打印一条包含该作业 PID 和违规信号描述的信息。

实现

01

二话不说,参照P525即可

void eval(char* cmdline) {
    char* argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;
    
    strcpy(buf, cmdline);
    bg = parseline(buf, argv);
    if (argv[0] == NULL) {
        // 忽略空白行
        return;
    }
    // 如果不是内置命令
    if (!builtin_cmd(argv)) {
        if ((pid = fork()) == 0) {
            // 子进程
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        // 父进程等待前台作业停止
        if (!bg) {
            int status;
            if (waitpid(pid, &status, 0) < 0) {
                unix_error("waitfg: waipid error");
            }
        }
        else {
            printf("%d %s", pid, cmdline);
        }
    }
    return;
}

测试:

make
make rtest01
make test01

02

在原先的基础上增加对内置命令的识别,我们参照P525的,修改下面的函数即可:

int builtin_cmd(char** argv) {
    if (strcmp(argv[0], "jobs") == 0) {
        listjobs(jobs);
        return 1;
    }
    else if (strcmp(argv[0], "bg") == 0) {
        do_bgfg(argv);
        return 1;
    }
    else if (strcmp(argv[0], "fg") == 0) {
        do_bgfg(argv);
        return 1;
    }
    else if (strcmp(argv[0], "kill") == 0) {
        return 1;
    }
    else if (strcmp(argv[0], "&") == 0) {
        return 1;
    }
    else if (strcmp(argv[0], "quit") == 0) {

        exit(0);
    }
    return 0;     /* not a builtin command */
}

测试:

make
make rtest02
make test02

03

做完02的测试,实际上03的也通过了

04

运行测试,我们发现,不一样的地方在于输出不同,我们要先输出当前作业的ID,再输出进程ID,再输出该命令。

主要修改eval函数:

    // 父进程等待前台作业停止
    if (!bg) {
        int status;
        if (waitpid(pid, &status, 0) < 0) {
            unix_error("waitfg: waipid error");
        }
    }
    else {
        printf("[%d] (%d) %s", pid2jid(pid),pid, cmdline);
    }

但是我们发现,我们的输出jid是0,而tshref输出是1,这我们到后面再解决(这涉及添加作业到全局作业列表中。)

05

在这个测试中,我们主要实现对全局作业列表的修改。

我们先修改eval函数:

	addjob(jobs, pid, bg ? BG : FG ,cmdline);
    // 父进程等待前台作业停止
    if (!bg) {
        int status;
        if (waitpid(pid, &status, 0) < 0) {
            unix_error("waitfg: waipid error");
        }
    }

当我们手动测试的时候,即:

make
./tsh
tsh>  ./myspin 20 &
[1] (835379)  ./myspin 20 &
tsh> ./myspin 20 &
[2] (835441) ./myspin 20 &
tsh> jobs
[1] (835379) Running  ./myspin 20 &
[2] (835441) Running ./myspin 20 &

我们发现,我们是可以正常运行的,但是当我们使用下面的方式进行测试的时候,我们发现我们的输出并不一致:

make test05
./sdriver.pl -t trace05.txt -s ./tsh -a "-p"
#
# trace05.txt - Process jobs builtin command.
#
tsh> ./myspin 2 &
[2] (837522) ./myspin 2 &
tsh> ./myspin 3 &
[4] (837524) ./myspin 3 &
tsh> jobs
[1] (837521) Foreground /bin/echo -e tsh> ./myspin 2 \046
[2] (837522) Running ./myspin 2 &
[3] (837523) Foreground /bin/echo -e tsh> ./myspin 3 \046
[4] (837524) Running ./myspin 3 &
[5] (837525) Foreground /bin/echo tsh> jobs

这是因为我们没有及时把已完成的作业从作业列表中删除,这个我们放到后面再解决。

06

在这个测试中, 要求我们完成对SIGINT信号的处理,即在sigint_handler函数中,我们要把SIGINT信号发送到整个前台进程组,参考教材和实验说明,我们应该使用-pid,同时在创建子进程后,我们应该调用setpgid(0, 0)设置当前进程的进程组ID为当前进程ID。

默认情况下,一个进程收到SIGINT时,会被终止,而每当子进程终止时候,会触发sigchld_handler函数,我们这时候需要进一步完善该函数。在该函数中,我们要区分是由于正常结束而触发事件,还是其他信号而触发的事件。

此外,为了顺便将已完成的前台作业从作业列表中删除,我们要修改eval函数,我们之前在判断是前台作业后,使用的是waitpid,但是我们为了能够在sigchld_handler函数中,回收子进程,我们在sigchld_handler函数中使用了waitpid,结合实验说明,在判断是前台作业后,我们应该调用waitfg,而该函数中,我们使用sleep函数将父进程挂起一段时间(只要前台作业没结束)。

还有最重要的一点就是,涉及全局变量修改的地方,需要避免出现竞争情况(参考教材P541),例如我们得确保addjob在deletejob之前。

这次改动地方比较多:

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -168,6 +168,8 @@ void eval(char* cmdline) {
     int bg;
     pid_t pid;

+    sigset_t mask_all,mask_one,prev_one;
+
     strcpy(buf, cmdline);
     bg = parseline(buf, argv);
     if (argv[0] == NULL) {
@@ -176,20 +178,34 @@ void eval(char* cmdline) {
     }
     // 如果不是内置命令
     if (!builtin_cmd(argv)) {
+        // 将每个信号添加到set中
+        sigfillset(&mask_all);
+        // 初始化空集合
+        sigemptyset(&mask_one);
+        // 将 SIGCHLD 信号添加到集合中
+        sigaddset(&mask_one, SIGCHLD);
+        // 暂时阻塞 SIGCHLD 信号,避免由于操作系统调用的原因,导致addjob在deletejob之后
+        sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
+
         if ((pid = fork()) == 0) {
             // 子进程
+            // 恢复信号集合,解除对 SIGCHLD 的阻塞(子进程继承了父进程的阻塞设置,因此它无法接收子进程的信号,所以我们必须解除阻塞)
+            sigprocmask(SIG_SETMASK, &prev_one, NULL);
+            setpgid(0, 0);
             if (execve(argv[0], argv, environ) < 0) {
                 printf("%s: Command not found.\n", argv[0]);
                 exit(0);
             }
         }
+        // 阻塞SIGCHLD
+        sigprocmask(SIG_BLOCK, &mask_all, NULL);
+
         addjob(jobs, pid, bg ? BG : FG ,cmdline);
+        // 解除阻塞SIGCHLD
+        sigprocmask(SIG_SETMASK, &prev_one, NULL);
         // 父进程等待前台作业停止
         if (!bg) {
-            int status;
-            if (waitpid(pid, &status, 0) < 0) {
-                unix_error("waitfg: waipid error");
-            }
+            waitfg(pid);
         }
         else {
             printf("[%d] (%d) %s", pid2jid(pid),pid, cmdline);
@@ -292,6 +308,10 @@ void do_bgfg(char** argv) {
  * waitfg - Block until process pid is no longer the foreground process
  */
 void waitfg(pid_t pid) {
+    while(fgpid(jobs)){
+        // 挂起 500 微妙
+        usleep(500);
+    }
     return;
 }

@@ -307,6 +327,38 @@ void waitfg(pid_t pid) {
   *     currently running children to terminate.
   */
 void sigchld_handler(int sig) {
+    int olderrno = errno;
+    sigset_t mask_all,prev_all;
+    sigfillset(&mask_all);
+    int status;
+    pid_t pid;
+
+    // tsh进程等待它的所有进程,采用非阻塞的方式
+    while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
+        // 如果是正常结束的进程
+        if(WIFEXITED(status)){
+
+            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
+            deletejob(jobs, pid);
+            sigprocmask(SIG_SETMASK,&prev_all,NULL);
+        } else if(WIFSIGNALED(status)){
+
+            // 如果是通过被信号终止的进程
+            struct job_t* current_job = getjobpid(jobs, pid);
+            int jid = current_job->jid;
+            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
+            // 删除该作业
+            if(deletejob(jobs, pid)){
+                printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
+            } else{
+                app_error("Job terminated failed\n");
+            }
+            sigprocmask(SIG_SETMASK,&prev_all,NULL);
+        } else {
+            printf("DEBUG (%d) other situation %d\n", pid, sig);
+        }
+    }
+    errno = olderrno;
     return;
 }

@@ -316,6 +368,18 @@ void sigchld_handler(int sig) {
  *    to the foreground job.
  */
 void sigint_handler(int sig) {
+    int olderrno = errno;
+    sigset_t mask, prev;
+    sigfillset(&mask);
+    sigprocmask(SIG_BLOCK, &mask, &prev);
+    // 获取当前前台作业的进程ID
+    pid_t pid = fgpid(jobs);
+    sigprocmask(SIG_SETMASK, &prev, NULL);
+    // 不为零时,代表存在前台作业
+    if(pid){
+        kill(-pid, sig);
+    }
+    errno = olderrno;
     return;
 }

07

这一部分主要测试我们是否能够在输入ctrl-c时候,只把该信号发送给前台作业,而不影响后台作业,即我们先创建一个在作业在后台运行,然后再创建一个前台作业,再输入ctrl-c,此时前台作业应该被停止,当我们输入jobs时候,只会一个后台作业(前提是我们要迅速输入jobs,防止后台作业完成了)。

如果06的代码修改没问题的话,这一个测试是可以直接过的。

08

这一部分主要是测试我们是否能够在输入ctrl-z时候,只把该信号发送给前台作业,而不影响后台作业。

这个也没什么难度,参照着对ctrl-c的处理方式即可

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -354,6 +354,14 @@ void sigchld_handler(int sig) {
                 app_error("Job terminated failed\n");
             }
             sigprocmask(SIG_SETMASK,&prev_all,NULL);
+        } else if (WIFSTOPPED(status)){
+            struct job_t* current_job = getjobpid(jobs, pid);
+            int jid = current_job->jid;
+            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
+            // 修改该作业为停止状态
+            current_job->state = ST;
+            sigprocmask(SIG_SETMASK,&prev_all,NULL);
+            printf("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status));
         } else {
             printf("DEBUG (%d) other situation %d\n", pid, sig);
         }
@@ -389,6 +397,18 @@ void sigint_handler(int sig) {
  *     foreground job by sending it a SIGTSTP.
  */
 void sigtstp_handler(int sig) {
+    int olderrno = errno;
+    sigset_t mask, prev;
+    sigfillset(&mask);
+    sigprocmask(SIG_BLOCK, &mask, &prev);
+    // 获取当前前台作业的进程ID
+    pid_t pid = fgpid(jobs);
+    sigprocmask(SIG_SETMASK, &prev, NULL);
+    // 不为零时,代表存在前台作业
+    if(pid){
+        kill(-pid, sig);
+    }
+    errno = olderrno;
     return;
 }

09

这一部分主要是测试我们是否处理内置命令bg,该命令发送SIGCONT信号,重启并在后台运行作业。因此我们要完善do_bgfg函数,在这个函数中,我们要先判断是bg还是fg,再判断该作业是由jid还是pid给出

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -85,6 +85,9 @@ void app_error(char* msg);
 typedef void handler_t(int);
 handler_t* Signal(int signum, handler_t* handler);

+
+int string2num(char *str, int start);
+
 /*
  * main - The shell's main routine
  */
@@ -297,10 +300,46 @@ int builtin_cmd(char** argv) {
     return 0;     /* not a builtin command */
 }

+int string2num(char *str, int start){
+    int ans = 0;
+    for(int i = start; str[i] != '\0'; i++){
+        ans = ans * 10 + str[i] - '0';
+    }
+    return ans;
+}
 /*
  * do_bgfg - Execute the builtin bg and fg commands
  */
 void do_bgfg(char** argv) {
+    sigset_t mask_all,prev_all;
+    sigfillset(&mask_all);
+    if(strcmp(argv[0], "bg") == 0){
+        // bg 命令
+        int jid;
+        pid_t pid;
+        struct job_t* select_job = NULL;
+        // 如果是以 jid 方式标识
+        if(argv[1][0] == '%'){
+            jid = string2num(argv[1], 1);
+            select_job = getjobjid(jobs, jid);
+            pid = select_job->pid;
+        } else{
+            // 如果是以 pid 方式标识
+            pid = string2num(argv[1], 0);
+            select_job = getjobpid(jobs, pid);
+            jid = select_job->jid;
+        }
+        sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
+        // 修改job状态为后台运行
+        select_job->state = BG;
+        sigprocmask(SIG_SETMASK,&prev_all,NULL);
+        kill(-pid, SIGCONT);
+        printf("[%d] (%d) %s", select_job->jid, select_job->pid, select_job->cmdline);
+    } else if (strcmp(argv[0], "fg") == 0){
+        // fg 命令
+    } else{
+        app_error("not bg or fg command\n");
+    }
     return;
 }

10

这一部分主要是测试我们是否处理内置命令bg,该命令发送SIGCONT信号,重启并在前台运行作业。参照09即可:

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -337,6 +337,26 @@ void do_bgfg(char** argv) {
         printf("[%d] (%d) %s", select_job->jid, select_job->pid, select_job->cmdline);
     } else if (strcmp(argv[0], "fg") == 0){
         // fg 命令
+        int jid;
+        pid_t pid;
+        struct job_t* select_job = NULL;
+        // 如果是以 jid 方式标识
+        if(argv[1][0] == '%'){
+            jid = string2num(argv[1], 1);
+            select_job = getjobjid(jobs, jid);
+            pid = select_job->pid;
+        } else{
+            // 如果是以 pid 方式标识
+            pid = string2num(argv[1], 0);
+            select_job = getjobpid(jobs, pid);
+            jid = select_job->jid;
+        }
+        sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
+        // 修改job状态为前台运行
+        select_job->state = FG;
+        sigprocmask(SIG_SETMASK,&prev_all,NULL);
+        kill(-pid, SIGCONT);
+        waitfg(pid);
     } else{
         app_error("not bg or fg command\n");
     }

11

在这个测试中, 调用mysplit,而该程序运行后会创建若干个进程,然后测试SIGINT信号能否传播到前台进程组中的每个进程,我们观察下面的输出,只要除了进程ID不一样,其他都一样的话,则算满足:

make rtest11
make test11

12

同上,只不过测试的是SIGTSTP

13

同上,只不过测试的是Restart ,即fg命令

14

在这个测试中,主要是针对一些错误情况,例如输入的命令不符合标准,我们只要对原先的程序进行修改即可。

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -311,6 +311,15 @@ int string2num(char *str, int start){
  * do_bgfg - Execute the builtin bg and fg commands
  */
 void do_bgfg(char** argv) {
+    if(argv[1] == NULL){
+        printf("%s command requires PID or %%jobid argument\n", argv[0]);
+        return;
+    }
+    // bg 或者 fg 后面的参数既不是数字也不是%
+    if ( (argv[1][0] < '0' || argv[1][0] > '9')  && argv[1][0] != '%') {
+        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
+        return;
+    }
     sigset_t mask_all,prev_all;
     sigfillset(&mask_all);
     if(strcmp(argv[0], "bg") == 0){
@@ -322,11 +331,19 @@ void do_bgfg(char** argv) {
         if(argv[1][0] == '%'){
             jid = string2num(argv[1], 1);
             select_job = getjobjid(jobs, jid);
+            if(select_job == NULL){
+                printf("%s: No such job\n", argv[1]);
+                return;
+            }
             pid = select_job->pid;
         } else{
             // 如果是以 pid 方式标识
             pid = string2num(argv[1], 0);
             select_job = getjobpid(jobs, pid);
+            if (select_job == NULL) {
+                printf("(%s): No such process\n", argv[1]);
+                return;
+            }
             jid = select_job->jid;
         }
         sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
@@ -344,11 +361,19 @@ void do_bgfg(char** argv) {
         if(argv[1][0] == '%'){
             jid = string2num(argv[1], 1);
             select_job = getjobjid(jobs, jid);
+            if(select_job == NULL){
+                printf("%s: No such job\n", argv[1]);
+                return;
+            }
             pid = select_job->pid;
         } else{
             // 如果是以 pid 方式标识
             pid = string2num(argv[1], 0);
             select_job = getjobpid(jobs, pid);
+            if (select_job == NULL) {
+                printf("(%s): No such process\n", argv[1]);
+                return;
+            }
             jid = select_job->jid;
         }
         sigprocmask(SIG_BLOCK,&mask_all,&prev_all);

15

在这个测试中,就是整合一起测试,我突然发现,命令找不到时候,输出语句,多了个字符,修改一下:

--- a/shell/tsh.c
+++ b/shell/tsh.c
@@ -196,7 +196,7 @@ void eval(char* cmdline) {
             sigprocmask(SIG_SETMASK, &prev_one, NULL);
             setpgid(0, 0);
             if (execve(argv[0], argv, environ) < 0) {
-                printf("%s: Command not found.\n", argv[0]);
+                printf("%s: Command not found\n", argv[0]);
                 exit(0);
             }
         }

16

在这个测试中,检查我们的tsh程序能否可以正确处理来自其他进程的SIGSTPSIGINT信号

完成代码

请移步【仓库】

总结

本实验涉及内容比较多,建议先看几遍教材的第8章,然后再做实验,对照着书来做。

历史评论
开始评论