CSAPP之ShellLab

写在前面

建议先看看WriteUp和配套的ppt

如果使用vscode连接Linux Subsystem完成这个作业报错identifier "sigset_t" is undefined可以检查一下配置,因为sig_set结构体不在c99标准里面,而是在POSIX标准里面的,所以需要把标准改为gnu99

简介

这个实验要求我们补全tsh.c的一些函数的代码

要求–来自WriteUp

  • shell的输入是函数/指令+空格+参数1+空格+参数2….
  • 我们写的tsh并不需要支持管道符号|和输入重定向符号<,>
  • 如果输入Ctrl+Z或者Ctrl+C应该要让SIGINT(SIGTSTP)信号被发送到所有前台进程组中的进程
  • 如果输入以&结束,那么应该在后台运行该程序
  • 通过process id(PID)和job id(JID)来标识进程,JID用%开头
  • 我们写的tsh应该支持四个内置命令:quit,jobs,bg <jobs>:发送一个SIGCONT信号给指定进程,让进程重启并在后台运行,fg <jobs>:发送一个SIGCONT信号给指定进程,让进程重启并在前台运行,jobs可以是PID也可以是JID
  • tsh应该回收所有僵尸子进程,如果一个作业因为收到了一个没有捕获的信号而终止的,那么tsh应该输出该进程的pid和相关信息
  • 有一个参考程序tshref,我们的示例程序的运行结果应该和它一样

各个函数

eval

其实这道题在书上有示例代码,看不懂可以去翻翻书:中文版p525

这个函数应该是核心程序了用户的输入在cmdline中,如果是内置函数,那么就直接执行,如果不是,那么就在子进程中运行
如果进程是在前台运行的,那么应该等待进程结束的返回值
注意:孩子要有独立的进程组(运行函数setpgid(0, 0)),确保在前台进程组中只有shell在运行,当按下Ctrl+c时,SIGINT信号会先发送给shell,再由shell转发给正确的程序

note:这一点待补充

这里我遇到了一个有一点蠢的逻辑错误:在等待前台运行程序终止后没有在job_t结构体数组中删除它 :cry:ChatGPT也无法给出我正确的解答(Why?这明明是一个很明显的逻辑错误),结果调试了很久

代码

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
void eval(char *cmdline) // completed
{
char buff[MAXLINE];
char *argv[MAXARGS];
int bg;
pid_t pid;
strncpy(buff, cmdline, MAXLINE);
sigset_t mask_all, maskchild, prev; // gnu99
bg = parseline(cmdline, argv); // 构建argv数组
if (argv[0] == NULL)
{
return;
}
if (!builtin_cmd(argv)) // argv不是内置命令
{
sigfillset(&mask_all); // 初始化为全部信号
sigemptyset(&maskchild); // 初始化为空集合
sigaddset(&maskchild, SIGCHLD); // 把SIGCHILD(子进程的终止)加入到mask集合中

sigprocmask(SIG_BLOCK, &maskchild, &prev); // 阻塞
if ((pid = fork()) == 0) // 子进程中运行
{
sigprocmask(SIG_SETMASK, &prev, NULL); // 子进程中解除阻塞
setpgid(0, 0); // 更改gpid
if (execve(argv[0], argv, environ) < 0)
{
printf("%s :Command not found!\n", argv[0]);
exit(0); // 为什么要exit??
}
}
else // 父进程
{
addjob(jobs, pid, bg ? BG : FG, cmdline); // 将任务添加到任务列表里
sigprocmask(SIG_SETMASK, &prev, NULL); // 解除阻塞
}
if (!bg) // 如果不是后台运行,则需要等待孩子结束退出
{
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg:waitpid error");
deletejob(jobs, pid); // 一定要deletejob!!!!!!!!!!!
}
else
printf("[1] (%d) %s", pid, cmdline);
}
return;
}

builtin_cmd

其实我第一个写的是这个函数,因为它相对简单 :laughing:

这个函数就是检查输入是否是内置函数,如果是内置函数就执行它,如果不是就返回0

代码

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
/*
* builtin_cmd - If the user has typed a built-in command then execute
* it immediately.
*/
int builtin_cmd(char **argv) // complete
{
if (strcmp(argv[0], "quit") == 0)
{
exit(0);
}
else if (strcmp(argv[0], "jobs") == 0)
{
listjobs(jobs);
return 1;
}
else if (strcmp(argv[0], "fg") == 0 || strcmp(argv[0], "bg") == 0)
{
do_bgfg(argv);
return 1;
}
else if (strcmp(argv[0], "&") == 0)
{
return 1;
}
return 0; /* not a builtin command */
}

do_bgfg

fg/bg指令的用法

可以通过 man bg来查看帮助文档

当我们正在运行一个前台程序时,我们可以通过Ctrl+Z暂停该程序,这时,屏幕上会打印[1]+ Stopped ./a.out如果输入 bg 1那就会把这个程序在后台继续运行,如果输入fg 1那就会把这个程序放在前台继续运行.

需要注意的地方

这个函数是shell内置的fg/bg指令

argv[1]中存放的是参数(PID或者JID)

可以通过sscanf字符串读取格式化输入来读取id,

代码

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
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv) // finished
{
if (argv[1] == NULL || argv[2] != NULL)
{
printf("%s command requires PID or %%jobid argument\n", argv[0]);
return;
}
int id;
struct job_t *job = NULL;
if (sscanf(argv[1], "%%%d", &id)) // JID
{
job = getjobjid(jobs, id);
if (!job)
{
printf("%%%d: no such job\n", id);
return;
}
}
else if (sscanf(argv[1], "%d", &id)) // PID
{
job = getjobpid(jobs, id);
if (!job)
{
printf("(%d): no such process\n", id);
return;
}
}
else // 输入有误!!!
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
if (strcmp(argv[0], "bg")) // fg--在前台运行//strcmp--匹配则返回0
{
kill(-(job->pid), SIGCONT);
job->state = FG;
waitfg(job->pid); // 等待
}
else // bg-在后台运行
{
kill(-(job->pid), SIGCONT);
job->state = FG;
printf("[%d](%d)+ %s", job->jid, job->pid, job->cmdline); // 打印相关信息
}
return;
}

waitfg

这个函数需要我们使用sigsuspend等待前台运行程序结束,没啥好讲的

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid) // doing
{
sigset_t child;
sigemptyset(&child); // 似乎不用阻塞任何信号?
while (fgpid(jobs)) // 当jobs中没有前台函数时,fgpid返回0
{
sigsuspend(&child);
}
return;
}

sigchld_handler

在这个程序中,sigchld_handler是进程收到了SIGCHLD函数以后的信号处理程序

WriteUp中有一句Hint:The WUNTRACED and WNOHANG options to waitpid will also be useful,查阅CSAPP,发现WNOHANG|WUNTRACED是指立即返回,如果等待集合中的子进程没有终止,就返回0,如果有一个停止或终止就返回该进程的pid

这个信号处理函数被调用应该有两种情况:

  • 正常退出:应该删除作业
  • 进程被暂停:应该打印信息,设置状态

为什么要保存和恢复errno:(见CSAPP中文版536面)因为有很多Linux异步信号安全的函数都会在出错时返回errno,如果不保存和恢复可能会干扰主程序中其他依赖errno的函数

代码

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
/*
* 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) // finished,OK?
{
int olderrno = errno; // 保存和恢复errno
pid_t pid;
sigset_t block, prev;
int status;
sigfillset(&block);
while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
{
if (WIFSTOPPED(status)) // 暂停,打印信息,设置状态
{
struct job_t *job = getjobpid(jobs, pid);
sigprocmask(SIG_BLOCK, &block, &prev);
printf("[%d](%d)+ %s stopped", job->jid, job->pid, job->cmdline);
job->state = ST;
sigprocmask(SIG_SETMASK, &prev, NULL);
}
else // 正常退出,删除作业
{
sigprocmask(SIG_BLOCK, &block, &prev);
deletejob(jobs, pid);
// printf("\n\nABCDEFG%d\n\n", pid);
sigprocmask(SIG_SETMASK, &prev, NULL);
}
}
errno = olderrno;
return;
}

sigint_handler

这是一个简单的的函数,当收到SIFGINT信号时,向前台进程发送信号即可

1
2
3
4
5
6
7
8
9
10
11
void sigint_handler(int sig)
{
int olderrno = errno;
pid_t pid = fgpid(jobs);
if (pid != 0)
{
kill(-pid, sig);
}
errno = olderrno;
return;
}

sigtstp_handler

这也是一个简单的的函数,当收到SIGTSTP信号时,向前台进程发送信号即可

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/*
* 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;
pid_t pid = fgpid(jobs);
if (pid != 0)
kill(-pid, sig);
errno = olderrno;
return;
}

CSAPP之ShellLab
https://20040702.xyz/2023/10/25/shlab/
作者
Seeker
发布于
2023年10月25日
许可协议