Skip to content

Shell Lab

Posted on:2022.02.04

TOC

Open TOC

Shell Lab

http://csapp.cs.cmu.edu/3e/shlab.pdf

Basic

Specification

tsh → tiny shell

项目结构

Makefile # Compiles your shell program and runs the tests
README # This file
tsh.c # The shell program that you will write and hand in
tshref # The reference shell binary.
# The remaining files are used to test your shell
sdriver.pl # The trace-driven shell driver
trace*.txt # The 15 trace files that control the shell driver
tshref.out # Example output of the reference shell on all 15 traces
# Little C programs that are called by the trace files
myspin.c # Takes argument <n> and spins for <n> seconds
mysplit.c # Forks a child that spins for <n> seconds
mystop.c # Spins for <n> seconds and sends SIGTSTP to itself
myint.c # Spins for <n> seconds and sends SIGINT to itself

我们需要修改 tsh.c

有一个参考程序 tshref 和一个 shell 驱动程序 sdriver.pl

驱动程序的用法如下

Usage: ./sdriver.pl [-hv] -t <trace> -s <shellprog> -a <args>
Options:
-h Print this message
-v Be more verbose
-t <trace> Trace file
-s <shell> Shell program to test
-a <args> Shell arguments
-g Generate output for autograder

键入如下命令进行 trace01 的测试

$ ./sdriver.pl -t trace01.txt -s ./tsh -a "-p"

Makefile 中提供了

$ make test01

还可以驱动参考程序

$ ./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
$ make rtest01

tshref.out 则给出了所有的参考程序的输出

trace01

直接就能过

trace02

处理一下内置命令 quit 即可

调用 parseline

trace03

需要在前台执行 /bin/echo

fork 出一个子进程并 execve

主进程还需要 waitpid

关键在于 waitpid 放在 sigchld_handler 中还是 waitfg 中

注意 fork / addjob / deletejob 的周围需要进行信号的阻塞

可以开启 verbose 进一步观察行为是否一致

trace04

需要在后台执行 ./myspin

这个程序的行为是 sleep

trace05

需要实现 jobs 内置命令

直接调用 listjobs 即可

trace06

需要实现 sigint_handler

<C-c> 会发送一个 SIGINT 信号到前台进程组中的每个进程

这里需要理解 job 的分发

/bin/echo -e tsh> ./myspin 4
./myspin 4

这两个 job 都是前台作业,/bin/echo 为 job [1],然后进入 sigchld_handler 被删除,所以 ./myspin 也为 job [1]

响应了 SIGINT 后,./myspin 被终止,随即进入 sigchld_handler 删除该 job,所以 waitfg 不能只用一个 pause 来实现

trace07

同时创建前台和后台作业,测试是否只对前台作业 SIGINT

正确实现 trace06 后,本阶段可以直接过

由于输出较长,考虑使用 diff 工具判断一致性

diff-so-fancy

尝试优化 Makefile,将 trace 索引作为参数传入

https://stackoverflow.com/questions/2826029/passing-additional-variables-from-command-line-to-make

.PHONY: clean
NO =
FILENAME := $(join trace, $(NO))
FILENAME := $(addprefix $(FILENAME), .txt)
test: $(FILES)
$(DRIVER) -t $(FILENAME) -s $(TSH) -a $(TSHARGS)
rtest: $(FILES)
$(DRIVER) -t $(FILENAME) -s $(TSHREF) -a $(TSHARGS)
output: $(FILES)
mkdir -p tmp
$(DRIVER) -t $(FILENAME) -s $(TSH) -a $(TSHARGS) > tmp/output
$(DRIVER) -t $(FILENAME) -s $(TSHREF) -a $(TSHARGS) > tmp/outputref
diff -u tmp/output tmp/outputref | diff-so-fancy
# clean up
clean:
rm -f $(FILES) *.o *~
rm -rf tmp

trace08

同时创建前台和后台作业,测试是否只对前台作业 SIGTSTP

发现前面写的有点问题:

trace09 & 10

实现内置命令 bg 和 fg

根据第二个参数来获取对应的 job,并发送 SIGCONT 信号

trace11 & 12

前台作业会创建子进程,它们共同构成了前台进程组

测试向前台进程组的每个进程发送 SIGINT 信号或 SIGTSTP 信号

可以直接过

trace13

尝试重启前台进程组的每个进程

可以直接过

trace14

对 do_bgfg 进行简单的错误处理

trace15

大杂烩

可以直接过

trace16

使用 mystop 和 myint 在其他进程中对 tsh 发出信号

通过 getpid 得到 tsh 的 pid,即调用者

最大的区别是 tsh 的 sigint_handler 和 sigtstp_handler 不会响应该信号(不是由 tsh 发出)

tsh 发现僵死进程后,直接进入 sigchld_handler

可以直接过

总结

最关键的地方在前半段对进程组的理解

tsh 利用进程组的概念,通过信号机制,实现了对进程的管理与控制,使进程可以呈现并发运行的假象(底层机制为上下文切换)

tsh 本质上是一个并发程序

为了做这个 lab,重新配置了 Vim,期间还尝试了 diff-so-fancy,优化了 Makefile 来更好的验证正确性

开启 verbose,行为与 tshref 完全一致

发现在 Vim 或 Makefile 中执行命令行,是没有 .bashrc 中的配置的

最后要仔细看书,程序中的所有细节均能从书中找到

代码

实际上不到 300 行

/*
* 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 eval(char *cmdline) {
char *argv[MAXARGS];
char buf[MAXLINE];
int bg;
pid_t pid;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if (argv[0] == NULL)
return;
if (!builtin_cmd(argv)) {
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
if ((pid = Fork()) == 0) {
Setpgid(0, 0);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
if (!bg) {
addjob(jobs, pid, FG, cmdline);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
waitfg(pid);
} else {
addjob(jobs, pid, BG, cmdline);
printf("[%u] (%u) %s", pid2jid(pid), pid, cmdline);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
}
return;
}
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv) {
if (!strcmp(argv[0], "quit"))
exit(0); // exit or _exit?
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
}
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}
return 0; /* not a builtin command */
}
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv) {
int olderrno = errno;
if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
errno = olderrno;
return;
}
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
int isbg = (!strcmp("bg", argv[0]));
int isjid = (argv[1][0] == '%');
const char *str = argv[1] + (isjid ? 1 : 0);
char *end;
unsigned long id = strtoul(str, &end, 10);
if (!strcmp(end, str)) {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
errno = olderrno;
return;
}
if (errno == ERANGE) {
printf("%s: argument out of range\n", argv[0]);
errno = olderrno;
return;
}
struct job_t *(*getjob)(struct job_t *, int) =
(isjid) ? getjobjid : getjobpid;
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjob(jobs, id);
if (job == NULL) {
if (isjid)
printf("%%%ld: No such job\n", id);
else
printf("(%ld): No such process\n", id);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
return;
}
job->state = (isbg) ? BG : FG;
kill(-(job->pid), SIGCONT);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (!isbg)
waitfg(job->pid);
else
printf("[%u] (%u) %s", job->jid, job->pid, job->cmdline);
errno = olderrno;
return;
}
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) {
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
while (1) {
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjobpid(jobs, pid);
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
if (job == NULL || job->state != FG)
break;
Sleep(1);
}
if (verbose)
printf("waitfg: Process (%u) no longer the fg process\n", pid);
}
/*****************
* 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 or SIGTSTP 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) {
if (verbose)
printf("sigchld_handler: entering\n");
int olderrno = errno;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
int status;
pid_t pid = Waitpid(-1, &status, WNOHANG | WUNTRACED);
if (pid == 0) {
errno = olderrno;
if (verbose)
printf("sigchld_handler: exiting\n");
return;
}
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
struct job_t *job = getjobpid(jobs, pid);
assert(job != NULL);
int jid = job->jid;
if (WIFSTOPPED(status)) {
job->state = ST;
printf("Job [%u] (%u) stopped by signal %u\n", jid, pid, WSTOPSIG(status));
} else {
deletejob(jobs, pid);
if (verbose)
printf("sigchld_handler: Job [%u] (%u) deleted\n", jid, pid);
if (WIFSIGNALED(status)) {
printf("Job [%u] (%u) terminated by signal %u\n", jid, pid,
WTERMSIG(status));
} else if (WIFEXITED(status)) {
if (verbose)
printf("sigchld_handler: Job [%u] (%u) terminates OK (status %u)\n",
jid, pid, WEXITSTATUS(status));
}
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
if (verbose)
printf("sigchld_handler: exiting\n");
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) {
if (verbose)
printf("sigint_handler: entering\n");
int olderrno = errno;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs);
if (pid == 0) {
printf("sigint_handler: no current foreground job\n");
assert(0);
} else {
kill(-pid, sig);
if (verbose)
printf("sigint_handler: Job (%u) killed\n", pid);
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
if (verbose)
printf("sigint_handler: exiting\n");
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) {
if (verbose)
printf("sigtstp_handler: entering\n");
int olderrno = errno;
sigset_t mask_all, prev_all;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
pid_t pid = fgpid(jobs);
if (pid == 0) {
printf("sigtstp_handler: no current foreground job\n");
assert(0);
} else {
kill(-pid, sig);
if (verbose)
printf("sigtstp_handler: Job [%u] (%u) stopped\n", pid2jid(pid), pid);
}
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
errno = olderrno;
if (verbose)
printf("sigtstp_handler: exiting\n");
return;
}