csapp – shell lab记录


不得不说,CMU的15213课程比SEU的计组和操作系统课强太多了(不过SEU的课也给我打下了一些基础,还是有用的)。布置的所有实验都有详细友好的指导手册,会提供程序的框架,不需要从零构建程序,让学生更专注于课程所学内容。同时还有完善的测试用例,学生在实验过程中就能知道自己写的程序是否正确。学习曲线很缓和,对学生很友好!

知识整理

进程(process)是一个运行着的应用程序(program)实例。

进程提供了两个关键的抽象:

  • 每个进程仿佛独占着CPU(上下文切换)
  • 每个进程仿佛独占着内存(虚拟内存)

上下文 = 地址空间 + 寄存器

代码

eval函数

这个函数的作用是解析tsh接受的命令行。需要注意的是,在fork出子进程之前,需要先block掉SIG_CHLD信号。这是因为子进程结束的时候,内核会给父进程发送SIG_CHLD,进而父进程执行sigchld_handler,而sigchld_handler会执行deletejob操作,如果子进程先于父进程执行addjob之前结束,就会出现错误。addjob完成之后就可以unblock了。

父进程block掉SIG_CHLD信号,子进程会继承这一结果。如果子进程创建它自己的子进程,当子子进程结束的时候,子进程将收不到SIG_CHLD,所以在execve之前,需要进行unblock操作。

从标准 Unix shell 运行 tsh时,tsh 正在前台进程组中运行。 如果tsh创建一个子进程,默认情况下该子进程也将是前台进程组的成员。由于键入 ctrl-c 会向前台组中的每个进程发送一个 SIGINT,因此键入 ctrl-c 将向tsh以及tsh创建的每个进程发送一个 SIGINT,这显然是不正确的。所以需要为子进程设置新的进程组号。在fork和execve之间执行setpgid(0, 0);即可。

void eval(char *cmdline)
{
    char *argv[MAXARGS];
    int bg;
    pid_t pid;

    sigset_t mask_one, mask_all, prev_mask;
    sigfillset(&mask_all);
    sigemptyset(&mask_one);
    sigaddset(&mask_one, SIGCHLD);

    bg = parseline(cmdline, argv); // bg=1时有两种情况,一是argv[0]为NULL,二是有&
    if (!argv[0])
    {
        return;
    }
    if (!builtin_cmd(argv))
    {
        sigprocmask(SIG_BLOCK, &mask_one, &prev_mask); // block sigchld
        if ((pid = fork()) == 0)                       // child process
        {
            setpgid(0, 0);                              //将当前进程的组号设置成其进程号,避免使用与tsh相同的gpid
            sigprocmask(SIG_SETMASK, &prev_mask, NULL); //子进程unblock sigchld,因为子进程需要处理自己的子进程
            if (execve(argv[0], argv, environ) < 0)     //注意:exec后子进程会清空所有自定义的sighandler
            {
                printf("%s: Command not found./n", argv[0]);
                exit(0); //结束子进程
            }
        }
        //父进程等待子进程返回
        sigprocmask(SIG_BLOCK, &mask_all, NULL);
        addjob(jobs, pid, bg ? BG : FG, cmdline);
        sigprocmask(SIG_SETMASK, &prev_mask, NULL);

        if (!bg)
        {
            waitfg(pid); //一定要解除对SIGCHLD的block之后再waitfg,不然会死锁
        }
        else
        {
            printf("[%d] (%d) %s", jobs->jid, pid, cmdline);
        }
    }
    else // buildin commands
    {
        switch (argv[0][0])
        {
        case 'q':
            exit(0);
            break;
        case 'f':
        case 'b':
            do_bgfg(argv);
            break;
        case 'j':
            listjobs(jobs);
            break;

        default:
            break;
        }
    }
    return;
}

buildin_cmd

int builtin_cmd(char **argv)
{
    if (strcmp("jobs", argv[0]) == 0 ||
        strcmp("bg", argv[0]) == 0 ||
        strcmp("fg", argv[0]) == 0 ||
        strcmp("quit", argv[0]) == 0)
        return 1; /* not a builtin command */
    else
        return 0;
}

do_bgfg

需要给指定进程组的所有进程都发送SIGCONT,并注意waitfg的使用即可。

void do_bgfg(char **argv)
{
    struct job_t *job;
    if (!argv[1])
    {
        printf("bg command requires PID or %%jobid argument/n");
        return;
    }
    if (argv[1][0] == '%')
    {
        int jid = atoi(argv[1] + 1);
        if (!jid)
        {
            printf("bg: argument must be a PID or %%jobid/n");
            return;
        }
        job = getjobjid(jobs, jid);
        if (!job)
        {
            printf("%%%d: No such job/n", jid);
            return;
        }
    }
    else
    {
        pid_t pid = atoi(argv[1]);
        if (!pid)
        {
            printf("bg: argument must be a PID or %%jobid/n");
            return;
        }
        job = getjobpid(jobs, pid);
        if (!job)
        {
            printf("(%d): No such process/n", pid);
            return;
        }
    }

    kill(-job->pid, SIGCONT); //这里需要给组内所有进程都发送信号

    if (argv[0][0] == 'f') // fg
    {
        job->state = FG;
        waitfg(job->pid);
    }
    else
    {
        job->state = BG;
        printf("[%d] (%d) %s", job->jid, job->pid, job->cmdline);
    }
    return;
}

waitfg

sleep函数在进程收到信号时会立即返回,所以这里每次sleep一秒也没关系。

void waitfg(pid_t pid)
{

    while (fgpid(jobs) == pid)
    {
        sleep(1);
    }
    return;
}

sigchld_handler

子进程结束(收到SIGINT或程序流程结束)后,内核会立即给父进程发送SIGCHLD。 而且,子进程stops(收到来自shell转发的SIGTSTP或其他进程发送的SIGTSTP)后,内核也会立即发送SIGCHLD给shell。

当options参数为0时,waitpid会等待指定的进程结束,如果指定的进程是一个zombie,则立即返回,否则阻塞。在options里添加WNOHANG,则waitpid不会阻塞,如果指定进程未结束(不是zombie),则返回错误-1。WUNTRACED选项让waitpid同时等待子进程进入STOP状态。通过宏WIFSTOPPED和WIFSIGNALED解析状态status来判断返回的进程是处于终止状态还是暂停状态。由于SIG_CHLD信号是没有队列的,因此进入sigchld_handler时,后续收到的SIG_CHLD会被丢弃,所以需要在while来reap或者处理暂停的进程。

这个handler统一对joblist处理,另外两个handler只需要转发tsh的信号就可以了。这样可以规避子进程无法继承父进程的handler所带来的问题。

void sigchld_handler(int sig)
{
    pid_t pid;
    int status;
    while ((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0)
    {
        /* returns true if the child process was stopped by delivery of a signal;
           this is possible only if the call was done using WUNTRACED or when the
           child is being traced (see ptrace(2)).
         */
        pid_t pid = fgpid(jobs);
        struct job_t *job = getjobpid(jobs, pid);
        if (WIFSTOPPED(status))
        {
            job->state = ST;
            printf("Job [%d] (%d) stopped by signal 20/n", job->jid, job->pid);
        }
        else
        {
            if (WIFSIGNALED(status))
            {
                printf("Job [%d] (%d) terminated by signal 2/n", job->jid, job->pid);
            }
            deletejob(jobs, pid);
        }
    }
    return;
}

sigint_handler

将tsh收到的来自ctrl+c的SIG_INT转发给前台进程的子进程组

void sigint_handler(int sig)
{
    pid_t pid = fgpid(jobs);
    struct job_t *job = getjobpid(jobs, pid);
    if (job)
    {
        // kill(-pid,sig)会给pid组的所有进程发送信号
        kill(-job->pid, sig);
    }
    return;
}

sigstp_handler

将tsh收到的来自ctrl+z的SIG_STP转发给前台进程的子进程组

void sigtstp_handler(int sig)
{
    pid_t pid = fgpid(jobs);
    struct job_t *job = getjobpid(jobs, pid);

    if (job)
    {
        kill(-job->pid, sig);
    }
    return;
}

原创文章,作者:bd101bd101,如若转载,请注明出处:https://blog.ytso.com/244986.html

(0)
上一篇 2022年4月18日
下一篇 2022年4月18日

相关推荐

发表回复

登录后才能评论