CSAPP实验6 : shlab

linking的部分看了一半跳过去了....据说你南的ICS讲linking非常不错,于是我就心安理得了
exception的部分还在看,估计这周末可以看完(吧),先留坑

我来填坑辣

终于赶在五一(假期前)写完了第六个lab,书也推完了第八章,四舍五入就是五月前搞定了前八章内容,还是很不戳的。整个四月都非常忙,各种ddl和期中满天飞。五月也不空闲,还有EL、听说读写ddl、马原、银川、大雾期中、军理。希望人没事.jpg

这次lab的一个明显特点就是答案都可以在书上找到,各种详细的例子书上都有,因此写起来只需要多翻书就好了。不过由于这次lab的测试比较弱,而我对自己的代码水平又没有什么信心,因此下面贴的代码看看就好,莫当真

说了不少废话,讲正题吧

前置姿势

这一章主要从硬件软件(系统)两个层面讲了异常控制流(exceptional control flow)的各种姿势,这次的lab主要关注的是软件方面的东西。软件的控制流交移是通过信号(signal)的发送和接受来进行的,这一点因为写过QT所以不是特别虚。看前面的内容如果不过瘾还可以搭配OS的教材一起看,正好就顺手入了SJTU的书,希望暑假能啃一啃。

本章引入的鲁棒性的问题非常重要,也就是多线程过程中的竞争问题。这种问题常常发生在不同线程对统一数据的修改和读取中。为了解决这个问题本章引入了原子性的概念,大意就是某些操作不可分割、控制流不能从中间被移接。事实上还有的概念,大意就是在修改、读取数据的时候阻塞其余信号,也就是给数据“上锁”

本次lab的测试非常之水,毕竟系统方面的测试用用例还是比较难调出问题(我对形式化验证这方面也很感兴趣)。

代码

eval

这一部分可以抄书。具体需要搞清楚fork()会创建一个新的进程,而execve()则相当于用新的进程"覆盖"当前进程。因此如果想要实现shell的调用需要先fork()再在子进程里execve()

根据writeup的提示需要给子进程分配组id,在eval中新建进程需要调用addjob()来修改进程表中的内容,因此需要阻塞其它信号以免出现问题。

void eval(char *cmdline) 
{
    int olderrno = errno;
    char *argv[MAXARGS];
    int bg = parseline(cmdline, argv);

    if (argv[0] == NULL) return ;
    if ( builtin_cmd(argv) ) return ;

    pid_t pid;
    // Child Process
    sigset_t mask, prev;
    sigemptyset(&mask);
    sigaddset(&mask, SIGCHLD);

    sigprocmask(SIG_BLOCK, &mask, &prev);
    pid = fork();
    if (pid < 0)
        unix_error("Fork error");
    if ( pid == 0) {
        sigprocmask(SIG_BLOCK, &prev, NULL);
        setpgid(0, 0);
        if ( execve(argv[0], argv, environ) < 0) {
            printf("%s: Command not found\n", argv[0]);
            exit(0);
        }
    }
    if (!bg) {
        addjob(jobs, pid, FG, cmdline);
        sigprocmask(SIG_SETMASK, &prev, NULL);
        waitfg(pid);
    } else {
        addjob(jobs, pid, BG, cmdline);
        struct job_t *job = getjobpid(jobs, pid);
        printf("[%d] (%d) %s", job->jid, job->pid, cmdline);
        sigprocmask(SIG_SETMASK, &prev, NULL);
    }
    errno = olderrno;
    return;
}

builtin_cmd

这个估计是最好写的,判断一下命令类型就好了。quitjobs都很好写,fgbg的具体操作被封装在do_bgfg()里面了,在这里直接用就好了

int builtin_cmd(char **argv) 
{
    char *builtin_args[4] = {"quit", "bg", "fg", "jobs"};
    for (int i = 0; i < 4; ++ i) {
        if ( strcmp(builtin_args[i], argv[0]) ) continue;

        switch (argv[0][0]) {
            case 'q': exit(0);
            case 'f': {
                do_bgfg(argv);
                break;
            }
            case 'b': {
                do_bgfg(argv);
                break;
            }
            case 'j': listjobs(jobs);
        }
        return 1;
    }
    return 0;     /* not a builtin command */
}

waitfg

这一段也很好写。这是用来等待前台进程结束的,那么就用一个死循环来不停地监听pid进程是否在前台

由于在监听的时候访问了全局数据结构,因此也要阻塞信号

void waitfg(pid_t pid)
{
    sigset_t mask, prev;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    struct job_t *job = getjobpid(jobs, pid);
    sigprocmask(SIG_SETMASK, &prev, NULL);
    for (; job != NULL && job->state == FG; ) {
        sigfillset(&mask);
        sigprocmask(SIG_BLOCK, &mask, &prev);
        job = getjobpid(jobs, pid);
        sigprocmask(SIG_SETMASK, &prev, NULL);
    }
    return;
}

do_bgfg

这个是最后写的,毕竟bgfg我也是上个月才会用的.....

大概就是利用kill发送信号,同时修改job对应的state。在发送信号的时候也要阻塞其余信号因为访问了全局数据结构

这里还要判一下输入是否合法,这个比较麻烦不过对着trace慢慢搞就好了

void do_bgfg(char **argv) 
{
    char *id_str = argv[1];

    //fg bg error handling
    if (id_str == NULL) {
        printf("%s command requires PID or %%jobid argument\n", argv[0]);
        return ;
    }

    int flag = (id_str[0] == '%'), jid, pid;
    id_str += flag;

    for (int i = 0, _len = strlen(id_str); i < _len; ++ i) {
        if (!isdigit(id_str[i])) {
            printf("%s: argument must be a PID or %%jobid\n", argv[0]);
            return ;
        }
    }

    sigset_t mask, prev;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);

    if (!flag) {
        sscanf(id_str, "%d", &pid);
        jid = pid2jid(jid);
    } else {
        sscanf(id_str, "%d", &jid);
    }

    struct job_t *job = getjobjid(jobs, jid);

    if (job == NULL) {
        if (flag) printf("%%%d: No such job\n", jid);
        else printf("(%d): No such process\n", pid);
        sigprocmask(SIG_SETMASK, &prev, NULL);
        return ;
    }

    if (argv[0][0] == 'b') {
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
        job->state = BG;
        kill(-(job->pid), SIGCONT);
    } else if (argv[0][0] == 'f') {
        job->state = FG;
        kill(-(job->pid), SIGCONT);
        sigprocmask(SIG_SETMASK, &prev, NULL);
        waitfg(job->pid);
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);
    return;
}

hanlders

三个放在一起说

sigint_handler()最好写,只需要利用fgpid()搭配kill()就好了

注意要阻塞信号

void sigint_handler(int sig) 
{
    int olderrno = errno;

    sigset_t mask, prev;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);
    pid_t pid = fgpid(jobs);
    if (pid == 0) return ;
    struct job_t *job = getjobpid(jobs, pid);
    if (kill(-pid, sig) < 0)
        unix_error("Sigint error");
    sigprocmask(SIG_SETMASK, &prev, NULL);
    
    errno = olderrno;
    return;
}

sigstp_handler()和上面是一样的,就不说了

需要注意的是,在这两个handler中我们没必要更改数据结构,而是可以等sigchld_handler()一起改,这样写起来可以清晰一些

sigchld_handler()有几个坑点

  1. 要注意waitpid()默认行为是挂起父进程直至子进程终止(terminate),写到这里的时候记得往回翻书,或者看writeup的提示也行
  2. 好像就没了....

回收的时候需要对子进程的死因讨论一下,同时记得阻塞就好了

在读这一章的时候深刻体会到了术语明确的重要性,比如说停止(stop)和终止(terminate)和中断(interrupt)这三个,放在中文里感觉就没有区别啊

void sigchld_handler(int sig) 
{
    int olderrno = errno;

    int status;
    pid_t pid = waitpid(-1, &status, WNOHANG | WUNTRACED);
    if (!pid) return ;

    sigset_t mask, prev;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &prev);

    struct job_t *job = getjobpid(jobs, pid);

    if (WIFSTOPPED(status)) {
        job->state = ST;
        printf("Job [%d] (%d) stopped by signal %d\n", job->jid, job->pid, WSTOPSIG(status) );
    } else {
        if (!WIFEXITED(status))
            printf("Job [%d] (%d) terminated by signal %d\n", job->jid, job->pid, WTERMSIG(status) );
        deletejob(jobs, job->pid);
    }
    sigprocmask(SIG_SETMASK, &prev, NULL);

    errno = olderrno;
    return;
}

于是就写完辣!

最后的最后我还写了一个小程序来快速检验答案是否正确,大概长这样

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

char str1[200];

int main(int argc, char const *argv[])
{
	system("make clean; make");
	for (int i = 1; i <= 16; ++ i) {
		sprintf(str1, "make test%02d > myp%02d.out", i, i);
		system(str1);
		sprintf(str1, "make rtest%02d > std%02d.out", i, i);
		system(str1);
		printf("id: %d completed\n", i);
	}
	for (int i = 1; i <= 16; ++ i) {
		sprintf(str1, "wc -l std%02d.out", i);
		system(str1);
		sprintf(str1, "wc -l myp%02d.out", i);
		system(str1);
	}
	return 0;
}

当然这个只是初步的,具体正确性还要自行比对一下....不过这个看起来也是可以自动化的,我太懒了就鸽了吧,毕竟要考大雾了淦

posted @ 2021-04-21 19:24  jjppp  阅读(444)  评论(0编辑  收藏  举报