csapp shlab

CSAPP shell Lab

Lab Assignment L5: **Writing Your Own Unix Shell! **

Introduction

The purpose of this assignment is to become more familiar with the concepts of process control and signalling. You’ll do this by writing a simple Unix shell program that supports job control.

Looking at the tsh.c (tiny shell) file, you will see that it contains a functional skeleton of a simple Unix shell. To help you get started, we have already implemented the less interesting functions. Your assignment is to complete the remaining empty functions listed below. As a sanity check for you, we’ve listed the approximate number of lines of code for each of these functions in our reference solution (which includes lots of comments).

  • eval: Main routine that parses and interprets the command line. [70 lines] 解析命令行
  • builtin cmd: Recognizes and interprets the built-in commands: quit, fg, bg, and jobs. [25 lines] 检测是否为内置命令quitfgbgjobs
  • do bgfg: Implements the bg and fg built-in commands. [50 lines] 实现内置命令fgbg
  • waitfg: Waits for a foreground job to complete. [20 lines] 等待前台作业执行完成
  • sigchld handler: Catches SIGCHILD signals. 80 lines] 处理SIGCHILD信号,即子进程停止或者终止
  • sigint handler: Catches SIGINT (ctrl-c) signals. [15 lines] 处理SIGINT信号,即键盘中断ctrl-c
  • sigtstp handler: Catches SIGTSTP (ctrl-z) signals. [15 lines]处理SIGSTP信号,即来自终端的停止信号

The tsh Specification

Your tsh shell should have the following features:

  • The prompt should be the string “tsh> ”.

  • The command line typed by the user should consist of a name and zero or more arguments, all separated by one or more spaces. If name is a built-in command, then tsh should handle it immediately and wait for the next command line. Otherwise, tsh should assume that name is the path of an executable file, which it loads and runs in the context of an initial child process (In this context, the term job refers to this initial child process).

  • tsh need not support pipes (|) or I/O redirection (< and >).

  • Typing ctrl-c (ctrl-z) should cause a SIGINT (SIGTSTP) signal to be sent to the current foreground job, as well as any descendents of that job (e.g., any child processes that it forked). If there is no foreground job, then the signal should have no effect.

  • If the command line ends with an ampersand &, then tsh should run the job in the background. Otherwise, it should run the job in the foreground.

  • Each job can be identified by either a process ID (PID) or a job ID (JID), which is a positive integer assigned by tsh. JIDs should be denoted on the command line by the prefix ’%’. For example, “%5” denotes JID 5, and “5” denotes PID 5. (We have provided you with all of the routines you need for manipulating the job list.)

  • tsh should support the following built-in commands:

    ​ – The quit command terminates the shell.

    ​ – The jobs command lists all background jobs.

    ​ – The bg command restarts by sending it a SIGCONT signal, and then runs it in the background. The argument can be either a PID or a JID.

    ​ – The fg command restarts by sending it a SIGCONT signal, and then runs it in the foreground. The argument can be either a PID or a JID.

  • tsh should reap all of its zombie children. If any job terminates because it receives a signal that it didn’t catch, then tsh should recognize this event and print a message with the job’s PID and a description of the offending signal.

开始冻手

Revisit: 回收子进程

一个终止了但还未被回收的进程称为僵死进程。对于一个长时间运行的程序(比如 Shell)来说,内核不会安排init进程去回收僵死进程,而它虽不运行却仍然消耗系统资源,因此实验要求我们回收所有的僵死进程。

waitpid是一个非常重要的函数,一个进程可以调用waitpid函数来等待它的子进程终止或停止,从而回收子进程,在本实验大量用到,我们必须学习它的用法:

  • 子进程终止
  • 子进程收到信号停止
  • 子进程收到信号重新执行

如果一个子进程在调用之前就已经终止了,那么函数就会立即返回,否则,就会阻塞,直到一个子进程改变状态。

等待集合以及监测那些状态都是用函数的参数确定的,函数定义如下:

1
pid_t waitpid(pid_t pid, int *wstatus, int options);

各参数含义及使用

  • pid:判定等待集合成员
    • pid > 0 : 等待集合为 pid 对应的单独子进程
    • pid = -1: 等待集合为所有的子进程
    • pid < -1: 等待集合为一个进程组,ID 为 pid 的绝对值
    • pid = 0 : 等待集合为一个进程组,ID 为调用进程的 pid
  • options:修改默认行为
    • WNOHANG:集合中任何子进程都未终止,立即返回 0
    • WUNTRACED:阻塞,直到一个进程终止或停止,返回 PID
    • WCONTINUED:阻塞,直到一个停止的进程收到 SIGCONT 信号重新开始执行
    • 也可以用或运算把 options 的选项组合起来。例如 WNOHANG | WUNTRACED 表示:立即返回,如果等待集合中的子进程都没有被停职或终止,则返回值为 0;如果有一个停止或终止,则返回值为该子进程的 PID
  • statusp:检查已回收子进程的退出状态
    • waitpid 会在 status 中放上关于导致返回的子进程的状态信息

Revisit: 并发编程原则

  1. 保存和恢复errno。很多函数会在出错时设置errno,在处理程序中调用这样的函数可能会告饶主程序中其他依赖于errno的部分,解决办法是在进入处理函数时用局部变量保存它,运行完成后再将其恢复
  2. 访问全局数据时,阻塞所有信号。
  3. 不可以用信号来对其它进程中发生的事情计数。未处理的信号是不排队的,即每种类型的信号最多只能有一个待处理信号。举例:如果父进程将要接受三个相同的信号,当处理程序还在处理一个信号时,第二个信号就会加入待处理信号集合,如果此时第三个信号到达,那么它就会被简单地丢弃,从而出现问题
  4. 注意考虑同步错误:竞争。

Revisit: 竞争

竞争的示例

这是一个 Unix Shell 的框架,父进程在一个全局列表中记录子进程,并设置了一个 SIGCHLD 处理程序来回收子进程,乍一看没问题,但是考虑如下可能的事件序列:

  • 第 29 行,创建子进程运行
  • 假设子进程在父进程运行到 32 行,即运行addjob函数之前就结束了,并发送一个 SIGCHLD 信号
  • 父进程接收到信号,运行信号处理程序,调用deletejob函数,而这个job本来就没有添加入列表
  • 返回父进程,调用addjob函数,而这个子进程已经终止并回收了,job早就不存在了

也就是说,在这里,deletejob函数的调用发生在了addjob之前,导致错误。我们称addjobdeletejob存在竞争。

解决方法即在父进程folk之前就阻塞SIGCHLD信号

解决方法

错误处理包装函数

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
/* Error wrapper function */
pid_t Fork(void);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
void Execve(const char *filename, char *const argv[], char *const envp[]);
void Setpgid(pid_t pid, pid_t pgid);
void Kill(pid_t pid, int sig);
pid_t Fork(void){
pid_t pid;
if((pid = fork()) < 0)
unix_error("Fork error");
return pid;
}
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset){
if(sigprocmask(how, set, oldset) < 0)
unix_error("Sigprocmask error");
}
void Sigemptyset(sigset_t *set){
if(sigemptyset(set) < 0)
unix_error("Sigprocmask error");
}
void Sigfillset(sigset_t *set){
if(sigfillset(set) < 0)
unix_error("Sigfillset error");
}
void Sigaddset(sigset_t *set, int signum){
if(sigaddset(set, signum) < 0)
unix_error("Sigaddset error");
}
void Execve(const char *filename, char *const argv[], char *const envp[]){
if(execve(filename, argv, envp) < 0){
printf("%s: Command not found.\n", argv[0]);
}
}
void Setpgid(pid_t pid, pid_t pgid){
if(setpgid(pid, pgid) < 0){
unix_error("Setpid error");
}
}
void Kill(pid_t pid, int sig){
if(kill(pid, sig) < 0){
unix_error("Kill error");
}
}

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
/* 
* 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]; //解析cmdline
int bg; //判断程序是前台还是后台执行
int state; //指示前台还是后台运行状态
pid_t pid; //执行程序的子进程的pid

strcpy(buf, cmdline);
bg = parseline(buf, argv); //解析参数
state = bg? BG:FG;
if(argv[0] == NULL) //空行,直接返回
return;
sigset_t mask_all, mask_one, prev_one;
Sigfillset(&mask_all);
Sigemptyset(&mask_one);
Sigaddset(&mask_one, SIGCHLD);
if(!builtin_cmd(argv)) { //判断是否为内置命令
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); //fork前阻塞SIGCHLD信号
if((pid = Fork()) == 0) { //创建子进程
Sigprocmask(SIG_SETMASK, &prev_one, NULL); //解除子进程的阻塞
Setpgid(0, 0); //创建新进程组,ID设置为进程PID
Execve(argv[0], argv, environ); //执行
exit(0); //子线程执行完毕后一定要退出
}
if(state==FG){
Sigprocmask(SIG_BLOCK, &mask_all, NULL); //添加工作前阻塞所有信号
addjob(jobs, pid, state, cmdline); //添加至作业列表
Sigprocmask(SIG_SETMASK, &mask_one, NULL);
waitfg(pid); //等待前台进程执行完毕
}
else{
Sigprocmask(SIG_BLOCK, &mask_all, NULL); //添加工作前阻塞所有信号
addjob(jobs, pid, state, cmdline); //添加至作业列表
Sigprocmask(SIG_SETMASK, &mask_one, NULL);
printf("[%d] (%d) %s",pid2jid(pid), pid, cmdline); //打印后台进程信息
}
Sigprocmask(SIG_SETMASK, &prev_one, NULL); //解除阻塞
}
return;
}

builtin_cmd

判断是否为内置命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* 
* 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);
if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) {
do_bgfg(argv);
return 1;
}
if (!strcmp(argv[0], "jobs")) {
listjobs(jobs);
return 1;
}
if (!strcmp(argv[0], "&"))
return 1;
return 0; /* not a builtin command */
}

do_bgfg

实现内置命令bg和fg,这两个命令的功能如下:

  • bg <job>:通过向<job>对应的作业发送SIGCONT信号来使它重启并放在后台运行
  • fg <job>:通过向 <job>对应的作业发送SIGCONT信号来使它重启并放在前台运行
  • 输入时后面的参数有%则代表jid,没有则代表pid
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
/* 
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
struct job_t *job = NULL; //要处理的job
int state; //输入的命令
int id; //存储jid或pid
if(!strcmp(argv[0], "bg")) state = BG;
else state = FG;
if(argv[1]==NULL){ //没带参数
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
if(argv[1][0]=='%'){ //说明是jid
if(sscanf(&argv[1][1], "%d", &id) > 0){
job = getjobjid(jobs, id); //获得job
if(job==NULL){
printf("%%%d: No such job\n", id);
return;
}
}
}
else if(!isdigit(argv[1][0])) { //其它符号,非法输入
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
else{ //pid
id = atoi(argv[1]);
job = getjobpid(jobs, id);
if(job==NULL){
printf("(%d): No such process\n", id);
return;
}

}
Kill(-(job->pid), SIGCONT); //重启进程, 这里发送到进程组
job->state = state;
if(state==BG)
printf("[%d] (%d) %s",job->jid, job->pid, job->cmdline);
else
waitfg(job->pid);
return;
}

waitfg

该函数从要求实现阻塞父进程,直到当前的前台进程不再是前台进程了。这里显然要显式的等待信号

解决方法是用sleep函数或sigsuspend函数,该函数相当于

1
2
3
sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

在调用sigsuspend之前阻塞 SIGCHLD 信号,调用时又通过sigprocmask函数,在执行pause函数之前解除对信号的阻塞,从而正常休眠。有同学可能会问了:这里并没有消除竞争啊?如果在第 1 行和第 2 行之间子进程终止不还是会发生永久休眠吗?

这就是sigsuspend与上述代码的不同之处了,它相当于上述代码的原子版本,即第 1 行和第 2 行总是一起发生的,不会被中断!

1
2
3
4
5
6
7
8
9
10
11
12
/* 
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{
sigset_t mask;
Sigemptyset(&mask);
while (fgpid(jobs) != 0){
sigsuspend(&mask); //暂停时取消阻塞,见sigsuspend用法
}
return;
}

信号处理函数

sigchld_handler

回收所有僵死进程

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
/* 
* 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 olderrno = errno; //由于errno是全局变量,注意保存和恢复errno
int status;
pid_t pid;
struct job_t *job;
sigset_t mask, prev;
sigfillset(&mask);
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){ //立即返回该子进程的pid
sigprocmask(SIG_BLOCK, &mask, &prev); //阻塞所有信号
if (WIFEXITED(status)){ //正常终止
deletejob(jobs, pid);
}
else if (WIFSIGNALED(status)){ //因为信号而终止, 打印
printf ("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFSTOPPED(status)){ //因为信号而停止, 打印
printf ("Job [%d] (%d) stoped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
job = getjobpid(jobs, pid);
job->state = ST;
}
sigprocmask(SIG_SETMASK, &prev, NULL);
}
errno = olderrno;
return;
}
sigint_handler

实现一个SIGINT信号处理函数,将信号传给前台程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 
* 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)
{
int olderrno = errno;
int pid;
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev); //jobs为全局变量
if((pid = fgpid(jobs)) != 0){
Sigprocmask(SIG_SETMASK, &prev, NULL);
Kill(-pid, SIGINT);
}
errno = olderrno;
return;
}

sigstp_handler

实现一个SIGSTOP信号处理函数,将信号传给前台程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*
* 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 olderrno = errno;
int pid;
sigset_t mask_all, prev;
Sigfillset(&mask_all);
Sigprocmask(SIG_BLOCK, &mask_all, &prev);
if((pid = fgpid(jobs)) > 0){
Sigprocmask(SIG_SETMASK, &prev, NULL);
Kill(-pid, SIGSTOP);
}
errno = olderrno;
return;
}

结果

test16

test15

test14

左边参考 右边自己实现

总结

  • 从未想过每天都在使用的shell需要考虑这么多:回收进程、竞争避免等
  • 第一次接触并行并发的概念,希望能再厉害一点点

csapp shlab
http://htwzxwj.github.io/2023/12/29/csapp-shlab/
作者
End0rph1n
发布于
2023年12月29日
许可协议