实验答案托管在我的GitHub

这一个月一来一直在看《深入理解计算机系统》的后几章,终于全部看完了(除了第4章:处理器体系结构外)。然后开始集中处理实验,以下是Shell Lab的实验解答。

实验简介

Shell Lab - Writing Your Own Unix Shell主要是有关于进程、信号以及异常处理的实验,对应于书本的第8章:异常控制流。主要通过实现一个带作业控制的Unix Shell来熟悉进程以及信号的概念。

本实验已经给出了Tiny Shell的源程序的基本框架(tsh.c),你需要做的是填充该框架中的eval builtin_cmd do_bgfg waitfg sigchld_handler sigint_handler sigtstp_handler等函数。使得编译后的Shell具有相应的功能。

关于本实验的具体介绍详见实验讲义

Shell介绍

Shell是一个代表用户运行程序的命令行解释器。一个Shell周期性的打印一个提示符,从标准输入流等待一条命令行输入,然后根据命令行的输入执行相应的功能。

一条命令行输入是由空格分隔ASCII文本词(words)。命令行输入的第一个词要不然是一个内建命令(built-in command)要不然是一个可执行文件的路径。剩余的词是命令行参数。如果第一个词是内建命令,Shell立刻在当前进程中执行该命令。否则,词会被假设为一个可执行程序的路径。在这种情况下,Shell会fork出一个子进程,在子进程的上下文中加载并运行这个程序。被创建的子进程被称作任务(job)。总的来说,一个任务可以包含通过Unix管道(Unix Pipe)连接的多个子进程。

如果命令行输入以'&'结尾,那么这个任务将会在后台执行,这意味着Shell在打印下一个提示符并等待下一条命令行输入之前不会等待当前任务终止。在默认情况下,任务运行在前台,这意味这Shell在下一条命令行输入之前会等待当前任务终止。在任何的情况下, 只能有一个任务运行在前台,但是,可以有多个任务运行在后台。

例如,输入tsh> jobs使得Shell执行内建的jobs命令,输入tsh> /bin/ls -l -d则在前台运行ls程序,同时,这个程序的argc == 3,argv[0] == “/bin/ls”a,argv[1] == “-l”,argv[2] == “-d”。相应地,输入tsh> /bin/ls -l -d &则会在后台运行ls程序。

Unix Shell支持作业控制的概念,这允许用户将任务在前台和后台间来回移动,并且改变一个任务中进程的状态(运行,停止,终止)。按下ctrl-c将会发送SIGINT信号到当前前台任务的每一个进程中。按下ctrl-z将会发送SIGTSTP信号到前台任务的每一个进程中,SIGTSTP信号的默认功能是将进程设置为停止状态,直到其被一个SIGCONT信号唤醒。Unix Shell提供了不同的内建命令来支持作业控制,如:

  • jobs - 打印运行的和停止的后台任务
  • bg - 将一个停止的后台任务转变为一个运行的后台任务
  • fb - 将一个运行的或是停止的后台任务转变为一个运行的前台任务
  • kill - 终止一个任务

实验要求

所实现的tsh shell应当具有如下的特性:

  • 提示符应当为"tsh> "
  • 用户的命令行输入应当包括一个名字和0个或多个参数,均由一个或多个空格隔开。如果名字是一个内建命令,那么tsh应当立刻处理它并且等待下一个命令行输入。否则,tsh就会假设它是一个可执行文件的路径,并且在一个独立的子进程中加载并执行它
  • tsh不需要支持管道(|)和I/O重定向(>和<)
  • 按下ctrl-c(ctrl-z)将会导致一个SIGINT(SIGTSTP)信号被发送至当前的前台任务,如果现在没有前台任务,那么这些信号将没有效果
  • 如果命令行输入以一个'&'结束,那么tsh将会在后台运行这个任务,否则它会在前台运行这个任务
  • 每一个任务都可以被一个进程ID(PID)或是一个被tsh分配的正整数的任务ID(JID)唯一的标识。JIDS可以被前缀'%'标识,例如,'%5'就代表了JID 5,而'5'就代表了PID 5
  • tsh应当提供如下的内建命令:
    • quit命令直接终止shell
    • jobs命令列出所有在后台运行的任务
    • bg 命令通过给发送SIGCONT信号将其重启,然后将其在后台运行。参数既可以是一个PID,也可以是一个JID
    • fg 命令通过给发送SIGCONT信号将其重启,然后将其在前台运行。参数既可以是一个PID,也可以使一个JID
  • tsh应当回收它的所有僵死子进程。如果任何任务因为收到了一个未被捕获的信号而终止,那么tsh应当识别对应的事件并且输出相应的信息

实验评测

  1. 参考答案 - 实验提供了一个作为参考的tshref可执行文件作为tsh的参考。你的tsh应当提供和tshref一致的输出(除了PIDs以外)
  2. Shell驱动 - sdriver.pl程序将shell作为一个子进程来执行,根据trace file来向它发送命令和信号,并且将shell的输出捕获并输出。实验总共提供了16个trace file。关于sdriver的具体用法请参考实验讲义。

实验提供了tshref.out作为16个trace file在tshref程序下的参考输出。为了方便比较,写了一个小的(可能会有很多错误)的shell脚本gather_output.sh用来生成这些trace file在tsh下的输出,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/usr/bin bash

output_file=tsh.out

echo "" > tsh.out

echo -e "\n" >> tsh.out

for trace_file in ./*.txt
do
echo -e "`./sdriver.pl -t $trace_file -s ./tsh -a "-p"`" >> tsh.out
done

echo -e "\n" >> tsh.out

实验思路

最早在做这个实验的时候一直没有什么头绪,即使已经参考了实验讲义,使用trace file来指导编程(trace file的难度自简单到复杂)。后来还是先好好的阅读了一遍给出的框架源代码,整理如下。

框架代码分析

描述
MAXLINE 定义了命令行1行允许的最大字符数 1024
MAXARGS 定义了命令行所允许的最大参数个数 128
MAXJOBS 定义了命令行同一时间允许的最多任务个数 16
MAXJID 定义了最大的任务ID 1<<16
UNDEF 未定义的状态 0
FG 任务运行在前台 1
BG 任务运行在后台 2
ST 任务停止 3

允许如下的状态转换
FG -> ST : ctrl-z
ST -> FG : fg命令
BG -> FG : fg命令
ST -> BG : bg命令

结构体

1
2
3
4
5
6
struct job_t {              //任务
pid_t pid; //任务的PID
int jid; //任务JID [1, 2, ...]
int state; //状态 - 包括UNDEF, BG, FG, ST
char cmdline[MAXLINE]; //对应的命令行输入
};

全局变量

变量名 变量类型 描述
environ char ** 定义在libc中 环境变量
prompt char[] 命令行提示符
verbose int 为1时输出额外信息
nextjid int 下一个待分配的JID
subf char[MAXLINE] 用于生成sprintf信息
jobs struct job_t [MAXJOBS] 全局任务表

函数列表

函数名 函数原型 描述
parseline int parseline(const char *cmdline, char **argv) 接收命令行输入以及参数列表,解析输入并且将解析到的参数存入参数列表,并且判断该命令是否以’&’结尾,是则返回1,否则返回0
sigquit_handler void sigquit_handler(int sig) SIGQUIT的信号处理函数
clearjob void clearjob(struct job_t *job) 将对应任务的PID,JID清零,状态设置为UNDEF,并且将命令行输入清空
initjobs void initjobs(struct job_t *jobs) 将任务列表中的所有任务清空
maxjid int maxjid(struct job_t *jobs) 返回任务列表中的最大JID
addjob int addjob(struct job_t jobs, pid_t pid, int state, char cmdline) 将给定任务添加至任务列表,如果nextjid超过了MAXJOBS,则将其重置为1
deletejob int deletejob(struct job_t *jobs, pid_t pid) 将对应任务从任务列表中删除,并将nextjid自增1
fgpid pid_t fgpid(struct job_t *jobs) 返回当前前台任务的PID,如果没有则返回0
getjobpid struct job_t getjobpid(struct job_t jobs, pid_t pid) 按照pid查询任务 如果没有则返回NULL
getjobjid struct job_t getjobjid(struct job_t jobs, int jid) 按照jid查询任务 如果没有则返回NULL
pid2jid int pid2jid(pid_t pid) 根据pid返回其jid,没有则返回0
listjobs void listjobs(struct job_t *jobs) 打印任务队列
usage void usage(void) 打印帮助信息
unix_error void unix_error(char *msg) 打印系统级错误信息并结束进程
app_error void app_error(char *msg) 打印应用级错误信息并结束进程
Signal handler_t Signal(int signum, handler_t handler) signal的包装函数
eval void eval(char *cmdline) 求值函数
builtin_cmd int builtin_cmd(char **argv) 处理内建函数
do_bgfg void do_bgfg(char **argv) 实现内建的bg和fg
waitfg void waitfg(pid_t pid) 等待前台的任务完成
sigchld_handler void sigchld_handler(int sig) SIGCHLD的信号处理程序
sigtstp_handler void sigtstp_handler(int sig) SIGTSTP的信号处理程序
sitint_handler void sigint_handler(int sig) SIGINT的信号处理程序

实验答案

为了简化篇幅,这里只附上几个相关的函数,已实现函数以及包装函数等就不一并附上。如有需要可以去GitHub中查看。

实验答案为我自己的实验解法,很可能不是最优解法,请不要直接抄袭。

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
void eval(char *cmdline)
{
char * argv[MAXARGS];
char buf[MAXLINE];
int bg; /* if run in background */
pid_t pid;

sigset_t mask_all, mask_chld, prev_chld;

/*initialize signal sets*/
Sigfillset(&mask_all);
Sigemptyset(&mask_chld);
Sigaddset(&mask_chld, SIGCHLD);

strcpy(buf, cmdline);
bg = parseline(buf, argv); /* parse the command line */
if (argv[0] == NULL) {
return;
} /* handles if there is empty input */

if (!builtin_cmd(argv)) { /* if the command line is builtin command */
Sigprocmask(SIG_BLOCK, &mask_chld, &prev_chld); /* block SIGCHLD to avoid potential race between addjob and deletejob */
if ((pid = Fork()) == 0) {
Setpgid(0, 0); /* give the child process a new group id to handle SIGINT correctly */
Sigprocmask(SIG_SETMASK, &prev_chld, NULL); /* unblock SIGCHLD */
Execve(argv[0], argv, environ);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* mask all signals to avoid potential races */
addjob(jobs, pid, bg + 1, cmdline); /* add job to joblist */
Sigprocmask(SIG_SETMASK, &prev_chld, NULL);
if (!bg) { /* if the process should be exeuted in foreground */
waitfg(pid); /* fg wait explictly */
} else {
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); /* bg print message*/
}
}

return;
}

int builtin_cmd(char **argv)
{
/* handles the builtin command in current context */
if (!strcmp(argv[0], "quit")) {
exit(0); /* quit: exit directly*/
}
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs); /* jobs: print the current job list */
return 1;
}
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv); /* bg|fg: handles in function do_bgfg */
return 1;
}
if (!strcmp(argv[0], "&")) {
return 1; /* returns 1 for empty & */
}
return 0; /* not a builtin command */
}

void do_bgfg(char **argv)
{
int number = 0; /* used to store the converted number */
char * ptr = argv[1]; /* get the pointer to argument 1 */
char * endptr = NULL; /* used for error handling */
struct job_t * job = NULL; /* used to store job pointer */
if (!argv[1]) { /* returns if missing argument */
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if (*ptr == '%') { /* if argument 1 is job ID */
ptr++; /* adjust pointer */
number = strtol(ptr, &endptr, 10);
if (ptr == endptr) { /* handles convert error */
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
job = getjobjid(jobs, number); /* get the job */
if (!job) {/* handles if there is no such job */
printf("%%%d: No such job\n", number);
return;
}
} else {/* if argument 1 is pid */
number = strtol(ptr, &endptr, 10);
if (ptr == endptr) { /* handles convert error */
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
job = getjobpid(jobs, (pid_t)number); /* get the job */
if (!job) {/* handles if there is no such job */
printf("(%d): No such process\n", number);
return;
}
}
/* change process state and update the job list*/
if (!strcmp(argv[0], "bg")) { /*bg: turns ST to BG, send SIGCONT */
if(job->state == ST) {
Kill(-(job->pid), SIGCONT);
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
job->state = BG;
}
} else if (!strcmp(argv[0], "fg")) {/* fg: turns ST/BG to FG, sent SIGCONT */
if(job->state == ST) {
Kill(-(job->pid), SIGCONT);
job->state = BG;
}
if(job->state == BG) {
job->state = FG;
waitfg(job->pid);
}
}
return;
}

void waitfg(pid_t pid)
{
while(fgpid(jobs)) { /* handles all zombie processes in signal handler */
sleep(0.01); /* busy loop only */
}
return;
}

void sigchld_handler(int sig)
{
int olderrno = errno; /* store old errno */
int status; /* used to trace pid's status */
sigset_t mask_all, prev_all;
pid_t pid;

Sigfillset(&mask_all);

while((pid = Waitpid(-1, &status, WUNTRACED | WNOHANG)) > 0) { /* waitpid without waiting(WNOHANG) */
if (WIFSTOPPED(status) && (WSTOPSIG(status) == SIGTSTP || WSTOPSIG(status) == SIGSTOP)) { /* if the process is stopped */
struct job_t * job = getjobpid(jobs, pid);
if (job && job->state != ST) { /* if the stop signal hasn't been catched */
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
struct job_t * stpjob = getjobpid(jobs, pid);
stpjob->state = ST;/* handles here */
}
}
if (WIFSIGNALED(status) && WTERMSIG(status) == SIGINT) { /* it the terminate signal hasn't been catched */
struct job_t * job = getjobpid(jobs, pid);
if (job) {
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
/* handles here */
}
}
if (WIFEXITED(status) || WIFSIGNALED(status)) {
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(jobs, pid);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}/* remove the job in job list accordingly */
}
errno = olderrno;
return;
}

void sigint_handler(int sig)
{
int olderrno = errno; /* store old errno */
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* block all signal in case the race between SIGINT and SIGCHLD */

pid_t pid;
pid = fgpid(jobs);
Kill(-pid, SIGINT); /* kill processes in fg job's process group */

printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, sig); /* print the message */

deletejob(jobs, pid); /* delete job in joblist */

Sigprocmask(SIG_SETMASK, &prev_all, NULL);

errno = olderrno;
return;
}

void sigtstp_handler(int sig)
{
int olderrno = errno; /* store old errno */
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* block all signal in case the race between SIGTSTP and SIGCHLD */

pid_t pid;
pid = fgpid(jobs);
Kill(-pid, SIGTSTP); /* send SIGTSTP to fg job's process group */

printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, sig); /* print the message */

struct job_t * stpjob = getjobpid(jobs, pid);
stpjob->state = ST; /* modify the job list */

Sigprocmask(SIG_SETMASK, &prev_all, NULL);

errno = olderrno;
return;
}

实验总结

总的来说不难的一个实验,关键在于要先理解整个框架中的代码,然后根据trace file渐进地完成程序。需要注意的地方实验讲义中的提示以及书本中都已经给出,这里不再赘述。需要强调的是SIGCHLD的信号处理程序需要处理未捕获的SIGTSTP和SIGINT信号。此外SIGINT/SIGTSTP和SIGCHLD的信号处理程序之间可能会有潜在的导致错误的冲突。(信号处理程序是可以被其他信号中断的,可以见trace16.txt)

此外,handler中的printf是异步不安全的,不推荐使用。以及书本中虽然使用了sigsuspend来实行同步,但是为了简化程序,根据实验讲义的提示,使用了忙循环处理前台等待,并且将回收僵死进程任务交给了sigchld_handler。这些都是本次实验中不足和可以修改的地方。