TshLab
开始日期:22.1.20
操作系统:linux
调试工具:linux terminal
Link:CS:APP3e
Preknowledge
- tshlab的意思是:tiny shell lab
- EOF(第八章)这一章务必看到8.6,笔者是把里面可运行的源代码都敲了一遍,想要方便的可以参考如下两个链接:源代码,csapp.h的使用
- write up务必认真看,里面的hint部分很重要。
- trace01.txt - Properly terminate on EOF,告诉我们,tsh的主要任务是恰当的终止异常
Functions
Attention
正式做实验时,要按照trace01 ~ 16的任务要求编写7个函数,一开始无从下手很正常,不用着急,多查查资料,多想想。本文主要借鉴了这两篇:CS:APP3e 深入理解计算机系统_3e ShellLab(tsh)实验 ,myk的CS学习之旅
要借鉴教材给出的相关函数。
一个pg(进程组)中有一个或多个pg(进程),一个pg也称为一个job(工作),jobs是所有工作的集合
jid是一个job在jobs中的序列[1, 2, ….]
pid:如果是child(子程序)就代表child的进程编号;
如果是parent(父程序)既代表parent的进程编号也代表parent所在程序组的编号(pgid)shell会分为foreground(前台)和background(后台)两项工作列表,其中foreground中只能有一个工作,但background中可以有多个工作
有
&
代表后台任务,无&
代表前台任务有
%
代表进程组,无%
代表进程额外设置了全局变量
flag
,用来表明前台工作是否终止(或停止)。(遇到foreground都要使用flag
)1
2volatile sig_atomic_t flag;
/* equal 1 => jobs terminated or stopped in fg; equal 0 => jobs running in fg */handler
系列函数要设置erron
复原main()
中,信号已经install(安装)完毕使用
&
取地址运算符来取得数据、状态的地址
eval()
主要功能:
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.
- 可以知道,eval()是用来评估用户键入的指令,如果是内嵌指令就立刻执行;
否则,就要创建并运行子程序,对于这个子程序,如果它运行在前台就要等待它终止或返回;
如果它运行在后台,那它就不能接收终止/停止信号(SIGINT/SIGTSTP
),这些信号可以通过用户键入ctrl-c/ctrl-z
发送
trace04.txt - Run a background job. 要调用运行一个后台工作,然后
printf()
出相关信息(jid,pid,进程名字),将会用到已经提供的函数:pid2jid()
trace06.txt - Forward SIGINT to foreground job;
trace07.txt - Forward SIGINT only to foreground job;
trace08.txt - Forward SIGTSTP only to foreground job;
trace11.txt - Forward SIGINT to every process in foreground process group;
trace12.txt - Forward SIGTSTP to every process in foreground process group.trace06, 07, 08, 11, 12涉及到信号,那就必须创建并运行child了(得写
sigchld_handler()
),由此便会产生child和parent之间的race(竞争),必须解决它。同时,因为涉及到SITINT
、SIGTSTP
,得写sigint_handler()
、sigtstp_handler()
。child和parent之间的race,按照书中方式,只需要在创建child之前block(阻塞)
SIGCHLD
,在创建child并运行之后unblock(解除阻塞)即可。注意write up中给出的提示:
After the fork, but before the execve, the child process should call setpgid(0, 0), which puts the child in a new process group whose group ID is identical to the child’s PID. This ensures that there will be only one process, your shell, in the foreground process group. When you type ctrl-c, the shell should catch the resulting SIGINT and then forward it to the appropriate foreground job (or more precisely, the process group that contains the foreground job).
因此我们调用
setpgid(0, 0)
,将foreground中进程组的pgid设置为child的pid。
对于parent,
before add/delete job, we must block all sigs
,防止信号干扰到job的增删- 如果是前台,先设置
flag = 1
,然后addjob()
,最后需要显式地等待工作终止即调用waitfg()
- 如果是后台,直接
addjob()
,然后unblock即可。
- 如果是前台,先设置
打印
Command not found.
(trace14)
1 | void eval(char *cmdline) { |
do_bgfg()
- 主要功能:Execute the builtin bg and fg commands.
- trace09.txt - Process bg builtin command
trace10.txt - Process fg builtin command.
trace13.txt - Restart every stopped process in process group;
trace14.txt - Simple error handling. - Add job to jobs(list) in bg will builds new jobs, so we can only change the state of job
bg部分:改state为BG再打印即可;
fg部分:改state为FG前,要注意信号的阻塞以及flag的设置,同时,前台的job必须等待终止或停止(waitfg) - 打印错误信息时,注意
(job_ptr == NULL)
必须在(job_ptr->state == UNDEF)
之前 - 满足restart,发送
SIGCONT
即可,注意区分发送至单个进程还是整个工作(进程组)
1 | void do_bgfg(char **argv) |
waitfg()
- 主要功能:Block until process pid is no longer the foreground process
eval()
和do_bgfg()
的foreground部分需要使用此函数
1 | void waitfg(pid_t pid) |
builtin_cmd()
- 主要功能:判断是不是内嵌指令(builtin_command),是就执行对应指令,否则返回
0
- trace02.txt - Process builtin quit command.
执行quit指令,该指令的作用是退出tsh程序,调用exit(0)
即可。该指令是前台任务(trace03.txt - Run a foreground job.) strcmp()
=>if equal, strcmp() return 0, so !strcmp() return 1
- trace05.txt - Process jobs builtin command.
执行jobs指令,该指令的作用是罗列所有工作,调用listjobs()
即可。
1 | int builtin_cmd(char **argv) |
sigchld_handler()
When each child terminates, the kernel notifies the parent by sending it a SIGCHLD signal. The parent catches the SIGCHLD, reaps one child, does some additional cleanup work (modeled by the sleep statement), and then returns.
- 当任意child终止时,kernel(shell)就会发送
SIGCHLD
给parent,sigchld_handler()
就要开始处理,开始回收zombie child
- 当任意child终止时,kernel(shell)就会发送
以下是教材内容:
WNOHANG | WUNTRACED: Return immediately, with a return value of 0, if none of the children in the wait set has stopped or terminated, or with a return value equal to the PID of one of the stopped or terminated children.
- WIFEXITED(status). Returns true if the child terminated normally, via a call to exit or a return.
- WIFSIGNALED(status). Returns true if the child process terminated because of a signal that was not caught.
- WTERMSIG(status). Returns the number of the signal that caused the child process to terminate. This status is only defined if WIFSIGNALED() returned true.
- WIFSTOPPED(status). Returns true if the child that caused the return is currently stopped.
- WSTOPSIG(status). Returns the number of the signal that caused the child to stop. This status is only defined if WIFSTOPPED() returned true.
WIFSIGNALED(status)
的描述出错了,应该是:Returns true if the child process terminated because of a signal that was caught.
调用
waitpid()
等待child pid,而后回收zombie child(实验要求这里使用if
判断),然后根据status打印信息,最后删除(即回收)这个child或者修改child的state为ST
。(注意:使用jid来修改是无效,必须用地址)if pid == fg_pid, job terminated or stopped in foreground
因为WNOHANG | WUNTRACED: or with a return value equal to the PID of one of the stopped or terminated children.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25void sigchld_handler(int sig)
{
int olderrno = errno;
int status;
pid_t pid, fg_pid = fgpid(jobs); //Note: only one job in fg
if ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
if(pid == fg_pid)
flag = 1;
if (WIFEXITED(status)) //normally terminated (return/exit)
deletejob(jobs, pid);
else if (WIFSIGNALED(status)){ //child terminated because SIGINT that was caught.
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(status));
deletejob(jobs, pid);
}
else if (WIFSTOPPED(status)){
struct job_t *job_ptr = getjobpid(jobs, pid);//getjobpid() return address of job
job_ptr->state = ST; //stopped the job
printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
}
}
errno = olderrno;
return;
}
sigint_handler()
- 处理信号
SIGINT
kill(pid, SIG)
是只发送信号给单个进程,kill(-pgid, SIG)
是发送信号给整个进程组
1 | void sigint_handler(int sig) |
sigstp_handler()
- 处理信号
SIGTSTP
1 | void sigtstp_handler(int sig) |
Put all together
1 | /* |
trace15 & 16
Conclusion
- 完成日期:22.1.25
- 有效时间大概12小时,除了24号是下午晚上都在电脑前,其他时间都是中午,因为晚上要学c++,24号debug完毕,25号把博客写完
- 本实验和教材关系极大,所以主要是理解难度大(进程,异常,信号)
- c语言果然是用来和底层交流的最好语言
- 要快点学完c++,然后用在算法上!
- 《枕边童话》很好听,晚上可以看《开端》结局了!(英文名叫reset,restart?发送
SIGCONT
?)