前言
视频现已刷完好几天了,lab才打完这个,不得不说shell lab也是规划的非常交心,注意事项简直都能在文档找到。
参阅资料:
课程视频链接:2015 CMU 15-213 CSAPP 深入了解计算机体系 课程视频
试验文档:shlab.dvi (cmu.edu)
一、试验须知
试验文件现已在main函数中为咱们完结了指令行参数的读取、给信号绑定handler等主体部分,并且供给了一系列有用好用的函数,咱们需求做的只有在tsh.c中完结以下函数:
- eval: 解析每条指令行的类型并履行
- builtin_cmd: 辨认和处理四条内置指令: quit, fg, bg 和 jobs.
- do_bgfg: 完结内置指令fg 和 bg
- waitfg: 等候前台作业完结
- sigchld_handler: 接纳SIGCHLD信号
- sigint_handler: 接纳SIGINT(Ctrl C)信号
- sigtstp_handler: 接纳SIGINT(Ctrl Z)信号
试验文件还供给了参阅的tshref,sdriver.pl以及许多tracexx.txt文件供咱们对比检验自己的tsh是否符合要求,运用方法如下:
root@Andrew:/usr/coding/shlab-handout# ./sdriver.pl -a "-p" -t trace01.txt -s ./tsh
#
# trace01.txt - Properly terminate on EOF.
#
root@Andrew:/usr/coding/shlab-handout# ./sdriver.pl -a "-p" -t trace01.txt -s ./tshref
#
# trace01.txt - Properly terminate on EOF.
#
root@Andrew:/usr/coding/shlab-handout#
-a “-p”表示输入不会有剩余的prompt,观感更佳
-t 指定tracexx.txt,即测验点,共有16个
-s 指定shell,一次指定参阅文件tshref,一次指定自己的tsh,查看两次的输出是否一致即可
二、编写代码
依照把trace01到trace16一个一个攻关的思路,就能一点点把shell造全了。
由于进程实在有些弯曲,并且也有部分失败的代码被抛弃了,就懒得展现每一步的进程了。直接上每个函数的代码然后说明。
1. eval
eval的参数cmdline是从指令行输入的一串字符,结束是换行符n,所以在输出cmdline是自带换行的
试验供给了函数parseline,用于解析cmdline中的每个参数,回来一个int值bg代表后台作业(结束是&符号的时候)
随后利用builtin_cmd判别是否为内置指令,是的话在其间履行,回来1;不是的话回来0,之后再fork execve调用可履行文件。注意到需求对不存在的可履行文件做异常处理,打印Command not found,并且指定子进程的进程组id为本身setgpid(0, 0),然后确保前台进程组只有咱们的shell。
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
由于咱们保护一个全局的job_t类型数组jobs,并在每次创立作业后调用addjob保存作业的信息到jobs当中。
可是由于咱们将要在sigchld_handler中对jobs数组做某些操作,比如运用deletejob。为了确保addjob和deletejob的顺序,咱们必须堵塞SIGCHLD 从fork进程之前 到 完结了addjob之后,才允许进程接纳SIGCHLD
完好代码如下:
void eval(char *cmdline)
{
char *argv[MAXARGS];
int bg = parseline(cmdline, argv);
if(argv[0] == NULL)
return;
pid_t pid;
if(!builtin_cmd(argv)){
// 堵塞SIGCHLD的接纳,确保addjob和deletejob的顺序
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL);
if((pid = fork()) < 0){
// Fork失败
unix_error("Fork error");
}else if(pid == 0){
// 子进程,免除SIGCHLD的堵塞,然后execve运转程序
sigprocmask(SIG_UNBLOCK, &mask, NULL);
setpgid(0, 0);
if(execve(argv[0], argv, environ) < 0){
printf("%s:Command not found.n",argv[0]);
exit(-1);
}
}else{
// 主进程
// 添加job后免除堵塞,确保add和delete的顺序
// 前台运转时,调用waitfg做自旋(spin)
addjob(jobs, pid, bg ? BG : FG, cmdline);
sigprocmask(SIG_UNBLOCK, &mask, NULL);
if(bg){
printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
}else{
waitfg(pid);
}
}
}
return;
}
2. builtin_cmd
内置指令包括四个:
- quit:遇到quit时,直接中止shell地点进程就行了,简略!
- jobs:也简略!试验现已给咱们写好了listjobs,直接调就好了
- bg和fg:辨认出来后就丢进do_bgfg里边,别的再处理
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;
}
return 0; /* not a builtin command */
}
3. sigint_handler 和 sigtstp_handler
经过函数fgpid拿到前台作业的pid,然后将信号发送给进程组中每个进程
int kill(pid_t pid, int sig)
假如 pid 大于零,那么 kill 函数发送信号号码 sig 给进程 pid。假如 pid 小于零,那么 kill 发送信号sig给进程组 abs(pid)中的每个进程。
void sigint_handler(int sig)
{
pid_t pid = fgpid(jobs);
// printf("Job [%d] (%d) terminated by signal 2n", pid2jid(pid), pid);
kill(-pid, SIGINT);
return;
}
void sigtstp_handler(int sig)
{
pid_t pid = fgpid(jobs);
// printf("Job [%d] (%d) stopped by signal 20n", pid2jid(pid), pid);
kill(-pid, SIGTSTP);
return;
}
这儿不打印前台作业被中止或中止的信息,是由于进程被中止或中止时会发送SIGCHLD信号,咱们能够在sigchld_handler中处理。
而非要在sigchld_handler处理的原因是:sigchld_handler不只能够监控前台作业,还能够监控后台作业的中止和中止(为了经过trace16),并且能够利用status获得中止或中止的原因
4. do_bgfg
bg 和 fg的处理思路高度相似,先解析参数有没有%,然后决定是运用getjobjid仍是getjobpid确认作业是否存在,然后做一系列error handling(trace13要求)
最后,向作业的pid所标识进程组发送信号SIGCONT,假如是bg就打印后台作业的信息,假如是fg就调用waitfg自旋
完好代码如下:
void do_bgfg(char **argv)
{
// parse argument
int jid = 0;
pid_t pid = 0;
int bg = !strcmp(argv[0], "bg");
struct job_t *job;
if(argv[1] == NULL){
printf("%s command requires PID or %%jobid argumentn", argv[0]);
return;
}
if(argv[1][0] == '%'){
jid = atoi(argv[1] 1);
}else{
pid = atoi(argv[1]);
}
if(jid){
job = getjobjid(jobs, jid);
if(job == NULL){
printf("%%%d: No such jobn", jid);
return ;
}
pid = job->pid;
}else if(pid){
job = getjobpid(jobs, pid);
if(job == NULL){
printf("(%d): No such processn", pid);
return ;
}
}else{
printf("%s: argument must be a PID or %%jobidn", argv[0]);
return ;
}
kill(-pid, SIGCONT);
if(bg){
job->state = BG;
printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
}else{
job->state = FG;
waitfg(pid);
}
return;
}
5. waitfg
前面说了几次调用waitfg自旋(在eval和do_bgfg中),为啥要自旋而不是waitpid呢?
由于假如咱们在waitfg中调用waitpid收回前台作业的话,当该作业中止或中止,它会发送信号SIGCHLD,这就造成了sigchld_handler和waitfg的竞争联系,那就不如让sigchld_handler收回就好了。
并且文档中也清晰主张了waitfg中仅运用一个循环 sleep:
One of the tricky parts of the assignment is deciding on the allocation of work between the waitfg and sigchld_handler functions. We recommend the following approach:
– In waitfg, use a busy loop around the sleep function.
– In sigchld_handler, use exactly one call to waitpid.
While other solutions are possible, such as calling waitpid in both waitfg and sigchld_handler, these can be very confusing. It is simpler to do all reaping in the handler.
代码如下:
void waitfg(pid_t pid)
{
while(fgpid(jobs) == pid){
sleep(1);
}
return;
}
6. sigchld_handler
最重量级的来了,前面说到:当子进程中止或中止时,都会给父进程发送一个SIGHCLD信号,告诉父进程该调用waitpid去收回它了
当然,waitpid也有很多花活:
pid_t waitpid(pidt pid,int *status, int options);
假如 pid>0,那么等候调集便是一个独自的子进程,它的进程ID等于 pid。
假如 pid=-1,那么等候调集便是由父进程一切的子进程组成的。
所以咱们这儿应该运用pid = -1等候任一子进程
能够经过用常量 WNOHANG 和WUNTRACED的不同组合来设置 options,修正默认行为:
WNOHANG: 假如没有等候调集中的任何子进程中止,那么就当即回来(回来值为0)。
WUNTRACED: 挂起调用进程的履行,直到等候调集中的一个进程变成中止的或许被暂停。回来的 PID为导致回来的中止或暂停子进程的PID。
WNOHANG | WUNTRACED: 当即回来,假如没有等候调集中的任何子进程中止或中止,那么回来值为0,或许回来值等于那个被中止或许中止子进程的PD。
咱们需求sigchld_handler在后台静静地处理中止和中止的子进程,而不会影响主进程的履行,所以options应该挑选WNOHANG | WUNTRACED
然后便是根据几个解释status的宏,处理每种发送SIGCHLD的状况
WIFEXITED(status): 假如子进程正常中止就回来真,也便是经过调用exit 或许一个回来(return)
WIFSIGNALED(status): 假如是由于一个未被捕获的信号造成了子进程的中止,那么就回来真
WIFSTOPPED(status): 假如引起回来的子进程当时是暂停的,那么就回来真。
完好代码:
void sigchld_handler(int sig)
{
pid_t pid;
int status;
while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED) ) > 0){
if(WIFEXITED(status)){
// 假如正常中止
deletejob(jobs, pid);
}else if(WIFSIGNALED(status)){
// 收到信号中止(为了能监控其他进程)
printf("Job [%d] (%d) terminated by signal 2n", pid2jid(pid), pid);
deletejob(jobs, pid);
}else if(WIFSTOPPED(status)){
// 假如进程中止
printf("Job [%d] (%d) stopped by signal 20n", pid2jid(pid), pid);
struct job_t *job = getjobpid(jobs, pid);
job->state = ST;
}
}
return;
}