LOADING

加载过慢请开启缓存 浏览器默认开启

Shell_Lab

2024/10/13

ShellLab

实验要用到的文件(注意查看时间线,我把完成的作业也push上来了)

注意此实验要在linux下完成。

我们首先仔细阅读几遍README.md,把陌生的概念搞懂。

这里强调一下第八章异常控制流的一些内容:

  • 信号中的输出全用封装后的Sio,而不是printf,因为异步信号安全问题
  • 所有的注册信号处理函数都要用大写S的那个,它已经给我们实现了可移植的信号处理。

sdriver.pl :.pl 文件是 Perl 脚本文件的扩展名。这是一种编程语言。

然后我们看tsh.c

里面内置的英文注释已经解释很多了。

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();
}
}

这是用来解析参数的

./tsh -h
# 比如说这一条是打印帮助信息

这里注意,在运行tsh之前,先输入make来编译它。

由于没有实现quit,所以想退的话,直接把终端删了重新运行吧。

我们需要实现:

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 

以及job类型的实现:

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

状态的宏定义

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

按理说,这7个函数可以先写简单的,但是如果不写eval根本没法测试,所以就先写它。

eavl

我们参考一下教材中的框架(深入理解计算机系统黑皮书第三版525页)

void eval(char *cmdline) {
    char* argv[MAXARGS]; /* 参数列表 execve() */
    char buf[MAXLINE];                      /* 将command line复制到这个数组 */
    int bg;                                /* 标志程序是前台还是后台 */
    pid_t pid;                              /* 创建的进程id */
    strcpy(buf, cmdline); //复制到buf  
    bg = parseline(buf, argv);   //检查是前台命令还是后台命令,将结果存入bg,同时在argv中存入参数列表
    if (argv[0] == NULL)
        return;   /* 无视空的输入 */
    if (!builtin_command(argv)) {   
        if ((pid = Fork()) == 0) {   //子进程运行该程序
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
        /* 接下来父进程检查是否是后台命令 */
        if (!bg) {  //如果不是,就等到子进程结束返回
            int status;
            if (waitpid(pid, &status, 0) < 0)
                unix_error("waitfg: waitpid error");
        }
        else//如果是后台命令,打印
            printf("%d%s", pid, cmdline);
    }
    return;
}

然后我们开始修改。

void eval(char *cmdline) {
    char* argv[MAXARGS];   //参数列表                 
    char buf[MAXLINE];     //将cmdline复制到此数组       
    pid_t pid;                              
    int state = UNDEF;     //启动的进程状态
    sigset_t mask, prev_mask; //用于信号阻塞和恢复


    strcpy(buf, cmdline);//将cmdline复制到buf
    
    //构建阻塞信号集合
    if (sigemptyset(&mask) < 0) {  
        unix_error("sigemptyset error");
    }
    if (sigaddset(&mask, SIGINT) < 0) {
        unix_error("sigaddset error");
    }
    if (sigaddset(&mask, SIGTSTP) < 0) {
        unix_error("sigaddset error");
    }
    if (sigaddset(&mask, SIGCHLD) < 0) {
        unix_error("sigaddset error");
    }
    //构建阻塞信号集合(SIGINT、SIGTSTP、SIGCHLD)完成
    
    //读取命令的前台后台
    if (parseline(buf, argv)) {
        state = BG;
    }
    else {
        state = FG;
    }
    //前台后台读取完成

    //如果命令是空行,直接返回
    if (argv[0] == NULL)
        return;  

    //如果不是内置命令
    if (!builtin_cmd(argv)) {
        
        //阻塞信号,防止竞争
        if (sigprocmask(SIG_BLOCK, &mask, &prev_mask) < 0) {
            unix_error("sigprocmask error");
        }

        //创建进程
        if ((pid = fork()) < 0) {
            unix_error("fork error");
        }

        if (pid == 0) {  //子进程控制流开始
           
            //在子进程中恢复信号
            if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
                unix_error("sigprocmask error");
            }

            //设置前台进程组,以便SIGINT信号的实现
            if (setpgid(0, 0) < 0) {
                unix_error("setpgid error");
            }
            //加载程序
            if (execve(argv[0], argv, environ) < 0) {
                printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }

        //将当前进程加入到job list中
        addjob(jobs, pid, state, cmdline);

        //shell进程恢复阻塞的信号
        if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
            unix_error("sigprocmask error");
        }

        if (state == FG) { //前台作业等待执行
            waitfg(pid);
        }
        else
            printf("[%d] (%d) %s", pid2jid(pid), pid, cmdline);    //后台作业打印
    }
    return;
}

增加了信号,以及添加进joblist中。

注:

1.每个子进程必须要设置进程组id,否则一个子进程和它的父进程在同一个进程组,这是很麻烦的,因为SIGINT和SIGTSTP要发送信号到前台进程组。而他们不能也把这个微型shell也终止了,所以子进程组和shell要区分。同时,我们不能因为前台进程只有一个,而仅仅将信号发到那一个进程,万一那个前台进程又fork了呢?发送到那一组,才可以使所有的前台进程都收到。

2.我们一定要在fork之前进行信号阻塞,这是一个非常经典的同步流错误,具体看第八章教材或者我的笔记

builtin_cmd

此函数的功能是识别和解释内置命令:quit、fg、bg和jobs

int builtin_cmd(char **argv)

也就是,如果argv是内置命令,就执行(有返回就返回true),如果不是,返回false

  • quit命令终止shell。
  • jobs命令列出所有后台作业。
  • bg 命令通过发送SIGCONT信号重新启动,然后在后台运行它。 参数可以是PID或JID。
  • fg 命令通过发送SIGCONT信号重新启动,然后在前台运行它。 参数可以是PID或JID。
int builtin_cmd(char **argv) {

    if (!strcmp(argv[0], "quit")) {  //如果是quit,退出shell
        exit(0);
    }
    else if (!strcmp(argv[0], "bg") || !strcmp(argv[0], "fg")) { //如果是bg或者fg命令,执行do_fgbg函数 
        do_bgfg(argv);
    }
    else if (!strcmp(argv[0], "jobs")) {  //如果命令是jobs,列出所有后台作业
        listjobs(jobs);
    }
    else {
        return 0;
    }
    return 1;

}

这个很容易

do_bgfg

bg :将停止的后台作业更改为正在运行的后台作业。通过发送SIGCONT信号重新启动,然后在后台运行它。参数可以是PID,也可以是JID。ST -> BG

fg :将已停止或正在运行的后台作业更改为前台正在运行的作业。通过发送SIGCONT信号重新启,然后在前台运行它。参数可以是PID,也可以是JID。ST -> FG,BG -> FG

fg和bg必须要有参数,而且这两个指令都有输出,以下是参考输出

tsh> ./bogus
./bogus: Command not found
tsh> ./myspin 4 &
[1] (26326) ./myspin 4 &
tsh> fg
fg command requires PID or %jobid argument
tsh> bg
bg command requires PID or %jobid argument
tsh> fg a
fg: argument must be a PID or %jobid
tsh> bg a
bg: argument must be a PID or %jobid
tsh> fg 9999999
(9999999): No such process
tsh> bg 9999999
(9999999): No such process
tsh> fg %2
%2: No such job
tsh> fg %1
Job [1] (26326) stopped by signal 20
tsh> bg %2
%2: No such job
tsh> bg %1
[1] (26326) ./myspin 4 &
tsh> jobs
[1] (26326) Running ./myspin 4 &

下面给出函数实现。

void do_bgfg(char **argv) {
    
    int id;
    struct job_t* obj;
    //如果没有参数,打印以下内容
    if (argv[1] == NULL) { 
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return;
    }

    //检测参数
    if (argv[1][0] == '%') {//如果是jid

        if (sscanf(&argv[1][1], "%d", &id) != 1) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]); //读取失败,打印
            return;
        }

        if ((obj = getjobjid(jobs, id)) == NULL) {
            printf("%%%d: No such process\n", id);  //jid找不到进程
            return;
        }

    }
    else {//如果是pid
        if (sscanf(&argv[1][0], "%d", &id) != 1) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);//读取失败,打印
            return;
        }

        if ((obj = getjobpid(jobs, id)) == NULL) {
            printf("(%d): No such process\n", id); //pid找不到进程
            return;
        }
    }
    //参数检测完成

    //如果是bg,则在后台运行
    if (!strcmp(argv[0], "bg")) {
        
        obj->state = BG;  //设置状态

        if (kill(-obj->pid, SIGCONT) < 0) {  //使用-pid表示整个进程组
            unix_error("kill error");
        }
        printf("[%d] (%d) %s", obj->jid, obj->pid, obj->cmdline);
    }
    //如果是fg,则在前台运行
    else if (!strcmp(argv[0], "fg")) {

        obj->state = FG; //设置状态
        if (kill(-obj->pid, SIGCONT) < 0) { //使用-pid表示整个进程组
            unix_error("kill error");
        }
        //前台运行的程序应该等待其运行结束
        waitfg(obj->pid);
    }
    else {
        exit(0);
    }
    return;
}

waitfg

waitfg - Block until process pid is no longer the foreground process

void waitfg(pid_t pid) {
    struct job_t* obj;
    sigset_t mask, prev_mask;

    //阻塞信号的集合
    if (sigemptyset(&mask) < 0) {
        unix_error("sigemptyset error");
    }
    if (sigaddset(&mask, SIGINT) < 0) {
        unix_error("sigaddset error");
    }
    if (sigaddset(&mask, SIGTSTP) < 0) {
        unix_error("sigaddset error");
    }
    if (sigaddset(&mask, SIGCHLD) < 0) {
        unix_error("sigaddset error");
    }
    //阻塞信号
    if (sigprocmask(SIG_BLOCK, &mask, &prev_mask) < 0) {
        unix_error("sigprocmask error");
    }

    obj = getjobpid(jobs, pid);
   //如果前台进程没有结束
    if (obj) {
        while (obj->state == FG) {
            sigsuspend(&prev_mask); //通过sigsuspend来实现显式等待信号(如果当前子进程的状态没有发生改变,则休眠)
            //sleep(1);
        }
    }
    //解除阻塞
    if (sigprocmask(SIG_SETMASK, &prev_mask, NULL) < 0) {
        unix_error("sigprocmask error");
    }
    return;
}

我们通过sigsuspend来实现显式等待信号。

sigchld_handler

要求:当子进程终止(变为僵尸进程)或因接收到SIGSTOPSIGTSTP信号而停止时,内核会向shell发送一个SIGCHLD信号。该处理程序会收获所有可用的僵尸子进程,但不会等待任何其他当前正在运行的子进程终止

使用waitpid,其中选项为WNOHANG,表示当子进程中没有终止的进程时,返回0,另一个选项是WUNTRACED,表示当子进程除了终止,停止的时候也会返回子进程的pid。因为子进程停止的时候,也会发送SIGCHILD信号。

我们也要使用教材里提供的异步信号安全的Sio函数

而且也要保存全局变量errno的值

void sigchld_handler(int sig) {

    int olderrno = errno;  //由于该信号处理函数使用系统调用,所以要保存全局变量errno的值

    pid_t pid;
    int status, jid;
    struct job_t* obj;

    //WNOHANG:若子进程仍然在运行,则返回0 (可以实现“不会等待运行的子进程终止,而仅仅只处理僵尸进程”)
    //WUNTRACED : 如果子进程由于传递信号而停止,则马上返回pid(可以实现,当有子进程停止的时候也会返回其pid)
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
        
        //当子进程的job已经删除了,表示有错误发生
        if ((obj = getjobpid(jobs, pid)) == NULL) {
            Sio_puts("lack job\n");
            errno = olderrno; //返回前重新设置errno
            return;
        }

        //接下来判断子进程发送信号时的状态
        jid = obj->jid;

        //子进程暂停了
        if (WIFSTOPPED(status)) {
            obj->state = ST;   //设置挂起状态
            Sio_puts("Job [");  //使用异步信号安全的输出函数
            Sio_putl(jid);
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") stopped by signal ");
            Sio_putl(WSTOPSIG(status));
            Sio_puts("\n");
        }
        //子进程调用exit或者从main返回导致终止,不输出内容
        else if (WIFEXITED(status)) {
            deletejob(jobs, pid);
        }

        //子进程收到信号而终止
        else if (WIFSIGNALED(status)) {
            deletejob(jobs, pid);
            Sio_puts("Job [");
            Sio_putl(jid);
            Sio_puts("] (");
            Sio_putl(pid);
            Sio_puts(") terminated by signal ");
            Sio_putl(WTERMSIG(status));
            Sio_puts("\n");
        }
    }
    //返回前重新设置errno
    errno = olderrno;
    return;
}

sigint_handler

当用户在键盘上按下 ctrl-c 时,内核会向 shell 发送一个 SIGINT 信号。捕获这个信号并将其传递给前台作业。

这个简单

void sigint_handler(int sig) {  
    pid_t pid;
    int olderrno = errno;//保护errno
    //得到前台进程的pid
    pid = fgpid(jobs);

    if (pid) {
        //向前台进程组发送信号
        if (kill(-pid, SIGINT) < 0)
            unix_error("kill (sigint) error");
    }

    errno = olderrno;
    return;
}

sigtstp_handler

当用户在键盘上按下 ctrl-z 时,内核会向 shell 发送一个 SIGTSTP 信号。捕获这个信号,并通过向前台作业发送一个 SIGTSTP 信号来挂起它。

void sigtstp_handler(int sig) 
{
    pid_t pid;
    int olderrno = errno; //保护errno
    //得到前台进程的pid
    pid = fgpid(jobs);

    if (pid) {
        //向前台进程组发送信号
        if (kill(-pid, SIGTSTP) < 0)
            unix_error("kill (sigtstp) error");
    }
    errno = olderrno;
    return;
}

这样,一个个测试样例即可,应该是都可以过的。

最后还有Sio,并没有给出,不过可以在csapp.c中找到

/* Sio (Signal-safe I/O) routines */
ssize_t sio_puts(char s[]);
ssize_t sio_putl(long v);
void sio_error(char s[]);

/* 经过封装的sio */
ssize_t Sio_puts(char s[]);
ssize_t Sio_putl(long v);
void Sio_error(char s[]);
//这些是智慧树中的异步信号安全的输出函数(csapp.h)


//下面是异步信号安全的Sio的实现(csapp.c)


static void sio_reverse(char s[])
{
    int c, i, j;

    for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
        c = s[i];
        s[i] = s[j];
        s[j] = c;
    }
}

/* sio_ltoa - Convert long to base b string (from K&R) */
static void sio_ltoa(long v, char s[], int b)
{
    int c, i = 0;

    do {
        s[i++] = ((c = (v % b)) < 10) ? c + '0' : c - 10 + 'a';
    } while ((v /= b) > 0);
    s[i] = '\0';
    sio_reverse(s);
}

/* sio_strlen - Return length of string (from K&R) */
static size_t sio_strlen(char s[])
{
    int i = 0;

    while (s[i] != '\0')
        ++i;
    return i;
}
/* $end sioprivate */

/* Public Sio functions */
/* $begin siopublic */

ssize_t sio_puts(char s[]) /* Put string */
{
    return write(STDOUT_FILENO, s, sio_strlen(s)); //line:csapp:siostrlen
}

ssize_t sio_putl(long v) /* Put long */
{
    char s[128];

    sio_ltoa(v, s, 10); /* Based on K&R itoa() */  //line:csapp:sioltoa
    return sio_puts(s);
}

void sio_error(char s[]) /* Put error message and exit */
{
    sio_puts(s);
    _exit(1);                                      //line:csapp:sioexit
}
/* $end siopublic */

/*******************************
 * Wrappers for the SIO routines
 ******************************/
ssize_t Sio_putl(long v)
{
    ssize_t n;

    if ((n = sio_putl(v)) < 0)
        sio_error("Sio_putl error");
    return n;
}

ssize_t Sio_puts(char s[])
{
    ssize_t n;

    if ((n = sio_puts(s)) < 0)
        sio_error("Sio_puts error");
    return n;
}

void Sio_error(char s[])
{
    sio_error(s);
}