HNU计算机系统shell Lab实验

shell lab

1. first of all

1.1 What to do

使用提供的tsh.c框架补全一个shell,主要需实现的函数如下:

void eval(char *cmdline);       //主例程,解析处理指令,fork相关进程完成job
int builtin_cmd(char **argv)    //若键入内置指令立即执行
void do_bgfg(char **argv);      //执行内置FG/BG指令
void waitfg(pid_t pid);         //等待前台指令完成
void sigchld_handler(int sig);  //响应signal信号
void sigtstp_handler(int sig);  //响应sigstp指令(Ctrl-Z)
void sigint_handler(int sig);   //响应sigint指令(Ctrl-C)

1.2 what should we do first

  • 使用命令tar xvf shlab-handout.tar -C ./shlab-handout 解压缩文件;
    需要先创建shlab-handout文件夹,直接使用给出的tar xvf shlab-handout.tar会导致文件直接解压到当前目录.
  • 使用make编译链接历程.

1.3 notice

通过比较./tshref和自己编译的./tsh输出是否一致判断是否完成.

1.4 辅助函数

int parseline(const char *cmdline, char **argv);    // 解析命令行参数,如果后台运行则返回 1
void sigquit_handler(int sig);                      // 接收到SIGQUIT信号调用exit()退出

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列表中添加job
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的job的jid
void listjobs(struct job_t *jobs);                                   // 打印jobs列表

void usage(void);           // 帮助信息
void unix_error(char *msg); // 错误信息
void app_error(char *msg);  // 向stdout流输入信息
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler); // 设置接收指定信号时的处理方式

2. 分析

2.1 需求分析

  • 提示符必须为"tsh> "

  • 用户键入的命令行必须包括命令名与0个及以上的参数,如果命令名为内置命令,则tsh应立即处理并等待下一个命令(如果可以),否则将name视为可执行文件路径,并新建一个子进程加载并运行

  • 不需要支持管道( | )与重定位( < / > )

  • 每一个子进程有唯一对应的进程组ID和作业JID,JID前缀"%“,如”%1"表示JID1,"1"表示PID1

  • 后台进程不接受SIGSTP与SIGINT信号

  • 键入Ctrl-C(Ctrl-Z)将向当前前台进程及其子进程发送SIGINT(SINGSTP)信号,如果没有前台进程,则无效

  • 若命令以"&"结束,则tsh应在后台运行该进程并输出PID,否则应在前台运行并等待其退出

  • tsh应支持如下四个内置指令:

    quit:退出tsh
    jobs:列出当前所有作业
    bg:令指定job后台运行
    fg:令指定job前台运行
    
  • tsh应回收所有zombie子进程

2.2 方法功能分析

2.2.1 void eval(char *cmdline)
  • 如果输入内置命令(quit,jobs,bg or fg),立即执行
  • 否则假定输入的是可执行文件路径,fork子进程并在子进程的上下文运行该文件
  • 若job为前台运行,则等待其终止
  • 每个子进程应具有唯一的进程组ID,且后台子进程不接受SIGINT(Ctrl-C)和SIGTSTP(Ctrl-Z)键盘输入
2.2.2 int builtin_cmd(char **argv)
  • 判断是否为内置指令,若是则立即执行
  • return 0表示非内置指令
2.2.3 void do_bgfg(char **argv)

执行bgfg指令:

bg < PID/JID >:令指定作业后台运行
fg < PID/JID >:令指定作业前台运行
参数前缀"%"为JID,否则为PID
2.2.4 void waitfg(pid_t pid)
  • 阻塞父进程直至当前前台进程不再是前台进程
2.2.5 void sigchld_handler(int sig)
  • 接收到SIGCHLD信号回收zombie进程
2.2.6 void sigtstp_handler(int sig)
  • 键入Ctrl-Z时发送SIGTSTP至当前前台进程以将其暂停
2.2.7 void sigint_handler(int sig)
  • 键入Ctrl-C时发送SIGSINT至当前前台进程以令其终止

3. Before

3.1 回收子进程

一个终止但未被回收的进程称为zombie进程,对于一个长期运行的程序(例如shell)内核无法通过init进程回收zombie进程,这就要求我们回收zombie进程。
waitpid()函数,进程可调用其使自身挂起以等待子进程终止或停止,从而回收子进程,其原型如下:

__pid_t waitpid (__pid_t __pid, int *__stat_loc, int __options);

各参数含义如下:

pid:等待指定集合

pid > 0 : 等待集合为pid对应的单独子进程
pid = -1: 等待集合为所有的子进程
pid < -1: 等待集合为一个进程组,ID为pid的绝对值
pid = 0 : 等待集合为一个进程组,ID为调用进程的pid

options:修改函数默认行为

WNOHANG             //集合中任何子进程都未终止,立即返回 0
WUNTRACED           //阻塞,直到一个进程终止或停止,返回PID
WCONTINUED          //阻塞,直到一个停止的进程收到SIGCONT信号重新开始执行
WUNTRACED|WCONTINUED//也可以通过或运算使用不同组合

status:存储退出状态

WIFEXITED(status)   //正常终止
WIFSIGNALED(status) //因信号而终止
WIFSTOPPED(status)  //因信号而停止

这个函数用来挂起调用进程的执行,直到pid对应的等待集合的一个子进程的改变才返回,包括三种状态的改变:

子进程终止
子进程收到信号停止
子进程收到信号恢复执行

此外如果一个子进程在调用之前就已经终止了,那么函数就会立即返回,否则,就会阻塞,直到一个子进程改变状态。

3.2 并发

  • errno为全局变量,很多函数错误返回时会设置errno,处理函数中应注意保存并恢复原errno
  • 访问全局变量时应阻塞所有信号,避免同步问题

3.3 竞争

对于如下代码:

    if (!builtin_cmd(argv)) // 判断是否为内置命令
    {
        if ((pid = fork()) == 0)                      // 创建子进程
        {
            Setpgid(0, 0);                             // 创建新进程组,ID设置为进程PID,同时保证当前进程组前台进程只有shell
            Execve(argv[0], argv, environ);            // 执行
            exit(0);                                   // 一定要有退出,避免execve()执行失败
        }
        if (state == FG)
        {
            addjob(jobs, pid, state, cmdline);       // 添加至作业列表
            waitfg(pid); // 等待前台进程执行完毕
        }
    }

考虑这样一个事件:

1.子进程创建执行

2.子进程在父进程执行addjob()前结束,发出SIGCHLD信号

3.父进程接收信号,调用信号处理例程,执行deletejob(),但此时还未执行addjob()

4.处理例程返回,父进程执行addjob(),但子进程已结束并回收

即deletejob()与addjob()产生竞争,对于类似的问题,处理方法主要是阻塞相关信号,例如阻塞SIGCHLD信号,直至执行完addjob()后再处理SIGCHLD信号

    sigset_t mask_all, mask_one, prev_one;
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    if (!builtin_cmd(argv)) // 判断是否为内置命令
    {
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // fork前阻塞SIGCHLD信号
        if ((pid = fork()) == 0)                      // 创建子进程
        {
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除子进程的阻塞
            Setpgid(0, 0);                             // 创建新进程组,ID设置为进程PID,同时保证当前进程组前台进程只有shell
            Execve(argv[0], argv, environ);            // 执行
            exit(0);                                   // 一定要有退出,避免execve()执行失败
        }
        if (state == FG)// 前台运行
        {
            Sigprocmask(SIG_BLOCK, &mask_all, NULL); // 添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);       // 添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);// 替换为仅阻塞SIGCHLD,waitfg()通过sigsuspend()原子性解除阻塞并挂起,避免竞争
            waitfg(pid); // 等待前台进程执行完毕
        }
    }

如上,在fork()前阻塞信号,直到父进程完成addjob()后再解除阻塞,处理相关信号,保证addjob()一定在deletejob()后执行

3.4 相关函数

3.4.1 信号相关

头文件signal.h中定义了一系列变量和函数用于描述和操作信号

typedef struct {
  unsigned long sig[_NSIG_WORDS];
} sigset_t;                                         //信号集,便于管理信号
int sigemptyset(sigset_t *set);                     //初始化由set指定的信号集,信号集的所有信号被清空
int sigfillset(sigset_t *set);                      //初始化由set指定的信号集,将包含linux支持的64种信号
int sigaddset(sigset_t *set, int signum);           //在set指向的信号集中加入signum信号
int sigdelset(sigset_t *set, int signum);           //在set指向的信号集中删除signum信号
int sigismember(const sigset_t *set, int signum);   //判定信号signum是否在set指向的信号集中
int kill(pid_t pid, int signum);                    //发送指定signum至指定pid进程
    /*特别地,pid有如下用法:
     *pid > 0:发送给指定进程
     *pid == 0:发送给调用者所在进程组的所有进程
     *pid == -1:发送给所有进程
     *pid < -1:发送给进程组为pid绝对值的所有进程*/
3.4.2 进程相关
__pid_t fork (void)//创建一个子进程
    /*返回值如下:
     *pid < 0:函数执行错误
     *pid == 0:子进程内返回
     *pid > 0:父进程内返回*/
int execve (const char *__path, char *const __argv[],char *const __envp[])//进程以指定程序替代当前程序执行
    /*path:包含准备载入当前进程空间的新程序的路径名。既可以是绝对路径,又可以是相对路径。
     *argv[]:指定了传给新进程的命令行参数,该数组对应于c语言main函数的argv参数数组,格式也相同,argv[0]对应命令名,通常情况下该值与path中的basename(绝对路径的最后一个)相同。
     *envp[]:最后一个参数envp指定了新程序的环境列表。参数envp对应于新程序的environ数组。
     */
int setpgid (__pid_t __pid, __pid_t __pgid)//将pid进程的进程组ID设置成pgid,创建一个新进程组或加入一个已存在的进程组
    /*一个进程只能为自己或子进程设置进程组ID,不能设置其父进程的进程组ID
     *对于pid和pgid,有如下用法:
     *pid == pgid :指定pid进程成为进程组长,进程组ID pgid=pid
     *pid == 0 :当前进程的pid作为进程组ID
     *pgid == 0 :将指定pid作为进程组ID*/

3.5 错误处理

Unix系统及函数在执行出错时通常返回-1并设置全局变量errno值,便于检查错误,为降低代码重复,重写一套错误处理函数

/* 错误处理 */
pid_t Fork(void);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
void Execve(const char *filename, char *const argv[], char *const envp[]);
void Setpgid(pid_t pid, pid_t pgid);
void Kill(pid_t pid, int sig);
pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("fork error");
    return pid;
}
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
{
    if (sigprocmask(how, set, oldset) < 0)
        unix_error("sigprocmask error");
}
void Sigemptyset(sigset_t *set)
{
    if (sigemptyset(set) < 0)
        unix_error("sigprocmask error");
}
void Sigfillset(sigset_t *set)
{
    if (sigfillset(set) < 0)
        unix_error("sigfillset error");
}
void Sigaddset(sigset_t *set, int signum)
{
    if (sigaddset(set, signum) < 0)
        unix_error("sigaddset error");
}
void Execve(const char *filename, char *const argv[], char *const envp[])
{
    if (execve(filename, argv, envp) < 0)
    {
        printf("%s: Command not found.\n", argv[0]);
    }
}
void Setpgid(pid_t pid, pid_t pgid)
{
    if (setpgid(pid, pgid) < 0)
    {
        unix_error("setpgid error");
    }
}
void Kill(pid_t pid, int sig)
{
    if (kill(pid, sig) < 0)
    {
        unix_error("kill error");
    }
}

4. 实现

4.1 eval()

4.1.1 功能
  • 如果输入内置命令(quit,jobs,bg or fg),立即执行
  • 否则假定输入的是可执行文件路径,fork子进程并在子进程的上下文运行该文件
  • 若job为前台运行,则等待其终止
  • 每个子进程应具有唯一的进程组ID,且后台子进程不接受SIGINT(Ctrl-C)和SIGTSTP(Ctrl-Z)键盘输入
4.1.2 思路

1.作为主例程,需要解析命令行并分别执行,解析命令行可通过预置函数parseline()将命令行参数分别存入argv[],并确定运行状态(fg / bg)

2.调用builtin_cmd()函数判断并执行内置命令

3.如果不是内置指令,则通过fork()创建新进程运行该程序,通过先前返回的运行状态确定是否等待其终止,若后台运行则输出进程信息,否则应等待其退出,fork()时应注意信号阻塞,避免竞争

4.创建新进程时应题义每个进程有唯一对应的进程组ID,则通过setpgid()将新进程pid设置为自己的进程组pgid,创建后应在父进程通过addjob()将子进程添加入job列表

5.如果是内置指令,直接在builtin_cmd()内完成执行

4.1.3 实现
/*
 * 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)
{
    char *argv[MAXARGS]; // 存放解析的参数
    char buf[MAXLINE];   // 解析cmdline
    int bg;              // 判断程序是前台还是后台执行
    int state;           // 指示前台还是后台运行状态
    pid_t pid;           // 执行程序的子进程的pid

    strcpy(buf, cmdline);
    bg = parseline(buf, argv); // 解析参数
    state = bg ? BG : FG;
    if (argv[0] == NULL) // 空行,直接返回
        return;

    sigset_t mask_all, mask_one, prev_one;
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    if (!builtin_cmd(argv)) // 判断是否为内置命令
    {
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // fork前阻塞SIGCHLD信号
        if ((pid = fork()) == 0)                      // 创建子进程
        {
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除子进程的阻塞
            Setpgid(0, 0);                             // 创建新进程组,ID设置为进程PID,同时保证当前进程组前台进程只有shell
            Execve(argv[0], argv, environ);            // 执行
            exit(1);                                   // 一定要有退出,避免execve()执行失败
        }
        if (state == FG)//前台运行
        {
            Sigprocmask(SIG_BLOCK, &mask_all, NULL); // 添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);       // 添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);//添加SIGCHLD阻塞,waitfg()通过sigsuspend()原子性解除阻塞并挂起,避免竞争
            waitfg(pid); // 等待前台进程执行完毕
        }
        else//后台运行
        {
            Sigprocmask(SIG_BLOCK, &mask_all, NULL); // 添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);       // 添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);//仍然阻塞SIGCHLD,printf()需要访问全局变量
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // 打印后台进程信息
        }
        Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除所有阻塞
    }
    return;
}

4.2 builtin_cmd()

4.2.1 功能
  • 判断是否为内置指令,若是则立即执行
  • return 0表示非内置指令
4.2.2 思路

1.一般而言,argv[0]始终为内置指令或可执行文件路径,因此只需判断argv[0]即可判断是否为内置指令

2.quit指令通过系统调用exit()退出tsh

3.bg/fg指令调用do_bgfg()函数执行,并返回1表示是内置指令

4.jobs指令调用预置函数listjobs()执行,并返回1表示是内置指令

4.2.3 实现
/*
 * 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") || !strcmp(argv[0], "QUIT")) // 退出
        exit(0);
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // bg/fg指令
    {
        do_bgfg(argv); // bg/fg处理
        return 1;
    }
    if (!strcmp(argv[0], "jobs"))
    {
        listjobs(jobs); // jobs处理
        return 1;
    }
    return 0; /* not a builtin command */
}

4.3 do_bgfg()

4.3.1 功能

执行bgfg指令:

//bg < PID/JID >:令指定作业后台运行
//fg < PID/JID >:令指定作业前台运行
//参数前缀"%"为JID,否则为PID
4.3.2 思路

1.首先判断是哪个指令,然后判断是否有id参数,如果缺少id参数则输出错误信息并返回

2.针对id参数的格式,jid为"%id",pid为"id",且argv是char二维数组,可通过argv[1] [0]判断id是否为"%"开头,如果是则按照jid处理,如果不是,先判断是否为数字,是则按照pid处理,否则为非法参数

3.获得pid与jid时应注意类型转换,C标准库提供sscanf()从字符串读取格式化输入,也提供atoi()从字符串转换为一个整形数,具体如下:

int sscanf(const char *str, const char *format, ...)//返回成功匹配和赋值的个数或者EOF
/*其中str为原字符串
 *format也是字符串,格式为[=%[*][width][modifiers]type=]
 *参数解释如下:
 * '*':表示数据是从流 stream 中读取的,但是可以被忽视
 * width:这指定了在当前读取操作中读取的最大字符数。
 * modifiers:为对应的附加参数所指向的数据指定一个不同于
 * 整型(针对 d、i 和 n)、无符号整型(针对 o、u 和 x)或浮点型(针对 e、f 和 g)的大小:
 * h :短整型(针对 d、i 和 n),或无符号短整型(针对 o、u 和 x)
 * l :长整型(针对 d、i 和 n),或无符号长整型(针对 o、u 和 x),或双精度型(针对 e、f 和 g)
 * L :长双精度型(针对 e、f 和 g)
 * 对于type有如下参数:
 * c :单个字符:读取下一个字符。如果指定了一个不为1的宽度width,函数会读取 width 个字符,并通过参数传递,把它们存储在数组中连续位置。在末尾不会追加空字符。(对应参数类型char *)
 * d :十进制整数:数字前面的+或-号是可选的。(对应参数类型int *)
 * e,E,f,g,G :浮点数:包含了一个小数点,一个可选的前置符号+或-,一个可选的后置字符e或E,以及一个十进制数字。两个有效的实例 -732.103 和 7.12e4。(对应参数类型folat *)
 * o :八进制整数。(对应参数类型int *)
 * s :字符串。这将读取连续字符,直到遇到一个空格字符(空格字符可以是空白、换行和制表符)。(对应参数类型char *)
 * u :无符号的十进制整数。(对应参数类型unsigned int *)
 * x,X :十六进制整数。(对应参数类型int *)
*/
int atoi (const char *__nptr)//返回值为int类型的整数
 /*1、该函数首先会丢弃尽可能多的空白字符,直到找到第一个非空白字符,然后,从这个字符开始,取一个可选的初识加号或者减号,后跟尽可能多的十进制数字,并将他们返回一个int类型的数值。
 *2、若该字符串是在整数的字符后包含其他字符,则这些字符将会被忽略,返回其他字符之前的整数,并且不会对该函数造成任何影响。
 *3、若该字符串中第一个非空字符序列表示有效的整数,或是一个空指针,或只包含空白字符,则不执行任何转换,并且返回零。
 */

4.可分别调用getjobjid()与getjobpid()获得指定jib,若找不到指定job则输出错误信息并返回

5.找到指定job后通过函数kill()向指定进程组发送SIGCONT重新运行信号,再根据前台运行或后台运行分别处理,注意信号阻塞,以及修改job相关信息

4.3.3 实现
void do_bgfg(char **argv)
{
    struct job_t *job = NULL; // 要处理的job
    int state;                // 输入的目标状态
    int id;                   // 存储jid或pid
    if (!strcmp(argv[0], "bg"))
        state = BG;
    else
        state = FG;
    if (argv[1] == NULL) // 缺少id参数
    {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    if (argv[1][0] == '%') // jid
    {
        if (sscanf(&argv[1][1], "%d", &id) > 0) // argv[]为str数组,需转换
        {
            job = getjobjid(jobs, id); // 通过jid获得job
            if (job == NULL)
            {
                printf("%%%d: No such job\n", id);
                return;
            }
        }
    }
    else if (!isdigit(argv[1][0])) // 非法字符
    {
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }
    else // PID
    {
        id = atoi(argv[1]);        // argv[]为str数组,需转换
        job = getjobpid(jobs, id); // 通过pid获得job
        if (job == NULL)
        {
            printf("(%d): No such process\n", id);
            return;
        }
    }
    sigset_t mask_one;                         // 信号集
    Sigemptyset(&mask_one);                    // 置空
    Sigaddset(&mask_one, SIGCHLD);             // 只添加SIGCHLD
    Sigprocmask(SIG_SETMASK, &mask_one, NULL); // 只屏蔽SIGCHLD
    Kill(-(job->pid), SIGCONT);                // 使进程恢复运行,信号发送到指定进程组
    job->state = state;
    if (state == BG)
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
    else
        waitfg(job->pid); // waitfg()通过sigsuspend()原子性解除阻塞并挂起等待,避免竞争
    return;
}

4.4 waitfg()

4.4.1 功能
  • 阻塞父进程直至当前前台进程不再是前台进程
4.4.2 思路

1.那么应该要显式等待信号,对于如下代码

while(fgpid(jobs) != 0)
    pause();

考虑这样一个事件

  • 父进程调用fgpid()判断完成进入循环
  • 父进程在pause()前子进程结束并发出SIGCHLD信号
  • 父进程接收信号并处理,处理完成后执行pause()

由于pause()仅在捕获到信号后返回,那么父进程将长眠不醒。

解决方法有两个:调用sleep()或者sigsuspend(),sleep()不依赖信号,可循环判断子进程状态。
但更好的方式是sigsuspend(),这相当于以下代码的原子化:

sigprocmask(SIG_SETMASK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);

只需在调用sigsuspend()前完成对SIGCHLD的阻塞,调用sigsuspend()后将先解除阻塞然后挂起等待,期间不会处理信号。

具体用法解释如下:

int sigsuspend( const sigset_t *sigmask );
 /*该函数执行时将之前的信号屏蔽词替换为传入的信号集,随后挂起调用者进程,
  *接收到传入信号集之外的信号时将唤醒进程,然后将信号屏蔽次恢复为之前的。*/
4.4.3 实现
/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    sigset_t mask;      // 信号集
    Sigemptyset(&mask); // 初始化置空
    while (fgpid(jobs) != 0)
    {
        sigsuspend(&mask); // 挂起时取消阻塞
    }
    return;
}

4.5 sigchld_handler()

4.5.1 功能
  • 接收到SIGCHLD信号回收zombie进程
4.5.2 思路

1.由于调用Kill()等函数可能修改errno,先保存原errno

2.涉及deletejob()等访问全局变量的调用,须注意信号阻塞

3.收到信号后调用waitpid()获得该子进程pid和退出状态

4.判断退出状态,这里只实现三种:正常退出、因信号而终止、因信号而停止,其中正常退出和因信号终止需调用deletejob()清理函数,因信号停止则只需修改对应job的state为ST表示停止状态即可

5.注意打印信息的格式

6.需注意退出状态的相关宏定义

WIFEXITED(status)//如果子进程正常结束则为非0值。
WEXITSTATUS(status)//取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束才能使用此宏。

WIFSIGNALED(status)//如果子进程是因为信号而结束则此宏值为真
WTERMSIG(status)//取得子进程因信号而中止的信号代码,一般会先用WIFSIGNALED来判断后才使用此宏。

WIFSTOPPED(status)//如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED时才会有此情况。
WSTOPSIG(status)//取得引发子进程暂停的信号代码

7.返回前恢复errno

4.5.3 实现
/*
 * 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)
{
    int old_errno = errno; // 由于errno是全局变量,注意保存和恢复errno
    int status;
    pid_t pid;
    struct job_t *job;
    sigset_t mask, prev;
    Sigfillset(&mask);                                            // 信号集置全1,64个信号
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) // 立即返回该子进程的pid
    {
        Sigprocmask(SIG_BLOCK, &mask, &prev); // 阻塞所有信号
        if (WIFEXITED(status))                // 正常终止
        {
            deletejob(jobs, pid); // 清除job信息
        }
        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));
            job = getjobpid(jobs, pid);
            job->state = ST; // job信息改为停止状态
        }
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞
    }
    errno = old_errno; // 恢复原errno
    return;
}

4.6 sigint_handler()

4.6.1 功能
  • 键入Ctrl-Z时发送SIGTSTP至当前前台进程组以将其暂停
4.6.2 思路

1.由于调用Kill()等函数可能修改errno,先保存原errno

2.收到SIGINT信号后首先获取前台进程pid,如果没有前台进程就恢复errno然后直接返回

3.如果有,就调用Kill()向其进程组发送SIGINT信号停止

4.由于会调用fgpid()访问全局变量,应注意阻塞和恢复信号

4.6.3 实现
/*
 * 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 old_errno = errno; // 保存errno以便恢复
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);                    // 所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev); // jobs为全局变量
    if ((pid = fgpid(jobs)) != 0)             // 判定当前是否有前台进程
    {
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除阻塞
        Kill(-pid, SIGINT);                    // 向前台进程的进程组发送SIGINT信号
    }
    errno = old_errno; // 恢复errno
    return;
}

4.7 sigtstp_handler()

4.7.1 功能
  • 键入Ctrl-C时发送SIGSINT至当前前台进程组以令其终止
4.7.2 思路

1.由于调用Kill()等函数可能修改errno,先保存原errno

2.收到SIGTSTP信号后首先获取前台进程pid,如果没有前台进程就恢复errno然后直接返回

3.如果有,就调用Kill()向其进程组发送SIGTSTP信号停止

4.由于会调用fgpid()访问全局变量,应注意阻塞和恢复信号

4.7.3 实现
/*
 * 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 old_errno = errno; // 保存errno以便恢复
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);                    // 所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev); // jobs为全局变量
    if ((pid = fgpid(jobs)) > 0)              // 判定当前是否有前台进程
    {
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除阻塞
        Kill(-pid, SIGTSTP);                   // 向前台进程的进程组发送SIGTSTP信号停止
    }
    errno = old_errno; // 恢复errno
    return;
}

5. 验证

5.1 改写Makefile

由于shell使用makefile控制编译和验证,阅读发现其验证分为16个test且需要分别make执行,于是添加tests项与rtests项便于一键验证所有test

tests:
    $(DRIVER) -t trace01.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace02.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace03.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace04.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace05.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace06.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace07.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace08.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace09.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace10.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace11.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace12.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace13.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace14.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace15.txt -s $(TSH) -a $(TSHARGS)
    $(DRIVER) -t trace16.txt -s $(TSH) -a $(TSHARGS)

rtests:
    $(DRIVER) -t trace01.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace02.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace03.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace04.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace05.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace06.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace07.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace08.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace09.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace10.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace11.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace12.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace13.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace14.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace15.txt -s $(TSHREF) -a $(TSHARGS)
    $(DRIVER) -t trace16.txt -s $(TSHREF) -a $(TSHARGS)

5.2 执行验证

终端分别输入

make tests > tsh.out
make rtests > tshref.out

将输出重定向至文件中便于比对

然后使用meld软件对两个输出文件进行比对

比对结果如下:

--- /home/anchor/HNU/Computer_system/LAB4-shelllab/shlab-handout/tsh.out
+++ /home/anchor/HNU/Computer_system/LAB4-shelllab/shlab-handout/tshref.out
@@ -1,203 +1,203 @@
-./sdriver.pl -t trace01.txt -s ./tsh -a "-p"
+./sdriver.pl -t trace01.txt -s ./tshref -a "-p"
 #
 # trace01.txt - Properly terminate on EOF.
 #
-./sdriver.pl -t trace02.txt -s ./tsh -a "-p"
+./sdriver.pl -t trace02.txt -s ./tshref -a "-p"
 #
 # trace02.txt - Process builtin quit command.
 #
-./sdriver.pl -t trace03.txt -s ./tsh -a "-p"
+./sdriver.pl -t trace03.txt -s ./tshref -a "-p"
 #
 # trace03.txt - Run a foreground job.
 #
 tsh> quit
-./sdriver.pl -t trace04.txt -s ./tsh -a "-p"
+./sdriver.pl -t trace04.txt -s ./tshref -a "-p"
 #
 # trace04.txt - Run a background job.
 #
 tsh> ./myspin 1 &
-[1] (15514) ./myspin 1 &
-./sdriver.pl -t trace05.txt -s ./tsh -a "-p"
+[1] (15708) ./myspin 1 &
+./sdriver.pl -t trace05.txt -s ./tshref -a "-p"
 #
 # trace05.txt - Process jobs builtin command.
 #
 tsh> ./myspin 2 &
-[1] (15519) ./myspin 2 &
+[1] (15713) ./myspin 2 &
 tsh> ./myspin 3 &
-[2] (15521) ./myspin 3 &
-tsh> jobs
-[1] (15519) Running ./myspin 2 &
-[2] (15521) Running ./myspin 3 &
-./sdriver.pl -t trace06.txt -s ./tsh -a "-p"
+[2] (15715) ./myspin 3 &
+tsh> jobs
+[1] (15713) Running ./myspin 2 &
+[2] (15715) Running ./myspin 3 &
+./sdriver.pl -t trace06.txt -s ./tshref -a "-p"
 #
 # trace06.txt - Forward SIGINT to foreground job.
 #
 tsh> ./myspin 4
-Job [1] (15527) terminated by signal 2
-./sdriver.pl -t trace07.txt -s ./tsh -a "-p"
+Job [1] (15726) terminated by signal 2
+./sdriver.pl -t trace07.txt -s ./tshref -a "-p"
 #
 # trace07.txt - Forward SIGINT only to foreground job.
 #
 tsh> ./myspin 4 &
-[1] (15537) ./myspin 4 &
+[1] (15731) ./myspin 4 &
 tsh> ./myspin 5
-Job [2] (15539) terminated by signal 2
-tsh> jobs
-[1] (15537) Running ./myspin 4 &
-./sdriver.pl -t trace08.txt -s ./tsh -a "-p"
+Job [2] (15733) terminated by signal 2
+tsh> jobs
+[1] (15731) Running ./myspin 4 &
+./sdriver.pl -t trace08.txt -s ./tshref -a "-p"
 #
 # trace08.txt - Forward SIGTSTP only to foreground job.
 #
 tsh> ./myspin 4 &
-[1] (15546) ./myspin 4 &
+[1] (15739) ./myspin 4 &
 tsh> ./myspin 5
-Job [2] (15548) stopped by signal 20
-tsh> jobs
-[1] (15546) Running ./myspin 4 &
-[2] (15548) Stopped ./myspin 5
-./sdriver.pl -t trace09.txt -s ./tsh -a "-p"
+Job [2] (15741) stopped by signal 20
+tsh> jobs
+[1] (15739) Running ./myspin 4 &
+[2] (15741) Stopped ./myspin 5
+./sdriver.pl -t trace09.txt -s ./tshref -a "-p"
 #
 # trace09.txt - Process bg builtin command
 #
 tsh> ./myspin 4 &
-[1] (15565) ./myspin 4 &
+[1] (15747) ./myspin 4 &
 tsh> ./myspin 5
-Job [2] (15567) stopped by signal 20
-tsh> jobs
-[1] (15565) Running ./myspin 4 &
-[2] (15567) Stopped ./myspin 5
+Job [2] (15749) stopped by signal 20
+tsh> jobs
+[1] (15747) Running ./myspin 4 &
+[2] (15749) Stopped ./myspin 5
 tsh> bg %2
-[2] (15567) ./myspin 5
-tsh> jobs
-[1] (15565) Running ./myspin 4 &
-[2] (15567) Running ./myspin 5
-./sdriver.pl -t trace10.txt -s ./tsh -a "-p"
+[2] (15749) ./myspin 5
+tsh> jobs
+[1] (15747) Running ./myspin 4 &
+[2] (15749) Running ./myspin 5
+./sdriver.pl -t trace10.txt -s ./tshref -a "-p"
 #
 # trace10.txt - Process fg builtin command.
 #
 tsh> ./myspin 4 &
-[1] (15577) ./myspin 4 &
-tsh> fg %1
-Job [1] (15577) stopped by signal 20
-tsh> jobs
-[1] (15577) Stopped ./myspin 4 &
-tsh> fg %1
-tsh> jobs
-./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
+[1] (15758) ./myspin 4 &
+tsh> fg %1
+Job [1] (15758) stopped by signal 20
+tsh> jobs
+[1] (15758) Stopped ./myspin 4 &
+tsh> fg %1
+tsh> jobs
+./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
 #
 # trace11.txt - Forward SIGINT to every process in foreground process group
 #
 tsh> ./mysplit 4
-Job [1] (15591) terminated by signal 2
-tsh> /bin/ps a
-    PID TTY      STAT   TIME COMMAND
-   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
-   3026 tty2     Sl+    0:31 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
-   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
-   5016 pts/0    Ss+    0:02 zsh
-   5021 pts/0    S      0:00 zsh
-   5057 pts/0    S      0:00 zsh
-   5058 pts/0    S      0:00 zsh
-   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15418 pts/1    Ss     0:00 zsh
-  15423 pts/1    S      0:00 zsh
-  15457 pts/1    S      0:00 zsh
-  15458 pts/1    S      0:00 zsh
-  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15499 pts/1    S+     0:00 make tests
-  15587 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tsh -a "-p"
-  15588 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tsh -a -p
-  15589 pts/1    S+     0:00 ./tsh -p
-  15594 pts/1    R      0:00 /bin/ps a
-./sdriver.pl -t trace12.txt -s ./tsh -a "-p"
+Job [1] (15768) terminated by signal 2
+tsh> /bin/ps a
+    PID TTY      STAT   TIME COMMAND
+   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
+   3026 tty2     Sl+    0:33 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
+   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
+   5016 pts/0    Ss+    0:02 zsh
+   5021 pts/0    S      0:00 zsh
+   5057 pts/0    S      0:00 zsh
+   5058 pts/0    S      0:00 zsh
+   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15418 pts/1    Ss     0:00 zsh
+  15423 pts/1    S      0:00 zsh
+  15457 pts/1    S      0:00 zsh
+  15458 pts/1    S      0:00 zsh
+  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15693 pts/1    S+     0:00 make rtests
+  15764 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace11.txt -s ./tshref -a "-p"
+  15765 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace11.txt -s ./tshref -a -p
+  15766 pts/1    S+     0:00 ./tshref -p
+  15774 pts/1    R      0:00 /bin/ps a
+./sdriver.pl -t trace12.txt -s ./tshref -a "-p"
 #
 # trace12.txt - Forward SIGTSTP to every process in foreground process group
 #
 tsh> ./mysplit 4
-Job [1] (15599) stopped by signal 20
-tsh> jobs
-[1] (15599) Stopped ./mysplit 4
-tsh> /bin/ps a
-    PID TTY      STAT   TIME COMMAND
-   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
-   3026 tty2     Sl+    0:31 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
-   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
-   5016 pts/0    Ss+    0:02 zsh
-   5021 pts/0    S      0:00 zsh
-   5057 pts/0    S      0:00 zsh
-   5058 pts/0    S      0:00 zsh
-   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15418 pts/1    Ss     0:00 zsh
-  15423 pts/1    S      0:00 zsh
-  15457 pts/1    S      0:00 zsh
-  15458 pts/1    S      0:00 zsh
-  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15499 pts/1    S+     0:00 make tests
-  15595 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace12.txt -s ./tsh -a "-p"
-  15596 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tsh -a -p
-  15597 pts/1    S+     0:00 ./tsh -p
-  15599 pts/1    T      0:00 ./mysplit 4
-  15600 pts/1    T      0:00 ./mysplit 4
-  15603 pts/1    R      0:00 /bin/ps a
-./sdriver.pl -t trace13.txt -s ./tsh -a "-p"
+Job [1] (15779) stopped by signal 20
+tsh> jobs
+[1] (15779) Stopped ./mysplit 4
+tsh> /bin/ps a
+    PID TTY      STAT   TIME COMMAND
+   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
+   3026 tty2     Sl+    0:33 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
+   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
+   5016 pts/0    Ss+    0:02 zsh
+   5021 pts/0    S      0:00 zsh
+   5057 pts/0    S      0:00 zsh
+   5058 pts/0    S      0:00 zsh
+   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15418 pts/1    Ss     0:00 zsh
+  15423 pts/1    S      0:00 zsh
+  15457 pts/1    S      0:00 zsh
+  15458 pts/1    S      0:00 zsh
+  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15693 pts/1    S+     0:00 make rtests
+  15775 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace12.txt -s ./tshref -a "-p"
+  15776 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace12.txt -s ./tshref -a -p
+  15777 pts/1    S+     0:00 ./tshref -p
+  15779 pts/1    T      0:00 ./mysplit 4
+  15780 pts/1    T      0:00 ./mysplit 4
+  15783 pts/1    R      0:00 /bin/ps a
+./sdriver.pl -t trace13.txt -s ./tshref -a "-p"
 #
 # trace13.txt - Restart every stopped process in process group
 #
 tsh> ./mysplit 4
-Job [1] (15608) stopped by signal 20
-tsh> jobs
-[1] (15608) Stopped ./mysplit 4
-tsh> /bin/ps a
-    PID TTY      STAT   TIME COMMAND
-   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
-   3026 tty2     Sl+    0:31 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
-   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
-   5016 pts/0    Ss+    0:02 zsh
-   5021 pts/0    S      0:00 zsh
-   5057 pts/0    S      0:00 zsh
-   5058 pts/0    S      0:00 zsh
-   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15418 pts/1    Ss     0:00 zsh
-  15423 pts/1    S      0:00 zsh
-  15457 pts/1    S      0:00 zsh
-  15458 pts/1    S      0:00 zsh
-  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15499 pts/1    S+     0:00 make tests
-  15604 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace13.txt -s ./tsh -a "-p"
-  15605 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh -a -p
-  15606 pts/1    S+     0:00 ./tsh -p
-  15608 pts/1    T      0:00 ./mysplit 4
-  15609 pts/1    T      0:00 ./mysplit 4
-  15612 pts/1    R      0:00 /bin/ps a
-tsh> fg %1
-tsh> /bin/ps a
-    PID TTY      STAT   TIME COMMAND
-   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
-   3026 tty2     Sl+    0:31 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
-   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
-   5016 pts/0    Ss+    0:02 zsh
-   5021 pts/0    S      0:00 zsh
-   5057 pts/0    S      0:00 zsh
-   5058 pts/0    S      0:00 zsh
-   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15418 pts/1    Ss     0:00 zsh
-  15423 pts/1    S      0:00 zsh
-  15457 pts/1    S      0:00 zsh
-  15458 pts/1    S      0:00 zsh
-  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
-  15499 pts/1    S+     0:00 make tests
-  15604 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace13.txt -s ./tsh -a "-p"
-  15605 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tsh -a -p
-  15606 pts/1    S+     0:00 ./tsh -p
-  15618 pts/1    R      0:00 /bin/ps a
-./sdriver.pl -t trace14.txt -s ./tsh -a "-p"
+Job [1] (15788) stopped by signal 20
+tsh> jobs
+[1] (15788) Stopped ./mysplit 4
+tsh> /bin/ps a
+    PID TTY      STAT   TIME COMMAND
+   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
+   3026 tty2     Sl+    0:33 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
+   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
+   5016 pts/0    Ss+    0:02 zsh
+   5021 pts/0    S      0:00 zsh
+   5057 pts/0    S      0:00 zsh
+   5058 pts/0    S      0:00 zsh
+   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15418 pts/1    Ss     0:00 zsh
+  15423 pts/1    S      0:00 zsh
+  15457 pts/1    S      0:00 zsh
+  15458 pts/1    S      0:00 zsh
+  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15693 pts/1    S+     0:00 make rtests
+  15784 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace13.txt -s ./tshref -a "-p"
+  15785 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref -a -p
+  15786 pts/1    S+     0:00 ./tshref -p
+  15788 pts/1    T      0:00 ./mysplit 4
+  15789 pts/1    T      0:00 ./mysplit 4
+  15792 pts/1    R      0:00 /bin/ps a
+tsh> fg %1
+tsh> /bin/ps a
+    PID TTY      STAT   TIME COMMAND
+   3024 tty2     Ssl+   0:00 /usr/libexec/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu /usr/bin/gnome-session --session=ubuntu
+   3026 tty2     Sl+    0:33 /usr/lib/xorg/Xorg vt2 -displayfd 3 -auth /run/user/1000/gdm/Xauthority -nolisten tcp -background none -noreset -keeptty -novtswitch -verbose 3
+   3137 tty2     Sl+    0:00 /usr/libexec/gnome-session-binary --session=ubuntu
+   5016 pts/0    Ss+    0:02 zsh
+   5021 pts/0    S      0:00 zsh
+   5057 pts/0    S      0:00 zsh
+   5058 pts/0    S      0:00 zsh
+   5060 pts/0    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15418 pts/1    Ss     0:00 zsh
+  15423 pts/1    S      0:00 zsh
+  15457 pts/1    S      0:00 zsh
+  15458 pts/1    S      0:00 zsh
+  15460 pts/1    Sl     0:00 /home/anchor/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.5.4 -s -1 -u -1 -d -1 -c -1 -m -1 -v FATAL -t 32
+  15693 pts/1    S+     0:00 make rtests
+  15784 pts/1    S+     0:00 /bin/sh -c ./sdriver.pl -t trace13.txt -s ./tshref -a "-p"
+  15785 pts/1    S+     0:00 /usr/bin/perl ./sdriver.pl -t trace13.txt -s ./tshref -a -p
+  15786 pts/1    S+     0:00 ./tshref -p
+  15795 pts/1    R      0:00 /bin/ps a
+./sdriver.pl -t trace14.txt -s ./tshref -a "-p"
 #
 # trace14.txt - Simple error handling
 #
 tsh> ./bogus
-./bogus: Command not found.
-tsh> ./myspin 4 &
-[1] (15625) ./myspin 4 &
+./bogus: Command not found
+tsh> ./myspin 4 &
+[1] (15802) ./myspin 4 &
 tsh> fg
 fg command requires PID or %jobid argument
 tsh> bg
@@ -213,50 +213,50 @@
 tsh> fg %2
 %2: No such job
 tsh> fg %1
-Job [1] (15625) stopped by signal 20
+Job [1] (15802) stopped by signal 20
 tsh> bg %2
 %2: No such job
 tsh> bg %1
-[1] (15625) ./myspin 4 &
-tsh> jobs
-[1] (15625) Running ./myspin 4 &
-./sdriver.pl -t trace15.txt -s ./tsh -a "-p"
+[1] (15802) ./myspin 4 &
+tsh> jobs
+[1] (15802) Running ./myspin 4 &
+./sdriver.pl -t trace15.txt -s ./tshref -a "-p"
 #
 # trace15.txt - Putting it all together
 #
 tsh> ./bogus
-./bogus: Command not found.
+./bogus: Command not found
 tsh> ./myspin 10
-Job [1] (15645) terminated by signal 2
+Job [1] (15820) terminated by signal 2
 tsh> ./myspin 3 &
-[1] (15647) ./myspin 3 &
-tsh> ./myspin 4 &
-[2] (15649) ./myspin 4 &
-tsh> jobs
-[1] (15647) Running ./myspin 3 &
-[2] (15649) Running ./myspin 4 &
-tsh> fg %1
-Job [1] (15647) stopped by signal 20
-tsh> jobs
-[1] (15647) Stopped ./myspin 3 &
-[2] (15649) Running ./myspin 4 &
+[1] (15822) ./myspin 3 &
+tsh> ./myspin 4 &
+[2] (15824) ./myspin 4 &
+tsh> jobs
+[1] (15822) Running ./myspin 3 &
+[2] (15824) Running ./myspin 4 &
+tsh> fg %1
+Job [1] (15822) stopped by signal 20
+tsh> jobs
+[1] (15822) Stopped ./myspin 3 &
+[2] (15824) Running ./myspin 4 &
 tsh> bg %3
 %3: No such job
 tsh> bg %1
-[1] (15647) ./myspin 3 &
-tsh> jobs
-[1] (15647) Running ./myspin 3 &
-[2] (15649) Running ./myspin 4 &
+[1] (15822) ./myspin 3 &
+tsh> jobs
+[1] (15822) Running ./myspin 3 &
+[2] (15824) Running ./myspin 4 &
 tsh> fg %1
 tsh> quit
-./sdriver.pl -t trace16.txt -s ./tsh -a "-p"
+./sdriver.pl -t trace16.txt -s ./tshref -a "-p"
 #
 # trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
 #     signals that come from other processes instead of the terminal.
 #
 tsh> ./mystop 2
-Job [1] (15674) stopped by signal 20
-tsh> jobs
-[1] (15674) Stopped ./mystop 2
+Job [1] (15837) stopped by signal 20
+tsh> jobs
+[1] (15837) Stopped ./mystop 2
 tsh> ./myint 2
-Job [2] (15677) terminated by signal 2
+Job [2] (15840) terminated by signal 2

输出结果除了进程pid外完全一致,满足题目要求

实验圆满完成

6. test解读

6.1 trace01

#
# trace01.txt - Properly terminate on EOF.
#
CLOSE
WAIT

正确终止EOF

6.2 trace02

#
# trace02.txt - Process builtin quit command.
#
quit
WAIT

检测内置quit

6.3 trace03

#
# trace03.txt - Run a foreground job.
#
/bin/echo tsh> quit
quit

这里解释一下/bin/echo:

  • eval函数先通过builtin_cmd查询cmdline是不是内置命令如quit,如果是则当前进程执行命令
  • 如果不是则创建一个子进程,在子进程中调用execve()函数通过argv[0]来寻找路径,并在子进程中运行路径中的可执行文件,如果找不到则说明命令为无效命令,输出命令无效,并用exit(0)结束该子进程

/bin/echo就是执行bin目录下的echo文件,echo可以理解为将其后面的内容当作字符串输出

所以第三关的任务是:

  • 首先是/bin/echo tsh> quit意思是打开bin目录下的echo可执行文件,在foregound开启一个子进程运行它(因为末尾没有&符号,如果有,就是在backgound运行)
  • 运行echo,在终端中输出tsh> quit
  • 最后在tsh中执行内置命令quit,退出tsh进程,回到我们的终端。

此后所有trace中/bin/echo行均不再解释,都是输出后续字符串

6.4 trace04

#
# trace04.txt - Run a background job.
#
/bin/echo -e tsh> ./myspin 1 \046
./myspin 1 &

检测后台作业功能,myspin是循环sleep函数,参数为循环次数

6.5 trace05

#
# trace05.txt - Process jobs builtin command.
#
/bin/echo -e tsh> ./myspin 2 \046
./myspin 2 &

/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &

/bin/echo tsh> jobs
jobs

检测jobs命令,列出所有当前job

6.6 trace06

#
# trace06.txt - Forward SIGINT to foreground job.
#
/bin/echo -e tsh> ./myspin 4
./myspin 4

SLEEP 2
INT

检测SIGINT信号处理,终止前台进程

6.7 trace07

#
# trace07.txt - Forward SIGINT only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo -e tsh> ./myspin 5
./myspin 5

SLEEP 2
INT

/bin/echo tsh> jobs
jobs

检测SIGINT信号处理,终止前台进程,对后台进程无影响

6.8 trace08

#
# trace08.txt - Forward SIGTSTP only to foreground job.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo -e tsh> ./myspin 5
./myspin 5

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

检测SIGTSTP信号处理,停止前台进程,对后台进程无影响

6.9 trace09

#
# trace09.txt - Process bg builtin command
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo -e tsh> ./myspin 5
./myspin 5

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

/bin/echo tsh> bg %2
bg %2

/bin/echo tsh> jobs
jobs

检测bg指令,基于trace08,停止前台进程后将其唤醒,在后台运行

6.10 trace10

#
# trace10.txt - Process fg builtin command.
#
/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

SLEEP 1
/bin/echo tsh> fg %1
fg %1

SLEEP 1
TSTP

/bin/echo tsh> jobs
jobs

/bin/echo tsh> fg %1
fg %1

/bin/echo tsh> jobs
jobs

检测fg指令,先将后台程序转为前台运行,然后调用TSTP挂起,验证,再验证fg唤醒进程至前台运行

6.11 trace11

#
# trace11.txt - Forward SIGINT to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4

SLEEP 2
INT

/bin/echo tsh> /bin/ps a
/bin/ps a

检测SIGINT信号处理,终止前台程序

6.12 trace12

#
# trace12.txt - Forward SIGTSTP to every process in foreground process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

/bin/echo tsh> /bin/ps a
/bin/ps a

检测SIGTSTP信号处理,停止前台程序

6.13 trace13

#
# trace13.txt - Restart every stopped process in process group
#
/bin/echo -e tsh> ./mysplit 4
./mysplit 4

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

/bin/echo tsh> /bin/ps a
/bin/ps a

/bin/echo tsh> fg %1
fg %1

/bin/echo tsh> /bin/ps a
/bin/ps a

检测SIGTSTP信号处理和bg指令执行,基于trace12,先停止前台进程,验证,然后通过fg唤醒至前台运行,再次验证

6.14 trace14

#
# trace14.txt - Simple error handling
#
/bin/echo tsh> ./bogus
./bogus

/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo tsh> fg
fg

/bin/echo tsh> bg
bg

/bin/echo tsh> fg a
fg a

/bin/echo tsh> bg a
bg a

/bin/echo tsh> fg 9999999
fg 9999999

/bin/echo tsh> bg 9999999
bg 9999999

/bin/echo tsh> fg %2
fg %2

/bin/echo tsh> fg %1
fg %1

SLEEP 2
TSTP

/bin/echo tsh> bg %2
bg %2

/bin/echo tsh> bg %1
bg %1

/bin/echo tsh> jobs
jobs

检测各种error处理,包括

  • 无该可执行文件路径
  • fg/bg缺少id参数
  • fg/bg id参数错误

最后再验证fg/bg指令执行

6.15 trace15

#
# trace15.txt - Putting it all together
#

/bin/echo tsh> ./bogus
./bogus

/bin/echo tsh> ./myspin 10
./myspin 10

SLEEP 2
INT

/bin/echo -e tsh> ./myspin 3 \046
./myspin 3 &

/bin/echo -e tsh> ./myspin 4 \046
./myspin 4 &

/bin/echo tsh> jobs
jobs

/bin/echo tsh> fg %1
fg %1

SLEEP 2
TSTP

/bin/echo tsh> jobs
jobs

/bin/echo tsh> bg %3
bg %3

/bin/echo tsh> bg %1
bg %1

/bin/echo tsh> jobs
jobs

/bin/echo tsh> fg %1
fg %1

/bin/echo tsh> quit
quit

验证所有功能

6.16 trace16

#
# trace16.txt - Tests whether the shell can handle SIGTSTP and SIGINT
#     signals that come from other processes instead of the terminal.
#

/bin/echo tsh> ./mystop 2
./mystop 2

SLEEP 3

/bin/echo tsh> jobs
jobs

/bin/echo tsh> ./myint 2
./myint 2

测试tsh能否处理进程被来自其他进程(非终端)的SIGTSTP(SIGINT)信号停止(终止)的状态

这里mystop为向自身进程组发送SIGTSTP,myint为向自身进程发送SIGINT,停止(终止)后该进程发送SIGCHLD由tsh处理

7. 附完整代码

/*
 * tsh - A tiny shell program with job control
 *
 * <Put your name and login ID here>
 */
#include <ctype.h>
#include <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

/* Misc manifest constants */
#define MAXLINE 1024   /* max line size */
#define MAXARGS 128    /* max args on a command line */
#define MAXJOBS 16     /* max jobs at any point in time */
#define MAXJID 1 << 16 /* max job ID */

/* Job states */
#define UNDEF 0 /* undefined */
#define FG 1    /* running in foreground */
#define BG 2    /* running in background */
#define ST 3    /* stopped */

/*
 * Jobs states: FG (foreground), BG (background), ST (stopped)
 * Job state transitions and enabling actions:
 *     FG -> ST  : ctrl-z
 *     ST -> FG  : fg command
 *     ST -> BG  : bg command
 *     BG -> FG  : fg command
 * At most 1 job can be in the FG state.
 */

/* Global variables */
extern char **environ;   /* defined in libc */
char prompt[] = "tsh> "; /* command line prompt (DO NOT CHANGE) */
int verbose = 0;         /* if true, print additional output */
int nextjid = 1;         /* next job ID to allocate */
char sbuf[MAXLINE];      /* for composing sprintf messages */

struct job_t
{                          /* The job struct */
    pid_t pid;             /* job PID */
    int jid;               /* job ID [1, 2, ...] */
    int state;             /* UNDEF, BG, FG, or ST */
    char cmdline[MAXLINE]; /* command line */
};
struct job_t jobs[MAXJOBS]; /* The job list */
/* End global variables */

/* Function prototypes */

/* Here are the functions that you will implement */
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);

/* Here are helper routines that we've provided for you */
int parseline(const char *cmdline, char **argv); // 解析命令行参数,如果后台运行则返回 1
void sigquit_handler(int sig);                   // 接收到SIGQUIT信号调用exit()退出

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列表中添加job
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的job的jid
void listjobs(struct job_t *jobs);                                   // 打印jobs列表

void usage(void);           // 帮助信息
void unix_error(char *msg); // 错误信息
void app_error(char *msg);  // 向stdout流输入信息
typedef void handler_t(int);
handler_t *Signal(int signum, handler_t *handler); // 设置接收指定信号时的处理方式

/* 错误处理 */
pid_t Fork(void);
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
void Sigemptyset(sigset_t *set);
void Sigfillset(sigset_t *set);
void Sigaddset(sigset_t *set, int signum);
void Execve(const char *filename, char *const argv[], char *const envp[]);
void Setpgid(pid_t pid, pid_t pgid);
void Kill(pid_t pid, int sig);
pid_t Fork(void)
{
    pid_t pid;
    if ((pid = fork()) < 0)
        unix_error("fork error");
    return pid;
}
void Sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
{
    if (sigprocmask(how, set, oldset) < 0)
        unix_error("sigprocmask error");
}
void Sigemptyset(sigset_t *set)
{
    if (sigemptyset(set) < 0)
        unix_error("sigprocmask error");
}
void Sigfillset(sigset_t *set)
{
    if (sigfillset(set) < 0)
        unix_error("sigfillset error");
}
void Sigaddset(sigset_t *set, int signum)
{
    if (sigaddset(set, signum) < 0)
        unix_error("sigaddset error");
}
void Execve(const char *filename, char *const argv[], char *const envp[])
{
    if (execve(filename, argv, envp) < 0)
    {
        printf("%s: Command not found.\n", argv[0]);
    }
}
void Setpgid(pid_t pid, pid_t pgid)
{
    if (setpgid(pid, pgid) < 0)
    {
        unix_error("setpgid error");
    }
}
void Kill(pid_t pid, int sig)
{
    if (kill(pid, sig) < 0)
    {
        unix_error("kill error");
    }
}

/*
 * main - The shell's main routine
 */
int main(int argc, char **argv)
{
    char c;
    char cmdline[MAXLINE];
    int emit_prompt = 1; /* emit prompt (default) */

    /* Redirect stderr to stdout (so that driver will get all output
     * on the pipe connected to stdout) */
    dup2(1, 2);

    /* Parse the command line */
    while ((c = getopt(argc, argv, "hvp")) != EOF)
    {
        switch (c)
        {
        case 'h': /* print help message */
            usage();
            break;
        case 'v': /* emit additional diagnostic info */
            verbose = 1;
            break;
        case 'p':            /* don't print a prompt */
            emit_prompt = 0; /* handy for automatic testing */
            break;
        default:
            usage();
        }
    }

    /* Install the signal handlers */

    /* 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 */

    /* This one provides a clean way to kill the shell */
    Signal(SIGQUIT, sigquit_handler);

    /* Initialize the job list */
    initjobs(jobs);

    /* Execute the shell's read/eval loop */
    while (1)
    {

        /* Read command line */
        if (emit_prompt)
        {
            printf("%s", prompt);
            fflush(stdout);
        }
        if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
            app_error("fgets error");
        if (feof(stdin))
        { /* End of file (ctrl-d) */
            fflush(stdout);
            exit(0);
        }

        /* Evaluate the command line */
        eval(cmdline);
        fflush(stdout);
        fflush(stdout);
    }

    exit(0); /* control never reaches here */
}

/*
 * 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)
{
    char *argv[MAXARGS]; // 存放解析的参数
    char buf[MAXLINE];   // 解析cmdline
    int bg;              // 判断程序是前台还是后台执行
    int state;           // 指示前台还是后台运行状态
    pid_t pid;           // 执行程序的子进程的pid

    strcpy(buf, cmdline);
    bg = parseline(buf, argv); // 解析参数
    state = bg ? BG : FG;
    if (argv[0] == NULL) // 空行,直接返回
        return;

    sigset_t mask_all, mask_one, prev_one;
    Sigfillset(&mask_all);
    Sigemptyset(&mask_one);
    Sigaddset(&mask_one, SIGCHLD);
    if (!builtin_cmd(argv)) // 判断是否为内置命令
    {
        Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // fork前阻塞SIGCHLD信号
        if ((pid = fork()) == 0)                      // 创建子进程
        {
            Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除子进程的阻塞
            Setpgid(0, 0);                             // 创建新进程组,ID设置为进程PID,同时保证当前进程组前台进程只有shell
            Execve(argv[0], argv, environ);            // 执行
            exit(1);                                   // 一定要有退出,避免execve()执行失败
        }
        if (state == FG) // 前台运行
        {
            Sigprocmask(SIG_BLOCK, &mask_all, NULL);   // 添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);         // 添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL); // 替换为仅阻塞SIGCHLD,waitfg()通过sigsuspend()原子性解除阻塞并挂起,避免竞争
            waitfg(pid);                               // 等待前台进程执行完毕
        }
        else // 后台运行
        {
            Sigprocmask(SIG_BLOCK, &mask_all, NULL);            // 添加工作前阻塞所有信号
            addjob(jobs, pid, state, cmdline);                  // 添加至作业列表
            Sigprocmask(SIG_SETMASK, &mask_one, NULL);          // 仍然阻塞SIGCHLD,printf()需要访问全局变量
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline); // 打印后台进程信息
        }
        Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除所有阻塞
    }
    return;
}

/*
 * parseline - Parse the command line and build the argv array.
 *
 * Characters enclosed in single quotes are treated as a single
 * argument.  Return true if the user has requested a BG job, false if
 * the user has requested a FG job.
 */
int parseline(const char *cmdline, char **argv)
{
    static char array[MAXLINE]; /* holds local copy of command line */
    char *buf = array;          /* ptr that traverses command line */
    char *delim;                /* points to first space delimiter */
    int argc;                   /* number of args */
    int bg;                     /* background job? */

    strcpy(buf, cmdline);
    buf[strlen(buf) - 1] = ' ';   /* replace trailing '\n' with space */
    while (*buf && (*buf == ' ')) /* ignore leading spaces */
        buf++;

    /* Build the argv list */
    argc = 0;
    if (*buf == '\'')
    {
        buf++;
        delim = strchr(buf, '\'');
    }
    else
    {
        delim = strchr(buf, ' ');
    }

    while (delim)
    {
        argv[argc++] = buf;
        *delim = '\0';
        buf = delim + 1;
        while (*buf && (*buf == ' ')) /* ignore spaces */
            buf++;

        if (*buf == '\'')
        {
            buf++;
            delim = strchr(buf, '\'');
        }
        else
        {
            delim = strchr(buf, ' ');
        }
    }
    argv[argc] = NULL;

    if (argc == 0) /* ignore blank line */
        return 1;

    /* should the job run in the background? */
    if ((bg = (*argv[argc - 1] == '&')) != 0)
    {
        argv[--argc] = NULL;
    }
    return bg;
}

/*
 * 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") || !strcmp(argv[0], "QUIT")) // 退出
        exit(0);
    if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) // bg/fg指令
    {
        do_bgfg(argv); // bg/fg处理
        return 1;
    }
    if (!strcmp(argv[0], "jobs"))
    {
        listjobs(jobs); // jobs处理
        return 1;
    }
    // if (!strcmp(argv[0], "&")) // 后台进程处理
    //     return 1;
    return 0; /* not a builtin command */
}

/*
 * do_bgfg - Execute the builtin bg and fg commands
 */
void do_bgfg(char **argv)
{
    struct job_t *job = NULL; // 要处理的job
    int state;                // 输入的目标状态
    int id;                   // 存储jid或pid
    if (!strcmp(argv[0], "bg"))
        state = BG;
    else
        state = FG;
    if (argv[1] == NULL) // 缺少id参数
    {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }
    if (argv[1][0] == '%') // jid
    {
        if (sscanf(&argv[1][1], "%d", &id) > 0) // argv[]为str数组,需转换
        {
            job = getjobjid(jobs, id); // 通过jid获得job
            if (job == NULL)
            {
                printf("%%%d: No such job\n", id);
                return;
            }
        }
    }
    else if (!isdigit(argv[1][0])) // 非法字符
    {
        printf("%s: argument must be a PID or %%jobid\n", argv[0]);
        return;
    }
    else // PID
    {
        id = atoi(argv[1]);        // argv[]为str数组,需转换
        job = getjobpid(jobs, id); // 通过pid获得job
        if (job == NULL)
        {
            printf("(%d): No such process\n", id);
            return;
        }
    }
    sigset_t mask_one, prev_one;                  // 信号集
    Sigemptyset(&mask_one);                       // 置空
    Sigaddset(&mask_one, SIGCHLD);                // 只添加SIGCHLD
    Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); // 只屏蔽SIGCHLD
    Kill(-(job->pid), SIGCONT);                   // 使进程重新运行,信号发送到指定进程组
    job->state = state;
    if (state == BG)
    {
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        Sigprocmask(SIG_SETMASK, &prev_one, NULL); // 解除阻塞
    }
    else
        waitfg(job->pid); // waitfg()通过sigsuspend()原子性解除阻塞并挂起等待,避免竞争
    return;
}

/*
 * waitfg - Block until process pid is no longer the foreground process
 */
void waitfg(pid_t pid)
{
    sigset_t mask;      // 信号集
    Sigemptyset(&mask); // 初始化置空
    while (fgpid(jobs) != 0)
    {
        sigsuspend(&mask); // 挂起时取消阻塞
    }
    return;
}

/*****************
 * Signal handlers
 *****************/

/*
 * 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)
{
    int old_errno = errno; // 由于errno是全局变量,注意保存和恢复errno
    int status;
    pid_t pid;
    struct job_t *job;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);                                        // 信号集置全1,64个信号
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) // 立即返回该子进程的pid
    {
        Sigprocmask(SIG_BLOCK, &mask_all, &prev); // 阻塞所有信号
        if (WIFEXITED(status))                    // 正常终止
        {
            deletejob(jobs, pid); // 清除job信息
        }
        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));
            job = getjobpid(jobs, pid);
            job->state = ST; // job信息改为停止状态
        }
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除信号阻塞
    }
    errno = old_errno; // 恢复原errno
    return;
}

/*
 * 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 old_errno = errno; // 保存errno以便恢复
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);                    // 所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev); // jobs为全局变量
    if ((pid = fgpid(jobs)) != 0)             // 判定当前是否有前台进程
    {
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除阻塞
        Kill(-pid, SIGINT);                    // 向前台进程的进程组发送SIGINT信号终止
    }
    errno = old_errno; // 恢复errno
    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 old_errno = errno; // 保存errno以便恢复
    int pid;
    sigset_t mask_all, prev;
    Sigfillset(&mask_all);                    // 所有信号
    Sigprocmask(SIG_BLOCK, &mask_all, &prev); // jobs为全局变量
    if ((pid = fgpid(jobs)) > 0)              // 判定当前是否有前台进程
    {
        Sigprocmask(SIG_SETMASK, &prev, NULL); // 解除阻塞
        Kill(-pid, SIGTSTP);                   // 向前台进程的进程组发送SIGTSTP信号停止
    }
    errno = old_errno; // 恢复errno
    return;
}

/*********************
 * End signal handlers
 *********************/

/***********************************************
 * Helper routines that manipulate the job list
 **********************************************/

/* clearjob - Clear the entries in a job struct */
void clearjob(struct job_t *job)
{
    job->pid = 0;
    job->jid = 0;
    job->state = UNDEF;
    job->cmdline[0] = '\0';
}

/* initjobs - Initialize the job list */
void initjobs(struct job_t *jobs)
{
    int i;

    for (i = 0; i < MAXJOBS; i++)
        clearjob(&jobs[i]);
}

/* maxjid - Returns largest allocated job ID */
int maxjid(struct job_t *jobs)
{
    int i, max = 0;

    for (i = 0; i < MAXJOBS; i++)
        if (jobs[i].jid > max)
            max = jobs[i].jid;
    return max;
}

/* addjob - Add a job to the job list */
int addjob(struct job_t *jobs, pid_t pid, int state, char *cmdline)
{
    int i;

    if (pid < 1)
        return 0;

    for (i = 0; i < MAXJOBS; i++)
    {
        if (jobs[i].pid == 0)
        {
            jobs[i].pid = pid;
            jobs[i].state = state;
            jobs[i].jid = nextjid++;
            if (nextjid > MAXJOBS)
                nextjid = 1;
            strcpy(jobs[i].cmdline, cmdline);
            if (verbose)
            {
                printf("Added job [%d] %d %s\n", jobs[i].jid, jobs[i].pid, jobs[i].cmdline);
            }
            return 1;
        }
    }
    printf("Tried to create too many jobs\n");
    return 0;
}

/* deletejob - Delete a job whose PID=pid from the job list */
int deletejob(struct job_t *jobs, pid_t pid)
{
    int i;

    if (pid < 1)
        return 0;

    for (i = 0; i < MAXJOBS; i++)
    {
        if (jobs[i].pid == pid)
        {
            clearjob(&jobs[i]);
            nextjid = maxjid(jobs) + 1;
            return 1;
        }
    }
    return 0;
}

/* fgpid - Return PID of current foreground job, 0 if no such job */
pid_t fgpid(struct job_t *jobs)
{
    int i;

    for (i = 0; i < MAXJOBS; i++)
        if (jobs[i].state == FG)
            return jobs[i].pid;
    return 0;
}

/* getjobpid  - Find a job (by PID) on the job list */
struct job_t *getjobpid(struct job_t *jobs, pid_t pid)
{
    int i;

    if (pid < 1)
        return NULL;
    for (i = 0; i < MAXJOBS; i++)
        if (jobs[i].pid == pid)
            return &jobs[i];
    return NULL;
}

/* getjobjid  - Find a job (by JID) on the job list */
struct job_t *getjobjid(struct job_t *jobs, int jid)
{
    int i;

    if (jid < 1)
        return NULL;
    for (i = 0; i < MAXJOBS; i++)
        if (jobs[i].jid == jid)
            return &jobs[i];
    return NULL;
}

/* pid2jid - Map process ID to job ID */
int pid2jid(pid_t pid)
{
    int i;

    if (pid < 1)
        return 0;
    for (i = 0; i < MAXJOBS; i++)
        if (jobs[i].pid == pid)
        {
            return jobs[i].jid;
        }
    return 0;
}

/* listjobs - Print the job list */
void listjobs(struct job_t *jobs)
{
    int i;

    for (i = 0; i < MAXJOBS; i++)
    {
        if (jobs[i].pid != 0)
        {
            printf("[%d] (%d) ", jobs[i].jid, jobs[i].pid);
            switch (jobs[i].state)
            {
            case BG:
                printf("Running ");
                break;
            case FG:
                printf("Foreground ");
                break;
            case ST:
                printf("Stopped ");
                break;
            default:
                printf("listjobs: Internal error: job[%d].state=%d ",
                       i, jobs[i].state);
            }
            printf("%s", jobs[i].cmdline);
        }
    }
}
/******************************
 * end job list helper routines
 ******************************/

/***********************
 * Other helper routines
 ***********************/

/*
 * usage - print a help message
 */
void usage(void)
{
    printf("Usage: shell [-hvp]\n");
    printf("   -h   print this message\n");
    printf("   -v   print additional diagnostic information\n");
    printf("   -p   do not emit a command prompt\n");
    exit(1);
}

/*
 * unix_error - unix-style error routine
 */
void unix_error(char *msg)
{
    fprintf(stdout, "%s: %s\n", msg, strerror(errno));
    exit(1);
}

/*
 * app_error - application-style error routine
 */
void app_error(char *msg)
{
    fprintf(stdout, "%s\n", msg);
    exit(1);
}

/*
 * Signal - wrapper for the sigaction function
 */
handler_t *Signal(int signum, handler_t *handler)
{
    struct sigaction action, old_action;

    action.sa_handler = handler;
    sigemptyset(&action.sa_mask); /* block sigs of type being handled */
    action.sa_flags = SA_RESTART; /* restart syscalls if possible */

    if (sigaction(signum, &action, &old_action) < 0)
        unix_error("Signal error");
    return (old_action.sa_handler);
}

/*
 * sigquit_handler - The driver program can gracefully terminate the
 *    child shell by sending it a SIGQUIT signal.
 */
void sigquit_handler(int sig)
{
    printf("Terminating after receipt of SIGQUIT signal\n");
    exit(1);
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值