CSAPP Lab06——Shell Lab通关思路

远距离的欣赏

近距离的迷惘

谁说太阳会找到月亮

——修炼爱情

 

完整代码见:CSAPP/shlab-handout at main · SnowLegend-star/CSAPP (github.com)

 

上来就遇到了些小问题:①本来想看看“tshref”支持的命令,结果命令居然被拒绝执行了,显示“-bash: ./tshref: Permission denied。然后又试了下“./sdriver.pl -h”这个命令,也是报错“-bash: ./sdriver.pl: Permission denied”。我还以为是用户权限的问题,试了下切到root,结果还是不能执行。最后查看了下文件,发现除了自己编译的文件,其他文件都是“rw-r--r--”。下面是GPT对文件默认权限问题的回答。

书上P239,关于子进程和父进程对各自内存里的内容进行操作后的结果。 

在正式进行实验之前,还得正式了解一番waitpid函数

pid_ t waitpid(pid_t pid, int *status, int options);

返回值:

  1. 当正常返回的时候waitpid返回收集到的子进程的进程ID;
  2. 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0
  3. 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:

    pid 

        Pid=-1, 等待任一个子进程。与 wait 等效。

        Pid>0. 等待其进程 ID 与 pid 相等的子进程。

   status:

        WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)

        WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)

   options:

        WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。

使用的注意事项: 

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。
  1. //pid_t ret=wait(NULL);                                                          
  2. //pid_t ret=waitpid(id,NULL,0);//等待指定为id的子进程。                          
  3. //pid_t ret=waitpid(-1,NULL,0);//设置成-1代表等待任意一个子进程,可能我们有10个子进程,这里只等待任意一个,等价于wait

④在运行“make testxx”时,注意是在linux的shell中运行,而不是在自己编写的tsh中运行。

eval(char *cmdline)

首先解析命令行,调用builtin_cmd判断是否是内置命令。如果是内置命令,执行对应的内置命令。否则,fork一个子进程,execv 对应的代码,将其添加到 job 列表中。

2023/12/15 23:08

书上的P525那份样例代码可以直接拿来用,以便于我们把代码的框架先搭建起来。

有一点我没有理解,为什么在调用fork()之前要先屏蔽SIGCHLD信号,而在fork()结束之后可以立即对SIGCHLD信号解除屏蔽?

        sigprocmask(SIG_BLOCK,&mask_one,&prev_one); 
        if((pid=fork())==0){    
            sigprocmask(SIG_SETMASK,&prev_one,NULL);    
} 

 我忽略了最重要的一点,解除对SIGCHLD信号的屏蔽只是作用在子进程,而父进程依然保持对SIGCHLD信号的屏蔽。只有当

sigprocmask(SIG_SETMASK,&prev_one,NULL);

父进程才会解除对SIGCHLD信号以及其他信号的屏蔽,进而可以着手处理待处理信号队列中的SIGCHLD,从而避免了竞争的问题。

而之所以子进程要解除对SIGCHLD信号的屏蔽,原因可以分为两部分来讲:①子进程 会继承父进程 的被阻塞集合②子进程 可能自己也要创建自己的子进程 ,但是没有屏蔽SIGCHLD信号的需求,所以 清空从 继承过来的被阻塞集合是比较合理的。

void eval(char *cmdline) 
{
    char *argv[MAXARGS];
    char buf[MAXLINE];
    int bg;
    pid_t pid;
    sigset_t mask_all,mask_one,prev_one;

    sigfillset(&mask_all);  //这一步的作用是什么?
    sigemptyset(&mask_one); //把一会儿要阻塞的信号集清空
    sigaddset(&mask_one,SIGCHLD);   //把子进程发送的SIGCHLD阻塞,避免父子进程的竞争现象
    // signal(SIGCHLD,sigchld_handler); 
    // signal(SIGTSTP,sigtstp_handler);
    // signal(SIGINT,sigint_handler);
    strcpy(buf,cmdline);
    bg=parseline(buf,argv);
    if(argv[0]==NULL)
        return ;    //ignore empty lines

    if(!builtin_cmd(argv)){
        sigprocmask(SIG_BLOCK,&mask_one,&prev_one); //把SIGCHLD阻塞之
        if((pid=fork())==0){    //子进程运行user job
            sigprocmask(SIG_SETMASK,&prev_one,NULL);    //取消对SIGCHLD的阻塞,确保子进程结束后父进程可以收到
            setpgid(0,0);                               //做到一个进程一组,不然父进程一会儿调用kill函数的时候也会给自己发送消息
            if(execve(argv[0],argv,environ)<0){         //execve函数成功不返回,失败才返回-1
                printf("%s: Command not found.\n",argv[0]);
                exit(0);
            }
        }

        //父进程添加jobs
        sigprocmask(SIG_BLOCK,&mask_all,NULL);
        int job_status;
        if(bg==0)
            job_status=FG;
        else
            job_status=BG;
        addjob(jobs,pid,job_status,cmdline);
        sigprocmask(SIG_SETMASK,&prev_one,NULL);

        //父进程等待前台job终止
        if(!bg){    //是前台进程
            waitfg(pid);
        }
        else{       //是后台进程
            printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);
        }        
            
    }

    return;
}

builtin_cmd(char **argv)

按实验要求,我们的tsh需要支持4个内置命令

-quit 终止命令

-jobs 列出所有的后台作业

-bg <job> 重新唤醒<job>,并把该作业运行在后台

-fg   <job> 重新唤醒<job>,并把该作业运行在前台

没什么技术含量,就是把命令行的第一个参数取出来与几个内置命令进行对比,每个内置命令的处理函数已经提供了,直接调用即可。

实现代码如下:
 

/* 
 * builtin_cmd - If the user has typed a built-in command then execute
 *    it immediately.  
 */
int builtin_cmd(char **argv) 
{
    if(!strcmp(argv[0],"quit"))     //如果是quit命令
        exit(0);
    if(!strcmp(argv[0],"&"))        //如果只有一个“&”,不用理会
        return 1;
    if(!strcmp(argv[0],"bg")){      //处理bg命令
        do_bgfg(argv);
        return 1;
    }              
    if(!strcmp(argv[0],"fg")){      //处理fg命令
        do_bgfg(argv);
        return 1;
    }           
    if(!strcmp(argv[0],"jobs")){    //处理jobs命令
        listjobs(jobs);
        return 1;
    }     
    return 0;     /* not a builtin command */
}

void waitfg(pid_t pid);

在这个函数体内调用sleep()函数就行,很粗糙的一个函数。

/* 
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    while(fgpid(jobs))   //如果检测到这个前台进程还没结束,系统就保持休眠1s,轮询执行直到该前台进程结束
        sleep(1);
    return;
}

void do_bgfg(char **argv)

       这个函数要求我们做好“fg”与“bg”命令的区分,同时“%num”表示用job号来进行查找,“num”则表示用process号来查找。这里有一个比较麻烦的东西,就是如果第二个命令行参数是“%123”,我们现在只需要提取出“123”,一般想法是创建一个新的数组tmp,再把“123”赋值过去。但是,由于我想用atoi()函数,所以又要把tmp数组装换为字符串形式,总之很是麻烦。

       看到CSDN一个博主的妙招之后,我直接被这记妙手所折服。

job_tmp=getjobjid(jobs,atoi(&argv[1][1]));  //很妙的操作

/* 
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv) 
{
    //做好bg、fg、pid、jid的区分就行
    struct job_t *job_tmp;
    
    if(argv[1]==NULL){
        printf("%s command requires PID or %%jobid argument\n",argv[0]);
        return ;
    }
    if(argv[1][0]!='%'&&!isdigit(argv[1][0])){   //如果命令行第二个参数的首字符不是数字也不是%
        printf("%s: argument must be a PID or %%jobid\n",argv[0]);
        return ;
    }
    if(argv[1][0]!='%'){    //如果是用pid找
        job_tmp=getjobpid(jobs,atoi(argv[1]));
        if(job_tmp==NULL){
            printf("(%d): NO such job\n",atoi(argv[1]));
            return ;
        }
    }
    else{       //用jid找
        job_tmp=getjobjid(jobs,atoi(&argv[1][1]));  //很妙的操作
        if(job_tmp==NULL){
            printf("%%%d: NO such job\n",atoi(&argv[1][1]));
            return ;
        }
    }

    kill(-job_tmp->pid,SIGCONT);
    if(!strcmp(argv[0],"fg")){   //如果是前台进程
        job_tmp->state=FG;    
        waitfg(job_tmp->pid);
    } 
    else{
        job_tmp->state=BG;
        printf("[%d] (%d) %s",pid2jid(job_tmp->pid),job_tmp->pid,job_tmp->cmdline);
    }
    return;
}

void sigchld_handler(int sig);

为了消除竞争问题,势必要对SIGCHLD信号做一定的处理。我们可以参照书上的P542,其中的关键一招就是在进行deletejob()的时候屏蔽所有信号。

while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)

在这个 while 循环中,waitpid 以非阻塞方式检查所有子进程的状态。如果有子进程的状态发生变化,waitpid 将返回该子进程的进程ID。如果没有子进程的状态发生变化,waitpid 将返回0,这时循环结束。

这种方式的使用通常用于轮询(polling)子进程的状态,而不是阻塞等待它们。这允许主程序在等待子进程的同时继续执行其他任务。如果没有设置 WNOHANG 标志,waitpid 将阻塞,直到有子进程的状态发生变化。

void sigchld_handler(int sig) 
{
    int olderrno=errno;
    sigset_t mask_all,prev_all;
    pid_t pid;
    int status;

    sigfillset(&mask_all);
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0){              //回收僵尸进程  waitpid(-1,NULL,0)有问题
        if(WIFEXITED(status)){    //如果这个子进程是正常终止的  WEXITSTATUS(status)
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            deletejob(jobs,pid);                                        //从job list里面删除该结束的子进程
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
        else if(WIFSIGNALED(status)){    //如果这个子进程是因为收到某个信号而终止的(SIGINT)
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            printf("Job [%d] (%d) terminated by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));            
            deletejob(jobs,pid);                                        //从job list里面删除该结束的子进程
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
        else{   //如果是SIGSTP信号 
            struct job_t *job_running=getjobpid(jobs,pid);
            sigprocmask(SIG_BLOCK,&mask_all,&prev_all);
            printf("Job [%d] (%d) stopped by signal %d\n",pid2jid(pid),pid,WTERMSIG(status));
            job_running->state=ST;
            sigprocmask(SIG_SETMASK,&prev_all,NULL);
        }
    }

    // if(errno!=ECHILD)
    //     printf("waitpid error");
    errno=olderrno;        

    return;
}

void sigtstp_handler(int sig);

由于SIGSTP信号是针对于前台进程的,所以在这个函数内部只需要用kill()发送一个信号给前台进程就好,其他的操作可以放到SIGCHLD信号的处理函数里面去。

这里可能会有一个令人费解的地方,明明SIGSTP的处理函数并没有和SIGCHLD的处理函数进行交互,怎么后者还可以处理前者应该处理的部分呢?

其中原因就在于sigchld_handler ()内部的waitpid()函数,一旦子进程接收到了SIGSTP信号,则该子进程的状态会立即发生改变从而导致waitpid()收到对应的pid,从而代码的实际执行部分就跳转到了sigchld_handler()函数内部。

void sigtstp_handler(int sig) 
{
    int olderrno = errno;
    pid_t fg_pid = fgpid(jobs);
    if(fg_pid){
        kill(-fg_pid,sig);
    }
    errno = olderrno;
    return;
}

void sigint_handler(int sig);

同上。

void sigint_handler(int sig) 
{
    int olderrno=errno;
    pid_t fg_pid=fgpid(jobs); ;//获取当前正在运行的前台进程的pid,如果没有就返回0
    if(fg_pid)
        kill(-fg_pid,sig);
    errno=olderrno;
    return;
}

Test01~04

完成eval()和bulitin_cmd()后,尝试执行了上面前三个test,发现输出都和参考输出一致。只有test04出现了输出格式的问题,修改下输出格式即可。

Test05

执行这个测试的时候,发现jobs输出为空。

猜测是出现了父子竞争的问题,开始着手修改eval()、waitfg()、sigchld_handler()。eval()和sigchld_handler()可以按照书上P543来修改,但是得注意sigchld_handler()内部有一处需要修改的地方。

while((pid=waitpid(-1,NULL,0))>0)

这条命令是等待任意进程结束,但如果进程结束的话我们就会从job list中删除这个进程,此时就不存在这个进程的相关信息了。按提示的意思,是一旦发现后台进程存在,waitpid()就立即返回,顺便打印出该后台进程的相关信息。所以waitpid(pid_t pid,int *status,int options)的options部分应改为“WNOHANG|WUNTRACED”才是正确的。

Test06~08

为什么父进程中有signal(SIGCHLD,sigchld_handler);这种处理SIGCHLD的函数,而并没有处理SIGSTPSIGINT的函数呢?

    signal(SIGCHLD,sigchld_handler); 
    // signal(SIGTSTP,sigtstp_handler);
    // signal(SIGINT,sigint_handler);

 我的理解是处理下面两个信号的函数在main()中已经调用了,所以不用在eval()函数中进行重复调用

    /* These are the ones you will need to implement */
    Signal(SIGINT,  sigint_handler);   /* ctrl-c */
    Signal(SIGTSTP, sigtstp_handler);  /* ctrl-z */
    Signal(SIGCHLD, sigchld_handler);  /* Terminated or stopped child */

之所以要在eval()中重新调用signal(SIGCHLD,sigchld_handler),是因为我们在eval()函数中阻塞了SIGCHLD信号,在对这个信号解阻塞后要重新对SIGCHLD信号进行处理,所以得再次调用signal(SIGCHLD,sigchld_handler)

Md,刚才我signal(SIGCHLD,sigchld_handler)删掉了再对样例进行测试,结果发现输出并没有丝毫变化。事实证明signal(SIGCHLD,sigchld_handler)只需要调用一次就好,在eval()函数中并不需要重新调用这个函数。怪不得连我自己都觉得解释显得很牵强附会,原来我压根儿就在胡说八道啊。

②很奇怪的问题

熄灯之后借着电脑屏幕的光瞄了眼书,结果看差了。应该调用的是WIFEXITED()函数而不是WEXITSTATUS()函数。调用了WEXITSTATUS()后之所以会出现上图结果,是因为子进程因为接收到了SIGINT信号而异常终止。对于非正常终止的进程,WEXITSTATUS()返回的值是未定义的,这可能就导致了进程出现后续一连串的问题。

Test09~10

这里开始要求处理“fg”“bg”命令,完成do_bgfg()即可。

Test11~13

输出的东西有点难看懂,粗略对比下,如果前10个都没问题的话这个应该也没问题。

Test14

测试各种输入错误的处理,简单更加错误处理部分即可。

Test15

将前面所有的测试内容都测试一遍,基本不用再修改代码。

Test16

是测试tsh能否处理不是来自终端而是来自其他进程的SIGSTP和SIGINT信号,顺利通过。

  • 12
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值