从零开始的Tsh Lab

从零开始的Tsh Lab

[!CAUTION]

本笔记仅供参考,请勿抄袭。

声明

本笔记的写作初衷在于,笔者在做Tsh Lab的时候受到Arthals学长很大启发,同时25Fall的计算机系统导论课程改制增加了10分的Lab测试(虽然往年的期中期末中也会有一两道Lab相关选择题,但分值不大)。故将心路历程写成此笔记,以便复习,并供后续选课同学参考。

Tsh Lab简要介绍

此Lab亦称Shell Lab,但笔者取其原称Tsh(Tiny Shell) Lab,以更好阐明语义,下均称Tsh Lab

Tsh Lab是《计算机系统导论》课程的第6个Lab,对应教材第八章《异常控制流》和第十章《系统级I/O》。该Lab旨在加强同学们对信号与进程的理解,使同学们深入探索Shell的相关知识,并认识到日常使用的Shell之奇妙。

Tsh Lab中,需要实现一个简单的Shell,逐步完善不同功能,最终使它通过31trace文件的测试,前7trace文件为4pts,后24trace文件为3pts,总分为100pts,以AutoLab上的得分为准(后文有提及为什么说这句话),虽然没有代码风格分要求,遵守相关工程规范,还是不可或缺的。与Cache LabPart A相似,Tsh Lab也有一个对照的标准模拟器tshref,可以使用它进行Debug

Tsh Lab中,在Tsh.c文件内,分别需要编写evalsigchld_handlersigint_handlersigtstp_handler四个函数,它们分别用于解析并解释命令行的主程序捕获SIGCHLD信号、捕获SIGINT(ctrl+C)信号以及捕获SIGTSTP(ctrl+Z)信号。此外,可以定义自己的辅助函数,以更好地符合工程规范。

值得一提的是,此Lab相比Cache Lab,又受到了PKU助教团队的大幅魔改加强。相比CMU的原版Tsh Lab,将测试样例从16个提升到31个,同时引入第十章的I/O重定向相关知识,并增加了Killnohup功能的实现任务。此Lab的难度为中高,笔者用时约为 $8 \sim 10$ 小时。

在动手之前

代码风格要求

  • 代码需要有良好的注释,于程序文件开头描述块注释,说明Shell功能。
  • 每个函数需要有注释,说明该函数的功能。
  • 相关代码行前需要添加注释。
  • 每行代码长度不能超过80个字符。
  • 不要为每个行单独添加注释。

有关代码风格及格式化的解决方法,笔者已在elainafan-从零开始的Cache Lab代码风格要求这一节提及,供参考。

如何测评

在提交到AutoLab之前,可以使用writeup中提供的几种本地测评方式进行测评。

显而易见的是,可以在终端中使用./tsh命令,运行tsh并尝试不同指令,尝试找出报错,但是这种方法并不高效。

于是,可以使用trace文件进行输出比对,在终端中运行以下指令:

1
2
./runtrace -f tracexx.txt -s ./tsh # 测试你的tsh输出
./runtrace -f tracexx.txt -s ./tshref # 使用标准测试器测试输出

亦可使用提供的sdriver.py进行单个trace文件或全部trace文件的测试,如下:

1
2
./sdriver # 测试全部trace文件
./sdriver -t 03 # 测试trace03.txt文件

然而,笔者发现,本地满分的代码,在提交到AutoLab上时,竟然不一定是满分,可能是本地测试数据过弱(最近很频繁遇上这种事,笔者是倒霉蛋)导致的。

同时,如果出现上述情况,也请考虑是否合理避免了进程竞争(见下回顾信号相关函数一节),以及是否有哪处信号阻塞不合理。

除掉VSCODE的报错

若使用Vscode进行远程连接编写相关代码,可能会出现以下错误,即未定义标识符sigset_t

如果像笔者一样患有每次存在报错就不改掉不开始写项目的强迫症的话,可以按ctrl+shift+P(windows)/cmd+shift+P,搜索C/C++ 编辑配置(json),将其中cStandard改为gnu11,如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/**"
            ],
            "defines": [],
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "gnu11",
            "cppStandard": "gnu++17",
            "intelliSenseMode": "linux-gcc-x64"
        }
    ],
    "version": 4
}

何为Shell?

可能某些同学认为写这节很好笑,但是笔者在写Tsh Lab之前确实对Shell背后的工作原理不甚了解,此部分可参考《深入理解计算机系统》8.4.6节阅读。

Shell是一种交互式命令行解释器,代表用户运行程序,它以循环的形式重复执行以下操作:显示提示符等待标准输入上的命令行根据命令行内容进行相关操作

命令行是以空格分隔的一系列ASCII文本单词,命令行中的第一个单词是内建命令的名称(立即执行该命令,若quit等),或者可执行文件的路径名(创建一个子进程,在子进程的上下文中加载并运行该程序),其余单词为命令行参数。

Shell为命令行的每条命令创建一个进程组,这是它从内核角度的名称,从Shell本身角度看,这个进程组就是这条命令对应的作业/Job

若命令行以&结尾,说明创建的作业为后台作业Shell不会等待作业终止,而是直接显示提示符并等待下一个命令行输入;反之,则为前台作业Shell需要等待作业终止后才会接受下一条命令。换言之,同一时间最多只能有一个前台作业和若干后台作业运行。

以下,为Shell编写的大致逻辑(伪代码):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
while (true) {
    读取命令行输入;
    if (命令为空) {
        continue;
    }
    if (命令为内建命令) {
        执行内建命令;
    } else {
        pid_t pid = fork();
        if (pid == 0) { // 子进程
            execve(程序路径, 参数, 环境变量);
        } else if (pid > 0) { // 父进程
            if (前台执行) {
                waitpid(pid, &status, 0);
            } else { // 后台执行
                printf("后台进程 PID: %d\\n", pid);
            }
        } else { // fork 失败
            perror("fork");
        }
    }
}

Shell支持多个通过Linux管道连接的子进程,例如以下命令:

1
ls -l | grep txt

即执行了ls -lgrep txt两个子进程,管道|右侧命令的输入当成左侧命令的输出(联想到Communication problems),意为列出当前目录下所有文件,然后筛选出名称中包含txt的那些。

Shell通常还包括I/O重定向功能,见下何为I/O重定向?一节。

认识Tsh.c中的相关结构

阅读tsh.c文件源码,找到job相关结构体:

1
2
3
4
5
6
7
8
struct job_t
{                                         /* The job struct */
    pid_t pid;                            /* job PID */
    int jid;                              /* job ID [1, 2, ...] */
    int state; /* UNDEF, BG, FG, or ST */ // 即未定义,后台,前台,挂起
    char cmdline[MAXLINE];                /* command line */
};
struct job_t job_list[MAXJOBS]; /* The job list */

这意味着,每个作业是一个job_t对象,它的成员对象有pid(对应的进程ID)jid(对应的作业ID)state(当前作业的状态)以及cmdline(当前作业对应的进程),所有作业被组织为一个数组job_list

根据writeup,实现的tsh无需实现管道功能,即一条命令行只对应一个进程,这也意味着此处的作业不存在pgid子进程组是合理的,更加简化地说,完全可以把pid看成进程意义下的唯一表示符,jid看成作业意义下的唯一表示符。

同时,有以下宏定义:

1
2
3
4
5
/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1    /* running in foreground */
#define BG 2    /* running in background */
#define ST 3    /* stopped */

这表示了每个Job对象的当前状态,即未定义、前台、后台或停止

再阅读tsh.c源码,有以下结构:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
struct cmdline_tokens
{
    int argc;                            /* Number of arguments */
    char *argv[MAXARGS];                 /* The arguments list */
    char *infile; /* The input file */   // 输入重定向
    char *outfile; /* The output file */ // 输出重定向
    enum builtins_t
    {               /* Indicates if argv[0] is a builtin command */
      BUILTIN_NONE, // 啥都没有
      BUILTIN_QUIT, // 退出
      BUILTIN_JOBS, // 展示全部job
      BUILTIN_BG,   // 转为后台
      BUILTIN_FG,   // 转为前台
      BUILTIN_KILL, // 杀死job
      BUILTIN_NOHUP
    } builtins; // 忽视SIGNUP,启动新进程
};

这意味着,命令行参数被组织为一个cmdline_tokens类,其中argc表示参数个数argv表示参数列表(由课本知识,argv[0]表示命令名称),infileoutfile是读写重定向文件名(见后文),builtins是内建命令。

根据writeup,需要实现以下内建命令:

quit:对应BUILTIN_QUIT,即退出Shell

jobs:对应BUILTIN_JOBS,列出全部Jobs

bg:对应BUILTIN_BG,将某个Job转为后台作业

fg:对应BUILTIN_FG,将某个Job转为前台作业

kill:对应BUILTIN_KILL,给某个Job发去某个信号,通常会终止这个作业。

nohup:对应BUILTIN_NOHUP,忽略SIGHUP信号,启动一个新进程。

None:即为外部命令,对应BUILTIN_NONE

有关这些命令的细致实现及其思路,见开始动手一节。

认识Tsh.c中的辅助函数

Tsh.c中,可以观察到,提供了许多辅助函数,认识它们可以极大地提高编写效率。

clearjob:将某个Job清空为初始状态。

initjobs:对job_list的每个job执行clearjob

maxjid:从job_list中找出最大的job

addjob:将给出pid,state,cmdline三项参数的job添加到job_list中。

deletejob:将PID=pidjobjob_list中删除。

fgpid:返回当前前台JOBpid

getjobpid:返回job_listPID=pidJOB,若找不到则返回空指针。

getjobjid:返回job_listJID=jidJOB,若找不到则返回空指针。

pid2jid:给定pid,转换为对应JOBjid

listjobs:列出job_list中的所有JOB及其对应信息。

unix_error:用于封装相关函数,便于Debug

sig_put:异步安全函数,接受类似printf的函数输出,支持%d(打印整数)%%(打印%)

何为I/O重定向?

按照正常进度,发布Tsh Lab时,临近阶段测试二(即4-8 chapter),换言之,此时并未教学第十章,因此笔者在此作简要介绍。

一个Linux系统文件,就是一个字节序列,所有的I/O设备都被模型化为文件,所有的输入输出都被当做对相应文件的读和写来执行。

当一个应用程序要求内核打开相应的文件,来宣告它想要访问一个I/O设备时,内核返回一个小非负整数,称为该文件的描述符,其后对该文件的任何操作都通过此描述符进行,这个过程又称打开文件

Shell创建的每个进程开始时都有三个打开的文件,即标准输入/STDIN_FILENO/描述符为0标准输出/STDOUT_FILENO/描述符为1标准错误/STDERR_FILENO/描述符为2

读写文件读操作即为从文件复制若干字节到内存,若剩余字节不够复制,则返回剩余字节数,下一次调用则返回EOF信号写操作即为从内存中复制若干字节到文件,并更新当前文件对应的位置。

关闭文件,即当应用完成对文件访问后,它通知内核关闭这个文件。内核释放文件打开时创建的数据结构,并把描述符恢复到可用描述符池中。当一个进程终止时,需要内核关闭所有打开的文件。

Linux Shell中,提供了I/O重定向操作符,允许用户将磁盘文件与标准输入联系起来,例如以下指令:

1
Linux> ls > foo.txt

即代表了让Shell执行ls程序,并将标准输出重定向到foo.txt中。

实际上,I/O重定向简要来说,就是将输入/输出从标准输入/输出改换为给定的文件的过程,一般在程序结束前再恢复标准输入/输出。

为了实现I/O重定向,通常需要使用以下函数:

1
int open(char *filename, int flags,mode_t mode);

这个函数意为将filename转换为一个文件描述符,从而可以对其进行读写,若成功则返回这个文件描述符,不成功则返回-1。简要来说,就是打开filename文件。

flags参数指明了进程如何访问这个文件:

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:可读可写

有关flagsmode的其他用法,请自行查阅课本,此处为方便不再赘述。

1
int close(int fd);

这个函数即关闭文件描述符fd的文件,若成功则返回0,若不成功(如关闭了不存在的文件)则返回-1。

1
int dup(int oldfd);

这个函数,复制一份oldfd的文件描述符给返回值。若成功则返回新的文件描述符,若失败则返回-1,并设置errno以指示错误(存储并恢复error原则的又一例证)。

1
int dup2(int oldfd, int newfd);

这个函数,复制文件描述符oldfdnewfd,若成功则返回newfd,若失败则返回-1,并设置errno以指示错误。

有关这些函数应该如何组合形成文件重定向,详见开始动手/I/O重定向一节。

回顾信号及其相关函数

1
pid_t fork();

老熟人了,调用一次返回两次,子进程返回0,父进程返回子进程的pid

1
int execve(const char *filename, const char *argv[], const char *envp[]);

加载并运行filename,将argvenvp分别作为参数环境变量传入,全局变量environ指向envp[0]

1
pid_t waitpid(pid_t pid,int* statusp,int options);

表示父进程等待子进程终止或者停止,下面是其参数含义:

  • pid:若大于0,则等待集合是PID=pid的子进程;若等于-1,则等待集合是该父进程的全部子进程,若等于-x,则等待集合是pid=x的进程组,若等于0,则等待集合是与调用进程在一个进程组的任意子进程。
  • options:表示为下面常量的并集组合
    • WNOHANG:如果所有子进程都没有终止,则立即返回;默认行为为挂起调用进程直到子进程终止。
    • WUNTRACED:挂起调用进程执行,直到等待集合中一个进程变为已终止或者被停止。
    • WCONTINUED:挂起调用进程的执行,直到等待集合中一个正在运行的进程终止或等待集合中一个被停止的进程收到SIGCONT信号重新开始执行。
    • options为0,则挂起调用进程,等待子进程退出。
  • statusp:若放入一个指针,则waitpid返回后会将子进程相关状态存储于该指针指向的位置,即将status放入其指向的值。
  • status的几个宏:
    • WIFEXITED(status):若子进程通过exit或者一个返回正常终止,则返回真。
    • WEXITSTATUS(status):在WIFEXITED(status)返回真时,返回子进程的退出状态。
    • WIFSIGNALED(status):若子进程因为一个未被捕获的信号终止,则返回真。
    • WTERMSIG:在WIFSGINALED(status)返回真时,返回导致子进程终止的信号的编号。
    • WIFSTOPPED(status):若引起返回的子进程当前是停止的,返回真。
    • WSTOPSIG(status):在WIFSTOPPED(status)返回真时,返回导致子进程停止的进程的编号。
    • WIFCONTINUED:如果子进程收到SIGCONT信号重新启动,返回真。
  • 若调用进程没有子进程,则waitpid返回-1,设置errnoECHILD
  • waitpid被一个函数终止,则返回-1,设置errnoEINTR
1
int setpgid(pid_t pid, pid_t pgid);

表示将进程pid的进程组改为pgid,若pid为0,则使用当前进程的PID,如果pgid为0,则用pid指定的进程的PID作为pgid。当pidpgid同时为0,则创建一个pgid为当前进程pid的进程组。

1
int kill(pid_t pid,int sig)
  • pid>0,将信号sig发送给PIDpid的进程。
  • pid=0,将信号sig发送给调用进程所述进程组的所有进程。
  • pid<0,将信号sig发送给pgid=|pid|的所有进程。
  • 若成功则返回0,若不成功则返回-1
1
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • sigprocmask:改变当前阻塞的信号集合,取决于how的值:
    • SIG_BLOCK:将set中的信号添加到blocked中。
    • SIG_UNBLOCK:从blocked中删除set中的信号。
    • SIG_SETMASKblock=set
  • 如果oldset非空,那么blocked的旧值保存在oldset中。
1
2
3
4
int sigemptyset(sigset_t *set); // 初始化信号集合为空
int sigfillset(sigset_t *set); // 将所有信号添加到信号集合中
int sigaddset(sigset_t *set, int signum); // 将指定信号添加到信号集合中
int sigdelset(sigset_t *set, int signum); // 从信号集合中删除指定信号

函数功能如上文注释所示,通常用于辅助sigprocmask使用。

1
int sigsuspend(const sigset_t *mask)

相当于以下版本的原子化(不可中断版本),显式等待信号,避免父子进程竞争。

1
2
3
sigprocmask(SIG_SETMASK,&mask,&prev);
pause();
sigprocmask(SIG_SETMASK,&prev,NULL);

既然讲到sigsuspend函数,那势必需要回顾一下竞争机制了:

竞争机制,即分出子进程后,父子进程并发,顺序不一定与代码顺序一致,只需要满足拓扑排序

可以参照以下两段代码深入理解:

 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
// 错误代码
while (1) {
    if ((pid = Fork()) == 0) {
        Execve("/bin/date", argv, NULL);
    }
    // 关注下一行
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    addjob(pid);
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
exit(0);

// 正确代码
while (1) {
    // 先阻塞 SIGCHLD
    Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
    if ((pid = Fork()) == 0) {
        // 分出子进程后解除阻塞
        Sigprocmask(SIG_SETMASK, &prev_one, NULL);
        Execve("/bin/date", argv, NULL);
    }
    Sigprocmask(SIG_BLOCK, &mask_all, NULL);
    addjob(pid);
    Sigprocmask(SIG_SETMASK, &prev_one, NULL);
}
exit(0);

由于需要保证addjob操作的原子性(即读写不可中断),错误代码先fork创建子进程,再阻塞信号。

然而,在Sigprocmask之前,子进程可能已经终止,向父进程发送SIGCHLD信号。

此时父进程还没阻塞SIGCHLD,信号会立即触发处理函数,但是addjob还没执行,导致处理了不存在的job

此外,还有以下常见信号需要使用:

笔者的提醒

笔者仍要提醒的是,这个Lab与Data LabCache Lab一样,每次修改文件,在进行测评前,都需要在终端内输入make命令进行编译。

Tsh Lab中,每个JOB要求可通过PID或者JID表示。JID是由tsh分配的正整数,在命令行中需以前缀%表示,例如%5表示JID为5的JOB,而5表示PID5的作业,这要求实现时需要对某些命令进行JIDPID的实现。

tsh需要回收所有僵尸子进程,若某个作业因收到未捕获的信号而终止,则tsh需要识别该事件,并输出包含该作业PID及相关信号描述的信息。

由于父进程与子进程之间可能存在竞争关系,而由于拓扑排序的不确定性,可能运行多次都通过相关测试点,但是助教团队在阅读源码后若发现可能导致竞争条件的代码,会扣除相应分数

其次,牢记课本中编写信号处理程序的相关要求,大致有以下几点:

  • 尽可能调用异步信号安全函数
  • 保存并恢复errno
  • 访问共享全局数据结构时,阻塞所有信号

课本相关知识

  • 8.4.6 一个eval函数的朴素实现
  • 各种信号及其相关函数
  • 竞争相关概念及其解决方式
  • I/O重定向

开始动手!

阅读源码

首先阅读main函数,看看它都干了什么:

 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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/*
 * main - The shell's main routine
 */
int main(int argc, char **argv)
{
    char c;
    char cmdline[MAXLINE]; /* cmdline for fgets */
    int emit_prompt = 1;   /* emit prompt (default) */

    /* Redirect stderr to stdout (so that driver will get all output
     * on the pipe connected to stdout) */
    dup2(1, 2);

    /* Parse the command line */
    while ((c = getopt(argc, argv, "hvp")) != EOF)
    {
        switch (c)
        {
        case 'h': /* print help message */
            usage();
            break;
        case 'v': /* emit additional diagnostic info */
            verbose = 1;
            break;
        case 'p':            /* don't print a prompt */
            emit_prompt = 0; /* handy for automatic testing */
            break;
        default:
            usage();
        }
    }

    /* Install the signal handlers */

    /* These are the ones you will need to implement */
    Signal(SIGINT, sigint_handler);   /* ctrl-c */
    Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
    Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
    Signal(SIGTTIN, SIG_IGN);
    Signal(SIGTTOU, SIG_IGN);

    /* This one provides a clean way to kill the shell */
    Signal(SIGQUIT, sigquit_handler);

    /* Initialize the job list */
    initjobs(job_list);

    /* Execute the shell's read/eval loop */
    while (1)
    {

        if (emit_prompt)
        {
            printf("%s", prompt);
            fflush(stdout);
        }
        if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
            app_error("fgets error");
        if (feof(stdin))
        {
            /* End of file (ctrl-d) */
            printf("\n");
            fflush(stdout);
            fflush(stderr);
            exit(0);
        }

        /* Remove the trailing newline */
        cmdline[strlen(cmdline) - 1] = '\0';

        /* Evaluate the command line */
        eval(cmdline);

        fflush(stdout);
        fflush(stdout);
    }

    exit(0); /* control never reaches here */
}

可以看到,它先解析了例如-h-v等的命令行参数,然后使用封装后的signal函数为几个信号安装了信号处理函数(部分需要后续手动编写),初始化JOB列表,接着进入打印提示符->读取输入->调用eval执行命令->刷新输出的循环。

也就是说,其实shell的主要逻辑都已经实现了,此处只需要实现eval函数执行命令。

封装函数

在开始编写eval函数之前,回顾课本,为了便于Debug以及输出错误信息,需要将给定函数封装为大写开头函数(便于区分)。

因此,直接先将回顾信号及其相关函数这节中函数共同封装进代码中,以备不时之需。

根据writeupExecve需要在加载外部指令失败时,输出Command not found指令,因此一并封装:

  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
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
// 一堆封装函数

pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

int Open(char *pathname, int flags, mode_t mode)
{
    int fd = open(pathname, flags, mode);
    if (fd < 0)
    {
        unix_error("open error");
        exit(1);
    }
    return fd;
}

int Dup(int oldfd)
{
    int fd = dup(oldfd);
    if (fd < 0)
    {
        unix_error("dup error");
        exit(1);
    }
    return fd;
}

void Dup2(int oldfd, int newfd)
{
    if (dup2(oldfd, newfd) < 0)
    {
        unix_error("dup2 error");
        exit(1);
    }
    return;
}

void Close(int fd)
{
    if (close(fd) < 0)
    {
        unix_error("close error");
        exit(1);
    }
    return;
}

void Kill(pid_t pid, int sig)
{
    if (kill(pid, sig) < 0)
    {
        unix_error("kill error");
        exit(1);
    }
    return;
}

int Execve(char *filepath, char *const argv[], char *const envp[])
{
    int exe = execve(filepath, argv, envp);
    if (exe < 0)
    {
        printf("%s: Command not found\n", argv[0]);
        exit(1);
    }
    return exe;
}

void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
{
    if (sigprocmask(how, set, oldset) < 0)
    {
        unix_error("sigprocmask error");
        exit(1);
    }
    return;
}

void Sigfillset(sigset_t *set)
{
    if (sigfillset(set) < 0)
    {
        unix_error("sigfillset error");
        exit(1);
    }
    return;
}

void Sigaddset(sigset_t *set, int sig)
{
    if (sigaddset(set, sig) < 0)
    {
        unix_error("sigaddset error");
        exit(1);
    }
    return;
}

void Sigemptyset(sigset_t *set)
{
    if (sigemptyset(set) < 0)
    {
        unix_error("sigemptyset error");
        exit(1);
    }
    return;
}

实现eval框架

好,基本准备工作都完成了,现在看看eval函数已给出的框架。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void eval(char *cmdline)
{
    int bg; /* should the job run in bg or fg? */
    struct cmdline_tokens tok;

    /* Parse command line */
    bg = parseline(cmdline, &tok);

    if (bg == -1) /* parsing error */
        return;
    if (tok.argv[0] == NULL) /* ignore empty lines */
        return;

    return;
}

可以看到,它引入参数bg用于表示当前JOB属于前台或者后台,接着调用parseline函数将bg解析出来,再阅读parseline函数:

  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
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
int parseline(const char *cmdline, struct cmdline_tokens *tok)
{

    static char array[MAXLINE];        /* holds local copy of command line */
    const char delims[10] = " \t\r\n"; /* argument delimiters (white-space) */
    char *buf = array;                 /* ptr that traverses command line */
    char *next;                        /* ptr to the end of the current arg */
    char *endbuf;                      /* ptr to end of cmdline string */
    int is_bg;                         /* background job? */

    int parsing_state; /* indicates if the next token is the
                          input or output file */

    if (cmdline == NULL)
    {
        (void)fprintf(stderr, "Error: command line is NULL\n");
        return -1;
    }

    (void)strncpy(buf, cmdline, MAXLINE);
    endbuf = buf + strlen(buf);

    tok->infile = NULL;
    tok->outfile = NULL;

    /* Build the argv list */
    parsing_state = ST_NORMAL;
    tok->argc = 0;

    while (buf < endbuf)
    {
        /* Skip the white-spaces */
        buf += strspn(buf, delims);
        if (buf >= endbuf)
            break;

        /* Check for I/O redirection specifiers */
        if (*buf == '<')
        {
            if (tok->infile)
            {
                (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
                return -1;
            }
            parsing_state |= ST_INFILE;
            buf++;
            continue;
        }
        if (*buf == '>')
        {
            if (tok->outfile)
            {
                (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
                return -1;
            }
            parsing_state |= ST_OUTFILE;
            buf++;
            continue;
        }

        if (*buf == '\'' || *buf == '\"')
        {
            /* Detect quoted tokens */
            buf++;
            next = strchr(buf, *(buf - 1));
        }
        else
        {
            /* Find next delimiter */
            next = buf + strcspn(buf, delims);
        }

        if (next == NULL)
        {
            /* Returned by strchr(); this means that the closing
               quote was not found. */
            (void)fprintf(stderr, "Error: unmatched %c.\n", *(buf - 1));
            return -1;
        }

        /* Terminate the token */
        *next = '\0';

        /* Record the token as either the next argument or the i/o file */
        switch (parsing_state)
        {
        case ST_NORMAL:
            tok->argv[tok->argc++] = buf;
            break;
        case ST_INFILE:
            tok->infile = buf;
            break;
        case ST_OUTFILE:
            tok->outfile = buf;
            break;
        default:
            (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
            return -1;
        }
        parsing_state = ST_NORMAL;

        /* Check if argv is full */
        if (tok->argc >= MAXARGS - 1)
            break;

        buf = next + 1;
    }

    if (parsing_state != ST_NORMAL)
    {
        (void)fprintf(stderr,
                      "Error: must provide file name for redirection\n");
        return -1;
    }

    /* The argument list must end with a NULL pointer */
    tok->argv[tok->argc] = NULL;

    if (tok->argc == 0) /* ignore blank line */
        return 1;

    if (!strcmp(tok->argv[0], "quit"))
    { /* quit command */
        tok->builtins = BUILTIN_QUIT;
    }
    else if (!strcmp(tok->argv[0], "jobs"))
    { /* jobs command */
        tok->builtins = BUILTIN_JOBS;
    }
    else if (!strcmp(tok->argv[0], "bg"))
    { /* bg command */
        tok->builtins = BUILTIN_BG;
    }
    else if (!strcmp(tok->argv[0], "fg"))
    { /* fg command */
        tok->builtins = BUILTIN_FG;
    }
    else if (!strcmp(tok->argv[0], "kill"))
    { /* kill command */
        tok->builtins = BUILTIN_KILL;
    }
    else if (!strcmp(tok->argv[0], "nohup"))
    { /* kill command */
        tok->builtins = BUILTIN_NOHUP;
    }
    else
    {
        tok->builtins = BUILTIN_NONE;
    }

    /* Should the job run in the background? */
    if ((is_bg = (*tok->argv[tok->argc - 1] == '&')) != 0)
        tok->argv[--tok->argc] = NULL;

    return is_bg;
}

大致可以认为,它将一行命令解释为argv重定向输入文件重定向输出文件前台/后台运行是否为内建命令,然后返回一个数表示前台/后台作业/错误。

回到eval函数,此时需要实现的任务大致明朗:

即,首先根据infileoutfile实现I/O重定向,然后根据cmdline_tokensbuitlins判断执行哪条命令,最后关闭文件并恢复标准I/O。其结构大致如下:

 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
void eval(char *cmdline)
{
    int bg; /* should the job run in bg or fg? */
    struct cmdline_tokens tok;

    /* Parse command line */
    bg = parseline(cmdline, &tok);

    if (bg == -1) /* parsing error */
        return;
    if (tok.argv[0] == NULL) /* ignore empty lines */
        return;

    /*
    文件重定向
    */

    switch (tok.builtins)
    {
    case BUILTIN_NONE:
        // 处理外部命令
        break;
    case BUILTIN_QUIT:
        //处理quit命令
        break; 
    case BUILTIN_JOBS:
        // 处理jobs命令
        break;
    case BUILTIN_BG:
        // 处理bg命令
        break;
    case BUILTIN_FG:
        // 处理fg命令
        break;
    case BUILTIN_KILL:
        // 处理kill命令
        break;
    case BUILTIN_NOHUP:
        // 处理nohup命令
        break;
    default:
        break;
    }

   /*
   关闭文件,恢复标准输入输出
   */

    return;
}

I/O重定向

根据何为I/O重定向一节,其定义将输入/输出从标准输入/输出改换为给定的文件的过程,一般在程序结束前再恢复标准输入/输出。

于是,需要在switch处理相关参数之前,进行改换输出。

通常的思路是,先利用dup函数复制一份当前标准输入/输出的文件描述符,随后使用open函数打开输入的重定项文件,并调整好相关参数为只读/只写,接着利用dup2函数覆盖原有标准输入/输出。

switch函数执行完主要命令后,利用Close关闭已打开的文件描述符,同时利用Dup2恢复原有标准输入/输出。

再根据上文封装的OpenCloseDupDup2函数,完善eval函数如下:

 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
64
65
66
67
68
69
70
71
72
73
74
void eval(char *cmdline)
{
    int bg; /* should the job run in bg or fg? */
    struct cmdline_tokens tok;

    /* Parse command line */
    bg = parseline(cmdline, &tok);

    if (bg == -1) /* parsing error */
        return;
    if (tok.argv[0] == NULL) /* ignore empty lines */
        return;

    // 文件重定向,如果存在输入输出文件,将标准输入输出重定向
    int input = STDIN_FILENO;
    int output = STDOUT_FILENO;

    // 复制一份标准输入输出,便于恢复
    int std_in = Dup(STDIN_FILENO);
    int std_out = Dup(STDOUT_FILENO);

    if (tok.infile)
    {
        input = Open(tok.infile, O_RDONLY, 0);
        Dup2(input, STDIN_FILENO); // 将标准输入赋值为打开的只读文件
    }

    if (tok.outfile)
    {
        output = Open(tok.outfile, O_WRONLY, 0);
        Dup2(output, STDOUT_FILENO); // 将标准输出赋值为打开的只写文件
    }

    switch (tok.builtins)
    {
    case BUILTIN_NONE:
        // 处理外部命令
        break;
    case BUILTIN_QUIT:
        //处理quit命令
        break; 
    case BUILTIN_JOBS:
        // 处理jobs命令
        break;
    case BUILTIN_BG:
        // 处理bg命令
        break;
    case BUILTIN_FG:
        // 处理fg命令
        break;
    case BUILTIN_KILL:
        // 处理kill命令
        break;
    case BUILTIN_NOHUP:
        // 处理nohup命令
        break;
    default:
        break;
    }

    if (tok.infile)
    {
        Close(input);
        Dup2(std_in, STDIN_FILENO); // 关闭输入文件并恢复标准输入
    }

    if (tok.outfile)
    {
        Close(output);
        Dup2(std_out, STDOUT_FILENO); // 关闭输出文件并恢复标准输出
    }

    return;
}

解析外部指令

首先,根据writeup,父进程必须在创建子进程前,使用sigprocmask阻塞SIGCHLD、SIGINTSIGTSTP信号。在调用addjob函数将子进程添加到job_list后,再使用sigprocmask解除这些信号的阻塞。由于子进程会继承父进程的信号阻塞集,因此子进程在加载新程序前,必须解除这些信号的阻塞。

笔者注:事实上,阻塞所有信号也是可行的。

先参照书上8.4.6的代码,得到一个简单实现:

 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
/* tsh.c/eval_none */
void eval_none(struct cmdline_tokens tok, int bg, char* cmdline) {
    pid_t pid;

    // 子进程
    if ((pid = fork()) == 0) {
        // 执行命令
        setpgid(0, 0);
        if (execve(tok.argv[0], tok.argv, environ) < 0) {
            printf("%s: Command not found.\n", tok.argv[0]);
            exit(0);
        }
    }

    // 父进程
    else {
        addjob(job_list, pid, bg ? BG : FG, cmdline);
        // 前台运行
        if (!bg) {
            int status;
            waitpid(pid, &status, 0);
        }
        // 后台运行
        else {
            printf("[%d] (%d) %s\n", pid2jid(pid), pid, cmdline);
        }
    }

    return;
}

可以看到,除了上述思路提到的问题,此处还存在一个问题。

即前台JOB与后台不同的点在于,前台JOB可以在终端内进行显式输出,而后台JOB则需要显式地加一行表示已经转为后台。

通常,使用终端时,运行一个前台JOBShell会运行这个JOB,直到其结束,用户才可输入下一行命令。

此处同理,在父进程创建一个前台子进程后,应该将显式等待前台JOB全部结束,再返回运行父进程。

但是,此处的waitpid貌似起到了等待的效果,但是由于其是一个阻塞函数,父进程会一直等待,直到子进程结束。若父进程此时接受到如SIGINT的其他信号,则可能造成永久阻塞,这则需要原子化的等待函数,即sigsuspend

注意到,辅助函数提供了fgpid函数,该函数可以找到job_list中的前台JOBPID,因此可以使用其与while循环遍历所有前台JOB,并使用suspend显式等待其结束。

而若为后台进程,则需要按照writeup中相关实例进行相应输出。

根据以上思路,编写代码,并在eval中添加相关调用如下:

 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
// 处理外部命令
void external_command(struct cmdline_tokens tok, int bg, char *cmdline)
{
    pid_t pid = 0;
    sigset_t mask, prev_mask;
    Sigemptyset(&mask); // 阻塞以下三个信号,防止竞争
    Sigaddset(&mask, SIGCHLD);
    Sigaddset(&mask, SIGINT);
    Sigaddset(&mask, SIGTSTP);
    Sigprocmask(SIG_BLOCK, &mask, &prev_mask);

    if ((pid = Fork()) == 0)
    {
        setpgid(0, 0);                              // 创建新子进程
        Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 恢复原来信号
        if (Execve(tok.argv[0], tok.argv, environ) < 0)
            exit(0);
    }
    else
    {
        Sigfillset(&mask); // 屏蔽全部信号
        Sigprocmask(SIG_SETMASK, &mask, NULL);
        addjob(job_list, pid, bg ? BG : FG, cmdline); // 添加新job
        Sigprocmask(SIG_SETMASK, &prev_mask, NULL);   // 恢复原来信号
        if (!bg)
        {
            while (fgpid(job_list)) // 前台进程,挂起父进程直到子进程结束
                sigsuspend(&prev_mask);
        }
        else
        {
            printf("[%d] (%d) %s\n", pid2jid(pid), pid, cmdline); // 直接输出
        }
    }
    return;
}

case BUILTIN_NONE:
    external_command(tok, bg, cmdline); // 处理外部命令
    break;

实现退出/quit

实现退出是非常简单的,只需要在switchBUILTIN_QUIT中加入exit(0)即可:

1
2
case BUILTIN_QUIT:
    exit(0); // 直接退出

列出全部JOB/jobs

仔细阅读认识tsh.c的辅助函数一节,发现listjobs能完美完成这个任务,只需要调用它即可:

1
2
3
case BUILTIN_JOBS:
    listjobs(job_list, output); // 调用辅助函数列出全部job
    break;

作业重启转后台/bg

根据writeupbg job命令通过发送SIGCONT信号重启JOB,并使其在后台运行,job的参数可以是PID(即数字)JID(即%数字)

由于需要找出具体哪个JOB,因此编写的辅助函数需要导入tok作为参数,从tok.argv[1][0]是否为%判断参数为PIDJID

随后,使用atoi函数将字符参数转化为对应整数,通过getjobpid或者getjobjid取出该参数对应的JOB,改变其状态,并使用封装后的Kill函数对该JOB发去一个SIGCONT信号。

根据writeup给出的实例,此时还需要输出相关语句表明成功转为后台。

根据以上思路,编写函数,并在eval中添加相关调用如下:

 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
// 将前台进程转化为后台进程
void turn_bg(struct cmdline_tokens tok)
{
    int jid;
    pid_t pid;
    struct job_t *job;
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        job = getjobjid(job_list, jid);
        pid = job->pid;
    }
    else
    {
        pid = atoi(tok.argv[1]);
        job = getjobpid(job_list, pid);
    } // 找到对应的job
    job->state = BG; // 改变状态
    printf("[%d] (%d) %s\n", pid2jid(pid), pid, job->cmdline);
    Kill(pid, SIGCONT); // 唤醒进程
    return;
}

case BUILTIN_BG:
    turn_bg(tok); // 进程转后台
    break;

作业重启转前台/fg

根据writeupfg job命令通过发送SIGCONT信号重启JOB,并使其在前台运行,job的参数可以是PID(即数字)JID(即%数字)

其思路与bg指令相似,根据argv取出对应PIDJID相同,以及发送SIGCONT信号也相同,这里不再赘述。

但是,如解析外部命令中所述,前台JOB与后台JOB不同的是,需要显式等待前台JOB结束,实现方式与其相同。

根据以上思路,编写函数,并在eval中添加相关调用如下:

 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
// 后台进程转前台进程
void turn_fg(struct cmdline_tokens tok)
{
    int jid;
    pid_t pid;
    struct job_t *job;
    sigset_t mask;
    Sigemptyset(&mask);
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        job = getjobjid(job_list, jid);
        pid = job->pid;
    }
    else
    {
        pid = atoi(tok.argv[1]);
        job = getjobpid(job_list, pid);
    } // 找到对应进程
    job->state = FG;        // 改变状态
    Kill(pid, SIGCONT);     // 唤醒进程
    while (fgpid(job_list)) // 挂起进程直到其结束
        sigsuspend(&mask);
    return;
}

case BUILTIN_FG:
    turn_fg(tok); // 进程转前台
    break;

发送信号终止/kill

根据writeupkill job命令,对相关每个进程发送SIGTERM信号,终止作业列表的某个JOB或某个进程组,参数可以是PID或者JID。若输入的的JID/PID为负数,则需要终止-JIDJOB以及-PID对应的JOB对应的进程组,若为正数则直接终止对应JOB。若需要终止的JOB或进程组不存在,则按照对应格式输出。

根据上文分析,在Tsh Lab中,每个进程组中只存在一个进程,因此可以看成某个JOB在两种意义上的表示。

还是与bg中相似的思路,先取出对应的JID/PID,再找到相应JOB,如果通过getjobpid/getjobjid取出的指针为空,则按照pid/jid是否为负数输出相应结果。若不为空则通过封装后的Kill函数发去对应信号。

根据以上代码,编写函数,并在eval中添加相关调用如下:

 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
// 杀死某个进程或进程组
void job_kill(struct cmdline_tokens tok, int sig)
{
    pid_t pid;
    int jid;
    struct job_t *job;
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        int new_jid = abs(jid);
        job = getjobjid(job_list, new_jid);
        if (!job)
        {
            if (jid < 0)
                printf("%%%d: No such process group\n", jid);
            else
                printf("%%%d: No such job\n", jid);
            return;
        }
        pid = job->pid;
    } // 根据jid找到对应进程/组,并输出
    else
    {
        pid = atoi(tok.argv[1]);
        int new_pid = abs(pid);
        job = getjobpid(job_list, new_pid);
        if (!job)
        {
            if (pid < 0)
                printf("(%d): No such process group\n", pid);
            else
                printf("(%d):No such job\n", pid);
            return;
        }
    } // 根据pid找到对应进程/组,并输出
    Kill(pid, sig); // 杀掉对应进程
    return;
}

case BUILTIN_KILL:
    job_kill(tok, SIGTERM); // 进行默认信号的杀死进程
    break;

忽略信号/nohup

根据writeup,需要忽略SIGHUP信号,启动一个新进程。

一个显然的思路是,通过信号掩码除掉SIGHUP信号,然后调用eval新建一个命令行解析函数,注意传进的命令行串此时需要加上nohup 这六个字符的偏移量,最后为当前eval恢复信号掩码。

根据以上思路,编写函数,并在eval中添加相关调用如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 使后续指令忽略所有该信号
void job_nohup(char *cmdline)
{
    sigset_t mask, prev_mask;
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGHUP);
    Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
    eval(cmdline + 6);                          // 启动一个新子进程,忽略"nohup "字样
    Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 恢复信号屏蔽
    return;
}

case BUILTIN_NOHUP:
    job_nohup(cmdline); // 忽略所有该信号,并启动新进程
    break;

SIGCHLD信号处理/回收子进程

main.c中,存在着几条signal(sig,handler)代码。

即需要手动编写它的信号处理程序。

参照回顾信号及其相关函数一节,SIGCHLD在一个子进程发生变化时被发出,它的默认行为是忽略。

而此处根据writeup,需要将信号处理程序改为回收对应子进程。根据课本,只需要不断使用waitpid等待子进程正常终止/被信号终止/被信号停止/由信号继续,而这几个功能都可以由waitpid中的status的几个宏实现。

笔者猜想读者到这里都忘了是哪些宏了,请参照上文回顾信号及其函数一节查阅。

细致讲来,即子进程正常终止,则调用deletejob辅助函数删除该JOB。若子进程被信号终止,则输出相应输出,并同上删除该JOB。若子进程被信号停止,则输出相应输出,并修改该子进程状态为停止。若停止的子进程收到SIGCONT信号,则修改其状态为后台。

根据身边统计学,许多同学漏掉收到SIGCONT信号这一判断分支,需要注意。

然后,由于此处为内核的信号处理程序,需要非常小心地遵循前文提到的几条规则:

尽可能调用异步信号安全函数,因此此处需要使用提供的sio_out函数。

保存并恢复errno,即在函数开头存储旧errno,在函数结尾恢复errno

访问共享全局数据结构时,阻塞所有信号。此处job_list为全局数据结构,因此每次访问它都需要使用sigprocmask阻塞全部信号。

最后,由于避免可能的没有需要回收的子进程却接到SIGCHLD信号的错误情况,考虑使用unix_error抛出一个错误。

根据以上思路,编写代码如下:

 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
// 回收所有变化子进程
void sigchld_handler(int sig)
{
    int old_errno = errno; // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    struct job_t *job;
    int status;
    int jid;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) // 遍历所有变化的子进程
    {
        job = getjobpid(job_list, pid); // 找到对应的job
        if (!job)
            continue; // 防止NULL解引用,好习惯
        jid = job->jid;
        if (WIFEXITED(status)) // 正常终止的子进程
        {
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            deletejob(job_list, pid);                   // 删除对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFSIGNALED(status)) // 被信号终止的子进程
        {
            sio_put("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); // 输出相关信息
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            deletejob(job_list, pid);                   // 删除对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFSTOPPED(status)) // 被信号停止的子进程
        {
            sio_put("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status)); // 输出相关信息
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            job->state = ST;                            // 停止对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFCONTINUED(status) && job->state == ST) // 收到SIGCONT被重启的进程
        {
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            job->state = BG;                            // 改变进程状态
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
    }
    if (pid < 0 && errno != ECHILD) // 如果没有需要回收的子进程,报错
    {
        unix_error("waitpid error");
    }
    errno = old_errno; // 恢复errno
    return;
}

SIGINT信号处理/Ctrl+C

根据writeup,当接到SIGINT信号时,需要向前台JOB发去一个SIGINT信号。

如上文所述,可使用fgpid函数获取当前前台JOBPID,然后调用封装后的Kill函数发去SIGINT信号。

SIGCHLD的信号处理程序类似,此处仍需遵循编写信号处理程序的三条规则。

根据以上思路,编写代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void sigint_handler(int sig)
{
    int old_errno = errno; // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    pid = fgpid(job_list); // 找到当前前台进程
    if (pid)
    {
        Sigfillset(&mask);
        Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
        Kill(pid, SIGINT);                          // 给它发SIGINT信号
        Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
    }
    errno = old_errno; // 恢复errno
    return;
}

SIGTSTP信号处理/Ctrl+Z

根据writeup,当接到SIGINT信号时,需要向前台JOB发去一个SIGTSTP信号。

如上文所述,可使用fgpid函数获取当前前台JOBPID,然后调用封装后的Kill函数发去SIGTSTP信号。

SIGCHLD的信号处理程序类似,此处仍需遵循编写信号处理程序的三条规则。

根据以上思路,编写代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void sigtstp_handler(int sig) {
    int old_errno = errno;  // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    pid = fgpid(job_list);  // 找到前台进程
    if (pid) {
        Sigfillset(&mask);
        Sigprocmask(SIG_BLOCK, &mask, &mask_prev);   // 屏蔽全部信号,保护全局数据结构
        Kill(pid, SIGTSTP);                          // 给它发SIGTSTP信号
        Sigprocmask(SIG_SETMASK, &mask_prev, NULL);  // 恢复屏蔽的信号
    }
    errno = old_errno;  // 恢复errno
    return;
}

成品代码

将上述全部代码拼在一起,得到成品代码:

   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
  64
  65
  66
  67
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
/*
 * tsh - A tiny shell program with job control
 *
 * <Put your name and login ID here>
 */
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <signal.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdarg.h>

#define abs(x) (x >= 0 ? x : -x)

/* Misc manifest constants */
#define MAXLINE 1024   /* max line size */
#define MAXARGS 128    /* max args on a command line */
#define MAXJOBS 16     /* max jobs at any point in time */
#define MAXJID 1 << 16 /* max job ID */

/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1    /* running in foreground */
#define BG 2    /* running in background */
#define ST 3    /* stopped */

/*
 * Jobs states: FG (foreground), BG (background), ST (stopped)
 * Job state transitions and enabling actions:
 *     FG -> ST  : ctrl-z
 *     ST -> FG  : fg command
 *     ST -> BG  : bg command
 *     BG -> FG  : fg command
 * At most 1 job can be in the FG state.
 */

/* Parsing states */
#define ST_NORMAL 0x0  /* next token is an argument */
#define ST_INFILE 0x1  /* next token is the input file */
#define ST_OUTFILE 0x2 /* next token is the output file */

/* Global variables */
extern char **environ;   /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0;         /* if true, print additional output */
int nextjid = 1;         /* next job ID to allocate */
char sbuf[MAXLINE];      /* for composing sprintf messages */

struct job_t
{                                         /* The job struct */
    pid_t pid;                            /* job PID */
    int jid;                              /* job ID [1, 2, ...] */
    int state; /* UNDEF, BG, FG, or ST */ // 即未定义,后台,前台,挂起
    char cmdline[MAXLINE];                /* command line */
};
struct job_t job_list[MAXJOBS]; /* The job list */

struct cmdline_tokens
{
    int argc;                            /* Number of arguments */
    char *argv[MAXARGS];                 /* The arguments list */
    char *infile; /* The input file */   // 输入重定向
    char *outfile; /* The output file */ // 输出重定向
    enum builtins_t
    {               /* Indicates if argv[0] is a builtin command */
      BUILTIN_NONE, // 啥都没有
      BUILTIN_QUIT, // 退出
      BUILTIN_JOBS, // 展示全部job
      BUILTIN_BG,   // 转为后台
      BUILTIN_FG,   // 转为前台
      BUILTIN_KILL, // 杀死job
      BUILTIN_NOHUP
    } builtins; // 忽视SIGHUP,启动新进程
};

/* End global variables */

/* Function prototypes */

pid_t Fork(void);
void eval(char *cmdline);

void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, struct cmdline_tokens *tok);
void sigquit_handler(int sig);

void clearjob(struct job_t *job);
void initjobs(struct job_t *job_list);
int maxjid(struct job_t *job_list);
int addjob(struct job_t *job_list, pid_t pid, int state, char *cmdline);
int deletejob(struct job_t *job_list, pid_t pid);
pid_t fgpid(struct job_t *job_list);
struct job_t *getjobpid(struct job_t *job_list, pid_t pid);
struct job_t *getjobjid(struct job_t *job_list, int jid);
int pid2jid(pid_t pid);
void listjobs(struct job_t *job_list, int output_fd);

void usage(void);
void unix_error(char *msg);
void app_error(char *msg);
ssize_t sio_puts(char s[]);
ssize_t sio_putl(long v);
ssize_t sio_put(const char *fmt, ...);
void sio_error(char s[]);

typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler);

/*
 * main - The shell's main routine
 */
int main(int argc, char **argv)
{
    char c;
    char cmdline[MAXLINE]; /* cmdline for fgets */
    int emit_prompt = 1;   /* emit prompt (default) */

    /* Redirect stderr to stdout (so that driver will get all output
     * on the pipe connected to stdout) */
    dup2(1, 2);

    /* Parse the command line */
    while ((c = getopt(argc, argv, "hvp")) != EOF)
    {
        switch (c)
        {
        case 'h': /* print help message */
            usage();
            break;
        case 'v': /* emit additional diagnostic info */
            verbose = 1;
            break;
        case 'p':            /* don't print a prompt */
            emit_prompt = 0; /* handy for automatic testing */
            break;
        default:
            usage();
        }
    }

    /* Install the signal handlers */

    /* These are the ones you will need to implement */
    Signal(SIGINT, sigint_handler);   /* ctrl-c */
    Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
    Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */
    Signal(SIGTTIN, SIG_IGN);
    Signal(SIGTTOU, SIG_IGN);

    /* This one provides a clean way to kill the shell */
    Signal(SIGQUIT, sigquit_handler);

    /* Initialize the job list */
    initjobs(job_list);

    /* Execute the shell's read/eval loop */
    while (1)
    {

        if (emit_prompt)
        {
            printf("%s", prompt);
            fflush(stdout);
        }
        if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
            app_error("fgets error");
        if (feof(stdin))
        {
            /* End of file (ctrl-d) */
            printf("\n");
            fflush(stdout);
            fflush(stderr);
            exit(0);
        }

        /* Remove the trailing newline */
        cmdline[strlen(cmdline) - 1] = '\0';

        /* Evaluate the command line */
        eval(cmdline);

        fflush(stdout);
        fflush(stdout);
    }

    exit(0); /* control never reaches here */
}

// 一堆封装函数

pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("Fork error");
    return pid;
}

int Open(char *pathname, int flags, mode_t mode)
{
    int fd = open(pathname, flags, mode);
    if (fd < 0)
    {
        unix_error("open error");
        exit(1);
    }
    return fd;
}

int Dup(int oldfd)
{
    int fd = dup(oldfd);
    if (fd < 0)
    {
        unix_error("dup error");
        exit(1);
    }
    return fd;
}

void Dup2(int oldfd, int newfd)
{
    if (dup2(oldfd, newfd) < 0)
    {
        unix_error("dup2 error");
        exit(1);
    }
    return;
}

void Close(int fd)
{
    if (close(fd) < 0)
    {
        unix_error("close error");
        exit(1);
    }
    return;
}

void Kill(pid_t pid, int sig)
{
    if (kill(pid, sig) < 0)
    {
        unix_error("kill error");
        exit(1);
    }
    return;
}

int Execve(char *filepath, char *const argv[], char *const envp[])
{
    int exe = execve(filepath, argv, envp);
    if (exe < 0)
    {
        printf("%s: Command not found\n", argv[0]);
        exit(1);
    }
    return exe;
}

void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
{
    if (sigprocmask(how, set, oldset) < 0)
    {
        unix_error("sigprocmask error");
        exit(1);
    }
    return;
}

void Sigfillset(sigset_t *set)
{
    if (sigfillset(set) < 0)
    {
        unix_error("sigfillset error");
        exit(1);
    }
    return;
}

void Sigaddset(sigset_t *set, int sig)
{
    if (sigaddset(set, sig) < 0)
    {
        unix_error("sigaddset error");
        exit(1);
    }
    return;
}

void Sigemptyset(sigset_t *set)
{
    if (sigemptyset(set) < 0)
    {
        unix_error("sigemptyset error");
        exit(1);
    }
    return;
}

/*
 * 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 external_command(struct cmdline_tokens tok, int bg, char *cmdline)
{
    pid_t pid = 0;
    sigset_t mask, prev_mask;
    Sigemptyset(&mask); // 阻塞以下三个信号,防止竞争
    Sigaddset(&mask, SIGCHLD);
    Sigaddset(&mask, SIGINT);
    Sigaddset(&mask, SIGTSTP);
    Sigprocmask(SIG_BLOCK, &mask, &prev_mask);

    if ((pid = Fork()) == 0)
    {
        setpgid(0, 0);                              // 创建新子进程
        Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 恢复原来信号
        if (Execve(tok.argv[0], tok.argv, environ) < 0)
            exit(0);
    }
    else
    {
        Sigfillset(&mask); // 屏蔽全部信号
        Sigprocmask(SIG_SETMASK, &mask, NULL);
        addjob(job_list, pid, bg ? BG : FG, cmdline); // 添加新job
        Sigprocmask(SIG_SETMASK, &prev_mask, NULL);   // 恢复原来信号
        if (!bg)
        {
            while (fgpid(job_list)) // 前台进程,挂起父进程直到子进程结束
                sigsuspend(&prev_mask);
        }
        else
        {
            printf("[%d] (%d) %s\n", pid2jid(pid), pid, cmdline); // 直接输出
        }
    }
    return;
}

// 将前台进程转化为后台进程
void turn_bg(struct cmdline_tokens tok)
{
    int jid;
    pid_t pid;
    struct job_t *job;
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        job = getjobjid(job_list, jid);
        pid = job->pid;
    }
    else
    {
        pid = atoi(tok.argv[1]);
        job = getjobpid(job_list, pid);
    } // 找到对应的job
    job->state = BG; // 改变状态
    printf("[%d] (%d) %s\n", pid2jid(pid), pid, job->cmdline);
    Kill(pid, SIGCONT); // 唤醒进程
    return;
}

// 后台进程转前台进程
void turn_fg(struct cmdline_tokens tok)
{
    int jid;
    pid_t pid;
    struct job_t *job;
    sigset_t mask;
    Sigemptyset(&mask);
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        job = getjobjid(job_list, jid);
        pid = job->pid;
    }
    else
    {
        pid = atoi(tok.argv[1]);
        job = getjobpid(job_list, pid);
    } // 找到对应进程
    job->state = FG;        // 改变状态
    Kill(pid, SIGCONT);     // 唤醒进程
    while (fgpid(job_list)) // 挂起进程直到其结束
        sigsuspend(&mask);
    return;
}

// 杀死某个进程或进程组
void job_kill(struct cmdline_tokens tok, int sig)
{
    pid_t pid;
    int jid;
    struct job_t *job;
    if (tok.argv[1][0] == '%')
    {
        jid = atoi(tok.argv[1] + 1);
        int new_jid = abs(jid);
        job = getjobjid(job_list, new_jid);
        if (!job)
        {
            if (jid < 0)
                printf("%%%d: No such process group\n", jid);
            else
                printf("%%%d: No such job\n", jid);
            return;
        }
        pid = job->pid;
    } // 根据jid找到对应进程/组,并输出
    else
    {
        pid = atoi(tok.argv[1]);
        int new_pid = abs(pid);
        job = getjobpid(job_list, new_pid);
        if (!job)
        {
            if (pid < 0)
                printf("(%d): No such process group\n", pid);
            else
                printf("(%d):No such job\n", pid);
            return;
        }
    } // 根据pid找到对应进程/组,并输出
    Kill(pid, sig); // 杀掉对应进程
    return;
}

// 使后续指令忽略所有该信号
void job_nohup(char *cmdline)
{
    sigset_t mask, prev_mask;
    Sigemptyset(&mask);
    Sigaddset(&mask, SIGHUP);
    Sigprocmask(SIG_BLOCK, &mask, &prev_mask);
    eval(cmdline + 6);                          // 启动一个新子进程,忽略"nohup "字样
    Sigprocmask(SIG_SETMASK, &prev_mask, NULL); // 恢复信号屏蔽
    return;
}

void eval(char *cmdline)
{
    int bg; /* should the job run in bg or fg? */
    struct cmdline_tokens tok;

    /* Parse command line */
    bg = parseline(cmdline, &tok);

    if (bg == -1) /* parsing error */
        return;
    if (tok.argv[0] == NULL) /* ignore empty lines */
        return;

    // 文件重定向,如果存在输入输出文件,将标准输入输出重定向
    int input = STDIN_FILENO;
    int output = STDOUT_FILENO;

    // 复制一份标准输入输出,便于恢复
    int std_in = Dup(STDIN_FILENO);
    int std_out = Dup(STDOUT_FILENO);

    if (tok.infile)
    {
        input = Open(tok.infile, O_RDONLY, 0);
        Dup2(input, STDIN_FILENO); // 将标准输入赋值为打开的只读文件
    }

    if (tok.outfile)
    {
        output = Open(tok.outfile, O_WRONLY, 0);
        Dup2(output, STDOUT_FILENO); // 将标准输出赋值为打开的只写文件
    }

    switch (tok.builtins)
    {
    case BUILTIN_NONE:
        external_command(tok, bg, cmdline); // 处理外部命令
        break;
    case BUILTIN_QUIT:
        exit(0); // 直接退出
    case BUILTIN_JOBS:
        listjobs(job_list, output); // 调用辅助函数列出全部job
        break;
    case BUILTIN_BG:
        turn_bg(tok); // 进程转后台
        break;
    case BUILTIN_FG:
        turn_fg(tok); // 进程转前台
        break;
    case BUILTIN_KILL:
        job_kill(tok, SIGTERM); // 进行默认信号的杀死进程
        break;
    case BUILTIN_NOHUP:
        job_nohup(cmdline); // 忽略所有该信号,并启动新进程
        break;
    default:
        break;
    }

    if (tok.infile)
    {
        Close(input);
        Dup2(std_in, STDIN_FILENO); // 关闭输入文件并恢复标准输入
    }

    if (tok.outfile)
    {
        Close(output);
        Dup2(std_out, STDOUT_FILENO); // 关闭输出文件并恢复标准输出
    }

    return;
}

/*
 * parseline - Parse the command line and build the argv array.
 *
 * Parameters:
 *   cmdline:  The command line, in the form:
 *
 *                command [arguments...] [< infile] [> oufile] [&]
 *
 *   tok:      Pointer to a cmdline_tokens structure. The elements of this
 *             structure will be populated with the parsed tokens. Characters
 *             enclosed in single or double quotes are treated as a single
 *             argument.
 * Returns:
 *   1:        if the user has requested a BG job
 *   0:        if the user has requested a FG job
 *  -1:        if cmdline is incorrectly formatted
 *
 * Note:       The string elements of tok (e.g., argv[], infile, outfile)
 *             are statically allocated inside parseline() and will be
 *             overwritten the next time this function is invoked.
 */
int parseline(const char *cmdline, struct cmdline_tokens *tok)
{

    static char array[MAXLINE];        /* holds local copy of command line */
    const char delims[10] = " \t\r\n"; /* argument delimiters (white-space) */
    char *buf = array;                 /* ptr that traverses command line */
    char *next;                        /* ptr to the end of the current arg */
    char *endbuf;                      /* ptr to end of cmdline string */
    int is_bg;                         /* background job? */

    int parsing_state; /* indicates if the next token is the
                          input or output file */

    if (cmdline == NULL)
    {
        (void)fprintf(stderr, "Error: command line is NULL\n");
        return -1;
    }

    (void)strncpy(buf, cmdline, MAXLINE);
    endbuf = buf + strlen(buf);

    tok->infile = NULL;
    tok->outfile = NULL;

    /* Build the argv list */
    parsing_state = ST_NORMAL;
    tok->argc = 0;

    while (buf < endbuf)
    {
        /* Skip the white-spaces */
        buf += strspn(buf, delims);
        if (buf >= endbuf)
            break;

        /* Check for I/O redirection specifiers */
        if (*buf == '<')
        {
            if (tok->infile)
            {
                (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
                return -1;
            }
            parsing_state |= ST_INFILE;
            buf++;
            continue;
        }
        if (*buf == '>')
        {
            if (tok->outfile)
            {
                (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
                return -1;
            }
            parsing_state |= ST_OUTFILE;
            buf++;
            continue;
        }

        if (*buf == '\'' || *buf == '\"')
        {
            /* Detect quoted tokens */
            buf++;
            next = strchr(buf, *(buf - 1));
        }
        else
        {
            /* Find next delimiter */
            next = buf + strcspn(buf, delims);
        }

        if (next == NULL)
        {
            /* Returned by strchr(); this means that the closing
               quote was not found. */
            (void)fprintf(stderr, "Error: unmatched %c.\n", *(buf - 1));
            return -1;
        }

        /* Terminate the token */
        *next = '\0';

        /* Record the token as either the next argument or the i/o file */
        switch (parsing_state)
        {
        case ST_NORMAL:
            tok->argv[tok->argc++] = buf;
            break;
        case ST_INFILE:
            tok->infile = buf;
            break;
        case ST_OUTFILE:
            tok->outfile = buf;
            break;
        default:
            (void)fprintf(stderr, "Error: Ambiguous I/O redirection\n");
            return -1;
        }
        parsing_state = ST_NORMAL;

        /* Check if argv is full */
        if (tok->argc >= MAXARGS - 1)
            break;

        buf = next + 1;
    }

    if (parsing_state != ST_NORMAL)
    {
        (void)fprintf(stderr,
                      "Error: must provide file name for redirection\n");
        return -1;
    }

    /* The argument list must end with a NULL pointer */
    tok->argv[tok->argc] = NULL;

    if (tok->argc == 0) /* ignore blank line */
        return 1;

    if (!strcmp(tok->argv[0], "quit"))
    { /* quit command */
        tok->builtins = BUILTIN_QUIT;
    }
    else if (!strcmp(tok->argv[0], "jobs"))
    { /* jobs command */
        tok->builtins = BUILTIN_JOBS;
    }
    else if (!strcmp(tok->argv[0], "bg"))
    { /* bg command */
        tok->builtins = BUILTIN_BG;
    }
    else if (!strcmp(tok->argv[0], "fg"))
    { /* fg command */
        tok->builtins = BUILTIN_FG;
    }
    else if (!strcmp(tok->argv[0], "kill"))
    { /* kill command */
        tok->builtins = BUILTIN_KILL;
    }
    else if (!strcmp(tok->argv[0], "nohup"))
    { /* kill command */
        tok->builtins = BUILTIN_NOHUP;
    }
    else
    {
        tok->builtins = BUILTIN_NONE;
    }

    /* Should the job run in the background? */
    if ((is_bg = (*tok->argv[tok->argc - 1] == '&')) != 0)
        tok->argv[--tok->argc] = NULL;

    return is_bg;
}

/*****************
 * Signal handlers
 *****************/

/*
 * 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, SIGTSTP, SIGTTIN or SIGTTOU 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)
{
    int old_errno = errno; // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    struct job_t *job;
    int status;
    int jid;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED | WCONTINUED)) > 0) // 遍历所有变化的子进程
    {
        job = getjobpid(job_list, pid); // 找到对应的job
        if (!job)
            continue; // 防止NULL解引用,好习惯
        jid = job->jid;
        if (WIFEXITED(status)) // 正常终止的子进程
        {
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            deletejob(job_list, pid);                   // 删除对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFSIGNALED(status)) // 被信号终止的子进程
        {
            sio_put("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status)); // 输出相关信息
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            deletejob(job_list, pid);                   // 删除对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFSTOPPED(status)) // 被信号停止的子进程
        {
            sio_put("Job [%d] (%d) stopped by signal %d\n", jid, pid, WSTOPSIG(status)); // 输出相关信息
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            job->state = ST;                            // 停止对应进程
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
        if (WIFCONTINUED(status) && job->state == ST) // 收到SIGCONT被重启的进程
        {
            Sigfillset(&mask);
            Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
            job->state = BG;                            // 改变进程状态
            Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
        }
    }
    if (pid < 0 && errno != ECHILD) // 如果没有需要回收的子进程,报错
    {
        unix_error("waitpid error");
    }
    errno = old_errno; // 恢复errno
    return;
}

/*
 * 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)
{
    int old_errno = errno; // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    pid = fgpid(job_list); // 找到当前前台进程
    if (pid)
    {
        Sigfillset(&mask);
        Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
        Kill(pid, SIGINT);                          // 给它发SIGINT信号
        Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
    }
    errno = old_errno; // 恢复errno
    return;
}

/*
 * 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)
{
    int old_errno = errno; // 存储旧errno,规则限制
    sigset_t mask, mask_prev;
    pid_t pid;
    pid = fgpid(job_list); // 找到前台进程
    if (pid)
    {
        Sigfillset(&mask);
        Sigprocmask(SIG_BLOCK, &mask, &mask_prev);  // 屏蔽全部信号,保护全局数据结构
        Kill(pid, SIGTSTP);                         // 给它发SIGTSTP信号
        Sigprocmask(SIG_SETMASK, &mask_prev, NULL); // 恢复屏蔽的信号
    }
    errno = old_errno; // 恢复errno
    return;
}

/*
 * sigquit_handler - The driver program can gracefully terminate the
 *    child shell by sending it a SIGQUIT signal.
 */
void sigquit_handler(int sig)
{
    sio_error("Terminating after receipt of SIGQUIT signal\n");
}

/*********************
 * End signal handlers
 *********************/

/***********************************************
 * Helper routines that manipulate the job list
 **********************************************/

/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job)
{
    job->pid = 0;
    job->jid = 0;
    job->state = UNDEF;
    job->cmdline[0] = '\0';
}

/* initjobs - Initialize the job list */
void initjobs(struct job_t *job_list)
{
    int i;

    for (i = 0; i < MAXJOBS; i++)
        clearjob(&job_list[i]);
}

/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *job_list)
{
    int i, max = 0;

    for (i = 0; i < MAXJOBS; i++)
        if (job_list[i].jid > max)
            max = job_list[i].jid;
    return max;
}

/* addjob - Add a job to the job list */
int addjob(struct job_t *job_list, pid_t pid, int state, char *cmdline)
{
    int i;

    if (pid < 1)
        return 0;

    for (i = 0; i < MAXJOBS; i++)
    {
        if (job_list[i].pid == 0)
        {
            job_list[i].pid = pid;
            job_list[i].state = state;
            job_list[i].jid = nextjid++;
            if (nextjid > MAXJOBS)
                nextjid = 1;
            strcpy(job_list[i].cmdline, cmdline);
            if (verbose)
            {
                printf("Added job [%d] %d %s\n",
                       job_list[i].jid,
                       job_list[i].pid,
                       job_list[i].cmdline);
            }
            return 1;
        }
    }
    printf("Tried to create too many jobs\n");
    return 0;
}

/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *job_list, pid_t pid)
{
    int i;

    if (pid < 1)
        return 0;

    for (i = 0; i < MAXJOBS; i++)
    {
        if (job_list[i].pid == pid)
        {
            clearjob(&job_list[i]);
            nextjid = maxjid(job_list) + 1;
            return 1;
        }
    }
    return 0;
}

/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *job_list)
{
    int i;

    for (i = 0; i < MAXJOBS; i++)
        if (job_list[i].state == FG)
            return job_list[i].pid;
    return 0;
}

/* getjobpid  - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *job_list, pid_t pid)
{
    int i;

    if (pid < 1)
        return NULL;
    for (i = 0; i < MAXJOBS; i++)
        if (job_list[i].pid == pid)
            return &job_list[i];
    return NULL;
}

/* getjobjid  - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *job_list, int jid)
{
    int i;

    if (jid < 1)
        return NULL;
    for (i = 0; i < MAXJOBS; i++)
        if (job_list[i].jid == jid)
            return &job_list[i];
    return NULL;
}

/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
    int i;

    if (pid < 1)
        return 0;
    for (i = 0; i < MAXJOBS; i++)
        if (job_list[i].pid == pid)
        {
            return job_list[i].jid;
        }
    return 0;
}

/* listjobs - Print the job list */
void listjobs(struct job_t *job_list, int output_fd)
{
    int i;
    char buf[MAXLINE << 2];

    for (i = 0; i < MAXJOBS; i++)
    {
        memset(buf, '\0', MAXLINE);
        if (job_list[i].pid != 0)
        {
            sprintf(buf, "[%d] (%d) ", job_list[i].jid, job_list[i].pid);
            if (write(output_fd, buf, strlen(buf)) < 0)
            {
                fprintf(stderr, "Error writing to output file\n");
                exit(1);
            }
            memset(buf, '\0', MAXLINE);
            switch (job_list[i].state)
            {
            case BG:
                sprintf(buf, "Running    ");
                break;
            case FG:
                sprintf(buf, "Foreground ");
                break;
            case ST:
                sprintf(buf, "Stopped    ");
                break;
            default:
                sprintf(buf, "listjobs: Internal error: job[%d].state=%d ",
                        i, job_list[i].state);
            }
            if (write(output_fd, buf, strlen(buf)) < 0)
            {
                fprintf(stderr, "Error writing to output file\n");
                exit(1);
            }
            memset(buf, '\0', MAXLINE);
            sprintf(buf, "%s\n", job_list[i].cmdline);
            if (write(output_fd, buf, strlen(buf)) < 0)
            {
                fprintf(stderr, "Error writing to output file\n");
                exit(1);
            }
        }
    }
}
/******************************
 * end job list helper routines
 ******************************/

/***********************
 * Other helper routines
 ***********************/

/*
 * usage - print a help message
 */
void usage(void)
{
    printf("Usage: shell [-hvp]\n");
    printf("   -h   print this message\n");
    printf("   -v   print additional diagnostic information\n");
    printf("   -p   do not emit a command prompt\n");
    exit(1);
}

/*
 * unix_error - unix-style error routine
 */
void unix_error(char *msg)
{
    fprintf(stdout, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

/*
 * app_error - application-style error routine
 */
void app_error(char *msg)
{
    fprintf(stdout, "%s\n", msg);
    exit(1);
}

/* Private sio_functions */
/* sio_reverse - Reverse a string (from K&R) */
static void sio_reverse(char s[])
{
    int c, i, j;

    for (i = 0, j = strlen(s) - 1; i < j; i++, j--)
    {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

/* sio_ltoa - Convert long to base b string (from K&R) */
static void sio_ltoa(long v, char s[], int b)
{
    int c, i = 0;

    do
    {
        s[i++] = ((c = (v % b)) < 10) ? c + '0' : c - 10 + 'a';
    } while ((v /= b) > 0);
    s[i] = '\0';
    sio_reverse(s);
}

/* sio_strlen - Return length of string (from K&R) */
static size_t sio_strlen(const char s[])
{
    int i = 0;

    while (s[i] != '\0')
        ++i;
    return i;
}

/* sio_copy - Copy len chars from fmt to s (by Ding Rui) */
void sio_copy(char *s, const char *fmt, size_t len)
{
    if (!len)
        return;

    for (size_t i = 0; i < len; i++)
        s[i] = fmt[i];
}

/* Public Sio functions */
ssize_t sio_puts(char s[]) /* Put string */
{
    return write(STDOUT_FILENO, s, sio_strlen(s));
}

ssize_t sio_putl(long v) /* Put long */
{
    char s[128];

    sio_ltoa(v, s, 10); /* Based on K&R itoa() */
    return sio_puts(s);
}

ssize_t sio_put(const char *fmt, ...) // Put to the console. only understands %d
{
    va_list ap;
    char str[MAXLINE]; // formatted string
    char arg[128];
    const char *mess = "sio_put: Line too long!\n";
    int i = 0, j = 0;
    int sp = 0;
    int v;

    if (fmt == 0)
        return -1;

    va_start(ap, fmt);
    while (fmt[j])
    {
        if (fmt[j] != '%')
        {
            j++;
            continue;
        }

        sio_copy(str + sp, fmt + i, j - i);
        sp += j - i;

        switch (fmt[j + 1])
        {
        case 0:
            va_end(ap);
            if (sp >= MAXLINE)
            {
                write(STDOUT_FILENO, mess, sio_strlen(mess));
                return -1;
            }

            str[sp] = 0;
            return write(STDOUT_FILENO, str, sp);

        case 'd':
            v = va_arg(ap, int);
            sio_ltoa(v, arg, 10);
            sio_copy(str + sp, arg, sio_strlen(arg));
            sp += sio_strlen(arg);
            i = j + 2;
            j = i;
            break;

        case '%':
            sio_copy(str + sp, "%", 1);
            sp += 1;
            i = j + 2;
            j = i;
            break;

        default:
            sio_copy(str + sp, fmt + j, 2);
            sp += 2;
            i = j + 2;
            j = i;
            break;
        }
    } // end while

    sio_copy(str + sp, fmt + i, j - i);
    sp += j - i;

    va_end(ap);
    if (sp >= MAXLINE)
    {
        write(STDOUT_FILENO, mess, sio_strlen(mess));
        return -1;
    }

    str[sp] = 0;
    return write(STDOUT_FILENO, str, sp);
}

void sio_error(char s[]) /* Put error message and exit */
{
    sio_puts(s);
    _exit(1);
}

/*
 * Signal - wrapper for the sigaction function
 */
handler_t *Signal(int signum, handler_t *handler)
{
    struct sigaction action, old_action;

    action.sa_handler = handler;
    sigemptyset(&action.sa_mask); /* block sigs of type being handled */
    action.sa_flags = SA_RESTART; /* restart syscalls if possible */

    if (sigaction(signum, &action, &old_action) < 0)
        unix_error("Signal error");
    return (old_action.sa_handler);
}

运行make./sdiver命令,得到以下结果:

提交至AutoLab,最终验证获得满分。

于是,完成了ICS的第六个Lab,Congratulations!

笔者注:不要忘了往tsh.c中加上注释,并上传至AutoLab喔~

参考资料

Arthals-更适合北大宝宝体质的Tsh Lab踩坑记

本博客已稳定运行
发表了43篇文章 · 总计265.43k字
使用 Hugo 构建
主题 StackJimmy 设计