Caffeinated 6.828:实验 3:用户环境

Caffeinated 6.828:实验 3:用户环境-1

简介

在本实验中,你将要实现一个基本的内核功能,要求它能够保护运行的用户模式环境(即:进程)。你将去增强这个 JOS 内核,去配置数据结构以便于保持对用户环境的跟踪、创建一个单一用户环境、将程序镜像加载到用户环境中、并将它启动运行。你也要写出一些 JOS 内核的函数,用来处理任何用户环境生成的系统调用,以及处理由用户环境引进的各种异常。

注意: 在本实验中,术语**“环境”** 和**“进程”** 是可互换的 —— 它们都表示同一个抽象概念,那就是允许你去运行的程序。我在介绍中使用术语**“环境”而不是使用传统术语“进程”**的目的是为了强调一点,那就是 JOS 的环境和 UNIX 的进程提供了不同的接口,并且它们的语义也不相同。

预备知识

使用 Git 去提交你自实验 2 以后的更改(如果有的话),获取课程仓库的最新版本,以及创建一个命名为 lab3 的本地分支,指向到我们的 lab3 分支上 origin/lab3

athena% cd ~/6.828/lab
athena% add git
athena% git commit -am 'changes to lab2 after handin'
Created commit 734fab7: changes to lab2 after handin
 4 files changed, 42 insertions(+), 9 deletions(-)
athena% git pull
Already up-to-date.
athena% git checkout -b lab3 origin/lab3
Branch lab3 set up to track remote branch refs/remotes/origin/lab3.
Switched to a new branch "lab3"
athena% git merge lab2
Merge made by recursive.
 kern/pmap.c |   42 +++++++++++++++++++
 1 files changed, 42 insertions(+), 0 deletions(-)
athena% 

实验 3 包含一些你将探索的新源文件:

inc/    env.h       Public definitions for user-mode environments
        trap.h      Public definitions for trap handling
        syscall.h   Public definitions for system calls from user environments to the kernel
        lib.h       Public definitions for the user-mode support library
kern/   env.h       Kernel-private definitions for user-mode environments
        env.c       Kernel code implementing user-mode environments
        trap.h      Kernel-private trap handling definitions
        trap.c      Trap handling code
        trapentry.S Assembly-language trap handler entry-points
        syscall.h   Kernel-private definitions for system call handling
        syscall.c   System call implementation code
lib/    Makefrag    Makefile fragment to build user-mode library, obj/lib/libjos.a
        entry.S     Assembly-language entry-point for user environments
        libmain.c   User-mode library setup code called from entry.S
        syscall.c   User-mode system call stub functions
        console.c   User-mode implementations of putchar and getchar, providing console I/O
        exit.c      User-mode implementation of exit
        panic.c     User-mode implementation of panic
user/   *           Various test programs to check kernel lab 3 code

另外,一些在实验 2 中的源文件在实验 3 中将被修改。如果想去查看有什么更改,可以运行:

$ git diff lab2

你也可以另外去看一下 实验工具指南,它包含了与本实验有关的调试用户代码方面的信息。

实验要求

本实验分为两部分:Part A 和 Part B。Part A 在本实验完成后一周内提交;你将要提交你的更改和完成的动手实验,在提交之前要确保你的代码通过了 Part A 的所有检查(如果你的代码未通过 Part B 的检查也可以提交)。只需要在第二周提交 Part B 的期限之前代码检查通过即可。

由于在实验 2 中,你需要做实验中描述的所有正则表达式练习,并且至少通过一个挑战(是指整个实验,不是每个部分)。写出详细的问题答案并张贴在实验中,以及一到两个段落的关于你如何解决你选择的挑战问题的详细描述,并将它放在一个名为 answers-lab3.txt 的文件中,并将这个文件放在你的 lab 目标的根目录下。(如果你做了多个问题挑战,你仅需要提交其中一个即可)不要忘记使用 git add answers-lab3.txt 提交这个文件。

行内汇编语言

在本实验中你可能发现使用了 GCC 的行内汇编语言特性,虽然不使用它也可以完成实验。但至少你需要去理解这些行内汇编语言片段,这些汇编语言(asm 语句)片段已经存在于提供给你的源代码中。你可以在课程 参考资料 的页面上找到 GCC 行内汇编语言有关的信息。

Part A:用户环境和异常处理

新文件 inc/env.h 中包含了在 JOS 中关于用户环境的基本定义。现在就去阅读它。内核使用数据结构 Env 去保持对每个用户环境的跟踪。在本实验的开始,你将只创建一个环境,但你需要去设计 JOS 内核支持多环境;实验 4 将带来这个高级特性,允许用户环境去 fork 其它环境。

正如你在 kern/env.c 中所看到的,内核维护了与环境相关的三个全局变量:

struct Env *envs = NULL;            // All environments
struct Env *curenv = NULL;          // The current env
static struct Env *env_free_list;   // Free environment list

一旦 JOS 启动并运行,envs 指针指向到一个数组,即数据结构 Env,它保存了系统中全部的环境。在我们的设计中,JOS 内核将同时支持最大值为 NENV 个的活动的环境,虽然在一般情况下,任何给定时刻运行的环境很少。(NENV 是在 inc/env.h 中用 #define 定义的一个常量)一旦它被分配,对于每个 NENV 可能的环境,envs 数组将包含一个数据结构 Env 的单个实例。

JOS 内核在 env_free_list 上用数据结构 Env 保存了所有不活动的环境。这样的设计使得环境的分配和回收很容易,因为这只不过是添加或删除空闲列表的问题而已。

内核使用符号 curenv 来保持对任意给定时刻的 当前正在运行的环境 进行跟踪。在系统引导期间,在第一个环境运行之前,curenv 被初始化为 NULL

环境状态

数据结构 Env 被定义在文件 inc/env.h 中,内容如下:(在后面的实验中将添加更多的字段):

struct Env {
    struct Trapframe env_tf;   // Saved registers
    struct Env *env_link;      // Next free Env
    envid_t env_id;            // Unique environment identifier
    envid_t env_parent_id;     // env_id of this env's parent
    enum EnvType env_type;     // Indicates special system environments
    unsigned env_status;       // Status of the environment
    uint32_t env_runs;         // Number of times environment has run

    // Address space
    pde_t *env_pgdir;          // Kernel virtual address of page dir
};

以下是数据结构 Env 中的字段简介:

  • env_tf: 这个结构定义在 inc/trap.h 中,它用于在那个环境不运行时保持它保存在寄存器中的值,即:当内核或一个不同的环境在运行时。当从用户模式切换到内核模式时,内核将保存这些东西,以便于那个环境能够在稍后重新运行时回到中断运行的地方。
  • env_link: 这是一个链接,它链接到在 env_free_list 上的下一个 Env 上。env_free_list 指向到列表上第一个空闲的环境。
  • env_id: 内核在数据结构 Env 中保存了一个唯一标识当前环境的值(即:使用数组 envs 中的特定槽位)。在一个用户环境终止之后,内核可能给另外的环境重新分配相同的数据结构 Env —— 但是新的环境将有一个与已终止的旧的环境不同的 env_id,即便是新的环境在数组 envs 中复用了同一个槽位。
  • env_parent_id: 内核使用它来保存创建这个环境的父级环境的 env_id。通过这种方式,环境就可以形成一个“家族树”,这对于做出“哪个环境可以对谁做什么”这样的安全决策非常有用。
  • env_type: 它用于去区分特定的环境。对于大多数环境,它将是 ENV_TYPE_USER 的。在稍后的实验中,针对特定的系统服务环境,我们将引入更多的几种类型。
  • env_status: 这个变量持有以下几个值之一:
    • ENV_FREE: 表示那个 Env 结构是非活动的,并且因此它还在 env_free_list 上。
    • ENV_RUNNABLE: 表示那个 Env 结构所代表的环境正等待被调度到处理器上去运行。
    • ENV_RUNNING: 表示那个 Env 结构所代表的环境当前正在运行中。
    • ENV_NOT_RUNNABLE: 表示那个 Env 结构所代表的是一个当前活动的环境,但不是当前准备去运行的:例如,因为它正在因为一个来自其它环境的进程间通讯(IPC)而处于等待状态。
    • ENV_DYING: 表示那个 Env 结构所表示的是一个僵尸环境。一个僵尸环境将在下一次被内核捕获后被释放。我们在实验 4 之前不会去使用这个标志。
  • env_pgdir: 这个变量持有这个环境的内核虚拟地址的页目录。

就像一个 Unix 进程一样,一个 JOS 环境耦合了“线程”和“地址空间”的概念。线程主要由保存的寄存器来定义(env_tf 字段),而地址空间由页目录和 env_pgdir 所指向的页表所定义。为运行一个环境,内核必须使用保存的寄存器值和相关的地址空间去设置 CPU。

我们的 struct Env 与 xv6 中的 struct proc 类似。它们都在一个 Trapframe 结构中持有环境(即进程)的用户模式寄存器状态。在 JOS 中,单个的环境并不能像 xv6 中的进程那样拥有它们自己的内核栈。在这里,内核中任意时间只能有一个 JOS 环境处于活动中,因此,JOS 仅需要一个单个的内核栈。

为环境分配数组

在实验 2 的 mem_init() 中,你为数组 pages[] 分配了内存,它是内核用于对页面分配与否的状态进行跟踪的一个表。你现在将需要去修改 mem_init(),以便于后面使用它分配一个与结构 Env 类似的数组,这个数组被称为 envs

练习 1、修改在 kern/pmap.c 中的 mem_init(),以用于去分配和映射 envs 数组。这个数组完全由 Env 结构分配的实例 NENV 组成,就像你分配的 pages 数组一样。与 pages 数组一样,由内存支持的数组 envs 也将在 UENVS(它的定义在 inc/memlayout.h 文件中)中映射用户只读的内存,以便于用户进程能够从这个数组中读取。

你应该去运行你的代码,并确保 check_kern_pgdir() 是没有问题的。

创建和运行环境

现在,你将在 kern/env.c 中写一些必需的代码去运行一个用户环境。因为我们并没有做一个文件系统,因此,我们将设置内核去加载一个嵌入到内核中的静态的二进制镜像。JOS 内核以一个 ELF 可运行镜像的方式将这个二进制镜像嵌入到内核中。

在实验 3 中,GNUmakefile 将在 obj/user/ 目录中生成一些二进制镜像。如果你看到 kern/Makefrag,你将注意到一些奇怪的的东西,它们“链接”这些二进制直接进入到内核中运行,就像 .o 文件一样。在链接器命令行上的 -b binary 选项,将因此把它们链接为“原生的”不解析的二进制文件,而不是由编译器产生的普通的 .o 文件。(就链接器而言,这些文件压根就不是 ELF 镜像文件 —— 它们可以是任何东西,比如,一个文本文件或图片!)如果你在内核构建之后查看 obj/kern/kernel.sym ,你将会注意到链接器很奇怪的生成了一些有趣的、命名很费解的符号,比如像 _binary_obj_user_hello_start_binary_obj_user_hello_end、以及 _binary_obj_user_hello_size。链接器通过改编二进制文件的命令来生成这些符号;这种符号为普通内核代码使用一种引入嵌入式二进制文件的方法。

kern/init.ci386_init() 中,你将写一些代码在环境中运行这些二进制镜像中的一种。但是,设置用户环境的关键函数还没有实现;将需要你去完成它们。

练习 2、在文件 env.c 中,写完以下函数的代码:

  • env_init()

初始化 envs 数组中所有的 Env 结构,然后把它们添加到 env_free_list 中。也称为 env_init_percpu,它通过配置硬件,在硬件上为 level 0(内核)权限和 level 3(用户)权限使用单独的段。

  • env_setup_vm()

为一个新环境分配一个页目录,并初始化新环境的地址空间的内核部分。

  • region_alloc()

为一个新环境分配和映射物理内存

  • load_icode()

你将需要去解析一个 ELF 二进制镜像,就像引导加载器那样,然后加载它的内容到一个新环境的用户地址空间中。

  • env_create()

使用 env_alloc 去分配一个环境,并调用 load_icode 去加载一个 ELF 二进制

  • env_run()

在用户模式中开始运行一个给定的环境

在你写这些函数时,你可能会发现新的 cprintf 动词 %e 非常有用 – 它可以输出一个错误代码的相关描述。比如:

    r = -E_NO_MEM;
    panic("env_alloc: %e", r);

中 panic 将输出消息 env_alloc: out of memory。

下面是用户代码相关的调用图。确保你理解了每一步的用途。

  • start (kern/entry.S)
  • i386_init (kern/init.c)
    • cons_init
    • mem_init
    • env_init
    • trap_init(到目前为止还未完成)
    • env_create
    • env_run
      • env_pop_tf