CS:APP-Shell Lab
总体要求
实现一个小小的shell
程序,涉及进程调度,信号处理,并发处理的知识,需要掌握教材第8章。
话不多说,打开电脑,带上键盘,开启实验!
一些说明
源文件已经帮我们实现了一部分函数,需要我们完成剩余的函数:
-
eval
:主要用于解析和解释命令行(70行左右) -
builtin_cmd
:识别并解释内置命令:quit、fg、bg 和 jobs(25行左右) -
do_bgfg
: 实现内置命令bg
和fg
. [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
程序能否可以正确处理来自其他进程的SIGSTP
和SIGINT
信号
完成代码
请移步【仓库】
总结
本实验涉及内容比较多,建议先看几遍教材的第8章,然后再做实验,对照着书来做。
本文由「黄阿信」创作,创作不易,请多支持。
如果您觉得本文写得不错,那就点一下「赞赏」请我喝杯咖啡~
商业转载请联系作者获得授权,非商业转载请附上原文出处及本链接。
关注公众号,获取最新动态!