CSAPP shelllab总结

书中一些函数的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
pid_t getpgrp(void); // 返回当前进程的进程组id
int setpgid(pid_t pid,pid_t pgid) // 改变自己或其他进程的进程组,成功返回0失败返回1
int kill(pid_t pid,int sig) // 进程通过调用kill函数发送信号给其他进程(包括自己),成 // 功返回0失败返回1
typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t handler); // 若成功则返回指向前次处理程序的指针,否则返回SIG_ERR
int sigprocmask(int how,const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set,int signum);
int sigdelset(sigset_t *set,int signum);
/* sigprocmask 函数改变当前阻塞的信号集合,具体行为依赖how的值
SIG_BLOCK 把set中的信号添加到blocked向量
SIG_UNBLOCK 从blocked中删除set的信号
SIG_SETMASK block=set
如果oldset非空,那么blocked位向量的值保存在oldset中
sigemptyset 初始化set为空集合
sigfillset 把每个信号都添加到set中
sigaddset 把signum添加到set
sigdelset 从set中删除signum
如果signum是set的成员,那么sigismember返回1,否则返回0 */
int sigismember(const sigset_t *set,int signum);

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
53
54
55
56
57
58
59
/* 
* 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]; // Argument list
char buf[MAXLINE]; // Holds modified command line
int bg; // Should the job run in background or foreground
pid_t pid; // Process id
sigset_t mask;
strcpy(buf,cmdline);
bg = parseline(buf,argv);
if(argv[0] == NULL){
return; // Ignore empty line
}

if(!builtin_cmd(argv)){
sigemptyset(&mask);
sigaddset(&mask,SIGCHLD);
sigprocmask(SIG_BLOCK,&mask,NULL);
if((pid = fork()) == 0){ // Child process runs user job
sigprocmask(SIG_UNBLOCK,&mask,NULL);
setpgid(0,0);
if(execve(argv[0], argv, environ) < 0){
printf("%s: Command not found.\n",argv[0]);
exit(0);
}
}
else{
if(!bg){
addjob(jobs,pid,FG,cmdline);
}
else{
addjob(jobs,pid,BG,cmdline);
}
sigprocmask(SIG_UNBLOCK,&mask,NULL);
}
// Parent waits for foreground job to terminate
if(!bg){
int status;
if(waitpid(pid,&status,0) < 0){
unix_error("waitfg: waitpid error");
}
waitfg(pid);
}
else{
printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
}
}
return;
}

eval函数不能直接按照书中代码进行实现,因为书中代码没有考虑信号处理,故我们需要在书中代码的基础上加入对各类中断信号的处理

eval函数的作用是读取用户输入的命令行并对该命令行求值

原理解释

  • 首先读入并解析指令,若指令为空则直接返回
  • 如果指令不是一个内置指令(用builtin_cmd判断,若是内置命令直接执行返回),先清空中断向量mask,然后传入SIGCHLD并用sigprocmask传入SIG_BLOCK对mask进行阻塞,这样做的目的是阻塞SIGCHLD防止addjob和deletejob出现竞争;接下来fork子进程执行输入的命令,将该子进程用setpgid添加到新的进程组,防止与shell程序冲突,最后UNBLOCK掉SIGCHLD并用execve执行即可
  • 父进程直接根据bg/fg信号调用addjob,并解除阻塞
  • 如果是前台进程,则需要等待前台进程完成;如果是后台进程则直接打印出进程信息即可(和结果保持一致)

builtin_cmd函数

依照书中方法实现即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 */
}

内置命令函数builtin_cmd实现较为简单,由writeup可知除了书中实现的quit和&以外还要实现bg、fg、jobs的判断与执行,按照定义添加即可

do_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
/* 
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv)
{
if(argv[1] == NULL){
printf("%s command requires an argument\n",argv[0]);
return;
}
struct job_t *job;
int id;
sigset_t mask,prev;
char *shell_para = argv[1];
sigfillset(&mask);
if(shell_para[0] == '%'){
id = atoi(shell_para + 1);
if(id == 0){
printf("%s: argument must be a legal pid or jid\n",param);
return;
}
}
else{
id = atoi(shell_para);
if(id == 0){
printf("%s: argument must be a legal pid or jid\n",param);
return;
}
sigprocmask(SIG_BLOCK,&mask,&prev);
id = pid2jid(id);
}
if(!strcmp(argv[0],"bg")){
kill(-(job->pid),SIGCONT);
job->state = BG;
printf("[%d] (%d) %s",job->jid,job->pid,job->cmdline);
}
else{
kill(-(job->pid),SIGCONT);
job->state = FG;
waitfg(job->pid);
}
return;
}

do_bgfg函数就是正常的执行内置的bg/fg命令

  • 如果后面无参数,直接按标准的输出printf即可printf("%s command requires an argument\n",argv[0]);
  • 如果有参数则按定义处理该参数即可,用atoi处理参数(注意分别pid/jid),如果是pid的话要先BLOCK信号然后将pid转化为jid,最后按照fg/bg定义执行即可(注意:要发送SIGCONT信号)

waitfg函数

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)
{
struct job_t* job;
job = getjobpid(jobs,pid);
while(job->state == FG){
sleep(1);
}
return;
}

waitfg函数阻塞进程直到没有前台进程执行

该函数实现简单,直接按照官网writeup的方式实现,用一个sleep不断等待即可

三个signal_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
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
/*****************
* Signal handlers
*****************/

/*
* 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 status;
pid_t pid;

while ((pid = waitpid(fgpid(jobs), &status, WNOHANG | WUNTRACED)) > 0) {
if (WIFSTOPPED(status)){
//change state if stopped
getjobpid(jobs, pid)->state = ST;
int jid = pid2jid(pid);
printf("Job [%d] (%d) Stopped by signal %d\n", jid, pid, WSTOPSIG(status));
}
else if (WIFSIGNALED(status)){
//delete is signaled
int jid = pid2jid(pid);
printf("Job [%d] (%d) terminated by signal %d\n", jid, pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFEXITED(status)){
//exited
deletejob(jobs, pid);
}
}
return;
}

/*
* 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 pid = fgpid(jobs);
if(pid == 0){
return;
}
kill(-pid,sig);
return;
}

/*
* 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 pid = fgpid(jobs);
if(pid == 0){
return;
}
kill(-pid,sig);
return;
}

sigchld_handler

sigchld的处理应该是三个要实现的信号处理中最复杂的一个:

首先我们需要等待该进程所有的子进程都结束就立即返回,按照书中的信号实现即wait中的参数为WNOHANG | WUNTRACED

如果退出的子进程为被终止的清空,那么改变该进程的state并直接打印标准输出即可;如果是被signal中断退出,那么打terminated by signal并调用deletejob删除即可;如果退出的子进程为正常退出的清空,那么直接调用delete删除即可

sigint_handler

sigint信号的处理比较简单,直接获取进程号,如果是子进程直接返回即可;如果是父进程直接kill掉该进程的所有进程组内的进程并返回即可

sigtstp_handler

与sigint的处理方式相同(效果一样)

总结

本次实验我对shell内部工作原理有了一定的理解,尤其是处理前后台进程,以及对各种信号产生的中断进行处理,最重要的是本次实验以及书中的章节对《操作系统概念》一书中没有仔细讲解的信号做了大量讲解,对我来说极大地补充了操作系统原理的知识面,是一次非常有收获的实验体验


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!