CSAPP实验-ShellLab

CSAPP实验-ShellLabshell-lab是csapp的配套实验之一,它要求我们实现一个功能和unixshell类似的tinyshell,在源文件tsh.c中已给出了基本框架,剩下的只需要完成实现指定的函数即可,该实验对应csapp的第8章内容。UnixShellShell可以认为是一个可交互的命令解释器,它替代用户运行程序,shell会从标准输入流stdin等待输入命令行,根据命令行内容的指示执行对应操作。命令行是由空格分隔的ascii单词序列组成,其中第一个单词为内置命令或可执行文件的路径(或环境变量path目录下的

shell-lab是csapp的配套实验之一,它要求我们实现一个功能和unix shell类似的tiny shell,在源文件tsh.c中已给出了基本框架,剩下的只需要完成实现指定的函数即可,该实验对应csapp的第8章内容。

Unix Shell

Shell可以认为是一个可交互的命令解释器,它替代用户运行程序,shell会从标准输入流stdin等待输入命令行,根据命令行内容的指示执行对应操作。命令行是由空格分隔的ascii单词序列组成,其中第一个单词为内置命令或可执行文件的路径(或环境变量path目录下的可执行文件相对路径),其余的单词就是命令行的参数了。如果是执行内置命令,那么shell将在当前的进程立即执行该命令;如果执行其它可执行文件,shell就会fork一个子进程,然后在该子进程的上下文加载并运行该程序,用于解释单个命令而创建的子进程统称为job(作业)。当命令行以&结尾时,作业将在后台运行,即shell打印提示并等待下一个命令之前,不会等待作业终止,否则作业将在前台运行,这样shell在等待下一个命令之前要等待作业终止,所以任何时候前台只能运行一个作业,而后台可以运行多个作业。
Unix Shell也支持 作业控制 的概念,允许用户切换作业为前台或后台运行,也允许通过信号修改作业的状态(running、stopped、terminated)。通过键盘可以产生信号,例如CTRL+C产生SIGINT(interrupt)终止信号,会终止前台作业并使其变为后台terminated状态;CTRL+Z产生SIGTSTP(stop)暂停信号,会暂停前台作业并使其变为后台stopped状态。通过命令也可以产生信号,例如bg和fg命令产生SIGCONT(continue)继续信号,会继续运行被暂停的作业并使其变为后台(前台)running状态,如果对应作业本身就为running状态,那么这两个命令只有切换前后台的作用。

tsh

我们需要按照题目的要求实现一个tiny shell,只有unix shell的部分功能,其要求如下:

  • 提示前缀为”tsh>“。
  • 用户键入的命令由一个name和0至多个参数构成,他们均由空格分隔。如果name是一个内置指令,则tsh要立即处理它并继续等待下一个命令;否则tsh假定name是可执行文件的路径,并创建一个子进程(作业),在该子进程的上下文中加载该文件并运行。
  • tsh不需要支持管道(|)和I/O重定向(>和<)。
  • 输入CTRL+C(CTRL+Z)应当发送SIGINT(SIGTSTP)信号给当前的前台作业和其任何子进程,如果没有前台作业则该信号无效。
  • 当命令行以&结尾时,tsh应当运行后台作业,否则将会在前台运行这个作业。
  • 每个作业都应当由进程ID(PID)或作业ID(JID)标识,JID是tsh分配的正整数,且JID在命令行需要加上前缀%
  • tsh需要支持如下内置指令:
    • quit命令终止tsh进程
    • jobs命令列出所有后台进程
    • bg <job>命令会向作业发送SIGCONT信号来重启job,并作为后台作业运行,参数可以是PID或JID
    • fg <job>同上,唯一区别是job以前台作业运行
  • tsh需要回收所有其子僵尸进程,如果有任何作业由于接收到未捕获的信号而终止,那么tsh需要识别此事件并打印一条带有该作业PID的消息以及对该问题信号的描述。

代码实现

tsh.c已经将主体的框架和非关键部分的函数实现好,我们只要完成eval、builtin_cmd、do_bgfg、waitfg这四个函数加上sigchld_handler、sigtstp_handler、sigint_handler这三个信号处理函数即可,很多代码可以参考书里第八章的部分代码,具体实现如下:

/* * 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. */
void eval(char *cmdline)
{ 
   
    sigset_t mask_all, mask_one, prev_one;
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    char *argv[MAXARGS];
    bool bg = parseline(cmdline, argv);
    if(argv[0]==NULL){ 
   
        return;
    }
    if(builtin_cmd(argv)){ 
   
        return;
    }
    else{ 
   
        //fork之前阻塞child信号
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one);
        pid_t pid = Fork();
        if(pid == 0){ 
    //子进程
            //子进程给自己设置独立的进程组
            Setpgid(0,0);
            //子进程继承了block位向量,所以需要恢复
            Sigprocmask(SIG_SETMASK, &prev_one, NULL);
            //在子进程上下文加载对应程序
            if(execve(argv[0], argv, environ)<0){ 
   
                printf("%s Command not found\n", argv[0]);
                exit(0);
            }
        }
        else{ 
    //父进程
            //访问全局数据结构时阻塞所有信号以避免被信号中断造成并发错误
            Sigprocmask(SIG_BLOCK, &mask_all, NULL);
            addjob(jobs, pid, bg?BG:FG, cmdline);
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); //恢复block向量到最初始状态
            //printf("%d\n", bg);
            if(!bg){ 
   
                waitfg(pid);
            }
            else{ 
   
                printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);
            }
        }
    }
}

/* * builtin_cmd - If the user has typed a built-in command then execute * it immediately. */
int builtin_cmd(char **argv)
{ 
   
    char *name = argv[0];
    if(name == NULL) { 
   
        return 0;
    }
    if(strcmp(name,"quit")==0){ 
   
        exit(0);
    }
    else if(strcmp(name,"jobs")==0){ 
   
        listjobs(jobs);
    }
    else if(strcmp(name, "bg")==0 || strcmp(name, "fg")==0){ 
   
        do_bgfg(argv);
    }
    else return 0;     /* not a builtin command */
    return 1;
}

/* * do_bgfg - Execute the builtin bg and fg commands */
void do_bgfg(char **argv)
{ 
   
    char *name = argv[0];
    char *id = argv[1];
    if(id==NULL){ 
   
        printf("%s command requires PID or %%jobid argument\n", name);
        return;
    }
    struct job_t *pjob;
    int pid;
    if(id[0]=='%'){ 
   //jid
        int jid = atoi(id+1);
        if(jid==0){ 
   
            printf("%s: argument must be PID or %%jobid\n", name);
            return;
        }
        pjob = getjobjid(jobs, jid);
        if(pjob==NULL){ 
   
            printf("%d: No such job\n", jid);
            return;
        }
        pid = pjob->pid;
    }
    else{ 
   //pid
        pid = atoi(id);
        if(pid==0){ 
   
            printf("%s: argument must be PID or %%jobid\n", name);
            return;
        }
        pjob = getjobpid(jobs, pid);
        if(pjob==NULL){ 
   
            printf("%d: No such process\n", pid);
            return;
        }
    }
    Kill(-pid, SIGCONT);
    if(strcmp(name, "bg")==0){ 
   
        pjob->state = BG;
        printf("[%d] (%d) %s",pid2jid(pid), pid, pjob->cmdline);
    }
    else if(strcmp(name, "fg")==0){ 
   
        pjob->state = FG;
        waitfg(pid);
    }
}

/* * waitfg - Block until process pid is no longer the foreground process */
void waitfg(pid_t pid)
{ 
   
    sigset_t mask_empty, mask_all, prev_all;;
    Sigemptyset(&mask_empty);
    Sigfillset(&mask_all);
    //等待直到没有前台作业(只等待不回收)
    while(true){ 
   
        Sigsuspend(&mask_empty); //保证原子性,等待信号中断并返回
        //访问jobs之前阻塞所有信号
        Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
        if(fgpid(jobs)==0){ 
   
            Sigprocmask(SIG_SETMASK, &prev_all, NULL);
            break;
        }
        else{ 
   
            Sigprocmask(SIG_SETMASK, &prev_all, NULL);
        }
    }
}

/* * sigchld_handler - The kernel sends a SIGCHLD to the shell whenever * a child job terminates (becomes a zombie), or stops because it * received a SIGSTOP or SIGTSTP signal. The handler reaps all * available zombie children, but doesn't wait for any other * currently running children to terminate. */
void sigchld_handler(int sig)
{ 
   
    sigset_t mask_all, prev_all;
    Sigfillset(&mask_all);
    int oldErrno = errno;
    pid_t pid;
    int status;
    //访问jobs之前阻塞所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    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 %d\n", pid2jid(pid), pid, WTERMSIG(status));
            deletejob(jobs, pid);
        }
        //收到信号停止
        else if(WIFSTOPPED(status)){ 
   
            printf("Job [%d] (%d) stopped by signal %d\n", pid2jid(pid), pid, WSTOPSIG(status));
            struct job_t *pjob = getjobpid(jobs, pid);
            pjob->state = ST;
        }
    }
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    //options传WNOHANG|WUNTRACED则不需要对errno检测
    // if(errno!=ECHILD){ 
   
    // Sio_error("waitpid error\n");
    // }
    errno = oldErrno;
}

/* * 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)
{ 
   
    sigset_t mask_all, prev_all;
    Sigfillset(&mask_all);
    int oldErrno = errno;
    //访问jobs之前阻塞所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    pid_t target_pid = fgpid(jobs);
    Sigprocmask(SIG_SETMASK, &prev_all, NULL);
    if(target_pid!=0){ 
   
        //不能使用包装函数Kill、传入负值表示进程组id
        if(kill(-target_pid, SIGINT)<0){ 
   
            Sio_error("Kill Error\n");
        }
    }
    errno = oldErrno;
}

/* * 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)
{ 
   
    sigset_t mask_all, prev_all;
    Sigfillset(&mask_all);
    int oldErrno = errno;
    Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
    pid_t target_pid = fgpid(jobs);
    Sigprocmask(SIG_BLOCK, &prev_all, NULL);
    if(target_pid!=0){ 
   
        if(kill(-target_pid, SIGTSTP)<0){ 
   
            Sio_error("Kill Error\n");
        }
    }
    errno = oldErrno;
}

实验提供了trace01~trace16十六个文本文件,作为sdrive.pl的输入驱动我们编写的tsh输出对应结果,模拟命令行执行的结果,开发阶段可以对tsh手动输入trace文本中的命令进行调试。另外实验也提供了tshref作为参考二进制文件,我们的目标就是使tsh的表现与tshref完全一致,makefile文件中给出了测试指令,使用make test和make rtest指令即可对tsh和tshref进行快速测试,tshref.out文件为参考输出结果。

注意

  • 由于我们引入了csapp源文件,所以会出现一些重复定义的编译错误,相应的需要修改源文件tsh.c,将重复定义的符号MAXLINE、unix_error、app_error、Signal注释掉;makefile文件也要修改加上对csapp的依赖,因为csapp.c用到的pthread库不是标准linux库,所以要在编译tsh文件时要加上-lpthread参数。makefile中添加如下内容:
tsh: tsh.c csapp.c csapp.h
	$(CC) $(CFLAGS) -o tsh csapp.c tsh.c -lpthread
  • 代码中大量使用了csapp.c提供的 错误处理包装函数(如Kill,Sigprocmask),而这些函数又使用了unix_error函数,其调用的exit函数不是 异步信号安全 的函数,所以在信号处理程序中不能使用,而要重新包装换用sio_error函数。也就是存在一种可能(尽管可能性很低),主程序在执行exit函数的过程中(因为其非原子过程)被信号中断,而信号处理程序中也恰好调用了exit函数,则可能会产生状态不一致的问题。但是代码中省略了重新包装的步骤,特此注明一下。
  • 不论是主程序还是信号处理程序在访问全局共享变量时(如jobs),都要临时阻塞所有信号,待访问结束后再解除阻塞,将访问操作变为不可中断的原子操作,避免信号中断引起的状态不一致。
  • 注意在信号处理程序中保存和恢复errno的状态。
  • 为了方便,在信号处理程序中使用的printf函数,其实应当替换为异步信号安全的sio_printf函数。

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

(0)
编程小号编程小号

相关推荐

发表回复

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