0%

CSAPP Shell Lab Writeup

Prerequisites

Signal

Signal是一种内核与进程之间交互的信息。诸如熟悉的Ctrl+C、Ctrl+Z等终止程序或休眠程序的实质都是发送相应的signal。Signal是异常控制流的其中一种,可以在本地异步运行。

贴一些可能见得多的signal:

  • SIGABRT: abort
  • SIGFPE: floating point exception
  • SIGINT: interrupt (when pressing Ctrl+C)
  • SIGSEGV: segment violation (a.k.a. segment fault)
  • SIGTSTP: stopped (when pressing Ctrl+Z)
  • SIGCHLD: Terminated or stopped child

Relative C Functions

在命令行里面,如果要给一个进程(或进程组)添加信号,可以用killkill并不只是用来杀进程的,实际上是添加信号。

在C里面也有一个叫kill的函数,需要指定pid和信号,用法大体类似。这就是我们可以用C写shell的原因之一。

kill可以作用于一个进程上,也可以作用于一整个进程组上,具体区别在pid的值:

  • 当pid为正,则作用于单个进程
  • 当pid为负,则作用于这个进程所在的进程组

相信见过像Vim等的一些按Ctrl+C退不出的程序,之所以退不出不是因为SIGINT无法发出,而是因为signal的handler是可以由我们来自定义的。

在C里面,我们可以通过调用signal函数,install一个signal的handler,覆盖原有的默认handler。signal大多数都能覆盖,除了SIGKILLSIGSTOP等。

signal handler需要自己去声明定义一个函数,可以形象地理解成比较底层的try-catch,catch相应的signal,执行handler里面的内容。

如果想要写shell,相信需要在程序里面创建进程,执行命令等等。相应的C函数也介绍一下:

  • int fork(): create a new process (child process). called once, return twice. return 0 in child process, return pid in parent process.
  • int execve(argv[0], argv, environ): load and run another program. called once, never return(if sucessfully executed).
  • void exit(int status): terminate the current process with status. Then it becomes a zombie process, waiting to be reaped by its parent.

如果我们fork了一次,父子两个进程的执行速度是无法确定的,有时儿子快有时儿子慢,这就导致了一次运行的结果可能不同。形象地,我们可以理解成在这种无约束的异步进程中,进程之间会相互比赛,竞争运行速度(卷起来了),我们称这种现象为race。

如果不想出现这种情况,需要显式地叫其中一个进程去等其他的进程,这种操作可以用waitpid来完成。

signal可以被屏蔽,即可以选择block掉指定类型的signal,以确保race不会在其中发生。分析可能发生的race是最复杂的部分。

Lab

Introduction

在这个lab里面我们需要实现一个小型的shell,名叫tsh(tiny shell),因为不用实现现代shell的功能,所以功能确实很朴素。

实验需要我们写的只有tsh.c一个文件,需要你定义若干个已经声明的函数。

tsh.c中也温馨地提供了几个helper functions。

用户运行了shell之后,会在stdin等待用户的输入。用户的输入以空格为分隔,第一个称为name,后面则为参数。

如果name是built-in command,则直接在前台运行,运行完直接等待下一个输入。

如果name是一个binary executable,则新开一个进程(或者说是add a new job),在子进程中执行命令。

命令有前台后台运行之分,如果最后带有一个&,则为后台运行,此时shell不用去等待job运行完毕,可以继续执行shell的功能。前台等待job完成的内容需要在waitfg函数之中完成。

这里有一个细节,因为我们的shell是一个程序,而默认我们创建的job会跟shell是同一个进程组,那么如果子进程被杀掉的话shell也会被杀掉,这不符合shell的性质,所以我们需要保证创建的子进程都跟shell自己处于不同的进程组。这部分是通过setpgid(0, 0)来完成的。

在子进程执行完成之后,子进程会变成一个zombie,不再会被执行,但会一直占着地方,它们需要被shell回收。相关的信号是SIGCHLD。这部分的工作是通过定义SIGCHLD的handler来完成的。

pid跟jid的管理我们不用操心,已经写好了,我们只需要调用addjobdeletejob这样的API来维护进程。

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
void eval(char *cmdline) {
char *argv[MAXARGS];
int bg = parseline(cmdline, argv);
if (argv[0] == NULL) return;
int builtin = builtin_cmd(argv);
if (builtin) return;

/* from now on, execute non-builtin command */
sigset_t mask, prev_mask, mask_all;
sigfillset(&mask_all);
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);

sigprocmask(SIG_BLOCK, &mask, &prev_mask); // block SIGCHLD

pid_t pid = fork(); // during fork, SIGCHLD is blocked
if (pid == 0) {
// child process
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // unblock SIGCHLD
setpgid(0, 0); // make sure programs are not in the same group as tsh
// must unblock signals before execve
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found\n", argv[0]);
exit(0);
}
} else {
// parent process
sigprocmask(SIG_BLOCK, &mask_all, NULL); // block all signals
addjob(jobs, pid, bg + 1, cmdline); // during addjob, all signals are blocked
if (bg) {
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
} else {
waitfg(pid);
}
sigprocmask(SIG_SETMASK, &prev_mask, NULL); // reset
}
}

builtin_cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int builtin_cmd(char **argv) {
if (!strcmp(argv[0], "quit")) {
exit(0);
} else if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
} else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
} else if (!strcmp(argv[0], "&")) {
return 1;
} else {
return 0; /* not a builtin command */
}
}

do_bgfg

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
void do_bgfg(char **argv) {

if (argv[1] == NULL) {
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
struct job_t *job = NULL;
if (argv[1][0] == '%') {
int jid = atoi(argv[1] + 1);
job = getjobjid(jobs, jid);
if (job == 0) {
printf("%s: No such job\n", argv[1]);
return;
}
} else if (isdigit(argv[1][0])) {
int pid = atoi(argv[1]);
job = getjobpid(jobs, pid);
if (job == 0) {
printf("(%s): No such process\n", argv[1]);
return;
}
} else {
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
kill(-(job->pid), SIGCONT); // send SIGCONT to the group, therefore negative
if (!strcmp(argv[0], "bg")) {
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
} else {
job->state = FG;
waitfg(job->pid);
}
}

waitfg

1
2
3
4
5
6
7
8
9
10
11

void waitfg(pid_t pid) {
// debug_printf("now in waitfg\n");
sigset_t mask;
sigemptyset(&mask);

while (fgpid(jobs) > 0) {
sigsuspend(&mask);
}
sigprocmask(SIG_SETMASK, &mask, NULL);
}

signal handlers

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
void sigchld_handler(int sig) {
// debug_printf("in SIGCHLD handler");
int prev_errno = errno;

sigset_t mask, prev;
sigfillset(&mask);

pid_t pid;
int status;
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
if (WIFEXITED(status)) {
// normally exited
sigprocmask(SIG_BLOCK, &mask, &prev);
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
} else if (WIFSIGNALED(status)) {
// exited via signal
struct job_t *job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask, &prev);
printf("Job [%d] (%d) Terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status));
deletejob(jobs, pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
} else {
// stopped
struct job_t *job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &mask, &prev);
job->state = ST;
printf("Job [%d] (%d) Stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status));
sigprocmask(SIG_SETMASK, &prev, NULL);
}
}

errno = prev_errno;
}

/*
* 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) {
// debug_printf("in SIGINT handler");
int prev_errno = errno;
pid_t pid = fgpid(jobs);
if (pid > 0) {
kill(-pid, sig);
}
errno = prev_errno;
}

/*
* 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) {
// debug_printf("in SIGTSTP handler");
int prev_errno = errno;
pid_t pid = fgpid(jobs);
if (pid > 0) {
kill(-pid, sig);
}
errno = prev_errno;
}

Result

最后实现的就是可以一个可以跑绝对路径的shell,支持前后台运行以及信号级别的进程交互。

最后发现tshref也连vim都打不开,我就平衡了(