CSAPP Lab5 解题分析

这个实验是要实现一个简易的shell程序,主要考虑的是异常和信号的处理。

前期工作

解压缩shlab-handout.tar,执行make clean | make,就可以得到编译好的文件。

文件夹中的tshref是正确的结果,可以用来验证。与其用

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

来测试,我觉得实验手册里提到的另一种方法更好用,

1
make test01

这是对自己写的tsh进行测试,要想对tshref进行测试,只需要在后面的test前面添加一个r即可,也就是make rtest01

需要实现的函数

  • void eval(char *cmdline) 执行用户输入的命令
  • int builtin_cmd(char **argv) 执行内部命令
  • void do_bgfg(char **argv) 执行bg和fg命令
  • void waitfg(pid_t pid) block进程直到它不再是前台进程
  • void sigchld_handler(int sig) SIGCHLD信号处理
  • void sigint_handler(int sig) SIGINT信号处理
  • void sigtstp_handler(int sig) SIGTSTP信号处理

builtin_cmd

这是比较好写的一个函数,我选择第一个把他完成。按照实验文档说的,当输入quit时,退出tsh。输入jobs就列出存在的job。输入fg/bg是前台后台执行命令。

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
/* 
* 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")) // quit
exit(0);
if (!strcmp(argv[0], "jobs")) // jobs
{
listjobs(jobs);
return 1;
}

if (!strcmp(argv[0], "echo")) // echo
{
printf("%s\n", argv[1]);
return 1;
}

if(!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // bg or fg
{
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}

大概闲着无聊,添加了个最简易最简易的echo233.

eval

执行函数算是基础,参考书中图8-23中的eval,可以大概知道eval函数应该是什么样子的。但那只是简易并有缺陷的shell,它并不能回收后台子进程,所以之后就要用到信号的相关知识。关于阻塞信号,可以用到以下三个函数:

  • int sigemptyset(sigset_t *set);
  • int sigaddset(sigset_t *set, int signum);
  • int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

这类似线程锁,SIG_BLOCK和SIG_UNBLOCK用来加锁和解除锁。是用sigprocmask来同步进程,可以保证父进程在相应的deletejob之前执行了addjob。

另外eval还要处理job的添加,这分前台执行和后台执行。

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
/* 
* 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;

strcpy(buf, cmdline);
bg = parseline(buf, argv);
if(argv[0] == NULL)
return;

if(!builtin_cmd(argv)) {
// Block SIGCHLD
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);


// Child process ,set new process group, run command
if((pid = fork()) == 0) {
if(setpgid(0, 0) < 0) {
unix_error("setpgid error");
}

if (execve(argv[0], argv, environ) < 0)
{
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}
else {
if(!bg){
addjob(jobs, pid, FG, cmdline); // add job to job list as fg
}
else {
addjob(jobs, pid, BG, cmdline); // add job to job list as bg
}
sigprocmask(SIG_UNBLOCK, &mask, NULL); // Parent unblocks SIGCHLD


if (!bg)
waitfg(pid);
else
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}
}
}

waitfg

waitfg就是要等待前台进程结束,所以只需要用循环即可

1
2
3
4
5
6
7
8
/* 
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs));
return;
}

do_bgfg

这是前台和后台的切换,一些条件细节就不多说,其中主要是JID和PID的判断。bg job 给后台 job 发送 SIGCONT 信号来继续执行该任务,fg job 给前台 job 发送 SIGCONT 信号来继续执行该任务。首先要判断进程是否继续执行。关于前后台的执行,主要做的就是修改job的state状态。

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
/* 
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
struct job_t *job;
char *id = argv[1];
int jid;

// If id does not exist
if(id == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}

// For a JID
if(id[0] == '%') {
jid= atoi(&id[1]);
if(!(job= getjobjid(jobs, jid))) {
printf("%s: No such job\n", id);
return;
}
}
// For a PID
else if(isdigit(id[0])) {
pid_t pid= atoi(id);

if(!(job= getjobpid(jobs, pid))) {
printf("(%d): No such process\n", pid);
return;
}

}

else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}

// Continue
if(kill(-(job->pid), SIGCONT) < 0) {
if(errno != ESRCH){
unix_error("kill error");
}
}

// To determine the bg and fg
if(!strcmp("fg", argv[0])) {
job->state = FG;
waitfg(job->pid);
}
else if(!strcmp("bg", argv[0])) {
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
job->state = BG;
}
else {
printf("bg/fg error: %s\n", argv[0]);
}
}

接下来就是比较麻烦的三个信号handler的处理。

sigint_handler

sigint_handler是主要的处理,也是比较简单的。它用来捕获键盘的Ctrl-C。当捕获到终止信号,它会查询当前前台执行的job的pid,使用kill来终止程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/* 
* 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)
{
pid_t pid = fgpid(jobs);
int jid = pid2jid(pid);
if (pid != 0)
{
if (kill(-pid, SIGKILL) < 0) // kill pid
{
unix_error("error when kill in sigint_handler");
return;
}
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, sig);
deletejob(jobs, pid);
}
return;
}

sigtstp_handler

同样,sigtstp_handler是捕获Ctrl-Z的挂起信号,通过获取当前前台job的pid,修改其state为ST,接着使用kill挂起进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
* 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 pid = fgpid(jobs);
int jid = pid2jid(pid);

if (pid != 0) {

printf("Job [%d] (%d) Stopped by signal %d\n", jid, pid, sig);
getjobpid(jobs, pid)->state = ST;
kill(-pid, SIGTSTP);
}
return;
}

sigchld_handler

sigchld_handler是用来捕获 SIGCHLD 信号的,比起其他两个信号处理函数,这个是用来回收进程的,可能会稍微有点麻烦。参考书中8.4.3相关内容,用到了WIFSTOPPED, WIFSIGNALED,WIFEXITED,分别代表判断进程被停止, 被未捕获的信号停止,exit的停止。其实就是回收过程中的僵尸进程的回收,Ctrl-Z、Ctrl-C信号下进程的回收。

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
/* 
* 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)
{
int status;
pid_t pid;

// Waiting for handling all of the child processes according to their status
while ((pid = waitpid(fgpid(jobs), &status, WNOHANG|WUNTRACED)) > 0) {
if (WIFSTOPPED(status)){
getjobpid(jobs, pid)->state = ST;
printf("[%d] Stopped %s\n", pid2jid(pid), jobs->cmdline);
}
else if (WIFSIGNALED(status)){
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs,pid);
}
else if (WIFEXITED(status)){
deletejob(jobs, pid);
}
}

if (errno != ECHILD) {
unix_error("waitpid error");
}

return;
}

其他

虽然job事务处理没有让我们写,但实验给出的源代码写的很好,很是值得读一读。