shell-lab

Shell Lab

在这个 tsh.c 文件里面,我们需要完成7个函数的编写:

1
2
3
4
5
6
7
8
void eval(char *cmdline);
int builtin_cmd(char **argv, char *cmdline);
void do_bgfg(char **argv, char *cmdline);
void waitfg(pid_t pid);

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

eval(char * cmdline)

在调用parseline解析输出后,我们首先判断这是一个内置命令(shell实现)还是一个程序(本地文件)。

fg %jobnumber 将后台的任务拿到前台来处理
bg %jobnumber 将任务放到后台中去处理

setpgid的用法:

1.如果pgid设置为0,该进程的进程组ID会被设置为该进程的ID

2.如果pid和pgid都指向同一个进程,那么就会重新创建一个进程组,该进程会成为该进程组的首进程

3.如果pid与pgid不一样,那么就会把pid指向的进程移动到另外一个进程组

4.如果pid设置为0,那么调用进程的进程组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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
void eval(char *cmdline)//evaluation 
{
char *argv[MAXARGS];
int bg_flag;

bg_flag = parseline(cmdline, argv);/* BG job的话为1,FG job为0 */

if (builtin_cmd(argv, cmdline)) /* built-in command */
{
return;//如果是内置命令,就进入builtin_cmd
}
else /* program (file) 否则创建子进程并在job列表里完成添加*/
{
//注意先要用access来判断是否存在这个文件,不然fork以后是无法回收的。
if (access(argv[0], F_OK)) /* do not fork and addset! */
{
fprintf(stderr, "%s: Command not found\n", argv[0]);
return;
}
//添加到job列表。
pid_t pid;
sigset_t mask, prev;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
//防止add_job和delete_job竞争
sigprocmask(SIG_BLOCK, &mask, &prev); /* block SIG_CHLD */
//fork子进程
if ((pid=fork()) == 0) /* child */
{
//如果是子进程就要先解锁
sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIG_CHLD */

if (!setpgid(0, 0))
{
//这里如果fork子进程出现了错误应该使用_exit()
if (execve(argv[0], argv, environ))//运行这个可执行文件
{
fprintf(stderr, "%s: Failed to execve\n", argv[0]);
_exit(1);
}
/* context changed */
}
else
{
fprintf(stderr, "Failed to invoke setpgid(0, 0)\n");
_exit(1);
}
}
else if (pid > 0)/* 父进程就是当前的shell , tsh */
{
if (!bg_flag) /*执行前端任务,立刻执行,结束后才进行下一个 */
{
fg_pid = pid;
fg_pid_reap = 0;
addjob(jobs, pid, FG, cmdline);//把这个进程状态设置为FG
sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIG_CHLD */
waitfg(pid);//忙等,只有当子进程回收之后才能进行下一个
}
else /* 执行后端任务,后端直接加工作,打印信息后就结束了,等有空再做后端 */
{
addjob(jobs, pid, BG, cmdline);//把进程状态设置为BG
sigprocmask(SIG_SETMASK, &prev, NULL); /* unblock SIG_CHLD */
printf("[%d] (%d) %s", maxjid(jobs), pid, cmdline);//打印
}
return;
}
else
{
unix_error("Failed to fork child");
}
}
return;
}

另外要注意一个线程并行竞争(race)的问题:fork以后会在job列表里添加job,信号处理函数sigchld_handler 回收进程后会在job列表中删除,如果信号来的比较早,那么就可能会发生先删除后添加的情况。这样这个job永远不会在列表中消失了(内存泄露),所以我们要先block SIGCHLD 这个信号,添加以后再还原。

builtin_cmd(char **argv,char *cmdline)

这个函数分情况判断是哪一个内置命令。如果用户仅仅按下回车键,那么argv 的第一个变量将是一个空指针。如果用这个空指针去调用 strcmp函数会引发 segmentation fault

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
int builtin_cmd(char **argv, char *cmdline)
{
char *first_arg = argv[0];

if (first_arg == NULL) //直接敲空格的情况
{
return 1;
}

if (!strcmp(first_arg, "quit"))//如果第一个参数是quit,那就直接退出
{
exit(0);
}
else if (!strcmp(first_arg, "jobs"))//如果第一个参数是jobs,那就列出当前的jobs
{
listjobs(jobs);
return 1;
}
else if (!strcmp(first_arg, "bg") || !strcmp(first_arg, "fg"))
{
//如果是bg和fg中的一种,就交个 do_bgfg这个函数去单独处理
do_bgfg(argv, cmdline);
return 1;
}

return 0;
}

do_bgfg(char *argv, char cmdline);

这个函数单独处理了bgfg这两个内置命令。这个函数的核心就两点:

  1. 区分bg和fg命令,以及传入pid或者jid参数对应的进程的状态。前者if,后者switch就可以包括所用的情况

  2. 注意用户输入错误处理,比如参数数量不够或者参数传入错误的情况

要注意fg有两个对应的情况:

  1. 后台程序是stopped的状态,这时我们需要设置相关变量,然后发送继续的信号。

  2. 如果这个进程本身就在运行,我们就只需要改变job的状态,设置相关变量,然后进入waitfg等待这个新的前台进程执行完毕。

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
 void do_bgfg(char **argv, char *cmdline)
{
char *first_arg = argv[0];
if (!strcmp(first_arg, "bg"))//如果是后端的工作
{
if (argv[1] == NULL)
{
fprintf(stderr, "bg command requires PID or %%jobid argument\n");
return;
}

if (argv[1][0] == '%') /* JID是%+数字的格式 */
{
int jid = atoi(argv[1] + 1);//跳过百分号读取JID
if (jid)
{
//获取当前job的指针
struct job_t *job_tmp = getjobjid(jobs, jid);
if (job_tmp != NULL)
{
job_tmp->state = BG;//将当前job状态换成BG
printf("[%d] (%d) %s", jid, job_tmp->pid, job_tmp->cmdline);
//打印当前jid, job的pid和cmdline
stopped_resume_child = job_tmp->pid;
//获取当前job的pid之后,给进程发送一个SIGCONT的信号
//因为之前这个进程可能处于停止的状态。
killpg(job_tmp->pid, SIGCONT);
return;
}
else
{
fprintf(stderr, "%%%s: No such job\n", argv[1] + 1);
}
}
else//如果只打了一个百分号,没有数字。
{
fprintf(stderr, "%%%s: No such job\n", argv[1] + 1);
}
}
else /* PID */
{
//获取pid
pid_t pid = atoi(argv[1]);
if(pid)
{
struct job_t *job_tmp = getjobpid(jobs, pid);//获取job的指针
if (job_tmp != NULL)
{
job_tmp->state = BG;//状态设为BG
printf("[%d] (%d) %s", job_tmp->jid, pid, job_tmp->cmdline);
stopped_resume_child = job_tmp->pid;
killpg(pid, SIGCONT);
return;
}
else
{
fprintf(stderr, "(%s): No such process\n", argv[1]);
}
}
else
{
fprintf(stderr, "bg: argument must be a PID or %%jobid\n");
}
}
}
else//如果是前端的工作
{
/* there are two case when using fg:
1. the job stopped
2. the job is running
*/

if (argv[1] == NULL)//第二个参数没有的话,就直接返回
{
fprintf(stderr, "fg command requires PID or %%jobid argument\n");
return;
}
if (argv[1][0] == '%') /* JID */
//如果第二个参数的第一个字符是%,就代表着是个JID
{
int jid = atoi(argv[1] + 1);
if (jid)
{
struct job_t *job_tmp = getjobjid(jobs, jid);
if (job_tmp != NULL)
{
int state = job_tmp->state;
fg_pid = job_tmp->pid;
/* this is the new foreground process */
fg_pid_reap = 0;
job_tmp->state = FG;
if (state == ST)//如果当前处于暂停状态
{
stopped_resume_child = job_tmp->pid;
/* set the global var in case of wait in SIGCHLD handler */
killpg(job_tmp->pid, SIGCONT);
}
//等待前台进程结束,进行下一个命令
waitfg(job_tmp->pid);
/* wait until the foreground terminate/stop */
return;
}
else
{
fprintf(stderr, "%%%s: No such job\n", argv[1] + 1);
}
}
else
{
fprintf(stderr, "%%%s: No such job\n", argv[1] + 1);
}
}
else /* PID */
//否则就是个PID
{
pid_t pid = atoi(argv[1]);
if(pid)
{
struct job_t *job_tmp = getjobpid(jobs, pid);
if (job_tmp != NULL)
{
int state = job_tmp->state;
fg_pid = job_tmp->pid; /* this is the new foreground process */
fg_pid_reap = 0;

job_tmp->state = FG;

if (state == ST)
{
stopped_resume_child = job_tmp->pid;
/* set the global var in case of wait in SIGCHLD handler */
killpg(pid, SIGCONT);
}

waitfg(job_tmp->pid);
/* wait until the foreground terminate/stop */
return;
}
else
{
fprintf(stderr, "(%s): No such process\n", argv[1]);
}
}
else
{
fprintf(stderr, "fg: argument must be a PID or %%jobid\n");
}
}
}
return;
}

void waitfg(pid_t pid)

之前声明了一个volatile sig_atomic_t的全局变量fg_pid_reap ,只要信号处理函数回收了前台进程,它就会将fg_pid_reap 置1,这样我们的waitfg函数就会退出,接着读取用户的下一个输入。在这里我们使用busy sleep,会导致一定的延迟

1
2
3
4
5
6
7
8
9
void waitfg(pid_t pid)
{
while (!fg_pid_reap)//如果没有回收前台进程,就一直在问回收了没,回收了没
{
sleep(1);//休眠
}
fg_pid_reap = 0;//回收以后,就
return;
}

void sigchld_handler(int sig)

从这里开始,是3个信号处理程序函数。

首先是 sigchld_handler ,也就是当一个子进程终止或者结束的时候,父进程会发送一个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
void sigchld_handler(int sig) 
/* When a child process stops or terminates, SIGCHLD is sent to the parent process. */
{
// 回收子进程之前先保留之前的错误信息
int olderrno = errno;

if (stopped_resume_child)
{
stopped_resume_child = 0;
return;
}

int status;
pid_t pid;

//如果任何一个子进程暂停/结束了,然后用这个函数来回收
if ((pid = waitpid(-1, &status, WUNTRACED)) > 0) /* don't use while! */
{
if (pid == fg_pid)//如果返回的是当前的前端进程的pid
{
fg_pid_reap = 1;
}
//如果子进程是正常结束的,那么就将这个子进程删除。
if (WIFEXITED(status)) /* returns true if the child terminated normally */
{
deletejob(jobs, pid);
}
else if (WIFSIGNALED(status))
/*如果这个子进程是收到一个信号之后结束的,就会返回true,打印SIGNAL之后再删除 */
/* since job start from zero, we add it one */
{
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else //如果是暂停的,就不删除,而是将这个进程的状态改成ST(stop),并打印
{
struct job_t *p = getjobpid(jobs, pid);
p->state = ST; /* Stopped */
printf("Job [%d] (%d) stopped by signal 20\n", pid2jid(pid), pid);
}
}

errno = olderrno;//将errno复原,保持回收子进程前后错误信息不能变.
return;
}

void sigint_handler (int sig)

这个函数是说 当我们使用 ctrl+c 的时候,kernel会使用kill函数给shell发一个 SIGINT信号给前台进程组。注意,这里是群发,因此要使用 killpg

1
2
3
4
5
6
7
8
9
10
void sigint_handler(int sig)
{
int olderrno = errno;
pid_t pgid = fgpid(jobs);
if (pgid)
killpg(pgid, SIGINT);

errno = olderrno;
return;
}

void sigtstp_handler(int sig)

和 sigint一样,这次发送的信号是 SIGTSTP

输入Ctrl+Z会发送一个SIGTSTP信号到前台进程组中的每个进程。默认情况下,结果是停止(挂起suspend)前台作业。在博客的这一章节 从键盘发送信号 有说过

1
2
3
4
5
6
7
8
9
10
11
12
13
void sigtstp_handler(int sig)
{
int olderrno = errno;

pid_t pgid = fgpid(jobs);
if (pgid)
{
killpg(pgid, SIGTSTP);
}

errno = olderrno;
return;
}
-------------本文结束,感谢您的阅读-------------