计算机组成原理-ShellLab实验


一、实验目的与要求

1、让学生更加理解进程控制的概念和具体操作方法;

2、让学生更加理解信号的概念和具体使用方法;

3、让学生更加理解Unix shell程序的原理和实现方法;

二、实验原理与内容

shell是一种交互式的命令行解释器,能代表用户运行程序。shell反复打印一个提示符等待stdin上的命令行,然后按照命令行的内容执行命令。

命令行是由空格分隔的ASCII字符串。命令行的第一个字符串要么是一个内置命令的名称,要么是一个可执行文件的路径名,剩下的字符串则为命令行参数。如果命令行的第一个字符串是一个内置命令,则shell会立即在当前进程中执行该命令。如果命令行的第一个字符串不是一个内置命令,shell会假定该字符串是一个可执行文件的路径名,在这种情况下shell会创建一个子进程并在子进程的上下文中加载和执行该可执行文件。每运行一个可执行文件将创建一个子进程,这些子进程组成shell的工作集,工作集中的各个子进程可以通过Unix管道进行连接。

如果命令行以符号“&”结尾,那么程序会在后台执行,这意味着shell不会等待程序的终止,shell会立即打印提示符并等待下一个命令输入。否则,程序会在前台执行,这意味着shell会等待程序终止后才能接收下一个命令行的输入。因此,在某一时刻系统中最多只能有一个前台任务,但是可以有任意数量的后台任务。

例如,输入命令行“jobs”会使得shell执行“jobs”这个内置命令。如果输入“/bin/ls -l -d”,则shell会在前台运行“/bin/ls”可执行文件,一般来说shell会保证程序会从可执行文件的main()函数开始执行,main()函数的声明如下所示:

int main(int argc, char *argv[])

在这个例子里参数argc和argv会有以下的值:

·argc == 3

·argv[0] == “/bin/ls”

·argv[1]== “-l”

·argv[2]== “-d”

如果输入命令“/bin/ls -l -d &”,则shell会在后台执行“/bin/ls”程序。

Unix shell支持任务控制的概念,它允许用户将任务在后台和前台之间来回切换,并更改任务中进程的状态(运行、停止或终止)。输入ctrl-c将导致一个SIGINT信号被传递到前台任务中的每个进程。这个SIGINT信号的默认操作是终止进程。类似地,输入ctrl-z会传递SIGTSTP信号给前台任务中的每个进程。SIGTSTP的默认操作是让一个进程处于停止状态,直到接收SIGCONT唤醒信号。Unix shell还提供各种支持任务控制的内置命令。例如:

• jobs: 列出正在运行或已经停止的后台任务.  

• bg <job>: 将一个停止的后台任务启动起来.

• fg <job>: 将一个正在执行的或已经停止的后台任务切换到前台并运行起来.  

• kill <job>:终止一个任务.

本实验的内容是编写一个简单的shell程序“tsh”,“tsh”要具有以下功能:

(1)命令行提示符字符串应为“tsh> ”。

(2)用户输入的命令行应由一个命令名称以及0个或多个参数所组成,命令名称和各个参数之间用一个或多个空格隔开。如果命令名称是一个内置命令,则tsh将在当前进程马上执行该命令然后才能接收下一个命令行输入。否则tsh应该假设该命令名称是一个可执行文件,并在子进程的上下文中加载和执行该可执行文件。

(3)tsh不需要有管道和IO重定向功能。

(4)输入ctrl-c(或ctrl-z)将发送一个SIGINT(或SIGTSTP)信号到当前前台任务以及该任务的任何子进程(它fork出来的任何子进程)。如果当前没有前台任务,则信号不应该产生任何作用。

(5)如果命令行是以“&”结尾的话,则命令要在后台执行,否则要在前台执行。

(6)tsh要给每一个任务分配一个正整数作为进程ID(PID)或任务ID(JID)。JID在命令行通过“%”进行引用,例如“%5”代表JID 5。而PID在命令行中直接通过数字引用,例如“5”代表PID 5。

(7)tsh要支持下列内置命令:

    quit:终止并退出tsh程序;

    jobs:列出所有后台任务;

    bg <job>:给<job>发送一个SIGCONT信号使其在后台继续运行起来,<job>参数可以是PID或JID。

    fg <job>:给<job>发送一个SIGCONT信号使其在前台继续运行起来,<job>参数可以是PID或JID。

(8)tsh要回收其所有的僵死子进程。如果任何进程因接收到没有在其进程中被捕获的信号而终止,那么tsh应该识别到这个事件,并打印一条带有进程PID和对该信号描述的信息。

三、实验设备与软件环境

1.Linux操作系统—64位 Ubuntu 18.04

2. C编译环境(gcc)

3. 计算机

四、实验过程

1.把shlab-handout.tar文件拷贝到虚拟机的某目录下(我这里是桌面),并在该目录下打开终端,输入命令“tar xvf shlab-handout.tar”对压缩包进行解压。

2.进入解压后的shlab-handout文件夹,编辑tsh.c文件,实现你自己的简单迷你shell。

  打开tsh.c文件,您会看到里面包含了一个简单Unix shell的功能框架。

任务是实现剩下的其它还没被实现的功能函数,如下所示:

void eval(char *cmdline):分析命令,并派生子进程执行 主要功能是解析cmdline并运行

int builtin_cmd(char **argv):解析和执行bulidin命令,包括 quit, fg, bg, and jobs

void do_bgfg(char **argv) 执行bg和fg命令

void waitfg(pid_t pid):实现阻塞等待前台程序运行结束

void sigchld_handler(int sig):SIGCHID信号处理函数

void sigint_handler(int sig):信号处理函数,响应 SIGINT (ctrl-c) 信号

void sigtstp_handler(int sig):信号处理函数,响应 SIGTSTP (ctrl-z) 信号

   通过阅读实验指导书我们知道此实验要求我们完成tsh.c中的七个函数从而实现一个简单的shell,能够处理前后台运行程序、能够处理ctrl+z、ctrl+c等信号。

查看tsh.c文件

首先定义了一些宏

定义了四种进程状态

然后定义了job_t的任务的类,并且创建了jobs[]数组

接着是需要我们完成的七个函数定义

void eval(char *cmdline):分析命令,并派生子进程执行 主要功能是解析cmdline并运行
int builtin_cmd(char **argv):解析和执行bulidin命令,包括 quit, fg, bg, and jobs
void do_bgfg(char **argv) 执行bg和fg命令
void waitfg(pid_t pid):实现阻塞等待前台程序运行结束
void sigchld_handler(int sig):SIGCHID信号处理函数
void sigint_handler(int sig):信号处理函数,响应 SIGINT (ctrl-c) 信号 
void sigtstp_handler(int sig):信号处理函数,响应 SIGTSTP (ctrl-z) 信号

下面就是一些辅助的函数

int parseline(const char *cmdline, char **argv);   //获取参数列表,返回是否为后台运行命令
void sigquit_handler(int sig);  //处理SIGQUIT信号
void clearjob(struct job_t *job);  //清除job结构体 
void initjobs(struct job_t *jobs);  //初始化任务jobs[]
int maxjid(struct job_t *jobs);   //返回jobs链表中最大的jid号。
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline);  //向jobs[]添加一个任务
int deletejob(struct job_t *jobs, pid_t pid);   //在jobs[]中删除pid的job
pid_t fgpid(struct job_t *jobs);  //返回当前前台运行job的pid号
struct job_t *getjobpid(struct job_t *jobs, pid_t pid);  //根据pid找到对应的job 
struct job_t *getjobjid(struct job_t *jobs, int jid);   //根据jid找到对应的job 
int pid2jid(pid_t pid);   //根据pid找到jid 
void listjobs(struct job_t *jobs);  //打印jobs 
void usage(void); //用于显示程序的使用方法
void unix_error(char *msg); //当UNIX系统调用失败时,这个函数会被调用
void app_error(char *msg); //用于处理程序自身的错误信息
typedef void handler_t(int); 
handler_t *Signal(int signum, handler_t *handler); //一个信号处理函数,用于安装或更改信号处理器

接着就是mian函数,作用是在文件中逐行获取命令,并且判断是不是文件结束(EOF),将命令cmdline送入eval函数进行解析。我们需要做的就是逐步完善这个过程

进入实验:

先使用make clean进行清空,再使用make命令编译tsh.c文件

1、eval()函数:

函数功能:eval()函数用于执行字符串参数作为shell命令,它首先扫描命令行参数,对其进行必要的变量替换和命令替换,然后执行替换后得到的命令。如果替换后的结果是一个复杂的命令或脚本,eval会再次扫描这个命令或脚本,直到所有嵌套的命令和变量都被解析并执行。如果用户请求一个内置命令quit、jobs、bg或fg(即内置命令)那么就立即执行。否则,fork子进程和在子进程的上下文中运行作业。如果作业正在运行前台,等待它终止,然后返回。

函数:void eval(char *cmdline); 传入的参数为cmdline,即命令行字符串

注意

  1. 注意每个子进程必须用户自己独一无二的进程组id,要不然就没有前台后台区分
  2. 在fork()新进程前后要阻塞SIGCHLD信号,防止出现竞争(race)这种经典的同步错误

实现思路:

1、首先调用parseline函数解析命令行,如果为空直接返回,接着使用builtin_cmd函数判断是否为内置命令,返回0说明不是内置命令,如果是内置命令直接执行。

2、如果不是内置命令,那么先阻塞信号(具体在第四点分析),再调用fork创建子进程。在子进程中,首先解除阻塞,设置自己的id号,然后调用execve函数来执行job。

3、父进程判断作业是否后台运行,是的话调用addjob函数将子进程job加入job链表中,解除阻塞,然后调用waifg函数等待前台运行完成。如果不在后台工作则打印进程组jid和子进程pid以及命令行字符串。

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

实现代码:

void eval(char *cmdline) 
{
    char* argv[MAXARGS];   //execve()函数的参数
    int state = UNDEF;  //工作状态,FG或BG 
    sigset_t set;
    pid_t pid;  //进程id
    // 处理输入的数据
    if(parseline(cmdline, argv) == 1)  //解析命令行,返回给argv数组
        state = BG;
    else
        state = FG;
    if(argv[0] == NULL)  //命令行为空直接返回
        return;
    // 如果不是内置命令
    if(!builtin_cmd(argv))
    {
        if(sigemptyset(&set) < 0)
            unix_error("sigemptyset error");
        if(sigaddset(&set, SIGINT) < 0 || sigaddset(&set, SIGTSTP) < 0 || sigaddset(&set, SIGCHLD) < 0)
            unix_error("sigaddset error");
        //在它派生子进程之前阻塞SIGCHLD信号,防止竞争 
        if(sigprocmask(SIG_BLOCK, &set, NULL) < 0)
            unix_error("sigprocmask error");

        if((pid = fork()) < 0)  //fork创建子进程失败 
            unix_error("fork error");
        else if(pid == 0)  //fork创建子进程
        {
            // 子进程的控制流开始
            if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)  //解除阻塞
                unix_error("sigprocmask error");
            if(setpgid(0, 0) < 0)  //设置子进程id 
                unix_error("setpgid error");
            if(execve(argv[0], argv, environ) < 0){
                printf("%s: command not found\n", argv[0]);
                exit(0);
            }
        }
        // 将当前进程添加进job中,无论是前台进程还是后台进程
        addjob(jobs, pid, state, cmdline);
        // 恢复受阻塞的信号 SIGINT SIGTSTP SIGCHLD
        if(sigprocmask(SIG_UNBLOCK, &set, NULL) < 0)
            unix_error("sigprocmask error");

        // 判断子进程类型并做处理
        if(state == FG)
            waitfg(pid);  //前台作业等待
        else
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);  //将进程id映射到job id   
    }
    return;
}

2、builtin_cmd()函数

函数功能:判断命令是否是内置指令,是的话立即执行,不是则返回,对单独的‘&’无视

内置命令: quit, fg, bg, 和 jobs

函数:int builtin_cmd(char **argv) ,参数为argv参数列表

实现思路:

1、当命令行参数为quit时,直接终止shell

2、当命令行参数为jobs时,调用listjobs函数,显示job列表

3、当命令行参数为bg或fg时,调用do_bgfg函数,执行内置的bg和fg命令

4、不是内置命令时返回0

实现代码:

int builtin_cmd(char **argv) 
{
    if(strcmp(argv[0],"quit")==0) //如果命令是quit,退出
        {//printf("exit\n");
	exit(0);}
    if(strcmp(argv[0],"jobs")==0) //如果命令是jobs,列出正在运行和停止的后台作业
        {
            listjobs(jobs);
            return 1;
        }
    if(strcmp(argv[0],"bg")==0 || strcmp(argv[0],"fg")==0) //如果是bg或者fg命令,执行do_fgbg函数 
        {
            do_bgfg(argv);
            return 1;
        }
    return 0;     /* not a builtin command */
}

3、do_bgfg()函数

函数功能:根据用户输入的命令参数来决定是执行bg操作(将作业放到后台运行)还是fg操作(在前台恢复执行),并相应地更改作业的状态。它通常接收作业号作为参数,用于指定要操作的作业。

函数:void do_bgfg(char **argv) ,参数argv参数列表

实现思路:

1、判断argv[]是否带%,若为整数则传入pid,若带%则传入jid。接着调用getjobjid函数来获得对应的job结构体,如果返回为空,说明列表中并不存在jid的job,要输出提示。

2、使用strcmp函数判断是bg命令还是fg命令

3、若是bg,使目标进程重新开始工作,设置状态为BG(后台),打印进程信息

4、若是fg,使目标进程重新开始工作,设置状态为FG(前台),等待进程结束

实现代码:

void do_bgfg(char **argv)  
{  
    int num;  // 定义一个整数用于存储解析后的作业ID或进程ID  
    struct job_t *job;  // 定义一个指向作业结构体的指针,用于指向找到的作业  
  
    // 如果没有提供作业ID或进程ID参数,则输出错误信息并返回  
    if(!argv[1]){    
        printf("%s command requires PID or %%jobid argument\n", argv[0]);  
        return;  
    }  
  
    // 判断参数的第一个字符,以确定是作业ID还是进程ID  
    if(argv[1][0] == '%'){  // 如果参数以'%'开头,则解析为作业ID  
        if((num = strtol(&argv[1][1], NULL, 10)) <= 0){  // 解析'%'后面的数字为整数  
            printf("%s: argument must be a PID or %%jobid\n",argv[0]);  // 如果解析失败或数字不合法,输出错误信息  
            return;  
        }  
        if((job = getjobjid(jobs, num)) == NULL){  // 根据作业ID查找作业  
            printf("%%%d: No such job\n", num);  // 如果没找到对应的作业,输出错误信息  
            return;  
        }  
    } else {  // 如果参数不是以'%'开头,则解析为进程ID  
        if((num = strtol(argv[1], NULL, 10)) <= 0){  // 解析参数为整数  
            printf("%s: argument must be a PID or %%jobid\n",argv[0]);  // 如果解析失败或数字不合法,输出错误信息  
            return;  
        }  
        if((job = getjobpid(jobs, num)) == NULL){  // 根据进程ID查找作业  
            printf("(%d): No such process\n", num);  // 如果没找到对应的进程,输出错误信息  
            return;  
        }  
    }  
  
    // 根据argv[0]的值判断是前台执行还是后台执行  
    if(!strcmp(argv[0], "bg")){  // 如果是"bg",则执行后台操作  
        job->state = BG;  // 将作业状态设置为后台  
        if(kill(-job->pid, SIGCONT) < 0)  // 向作业所在的进程组发送SIGCONT信号,恢复进程执行  
            unix_error("kill error");  // 如果发送信号失败,输出错误信息  
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);  // 输出作业信息  
    } else if(!strcmp(argv[0], "fg")) {  // 如果是"fg",则执行前台操作  
        job->state = FG;  // 将作业状态设置为前台  
        if(kill(-job->pid, SIGCONT) < 0)  // 向作业所在的进程组发送SIGCONT信号,恢复进程执行  
            unix_error("kill error");  // 如果发送信号失败,输出错误信息  
        // 当进程被设置为前台执行时,当前程序(即shell)应等待该进程结束  
        waitfg(job->pid);  // 调用waitfg函数等待前台进程结束  
    } else {  // 如果argv[0]既不是"bg"也不是"fg",则输出内部错误并退出程序  
        puts("do_bgfg: Internal error");  
        exit(0);  
    }  
    return;  // 函数执行完毕,返回  
}

4、waitfg()函数

函数功能:等待前台任务完成,确保在任务结束之前,shell不会继续执行下一条指令或接受新的输入

函数:void waitfg(pid_t pid),参数为进程ID

实现思路:

通过sigsuspend函数来实现这一阻塞,并依赖于前台作业结束时的信号来唤醒进程。

代码实现:

void waitfg(pid_t pid)  
{  
    // 定义一个信号集,用于sigsuspend函数中的信号掩码  
    sigset_t mask;  
      
    // 初始化信号集mask,使其不包含任何信号  
    sigemptyset(&mask);  
      
    // 循环检查前台作业是否完成  
    // fgpid函数返回当前前台作业的PID,如果没有前台作业则返回0  
    while (fgpid(jobs) != 0) {  
        // 使用sigsuspend函数暂停当前进程的执行  
        // sigsuspend会替换当前进程的信号掩码为mask,并等待mask中未阻塞的信号发生  
        // 由于mask被初始化为空集,所以sigsuspend会暂停进程直到接收到一个信号  
        // 在这里,sigsuspend的作用类似于一个条件等待,等待前台作业结束或收到其他信号  
        sigsuspend(&mask);      //暂停时取消阻塞,见sigsuspend用法  
    }  
      
    // 当前台作业结束(即fgpid返回0),函数返回  
    return;  
}

信号处理函数的实现

5、sigchld_handler函数

sigchld_handler函数:处理SIGCHLD信号。当子进程停止或终止时,操作系统会向父进程发送SIGCHLD信号。父进程通过调用sigchld_handler函数来响应这个信号,并执行相应的处理逻辑

函数:void sigchld_handler(int sig)  ,参数为信号类型

回顾知识:父进程回收子进程的过程:子进程终止或者停止时,内核会发送一个SIGCHLD信号给父进程。父进程通过系统调用获取子进程的终止状态,以此来回收子进程,然后清理子进程资源,父进程可以继续执行其他任务。如果回收成功,则返回为子进程的 PID, 如果 WNOHANG, 则返回为 0, 如果其他错误,则为 -1。

实现思路:

非阻塞地检查并回收子进程资源,同时确保处理过程的原子性,避免被其他信号打断。同时,根据不同子进程的状态,更新作业信息,以便用户能够知道子进程当前的状态,并进行相应的操作。

实现代码:

void sigchld_handler(int sig)   
{  
    // 保存原来的errno值,用于恢复  
    int olderrno = errno;  
    // 进程状态变量  
    int status;  
    // 进程ID  
    pid_t pid;  
    // 信号集,用于阻塞所有信号  
    sigset_t mask_all, prev_all;  
    // 初始化prev_all信号集为空集  
    sigemptyset(&prev_all);  
    // 初始化mask_all信号集为包含所有信号的集合  
    sigfillset(&mask_all);  
      
    // 循环调用waitpid,尽可能回收已终止或停止的子进程  
    // WNOHANG:非阻塞模式,没有子进程可回收时立即返回  
    // WUNTRACED:返回停止的子进程  
    while((pid = waitpid(-1,&status,WNOHANG | WUNTRACED)) > 0)  
    {  
        // 阻塞所有信号,防止在处理子进程状态时被其他信号打断  
        sigprocmask(SIG_BLOCK,&mask_all,&prev_all);  
          
        // 根据子进程的PID获取作业信息  
        struct job_t* job = getjobpid(jobs,pid);  
          
        // 如果子进程因为接收到SIGINT信号而终止  
        if(WIFSIGNALED(status) && WTERMSIG(status) == SIGINT && job->state != UNDEF)  
        {  
            // 输出子进程终止信息  
            printf("Job [%d] (%d) terminated by signal 2\n",job->jid,job->pid);  
        }  
        // 如果子进程因为接收到SIGTSTP信号而停止  
        else if(WIFSTOPPED(status) && WSTOPSIG(status) == SIGTSTP && job->state != ST)  
        {  
            // 输出子进程停止信息  
            printf("Job [%d] (%d) terminated by signal 20\n",job->jid,job->pid);  
            // 更新作业状态为停止状态  
            job->state = ST;  
        }  
          
        // 如果作业状态不是停止状态,则删除作业信息  
        // 因为如果是停止状态,之后可能还需要通过BG或FG命令恢复执行  
        if(getjobpid(jobs,pid)->state != ST) deletejob(jobs,pid);  
          
        // 恢复原来的信号掩码  
        sigprocmask(SIG_SETMASK,&prev_all,NULL);  
    }  
      
    // 恢复原来的errno值  
    errno = olderrno;  
      
    // 函数返回  
    return;  
}

6、sigint_handler函数

函数功能:是一个自定义的信号处理函数,通常用于处理SIGINT信号。SIGINT信号通常在用户按下Ctrl+C时被发送,用于中断当前正在运行的进程。

函数:void sigint_handler(int sig) 参数为信号类型

实现思路:

处理SIGINT信号时,进程能够安全地终止前台作业,并输出相应的信息,同时避免在处理过程中被其他信号打断。通过阻塞所有信号,实现了处理过程的原子性。在处理完信号后,及时恢复信号掩码,确保进程能够正常响应后续的信号。

代码实现:

void sigint_handler(int sig)   
{  
    // 获取前台作业的PID  
    int fg_pid = fgpid(jobs);  
    // 获取前台作业的JID  
    int fg_jid = pid2jid(fg_pid);  
      
    // 如果没有前台作业,则直接返回  
    if(!fg_pid)    return;  
      
    // 定义信号集,用于阻塞所有信号  
    sigset_t mask_all, prev_all;  
    // 初始化mask_all信号集为包含所有信号的集合  
    sigfillset(&mask_all);  
    // 初始化prev_all信号集为空集  
    sigemptyset(&prev_all);  
      
    // 阻塞所有信号  
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);  
      
    // 根据前台作业的PID获取作业信息  
    struct job_t* job = getjobpid(jobs, fg_pid);  
      
    // 将作业状态设置为未定义状态  
    job->state = UNDEF;  
      
    // 向前台作业发送SIGINT信号(终止信号)  
    // 注意:kill函数的第一个参数为-fg_pid,表示向进程组发送信号  
    kill(-fg_pid, SIGINT);  
      
    // 输出前台作业被终止的信息  
    printf("Job [%d] (%d) terminated by signal 2\n", fg_jid, fg_pid);  
      
    // 恢复原来的信号掩码  
    sigprocmask(SIG_SETMASK, &prev_all, NULL);  
      
    // 函数返回  
    return;  
}

7、sigtstp_handler函数

函数功能:捕获并处理SIGTSTP信号是由用户按下Ctrl+Z组合键时发送的,用于暂停当前正在前台运行的进程。

函数: void sigtstp_handler(int sig) ,参数为信号类型

实现思路:

  1. 获取前台作业信息
  2. 检查进程ID的有效性
  3. 通知用户作业已暂停
  4. 更新作业状态
  5. 暂停整个进程组

它允许用户通过Ctrl+Z来暂停前台作业,并在之后使用其他命令或恢复该作业。

代码实现:

void sigtstp_handler(int sig)   
{  
    // 获取前台作业的PID和JID  
    int fg_pid = fgpid(jobs);  // 调用fgpid函数获取前台作业的PID  
    int fg_jid = pid2jid(fg_pid);  // 调用pid2jid函数将PID转换为JID  
  
    // 如果没有前台作业,则直接返回  
    if(!fg_pid)      
        return;  
      
    // 定义两个信号集变量,用于阻塞和恢复信号  
    sigset_t mask_all, prev_all;  
      
    // 初始化mask_all信号集,包含所有信号  
    sigfillset(&mask_all);  
    // 初始化prev_all信号集为空  
    sigemptyset(&prev_all);  
      
    // 阻塞所有信号  
    sigprocmask(SIG_BLOCK, &mask_all, &prev_all);  
      
    // 根据前台作业的PID获取作业信息  
    struct job_t* job = getjobpid(jobs, fg_pid);  
      
    // 将作业状态设置为停止状态(ST)  
    job->state = ST;  
      
    // 向前台作业发送SIGTSTP信号(停止信号),使前台作业停止执行  
    // 注意:kill函数的第一个参数为-fg_pid,表示向进程组发送信号  
    kill(-fg_pid, SIGTSTP);  
      
    // 输出前台作业被停止的信息  
    printf("Job [%d] (%d) stopped by signal 20\n", fg_jid, fg_pid);  
      
    // 恢复原来的信号掩码  
    sigprocmask(SIG_SETMASK, &prev_all, NULL);  
}

当tsh.c文件编辑完成后,在shlab-handout文件夹里打开命令终端,输入命令“make clean”、“make”对文件进行重新编译。编译后可以得到你所实现的tsh程序,你可以通过命令“./tsh”来运行该程序。

退出tsh:使用quit命令

最后进行测试

在shlab-handout文件夹里面有16个包含了一系列交互命令的轨迹文件(trace01~trace16),编号小的那些轨迹文件包含数量较少而且非常简单的交互命令,编号大的那些轨迹文件包含数量较多而且较为复杂的交互命令。

1、打开trace01.txt查看里面的内容:

输入make test01和make rtest01查看运行结果

tsh实验结果和tshref一致,结果正确

2、打开 trace02.txt查看文件内容

输入的命令quit退出shell进程,我们需要解析cmdline(输入的命令),判断是不是“quit”字符串,是就退出。

输入make test02和make rtest02查看运行结果

tsh实验结果和tshref一致,结果正确

3、打开 trace03.txt查看文件内容

输入make test03和make rtest03查看运行结果

tsh实验结果和tshref一致,结果正确

4、打开 trace04.txt查看文件内容

先在前台执行echo命令,等待程序执行完毕回收子进程。&代表是一个后台程序,myspin睡眠1秒,然后停止。因为在后台,所以显示下面一句,如果在前台则无。

输入make test04和make rtest04查看运行结果

tsh实验结果和tshref一致,结果正确

5、打开 trace05.txt查看文件内容

分别运行了前台echo、后台myspin、前台echo、后台myspin,然后需要实现一个内置命令job,功能是显示目前任务列表中的所有任务的所有属性

输入make test05和make rtest05查看运行结果

tsh实验结果和tshref一致,结果正确

6、打开 trace06.txt查看文件内容

输入make test06和make rtest06查看运行结果

tsh实验结果和tshref一致,结果正确

7、打开 trace07.txt查看文件内容

输入make test07和make rtest07查看运行结果

tsh实验结果和tshref一致,结果正确

8、打开 trace08.txt查看文件内容

输入make test08和make rtest08查看运行结果

tsh实验结果和tshref一致,结果正确

9、打开 trace09.txt查看文件内容

输入make test09和make rtest09查看运行结果

tsh实验结果和tshref一致,结果正确

10、打开 trace10.txt查看文件内容

输入make test10和make rtest10查看运行结果

tsh实验结果和tshref一致,结果正确

11、打开 trace11.txt查看文件内容

输入make test11和make rtest11查看运行结果

tsh实验结果和tshref一致,结果正确

12、打开 trace12.txt查看文件内容

输入make test12和make rtest12查看运行结果

tsh实验结果和tshref一致,结果正确

13、打开 trace13.txt查看文件内容

输入make test13和make rtest13查看运行结果

tsh实验结果和tshref一致,结果正确

14、打开 trace14.txt查看文件内容

输入make test14和make rtest14查看运行结果

tsh实验结果和tshref一致,结果正确

15、打开 trace15.txt查看文件内容

输入make test15和make rtest15查看运行结果

tsh实验结果和tshref一致,结果正确

16、打开 trace16.txt查看文件内容

输入make test16和make rtest16查看运行结果

tsh实验结果和tshref一致,结果正确

五、实验总结

       ShellLab实验的主要目的是熟悉进程控制和信号机制,实现一个支持任务功能的简易shell。在实验中,我们需要实现七个函数,包括作业管理、前台和后台执行、信号处理等功能。

  • 19
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值