CSAPP-SHELL LAB

整个过程中要考虑显式阻塞

1.在访问全局变量(jobs)以及调用给定函数的时候,要阻塞所有的信号,务必保证这些使用for循环遍历的函数不被中断。

2.在一些函数或者指令有必须的先后顺序的时候,要阻塞,保证前一个函数调用完成,再调用后面一个函数。

其他事项:

filename already exists, renaed

1
2
//判断是否是当前引起停止信号的是否是前台进程
volatile sig_atomic_t fg_stop_or_exit;

eval函数

功能是对用户输入的参数进行解析,命令有两种,一种是内置的命令,会立刻执行,否则就要fork一个新的子进程并且把该任务在子进程的上下文中运行。如果是前台任务则需要等到它运行结束才返回

每个子进程必须有一个独一无二的进程组id,通过在fork()之后子进程的Setpgid(0,0)实现,这样当我们向前台程序发送ctrl+c或者ctrl+z命令才不会影响到后台程序。否则所有的子进程会与当前的tsh shell进程为同一个进程组,发送信号的时候,前后台子进程都会收到。

同时fork新进程的前后要阻塞SIGCHLD信号,防止出现竞争的同步错误:fork之后会在job列表里添加job,信号处理函数sigchld_handler回收进程后会在job列表中删除,如果信号来得很早,那么就可能发生先删除后添加的情况,那么job就会永远在列表中(内存泄漏?),所以我们先block掉SIGCHLD,添加job后再还原。

说白了就是要避免僵尸进程,防止父进程没有给子进程收尸,屏蔽这个信号,那么父亲进程就会不关心这个子进程,子进程结束将由init进程去处理。

setpgid 函数
1
int setpgid (pid_t pid,pgid_t pgid);

该函数的意义是找到进程ID为pid的进程,将其进程组ID修改为pgid,如果pid=0,说明要修改进程组ID。如果是

1
setpgid(0,0)

表示创立新的进程组,并且指定的进程会成为进程组的首进程。

如果执行成功就返回组识别码,如果有错误则返回-1,错误原因保存在errno中。

具体实现

eval函数实现如下:

if builtin_command return 0,then shell starts a new child process,and execute the requested programs in the child process,if the user asks for running the program in background, then shell return back to the top of the loop,waiting for next command. otherwise shell uses the waitpid function to wait for the jobs ‘ termination. when jobs terminates,shell begin a new loop.

参考:
https://blog.csdn.net/zxygww/article/details/25976107

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 eval(char *cmdline)
{
char *argv[MAXARGS]; //argument list execve()
char buf[MAXLINE];
int argc;
int bg; //whether the job is in fg or bg
bg = parseline(cmdline,argv);
sigset_t mask_chld,mask_all,mask_prev;
pid_t pid;

sigemptyset(&mask_chld);
#把SIGCHLD信号赋给mask_chld
sigaddset(&mask_chld,SIGCHLD);
#fill所有SIG信号给mask_all
sigfillset(&mask_all);
strcpy(buf,cmdline);

//empty command
if(argv[0]==NULL){
return ;
}
else if(!builtin_cmd(argv)){
//如果不是内部函数,首先要把SIGCHLD信号阻塞住,以防出现竞争条件。
//子进程要解决信号阻塞,并执行相关的函数
//if the below code are outside of the buildin_cmd function,
//then these locks won't be realeased when executing inner commands
//block the SIGCHLD in order to prevent child process ends between father process
//先要阻塞SIGCHLD信号
sigprocmask(SIG_BLOCK,&mask_chld,&mask_prev);
//codes below won't be interrupt by signal SIGCHLD

//running a child process
//
if((pid=fork())==0){
//由于子进程会继承block的特性,所以子进程要记得unblock。
sigprocmask(SIG_SETMASK,&mask_prev,NULL);//unblock the order
//change the process 's group, not the same as tsh's group
setpgid(0,0);
if(execve(argv[0],argv,environ)<0){
printf("%s: Command not found\n",argv[0]);

}
//if execve cannot process then child process will execute main process
exit(0);
}

//blcok all signal
//为我阻挡一切!!就算天塌下来也要先addjob不然顺序乱就gg
sigprocmask(SIG_BLOCK,&mask_all,NULL);
//foreGround
if(!bg){
addjob(jobs,pid,FG,cmdline);
}else{
addjob(jobs,pid,BG,cmdline);
}
//block sigchld again
sigprocmask(SIG_SETMASK,&mask_chld,NULL);

//father process wait until front process stops


//父进程要判断子进程是前台进程还是后台进程,如果是前台进程,则调用waitpid来等待前台进程,如果是后台,把新添加进程利用addjob添加到工作组中。
if(!bg){
//Block until process pid is no longer the foreground process
waitfg(pid);
}
else{
sigprocmask(SIG_BLOCK,&mask_all,NULL);
struct job_t * currbgmask = getjobpid(jobs,pid);
printf("[%d] (%d) %s",currbgmask->jid,currbgmask->pid,currbgmask->cmdline);
}

//unblock all signals
sigprocmask(SIG_SETMASK,&mask_prev,NULL);
}

return;
}

builtin_command

注意访问全局变量jobs的时候要阻塞全部信号就是了

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
int builtin_cmd(char **argv)
{

sigset_t mask_all,mask_prev;

sigfillset(&mask_all);

if(!strcmp(argv[0],"quit")){
exit(0);
}
else if(!strcmp(argv[0],"&")){
return 1;
}
else if(!strcmp(argv[0],"jobs")){
//when visit a global variance,you need to block all signals
sigprocmask(SIG_BLOCK,&mask_all,&mask_prev);
listjobs(jobs);
sigprocmask(SIG_SETMASK,&mask_prev,NULL);
return 1;
}
else if(!strcmp(argv[0],"bg")||!strcmp(argv[0],"fg")){
do_bgfg(argv);
return 1;
}
return 0; /* not a builtin command */
}

waitfg

只要进程号一直是前台程序,就一直sleep等待

但奇怪的是,这个版本的waitfg函数运行有错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 
* waitfg - Block until process pid is no longer the foreground process
*/

void waitfg(pid_t pid)
{

//fgpid return the pid of the front process id

while((pid==fgpid(jobs))){
sleep(0);
}
return;
}

以下版本:
是书中545中介绍的一种显式接收信号的方法

只要信号处理函数回收了前台进程,它就会将fg_stop_or_exit(注意用volatile关键字声明) 置1,这样我们的waitfg函数就会退出,接着读取用户的下一个输入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/*
* waitfg - Block until process pid is no longer the foreground process
*/
void waitfg(pid_t pid)
{

sigset_t mask;
sigemptyset(&mask);
fg_stop_or_exit = 0;

////只有发出这个信号的子进程是前台进程才设置fg_stop_or_exit标志。
while(!fg_stop_or_exit){
sigsuspend(&mask);
}

return;
}

sigint_handler & sigtstp_handler

思路:

1.获取前台进程(fgpid),判断当前是否有前台进程,如果没有则直接返回,有则进行步骤2

2.使用kill函数,发送SIGINT/SIGTSTP信号给前台进程组

kill函数使用
1
2
3
int kill(pid_t pid,int sig);

//如果pid大于0,那么kill函数发送信号号码sig给进程pid,如果pid==0,那么kill发送信号sig给调用进程所在进程组中的每个进程,包括调用进程自己。如果pid<0,则发送sig给进程组|pid|中的每个进程。

代码如下

1.访问jobs的时候要阻塞所有信号

2.kill的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

/*
* 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;
sigset_t mask_all,prev_all;
pid_t pid;
sigfillset(&mask_all);
//execute global function, so block all signals
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
pid=fgpid(jobs);
sigprocmask(SIG_SETMASK,&prev_all,NULL);

//only process the front process
//pid==0 means background process?

if(pid!=0){
kill(-pid,SIGINT);
//printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,sig);
}

errno = olderrno;
return;
}

代码如下

1.注意如果进程已经停止,就不要再把它设置为停止了否则会出错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

/*
* 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)
{

pid_t pid;
pid=fgpid(jobs);

if(pid!=0){
struct job_t *job = getjobpid(jobs,pid);
if(job->state==ST)
return;
else
kill(-pid,SIGTSTP);
}

return;
}

sigchld_handler

status表示中止进程或者停止进程的原因,WNOHANG|WUNTRACED作用是判断当前进程中是否存在已经停止或者终止的进程,如果存在则返回pid,不存在立即返回

WIFSTOPPED(status):表示如果进程是因为停止的信号而停止,那么返回true

WIFSIGNALED(status):表示进程是因为捕获的信号而中止,返回true

WIFEXITED(status): 表示进程通过调用exit()或者return正常结束,则返回true。

参考:https://www.cnblogs.com/sky-heaven/p/8074273.html

filename already exists, rnamed

代码如下:

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
/*
* 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;
sigset_t mask_all,prev_all;
pid_t pid;
struct job_t *gc_job;
int status;

sigfillset(&mask_all);

//尽可能回收子进程,使用WNOHANG,使得如果当前进程都没有停止的时候直接返回,
//而不是挂起该回收进程,这样可能会阻碍无法两个短时间结束的后台进程

while((pid = waitpid(-1,&status,WNOHANG|WUNTRACED))>0){
sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
gc_job = getjobpid(jobs,pid);
//说明当前引起停止的确实是前台进程
if(pid==fgpid(jobs)){
fg_stop_or_exit=1;
}
//子进程正常结束,返回一个非0值
if(WIFEXITED(status)){
deletejob(jobs,pid);
}
//子进程被暂停,只有暂停不用deletejobs
else if(WIFSTOPPED(status)){
//子进程停止引起waitpid函数返回,再判断该进程是否是前台进程
gc_job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", gc_job->jid, gc_job->pid, WSTOPSIG(status));
}
//因捕获信号而终止
else if (WIFSIGNALED(status)){
//子进程终止引起的返回,判断是否是前台进程
//并且判断该信号是否是未捕获的的信号
printf("Job [%d] (%d) terminated by signal %d\n", gc_job->jid, gc_job->pid, WTERMSIG(status));
deletejob(jobs,pid);
}
fflush(stdout);
//unblock all signals
sigprocmask(SIG_SETMASK,&prev_all,NULL);
}
errno = olderrno;
return;
}

do_fgbg

1.输入时%num 代表jobsid,num代表进程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
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
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{

//parameters
char *para = argv[1];

//lack parameters
if(para==NULL){
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return;
}

//full dirname
char *cmd = argv[0];

struct job_t*curr_job;
sigset_t mask_all,mask_prev;
int curr_jid;
sigfillset(&mask_all);

//first character of the paramaters
//linux command: fg %n bring process n from background to frontground
if(para[0]=='%'){

// the argument is a job id
curr_jid = atoi(&(para[1]));
//mistake process2
curr_job = getjobjid(jobs,curr_jid);
if(curr_job==NULL)
{
printf("%%%d: No such job\n",curr_jid);
return;
}
}

else{
// the argument is a process id
curr_jid = atoi(para);
if(curr_jid==0){
printf("%s: argument must be a PID or %%jobid\n",cmd);
fflush(stdout);
return;
}
//block all signals when visit global vairance
sigprocmask(SIG_BLOCK,&mask_all,&mask_prev);
curr_jid = pid2jid(curr_jid);
}

//block all signals when visit global vairance
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;
}
//bg
if(!strcmp(cmd,"bg")){
switch(curr_job->state){
case ST:
//change from stop to bg ST->BG
//meanwhile send signal to child process
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 UNDEF:
case FG:
unix_error("bg or undef error");
break;
}
}


//要用waitfg指令,等待前台作业结束后再退出
else{
switch(curr_job->state){
//如果作业本身是STOP的话,要记得发送信号(SIGCONT,让其继续运行)
case ST:
//change from stop to bg ST->BG
//meanwhile send signal to child process
curr_job->state =FG;
//发射信号给前台进程组,所有前台进程都会受到信号
kill(-(curr_job->pid),SIGCONT);
//if change to fg,then you need to wait until it dies
waitfg(curr_job->pid);
break;
case BG:
curr_job->state =FG;
waitfg(curr_job->pid);
break;
case UNDEF:
case FG:
unix_error("bg or undef error");
break;
}
}
sigprocmask(SIG_SETMASK,&mask_prev,NULL);
return;
}

总结

当我们在真正的shell(例如bash)中执行tsh时,tsh本身也是被放在前台进程组中的,它的子进程也会在前台进程组中,例如下图所示:

upload succsful

引用:

1.https://www.cnblogs.com/liqiuhao/p/8120617.html

2.https://blog.csdn.net/xiaolian_hust/article/details/80087376