0%

[CSAPP Lab]-V Shell Lab

前置知识

  • Exception Control FLow

  • Signal 与 Signal 阻塞
    下面我们常用的一个函数 sigprocmask 的原型是这样定义的

    1
    int sigprocmask(int how, const sigset_t *set, sigset_t *oldest)

    其中参数 how 是这样的

    • SIG_BLOCK:将 set 中的信号添加到 blocked
    • SIG_UNBLOCK:从 blocked 中删除 set 中的信号
    • SIG_SETMASK: block=set
  • 进程与进程组
    一个进程的进程组 ID 是其父进程的 PID

    1
    int setpgid(pid_t pid, pid_t pgid); 

    setpgid() 函数可以修改进程的进程组 ID

  • kill 函数
    kill 定义如下

    1
    int kill(pid_t pid, int sig);

    其中参数 pid 有以下几种

    1. pid 大于0,pid是信号欲送往的进程的标识
    2. pid 等于0时,信号将送往所有与调用kill()的那个进程属同一个使用组的进程
    3. pid 等于-1时,信号将送往所有调用进程有权给其发送信号的进程,除了进程1(init)
    4. pid 小于-1时,信号将送往以 -pid 为组标识的进程

目标

这次是要实现一个简单的 Shell,叫做 tsh,tsh 应该有这些 feature

  • 提示符以 >tsh 开始
  • 这个 shell 应该处理 nameargv,如果 name 是一个内置的命令,tsh 应该立刻运行并且等待下一个命令;如果 name 是一个可执行文件的目录,则 tsh 需要在子进程中运行
  • tsh 并不需要管道(|)和输入输出重定向(<和>)
  • 输入 Ctrl-C 的时候需要引发一个 SIGINT 信号发送给当前的前台任务;如果没有任何前台任务,则 Ctrl-C 没有作用
  • 如果命令行以 & 结尾,则 tsh 需要在后台运行这个任务
  • 每一个任务可以由 process ID(PID) 和 job ID(JID) 区分,这两者是由 tsh 所制定的正整数。JID 需要由 % 开头
  • tsh 应该支持 quit jobs bg <job> fg <job> 指令
    • quit 退出 shell
    • jobs 列出所有任务
    • bg <job> 给后台 job 发送 SIGCONT 信号, job 可以是 PID 或 JID
    • fg <jog> 给前台 job 发送 SIGCONT 信号, job 可以是 PID 或 JID
  • tsh 应该 reap(收割?) 僵尸子进程

目录下的 sldriver.pl 是可以检验的,可以通过 ./sldriver.pl -h 查看使用方法。同时 tshref 是一个参考样例,可以用来验证

任务

我们需要完善 tsh.c 的 7 个函数,分别是

1
2
3
4
5
6
7
8
void eval(char *cmdline); //运行命令行
int builtin_cmd(char **argv); //运行内置命令
void do_bgfg(char **argv); //运行内置的 `bg` 和 `fg` 命令
void waitfg(pid_t pid); //阻塞直到前台进程结束

void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);

Code

eval

在编写 eval 的时候要考虑到阻塞的设计,原则有二

  1. 在访问全局变量的时候需要阻塞所有信号
  2. 在执行有前后顺序的函数时需要阻塞(如addjobdeletejob)
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
void eval(char *cmdline) 
{
char *argv[MAXARGS];
char buf[MAXLINE];
int state;
int argc;
pid_t curr_pid; //当前pid
sigset_t mask_all, mask_child, mask_prev;

sigemptyset(&mask_child);
sigaddset(&mask_child, SIGCHLD); //mask_one 只阻塞 SIGCHILD
sigfillset(&mask_all); // mask_all 阻塞所有

strcpy(buf, cmdline);
state = parseline(buf, argv) ? BG : FG;

if (!builtin_cmd(argv)) {
// 不是内置命令,阻塞SIGCHILD
sigprocmask(SIG_BLOCK, &mask_child, &mask_prev);
if ((curr_pid = fork()) == 0) {
// 子进程中,接触 SIGCHILD 阻塞,正确的处理子进程的子进程
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
setpgid(0,0); //创立新的进程组
if (execve(argv[0], argv, environ) < 0) {
printf("%s: Command not found.\n", argv[0]);
}
exit(EXIT_SUCCESS); //如果没有execve的话,务必记得退出子进程
}

//阻塞所有信号,天塌下来也要让我先执行完
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, curr_pid, state, cmdline);
sigprocmask(SIG_SETMASK, &mask_child, NULL); //阻塞SIGCHILD

if (state == FG) {
waitfg(curr_pid);
} else {
sigprocmask(SIG_BLOCK, &mask_all, NULL); //要读取全局变量,避免被打断
struct job_t *curr_bgjob = getjobpid(jobs, curr_pid);
printf("[%d] (%d) %s", curr_bgjob->jid, curr_bgjob->pid, curr_bgjob->cmdline);
}

sigprocmask(SIG_SETMASK, &mask_prev, NULL); //解除所有阻塞
}

return;
}

builtin_cmd

这个函数比较简单,只需要比较下字符串然后执行对应的函数就可以了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int builtin_cmd(char **argv) 
{
char *cmd = argv[0];
sigset_t mask_all, mask_prev;

sigfillset(&mask_all);

if (!strcmp(cmd, "quit")) {
exit(EXIT_SUCCESS); //直接退出,可以考虑杀掉所有的子进程
} else if (strcmp(cmd, "fg") == 0 || strcmp(cmd, "bg") == 0) {
do_bgfg(argv);
return 1;
} else if (!strcmp(cmd, "jobs")) {
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev); //访问全局变量,屏蔽所有Signal
listjobs(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
return 1;
}
return 0; /* not a builtin command */
}

do_bgfg

在 Linux 中,一个进程可以接受 SIGSTP 后停止,知道接收到 SIGCONT 后再继续执行

bg 就是给一个进程发送 SIGCONT ,使之在后台继续执行;同理,fg 就是发送 SIGCONT 并且在前台执行

了解了 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
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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
void do_bgfg(char **argv) 
{
char *cmd = argv[0];
char *para = argv[1];
struct job_t *curr_job;
sigset_t mask_all, mask_prev;
int curr_jid;

sigfillset(&mask_all);

//判断传入的是pid还是jid
if (para[0] == '%') {
curr_jid = atoi(&(para[1]));
if (curr_jid == 0) {
printf("%s:argument must be a PID or %%jobid\n", cmd);
fflush(stdout);
return;
}
} else {
curr_jid = atoi(para);
if (curr_jid == 0) {
printf("%s:argument must be a PID or %%jobid\n", cmd);
fflush(stdout);
return;
}
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_jid = pid2jid(curr_jid);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
}

sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_job = getjobjid(jobs, curr_jid);

if (curr_job == NULL) {
printf("(%s): No such process\n", para);
fflush(stdout);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
return;
}

if (!strcmp(cmd, "bg")) {
// bg
switch(curr_job->state) {
case ST:
curr_job->state = BG;
kill(-(curr_job->pid), SIGCONT);
printf("[%d] (%d) %s", curr_job->jid, curr_job->pid, curr_job->cmdline);
break;
case BG:
break;
case FG:
case UNDEF:
unix_error("bg error\n");
break;
}
} else {
//fg
switch(curr_job->state) {
case ST:
curr_job->state = FG;
kill(-(curr_job->pid), SIGCONT);
waitfg(curr_job->pid);
break;
case BG:
curr_job->state = FG;
waitfg(curr_job->pid);
break;
case FG:
case UNDEF:
unix_error("fg error\n");
break;
}
}

sigprocmask(SIG_SETMASK, &mask_prev, NULL);

return;
}

waitfg

首先我们需要定义一个全局变量 fg_stop_or_exit 来判断前台进程是否停止,但是在声明此变量时,应该使用特殊的标志

1
volatile sig_atomic_t fg_stop_or_exit;

其中

  1. volatile 告诉编译器不要缓存此变量,避免变量在信号 handler 中改变后 main 函数看不到改变的变量
  2. sig_atomic_t 是一个 atomic 的整形变量
1
2
3
4
5
6
7
8
9
10
11
void waitfg(pid_t pid)
{
sigset_t mask_empty;
sigemptyset(&mask_empty);

fg_stop_or_exit = 0;
while(!fg_stop_or_exit) {
sigsuspend(&mask_empty);
}
return;
}

(可以看 CSAPP 上545页

sigchld_handler

sigchld_handler 的核心是 waitpid 函数,该函数的具体使用方法在这里这里

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 sigchld_handler(int sig) 
{
int olderrno = errno;
sigset_t mask_all, mask_prev;
pid_t child_pid;
struct job_t *child_job;
int status;

sigfillset(&mask_all);

while ((child_pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
child_job = getjobpid(jobs, child_pid);
if (child_pid == fgpid(jobs)) {
// 如果是前台进程
fg_stop_or_exit = 1;
}
if (WIFSTOPPED(status)) {
// 如果子进程被暂停
child_job->state = ST;
// WSTOPSIG(status)用来获得被暂停的子进程的pid
printf("Job [%d] (%d) terminated by signal %d\n", child_job->jid, child_job->pid, WSTOPSIG(status));
} else {
// 子进程终止
if (WIFSIGNALED(status)) {
//子进程因为有未捕获的信号而中止
printf("Job [%d] (%d) terminated by signal %d\n", child_job->jid, child_job->pid, WTERMSIG(status));
}
deletejob(jobs, child_pid);
}
fflush(stdout);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
}

errno = olderrno;

return;
}

sigint_handler & sigtstp_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
35
36
37
38
39
40
41
void sigint_handler(int sig) 
{
int olderrno = errno;
sigset_t mask_all, mask_prev;
pid_t curr_fg_pid;

sigfillset(&mask_all);
//访问全局结构体数组,阻塞信号
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_fg_pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);

if(curr_fg_pid != 0){
kill(-curr_fg_pid, SIGINT);
}

errno = olderrno;

return;
}

void sigtstp_handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, mask_prev;
pid_t curr_fg_pid;

sigfillset(&mask_all);

sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_fg_pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);

if(curr_fg_pid != 0){
kill(-curr_fg_pid, SIGTSTP);
}

errno = olderrno;

return;
}

最后的原始代码在这里

总结

Linux 编程还是挺有意思的,以后有机会可以看看 Advanced Programming in the UNIX® Environment