CSAPP shlab实验

CSAPP shlab实验实验目的熟悉过程控制和信号传递的概念,搭建一个简单的unixshell外壳程序要求完成7个主要函数,其原型都在教材第八章中voideval(char*cmdline);intbuiltin_cmd(char**argv);voiddo_bgfg(char**argv);voidwaitfg(pid_tpid);voidsigchld_handler(intsig);voidsigtstp_handler(intsig);voidsigint_handle

实验目的

  • 熟悉过程控制和信号传递的概念,搭建一个简单的unix shell外壳程序
  • 要求完成7个主要函数,其原型都在教材第八章中
  • void eval(char *cmdline);
  • int builtin_cmd(char **argv);
  • void do_bgfg(char **argv);
  • void waitfg(pid_t pid);
  • void sigchld_handler(int sig);
  • void sigtstp_handler(int sig);
  • void sigint_handler(int sig);

环境搭建,熟悉指令要求

一. 学会编译tsh.,调用tsh文件 tracexx. txt的功能验证方法

①使用make clean和make对tsh文件进行编译

unix>make clean
unix>make

对tsh文件进行编译
无报错警告,编译成功
在这里插入图片描述
②使用make testxx或者./sdriver.pl -t tracexx.txt -s ./tsh -a “-p”来调用tsh文件 tracexx. txt的功能验证:

  • unix> make test01
  • unix> ./sdriver.pl -t trace01.txt -s ./tsh -a “-p”
  • 如果在文件名前面加上r,则是执行标准的tshref,或者将tsh变为tshref
    unix> make rtest01
    unix> ./sdriver.pl -t trace01.txt -s ./tshref -a “-p”

通过比对标准tshref和自制tsh的执行结果结果,可以观察tsh的功能是否正确。如果tsh的执行结果和tshref结果一致,说明结果是正确的。执行结果如下图:
在这里插入图片描述

二. 用 trace01和trace02比较tsh和 tshr执行结果并分析

(1)用trace01分析比较tsh和 tshr执行结果

//trace01.txt中的命令行关闭文件并等待:
#
# trace01.txt - Properly terminate on EOF.
# CLOSE
WAIT

在这里插入图片描述
① 分析:trace01中的命令通过文件流的方式被shell文件tsh读取,并且执行,trace01中的指令功能是读取文本内容,在读取到CLOSE命令的时候关闭文件,读取到WAIT命令的时候等待。
结论:终端显示文本内容# trace01.txt – Properly terminate on EOF.,停顿一段时间(实际上我并没有看到),tsh实验现象和tshref一致。

(2)用trace02分析比较tsh和 tshr执行结果

//trace02.txt中的命令行退出并等待:
#
# trace02.txt - Process builtin quit command.
# quit
WAIT

在这里插入图片描述
① 分析:trace02文件中的命令包括quit和wait命令,quite表示退出trace02文件执行,wait表示等待
② 由于tsh.c中的quite内置命令还没有实现(下一个任务点才会实现),所以tsh.c中没有对应quit命令行语句的函数,所以其停留在输入命令行语句的界面,相反tshref执行文件执行时遇到quit语句后退出。

三.学习 trace测试文件符号、命令、用户程序 mypin含义

符号:

① 空格:分隔指令作用
② &:如果命令以&结尾,表示标该作业在后台运行
③ #:直接打印#后一行的文本内容
④ %:后接一个整数,表示job的ID号。 以下是一个测试程序验证符号功能:

命令:

• jobs: 列出正在运行和停止的后台作业
bg <job>: 将停止的后台作业更改为正在运行的后台作业
fg <job>:将已停止或正在运行的后台作业更改为前台正在运行的作业
• kill : 终止一个作业

用户程序 myspin

myint程序:函数睡眠,使程序睡眠n秒,运行结束后不会自动退出,并会检测系统错误;
myspin程序:函数睡眠,使程序睡眠n秒,在睡眠结束后就自动退出,不检测系统错误;
mysplit程序:函数睡眠,使程序睡眠n秒,创建一个子进程进行睡眠,然后父进程等待子进程正常睡眠n秒后,继续运行;
mystop程序:让进程暂定n秒,并发送信号。

四.了解 eval()与 execve()执行流程和fork()多进程运行方式。

(1)eval执行流程如下:

① eval函数首先解析命令行
② 判断第一个参数是否为内置命令,如果是则立即解释这个命令;否则外壳创建一个子进程,在新的子进程的上下文中加载并运行这个文件。
③ 如果用户要求在后台运行该程序,那么外壳返回到循环的顶部,等待下一个命令行。否则使用waitpid()函数等待作业终止。再开始下一轮迭代。(参考自CSAPP)

(2)execve 函数在当前进程的上下文中加载并运行一个新程序,其执行流程如下

① 加载第一个参数 filename可执行文件名
② 调用启动代码
③ 启动代码设置栈,并将控制传递给新程序的主函数
④ 在新的主函数中执行程序

(3)fork()多进程运行方式: fork()用于从已存进程中树立一个新进程,新进程为子进程,老进程为父进程.

① 调用一次,返回两次。fork 函数被父进程调用一次,但是却返回两次——一次是返回到 父进程,一次是返回到新创建的子进程。
② 并发执行。父进程和子进程是并发运行的独立进程。内核能够以任意方式交替执行它们的 逻辑控制流中的指令。
③ 相同的但是独立的地址空间。每个进程有相同的用户栈、相 同的本地变量值、相同的堆、相同的全局变量值,以及相同的代码。
④ 共享文件。子进程继承了父进程所有的打开文件。

五.了解接收信号、信号处理、信号阻塞概念

  • ① 接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
  • ② 信号处理:执行信号处理程序即为处理信号,对信号做出相应的响应。
  • ③ 信号阻塞:当进程捕获了一个类型为k的信号并在处理时,那么k信号就被阻塞i,被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执⾏递达的动作。

根据要求编写函数

(1)builtin_cmd函数

  • 明确函数的功能:

按照tsh.c中的提示,buildin_cmd函数的功能是:如果用户输入了内置命令,则执行。结合英文文档,可以发现tsh要实现的内置命令包括:quit,jobs,bg,<job>,fg <job>这些内置函数

  • ② 实现思路:

用strcmp函数判断argv参数,分别调用相应处理函数即可。

  • 实现代码如下:
int builtin_cmd(char **argv,int argc) 
{ 
   
    if(!strcmp(argv[0],"quit"))//如果命令是quit,退出 
        exit(0);
    if(!strcmp(argv[0],"jobs")){ 
   //如果命令是jobs,列出正在运行和停止的后台作业
        listjobs(jobs);
        return 1;
    }
    if(!strcmp(argv[0],"fg")||!strcmp(argv[0],"bg")){ 
   //如果是bg或者fg命令
        do_bgfg(argv,argc);
        return 1;
    }
    return 0;     /* not a builtin command */
}

(2)eval()函数:

  • 明确函数功能:

eval函数用于解析和解释命令行的主例程.eval首先解析命令行,如果用户请求一个内置命令quit、jobs、bg或fg那么就立即执行。否则,派生子进程和在子进程的上下文中运行作业。如果作业正在运行前台,等待它终止,然后返回。

  • 实现思路:

①首先使用parseline函数解析命令行,然后判断命令行是否为空如果是,那么直接返回,否则就要判断是否是内置命令了。判断内置命令可以用上面的函数builtin_cmd,如果返回值是0说明不是内置命令(否则会在builtin_cmd中执行完毕),这个时候要用fork创建新的子进程,并且execve函数在子进程的上下文中运行作业。

② 在父进程中判断作业是否在后台中运行,如果是,那么用addjob函数将子进程job加入到job list中,然后调用调用waifg函数等待前台运行完成;如果否,则打印进程组jid和子进程pid以及命令行字符串。

阻塞信号:在pdf中有一段如下的提示。因为子进程继承了他们父进程的阻塞向量,所以在执行新程序之前,子程序必须确保解除对SIGCHLD信号的阻塞。父进程必须使用sigprocmask在它派生子进程之前也就是调用fork函数之前阻塞SIGCHLD信号,之后解除阻塞;在通过调用addjob将子进程添加到作业列表之后,再次使用sigprocmask,解除阻塞。

阻塞信号的原则:

原则1:
在访问全局变量(jobs)必须阻塞所有信号,包括调用老师给的那些函数。由于这些函数基本都是使用for循环遍历完成功能的,所以,务必保证函数执行中不能被中断。

原则2:
在一些函数或者指令有必须的前后执行顺序时,请阻塞,保证前一个函数调用完成后(比如必须先addjob,再deletejob)

void eval(char *cmdline) 
{ 
   
    char* argv[MAXARGS];  //execve()函数的参数
    char buf[MAXLINE];    //保存修改后的命令行
    sigset_t mask;
    int bg;               //判断作业在前台还是后台
    pid_t pid;            //进程id 
    strcpy(buf,cmdline);  //拷贝命令行 
    int argc=0;      
    bg=parseline(buf,argv,&argc);  //解析命令行,返回给argv数组
    if(argv[0]==NULL) return;//忽略空行
    
    if(!builtin_cmd(argv,argc))//如果是内置命令那么结束,否则创建新的子进程
    { 
   
        sigemptyset(&mask);
        sigaddset(&mask,SIGCHLD);//在它派生子进程之前阻塞SIGCHLD信号
        sigprocmask(SIG_BLOCK,&mask,NULL);
        pid=fork();       //fork创建子进程
        if(pid==0)        //在子进程的上下文中运行作业
        { 
   
            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);
            }
        }
        /*父进程等待前台作业终止*/
        //前台作业等待
        if (!bg){ 
   
            if (addjob(jobs,pid,FG,cmdline)==0)//将该作业加入jobs列表
                app_error("create job failed!"); // 
            sigprocmask(SIG_UNBLOCK,&mask,NULL);//解除阻塞 
            waitfg(pid);//等待该进程结束
        } 
        else 
        { 
   
            if (addjob(jobs,pid,BG,cmdline)==0)//将该作业加入jobs列表
                app_error("create job failed!"); //
            sigprocmask(SIG_UNBLOCK,&mask,NULL);//解除阻塞
            printf("[%d] (%d) %s",pid2jid(pid),pid,cmdline);//将进程id映射到job id 
        }
    }
    return;
}

(3)waitfg()等待函数实现

分析:

  • ①首先要明白waitfg函数用来干什么:

在tsh.c中有一段注释:Block until process pid is no longer the foreground
process,也就是说waitfg函数将阻塞一个前台的进程直到这个进程变为后台进程。

  • ②这样一来实现就很简单了,只要判断当前的前台的进程组pid是否和当前进程的pid是否相等,如果相等则sleep(0)等待。同样的用fgpid获取前台进程的pid
  • ③代码实现:
void waitfg(pid_t pid)
{ 
      
    /*struct job_t *job; job = getjobpid(jobs, pid); while(1){ pause(); if((job->pid == 0 && job->state == UNDEF) || (job->pid == pid && job->state == ST)){ if(verbose){ printf("waitfg: Process (%d) no longer the fg process\n", pid); } break; } }*/
    while(pid == fgpid(jobs)) sleep(0) ;
    return;
}

(4)sigint_handler捕获INT响应

分析:
(1) 首先明确SIGINT信号会干什么:

在CSAPP教材508页有介绍:SIGINT信号默认行为为终止,是来自键盘的中断CTR+C,在键盘上输入 CTR+C 会导致一个
SIGINT 信号被发送到外壳。外壳捕获该信号,然后发送 SIGINT 信号到这个前台进程组中的每个进程。在默认情况下,结果是终止前台作业。

(2)通过上面的分析可知问题可以分成两步

①第一步要获取前台进程,判断当前是否有前台进程,如果没有直接返回。如何获取前台的进程呢?可以用fgpid(jobs)获取前台进程组。
②第二步就需要发送SIGINT信号给前台进程组。如何将SIGINT信号给前台进程组发送给前台进程组呢?可以用kill(-pid,sig)函数发送SIGINT信号。另外在教材上还有介绍kill函数:如果
pid 大于零,那么 kill 函数发送信号 sig 给进程 pid。如果 pid 小于零,那么kill 发送信号 sig 给进程组 abs
(pid) 中的每个进程。所以说要发送SIGINT信号给前台进程组所有进程需要传递-pid

(3)代码实现步骤:

① 调用函数fgpid返回前台进程pid
② 如果当前进程pid不为0,那么调用kill函数发送SIGINT信号给前台进程组.
③ 在②中调用kill函数如果返回值为-1,如果返回值为-1表示进程不存在。输出error. 代码实现

void sigint_handler(int sig) 
{ 
   
    //if(sig!=SIGINT) return;
    pid_t pid=fgpid(jobs);//获取前台进程组pid
    if(pid)
    { 
   
        if(kill(-pid,SIGINT)==-1)//给进程组中的所有进程发送SIGINT信号
        { 
   
            unix_error("SIGNAL ERROR!");
        }
    }
    return;
}

(5)sigchld_handler回收僵死

(1)首先要弄清楚回收子进程的工作模式:

当一个子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。因此父进程必须回收子进程,以避免在系统中留下僵死进程。父进程捕获这个SIGCHLD
信号,回收一个子进程。那么如何回收僵死的子进程呢?

(2)一个进程可以通过调用 waitpid 函数来等待它的子进程终止或者停止。如果回收成功,则返回为子进程的 PID, 如果 WNOHANG, 则返回为 0, 如果其他错误,则为 -1。我们的任务是调用waitpid函数来回收子进程,并且检查己回收子进程的退出状态输出提示消息,具体的:

① 用while循环调用waitpid直到它所有的子进程终止。并且将判断条件设置为WNOHANG|WUNTRACED立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为 0,或者返回值等于那个被停止或者已终止的子进程的 PID。

② 检查己回收子进程的退出状态,我这里共检查了三个状态:WIFSTOPPED:引起返回的子进程当前是被停止的;WIFSIGNALED:子进程是因为一个未被捕获的信号终止;WIFEXITED:子进程通过调用exit 或者return正常终止;然后分别用WSTOPSIG,WTERMSIG,WEXITSTATUS提取以上三个退出状态注意如果引起返回的子进程当前是被停止的进程,那么要将其状态设置为ST

  • 代码实现:
void sigchld_handler(int sig) //尽可能的回收子进程,同时使用WNOHANG选项使得如果当前进程都没有终止时,直接返回,而不是挂起该回收进程。这样可能会阻碍无法两个短时间结束的后台进程
{ 
   
    pid_t pid;
    int status;
    while((pid=waitpid(-1,&status,WNOHANG|WUNTRACED))>0)//立即返回,如果等待集合中没有任何子进程被停止或已终止, 那么返回值为 0, 或者返回值等于那个被停止或者已终止的子进程的 PID
    { 
   
        int jid=pid2jid(pid);//获取pid作业的jid
        //检查己回收子进程的退出状态
        if(WIFSTOPPED(status))//如果引起返回的子进程当前是被停止的
        { 
   
        	struct job_t* job=getjobpid(jobs,pid);
        	job->state=ST;
            printf("Job [%d] (%d) stopped by signal %d\n", jid, pid,WSTOPSIG(status));//WSTOPSIG(status)返回导致其停止的信号
        }
        else if(WIFSIGNALED(status))//如果子进程是因为一个未被捕获的信号终止的
        { 
   
            deletejob(jobs,pid);//清除进程
            printf("Job [%d] (%d) stopped by signal %d\n",jid,pid,WTERMSIG(status)); //返回导致子进程终止的信号的数量
        }        
        else if(WIFEXITED(status))//如果子进程通过调用 exit 或者一个返回 (return) 正常终止
        { 
   
            deletejob(jobs,pid);//清除进程
            //printf("Job [%d] (%d) terminated normally with exit status=%d\n",jid,pid,WEXITSTATUS(status)); //WEXITSTATUS(status)返回状态
        }
    }
    //if(errno!=ECHILD) unix_error("waitpid error");
    return;
}

(6) sigtstp_handler捕获TSTP响应

分析:捕获TSTP信号和捕获SIGINT信号类似

  • 首先明确这个SIGTSTP信号会干什么:

SIGTSPT信号默认行为是停止直到下一个 SIGCONT,是来自终端的停止信号,在键盘上输入 CTR+Z会导致一个 SIGTSPT信号被发送到外壳。外壳捕获该信号,然后发送SIGTSPT信号到这个前台进程组中的每个进程。在默认情况下,结果是停止或挂起前台作业。

  • 通过上面的分析可知问题可以分成两步

第一步要获取前台进程,用fgpid(jobs)获取前台进程pid,判断当前是否有前台进程,如果没有直接返回。
第二步就需要用kill(-pid,sig)函数发送SIGTSPT信号给前台进程组。另外对于kill函数,如果 pid 小于零才会发送信号sig 给进程组中的每个进程。所以说要发送SIGTSPT信号给前台进程组所有进程需要传递-pid。
代码实现:

void sigtstp_handler(int sig) 
{ 
   
    //if(sig!=SIGTSTP) return;
    pid_t pid=fgpid(jobs);//获取前台进程组pid
    if(pid)
    { 
   
        if(kill(-pid,SIGTSTP)==-1)//给进程组中的所有进程发送SIGTSTP信号
        { 
   
            unix_error("SIGNAL ERROR!");
        }
    }
    return;
}

(7)编程实现内建命令bg和fg的 do_bgfg()处理函数

  • 首先要明确的是bg和bg的作用:
  • bg <job>:将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动<job>,然后在后台运行它。<job>参数可以是PID,也可以是JID。ST -> BG
  • fg <job>:将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启<job>,然后在前台运行它。<job>参数可以是PID,也可以是JID。ST -> FG,BG -> FG
  • 根据以上的分析可以确定编程任务:
    ① 解析指令:

由于bg指令后面分为带%和不带%两类,所以要根据是否带有”%”符号判断传入的pid还是jid,并且获取对应的job结构体,其中结构体可以由getjobjid函数获取,如果返回为空,说明列表中并不存在jid的job,要输出提示。除此之外,还要判断参数的合法性,我增加了一个参数argc表示指令参数个数,如果这个参数不为2,说明命令错误,输出提示;(注意,提示是样例test14要求的,如果不提示将会和参考程序结果不一致)

② 判断是bg命令还是fg命令:可以使用strcmp函数判断
③ 对于bg命令:

判断作业的state状态,当且仅当作业状态为ST停止状态时才更改为正在运行的后台作业,用kill函数发送SIGCONT信号给对应的子进程。如果UNDEF或者FG状态的工作出现了,说明发生了错误,应该输出提示

④ 对于fg命令:

判断作业的state状态,当作业状态为ST停止状态时重启这个进程,设置state为FG前台,用kill函数发送SIGCONT信号给对应的子进程,然后用waitfg函数回收。如果是后台进程FG,那么更改他的状态为FG前台,调用waitfg函数回收。如果UNDEF或者FG状态的工作出现了,说明发生了错误,应该输出提示消息。

⑤ 另外处理阻塞信号:

为了确保访问全局变量(jobs),或调使用for循环遍历完成功能的函数时不被中断,需要在pid2jid,getjobjid函数前阻塞,后取消阻塞。

  • 函数实现如下
void do_bgfg(char **argv,int argc)
{ 
   
    char* cmd = argv[0];//变量
    char* para = argv[1];
    struct job_t* job;
    sigset_t mask_all, mask_prev;
    int jid;
    //判断传入的pid还是jid,并且获取对应的job结构体
    sigfillset(&mask_all);
    if(argc!=2){ 
       //判断参数
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        fflush(stdout);
        return;
    }
    if(para[0] == '%'){ 
   //JID
        jid = atoi(&(para[1]));//错误处理2,如果传入的参数不是规定的格式,报错返回
        if(jid == 0){ 
   
            printf("%s:argument must be a PID or %%jobid\n", cmd);
            fflush(stdout);
            return;
        }
    }
    else{ 
   //PID
        jid = atoi(para);
        if(jid == 0){ 
   
            printf("%s:argument must be a PID or %%jobid\n", cmd);
            fflush(stdout);
            return;
        }
        sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
        jid = pid2jid(jid);
        sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    }
    sigprocmask(SIG_BLOCK, &mask_all, &mask_prev);
    job = getjobjid(jobs, jid);
    sigprocmask(SIG_SETMASK, &mask_prev, NULL);
    if(job == NULL){ 
   
        if(para[0] == '%')  printf("(%s): No such job\n", para);
        else printf("(%s): No such process\n", para);
        fflush(stdout);
        return;
    }
    //区分bg还是fg 
    if(!strcmp(cmd,"bg"))///bg
    { 
   
        if(job->state==ST){ 
   
            //如果任务处于停止状态ST,则将其运行到后台,ST->BG,用kill函数发送SIGCONT信号给对应的子进程
            job->state = BG;//修改状态为BG后台
            kill(-(job->pid), SIGCONT);//发送SIGCONTT信号给对应的子进程
            printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);            
        }
        else if(job->state==UNDEF||job->state==FG) 
            unix_error("bg 出现undef或者FG的进程\n");//如果bg前台或者UNDEF,说明发生了错误
    }
    else//fg
    { 
   
        if(job->state==ST){ 
   //如果是一个STOP的进程,那么重启它,等待它回收终止
            job->state = FG;
            kill(-(job->pid),SIGCONT);
            waitfg(job->pid);
        }
        else if(job->state==BG){ 
   //如果是一个bg后台进程,那么将它的状态转为前台进程,然后等待它终止
            job->state = FG;
            waitfg(job->pid);
        }
        else //如果本身就是前台进程或UNDEF,说明出错了
            unix_error("fg 出现undef或者FG的进程\n");
    }
    return;
}

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/38117.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注