今天,我们能够跟随大佬的思路,亲自搭建自己的shell程序,虽然是阉割版,但是也能够非常激动。花了三个晚上终于完成了。
再一次感谢csapp这本书,以及老师的习题和lab。废话不多说,进入正题:
这次的任务十分简单清晰,直接。在做题之前请务必认真的读了实验lab的指导书,每个单词都很重要。
http://csapp.cs.cmu.edu/3e/shlab.pdf
总结下,我觉得其中最终的就是两点:
1. 请务必把异常控制流的每个字都认真读一遍。(不认真读,绝对会走很多弯路,而且最后你最后还是得乖乖回来看的)。
2. 请珍惜认真使用老师编写的测试程序,一个一个test做,一个个功能晚上做。不要一起写完了,再妄想一次通过,对于我这种萌新来说是不可能的,所以请踏踏实实的一个个来。
其他的细节,见实验指导书。具体说:这次的目标我们需要完成6个函数。
int builtin_cmd(char **argv, int argc);
void do_bgfg(char **argv, int argc);
void waitfg(pid_t pid);
void sigchld_handler(int sig);
void sigtstp_handler(int sig);
void sigint_handler(int sig);
注意,这里我修改了原来的函数原型,添加了在builtin_cmd函数和do_bgfg函数的参数列表中添加了argv,表示输入指令的数目,同时修改了parseline函数,使得它能够它argv数目通过argcp指针正确传递回来。为的是,正确处理一些错误输入。
----------------------分割线-------------------
全局变量:
//用于void sigchld_handler中,判断是否是当前引起停止信号的是否是前台进程,
//这种标志的作法,借鉴了书中P546页的做法
volatile sig_atomic_t fg_stop_or_exit;
下面一个个函数的说:
首先是eval函数,算是最需要好好考虑的函数之一:
思路和步骤:
step 1 初始化各变量以及设置信号阻塞合集
step 2 解析命令行,得到是否是后台命令,置位state(前后台标志)
step 3 判断是否时内置命令,如果是立即执行。否则fork子进程。
子进程调用执行具体的命令文件。然后exit
父进程执行step4.
step 4 将子进程job加入到job list中。
step 5 判断是否是bg,fg调用waifg函数等待前台运行完成,bg打印消息即可
整个过程中,需要仔细考虑显式阻塞的安排和设计,什么地方需要阻塞哪些信号。
原则1:在访问全局变量(jobs)必须阻塞所有信号,包括调用老师给的那些函数。由于这些函数基本都是使用for循环遍历完成功能的,所以,务必保证函数执行中不能被中断。
原则2:在一些函数或者指令有必须的前后执行顺序时,请阻塞,保证前一个函数调用完成后(比如必须先addjob,再deletejob)
细节见注释。
void eval(char *cmdline)
{
//step 1 初始化各变量以及信号阻塞合集
char* argv[MAXARGS];
char buf[MAXLINE];
int state;
int argc;
pid_t curr_pid;//储存当前前台pid
sigset_t mask_all, mask_one, mask_prev;
//设置阻塞集合
sigemptyset(&mask_one);
sigaddset(&mask_one, SIGCHLD);
sigfillset(&mask_all);
//step 2 解析命令行,得到是否是后台命令,置位state
strcpy(buf, cmdline);
state = parseline(buf, argv, &argc)? BG : FG;
//step 3 判断是否时内置命令
if(!builtin_cmd(argv, argc)){
//不是内置命令,阻塞SIGCHLD,防止子进程在父进程之间结束,也就是addjob和deletejob之间,必须保证这个拓扑顺序
sigprocmask(SIG_BLOCK, &mask_one, &mask_prev);
if((curr_pid = fork()) == 0){
//子进程,先解除对SIGCHLD阻塞
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
//改进进程的进程组,不要跟tsh进程在一个进程组,然后调用exevce函数执行相关的文件。
setpgid(0, 0);
if(execve(argv[0], argv, environ) < 0){
//没找到相关可执行文件的情况下,打印消息,直接退出
printf("%s: Command not found.\n", argv[0]);
}
//这里务必加上exit(0),否则当execve函数无法执行的时候,子进程开始运行主进程的代码,出现不可预知的错误。
exit(0);
}
//step 4
//创建完成子进程后,父进程addjob,整个函数执行期间,必须保证不能被中断。尤其是玩意在for循环过程中中断了不堪设想
//因此,阻塞所有信号,天塌下来也要让我先执行完
sigprocmask(SIG_BLOCK, &mask_all, NULL);
addjob(jobs, curr_pid, state, cmdline);
//再次阻塞SIGCHLD
sigprocmask(SIG_SETMASK, &mask_one, NULL);
//step 5 判断是否是bg,fg调用waifg函数等待前台运行完成,bg打印消息即可
//还有一个问题是,如果在前台任务,如果我使用默认的waitpid由于该函数是linux定义的原子性函数,无法被信号中断,那么前台
//函数在执行的过程中,无法相应SIGINT和SIGSTO信号,这里我使用sigsuspend函数加上while判断fg_stop_or_exit标志的方法。具体见waitfg函数
if(state == FG){
waitfg(curr_pid);
}
else{
//输出后台进程的信息
//读取全局变量,阻塞所有的信号防止被打断
sigprocmask(SIG_BLOCK , &mask_all, NULL);
struct job_t* curr_bgmask = getjobpid(jobs, curr_pid);
printf("[%d] (%d) %s", curr_bgmask->jid, curr_bgmask->pid, curr_bgmask->cmdline);
}
//解除所有的阻塞
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
}
return;
}
然后是buildin_cmd函数,相对来说逻辑很简单,就是判断是哪个内置命令转到相应的函数,然后return 1;否则,就不是内置命令,啥也不做,返回0。
上面两条原则需要时刻注意!
int builtin_cmd(char **argv, int argc)
{
//初始化变量以及阻塞集合
char* cmd = argv[0];
sigset_t mask_all, mask_prev;
sigfillset(&mask_all);
if(!strcmp(cmd, "quit")){
//直接退出,这里可以考虑更加完善些,比如如果当前任务列表中还有没运行完成的,给列表中所有的进程组,发送9信号,kill掉。
exit(0);
}
else if(strcmp(cmd, "fg") == 0 || strcmp(argv[0], "bg") == 0){
do_bgfg(argv, argc);
return 1;
}
else if(!strcmp(cmd, "jobs")){
//访问全局变量,需要阻塞全部信号
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
listjobs(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
return 1;
}
return 0; /* not a builtin command */
}
接着,是do_bgfg函数,就是实现bg和fg两条内置命令。
这个函数的核心就两点:
1. 区分bg和fg命令,以及传入pid或者jid参数对应的进程的状态。前者if,后者switch就可以包括所用的情况
2. 注意用户输入错误处理,比如参数数量不够或者参数传入错误的情况
上面两条原则需要时刻注意!说三遍!
/*
* do_bgfg - Execute the builtin bg and fg commands
*/
void do_bgfg(char **argv, int argc)
{
//其实这里应该加上错误判断,比如使用输入了三个参数
if(argc != 2){
printf("%s command requires PID or %%jobid argument\n", argv[0]);
fflush(stdout);
return;
}
//初始化变量
char* cmd = argv[0];
char* para = argv[1];
struct job_t* curr_job;
sigset_t mask_all, mask_prev;
int curr_jid;
//判断传入的pid还是jid,并且获取对应的job结构体
sigfillset(&mask_all);
if(para[0] == '%'){
curr_jid = atoi(&(para[1]));
//错误处理2,如果传入的参数不是规定的格式,报错返回
if(curr_jid == 0){
printf("%s:argument must be a PID or %%jobid\n", cmd);
fflush(stdout);
return;
}
}
else{
curr_jid = atoi(para);
if(curr_jid == 0){
printf("%s:argument must be a PID or %%jobid\n", cmd);
fflush(stdout);
return;
}
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_jid = pid2jid(curr_jid);
}
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还是fg
if(!strcmp(cmd, "bg")){
//区分job的状态
switch(curr_job->state){
case ST:
//bg命令,改变该任务的运行状态,ST->BG,同时发送信号给对应的子进程
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;
//如果bg前台或者unfef,那么肯定哪里出错了
case UNDEF:
case FG:
unix_error("bg 出现undef或者FG的进程\n");
break;
}
}
else{
//fg 命令
switch(curr_job->state){
//如果fg挂起的进程,那么重启它,并且挂起主进程等待它回收终止。
case ST:
curr_job->state = FG;
kill(-(curr_job->pid), SIGCONT);
waitfg(curr_job->pid);
break;
//如果fg后台进程,那么将它的状态转为前台进程,然后等待它终止
case BG:
curr_job->state = FG;
waitfg(curr_job->pid);
break;
//如果本省就是前台进程,那么出错了 。
case FG:
case UNDEF:
unix_error("fg 出现undef或者FG的进程\n");
break;
}
}
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
return;
}
再是waitfg函数,即eval函数调用的那个:
这里的精髓,书中讲的很详细了,不赘述了。
void waitfg(pid_t pid)
{
//注意到,进来之间阻塞了SIGCHLD信号
sigset_t mask;
sigemptyset(&mask);
//前台进程的pid和挂起标志
//FGPID = 0;
fg_stop_or_exit = 0;
//让SIGCHLD信号处理程序处理任何子进程传回来的SIGCHLD信号,注意子进程挂起或者终止都会返回这个信号,所以信号处理程序需要区分,处理不同的情况
//只有发出这个信号的子进程是前台进程才设置fg_stop_or_exit标志。
while(!fg_stop_or_exit){
sigsuspend(&mask);
}
return;
}
接着是信号处理程序部分,共计三个:
首先,当然是万众瞩目的:
这里的细节,其实书上也讲得很清楚了,我们要做的就是学以致用,把多种情况结合起来使用:
这里的思想步骤,我后续再更,快12点了,赶紧弄完了,回去。
void sigchld_handler(int sig)
{
int olderrno = errno;
sigset_t mask_all, mask_prev;
pid_t gc_pid;
struct job_t* gc_job;
int status;
sigfillset(&mask_all);
//尽可能的回收子进程,同时使用WNOHANG选项使得如果当前进程都没有终止时,直接返回,而不是挂起该回收进程。这样可能会阻碍无法两个短时间结束的后台进程
//即trace05.txt
while((gc_pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0){
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
gc_job = getjobpid(jobs, gc_pid);
if(gc_pid == fgpid(jobs)){
fg_stop_or_exit = 1;
}
if(WIFSTOPPED(status)){
//子进程停止引起的waitpid函数返回,再判断该进程是否是前台进程
//struct job_t* stop_job = getjobpid(jobs, gc_pid);
gc_job->state = ST;
printf("Job [%d] (%d) terminated by signal %d\n", gc_job->jid, gc_job->pid, WSTOPSIG(status));
}
else{
//子进程终止引起的返回,判断是否是前台进程
//并且判断该信号是否是未捕获的信号
if(WIFSIGNALED(status)){
//struct job_t* gc_job = getjobpid(jobs, gc_pid);
printf("Job [%d] (%d) terminated by signal %d\n", gc_job->jid, gc_job->pid, WTERMSIG(status));
}
//终止的进程直接回收
deletejob(jobs, gc_pid);
}
fflush(stdout);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
}
errno = olderrno;
后面两个信号处理程序,思路简单相似:
1. 获取前台进程,判断当前是否有前台进程。如果没有直接返回。有则步骤2
2. 使用kill函数,发送SIGINT/SIGTSTP信号给前台进程组。
/*
* 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, mask_prev;
pid_t curr_fg_pid;
sigfillset(&mask_all);
//访问全局结构体数组,阻塞信号
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_fg_pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
if(curr_fg_pid != 0){
kill(-curr_fg_pid, SIGINT);
}
errno = olderrno;
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 olderrno = errno;
sigset_t mask_all, mask_prev;
pid_t curr_fg_pid;
sigfillset(&mask_all);
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
curr_fg_pid = fgpid(jobs);
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
if(curr_fg_pid != 0){
/* 臃肿的代码,保留了我调试的过程。
fg_stop_or_exit = 1;
sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
struct job_t* stop_fgjob = getjobpid(jobs, curr_fg_pid);
printf("Job [%d] (%d) stopped by signal 20\n", stop_fgjob->jid, stop_fgjob->pid);
stop_fgjob->state = ST;
sigprocmask(SIG_SETMASK, &mask_prev, NULL);
*/
kill(-curr_fg_pid, SIGTSTP);
}
errno = olderrno;
return;
}
------------------------------分割线-------------------
最后补充测试的部分样图,左侧为测试自己的程序,右侧为老师提供的样本程序。我们需要的就是保证自己程序的行为完全跟它一样(除了pid)
最后,感谢网上的一些参考资料,在我看题时,让我更快的理解题意:
http://wdxtub.com/2016/04/16/thick-csapp-lab-5/
总结:
1. 以后我也要向老师这种模式学习,边测试边添加,这样心里有底,bug更少。python写测试可以学习下。哈哈
2. 这次学习让我又一次认识到,编程永远是,先思考,先画图,一个流程图肯定要画个大概的,起码要想清楚60%以上再开始,所以一定要保证一个函数的健壮性,然后测试通过,我完全相信它提供的服务和功能。微小的一个个函数堆积起来就是复杂庞大的系统。