10. 用户进程

本章代码对应 commit :825c1d6cd68da8a99b6b1bee81c25b78cc08dacd

由于本人水平有限,在写用户进程的时候页表炸了;且 ddl 在即,所以把学长写的页表抄了过来,用于后续用户进程和文件系统的实现。。。有时间了(暑假或者下学期)我再填这个坑

可支持用户进程的页表

所以首先我们来抄页表

不想抄的话我这里也做了一个 commit: f6e9d1603d0949dd2187e873ec38307e7d3c1f56 ,直接在这个 commit 的基础上实现本章的内容也行

首先,把 bbl.c 里的 static void setup_page_table_sv32 函数改回来:

static void setup_page_table_sv32()
{
  // map kernel [0x300..] 0x80000000 -> 0xC0000000..
  int i_end = dtb_output() / MEGAPAGE_SIZE;
  for (int i = 0x200; i < i_end + 1; ++i)
  {
    root_table[i + 0x100] = pte_create(i << RISCV_PGLEVEL_BITS, PTE_R | PTE_W | PTE_X);
  }
  // map recursive [0x3fd] (V), [0x3fe] (VRW), [0x3ff] (VRW)
  uintptr_t root_table_ppn = (uintptr_t)root_table >> RISCV_PGSHIFT;
  root_table[0x3fd] = pte_create(root_table_ppn, 0);
  root_table[0x3fe] = pte_create(root_table_ppn, PTE_R | PTE_W);
}

然后下载一下学长写的页表

创建 memory_set 目录,加入 这些文件 ,然后在 lib.rs 中加入 mod memory_set

paging.rs 内容全删掉,换成 这个

consts.rs 里加上 pub const RECURSIVE_INDEX: usize = 0x3fd ,页表

最后,把 memory/mod.rs 里的 mod paging 改为 pub mod paging ,然后把 remap_kernel 改为:

执行一下 make run ,输出结果和之前是一样的。。。

wohaocaia

实现用户进程

用户程序

动态的添加用户程序是我们所期望的,但是由于缺少文件系统,本章将用户程序直接编译进内核。

下一章将支持动态执行用户程序

首先,创建目录 usr ,执行 cargo new rust --bin --edition 2018

将自动生成的 main.rs 放入 usr/rust/src/bin 目录下(需要创建),然后修改为:

本章我们需要实现两个 syscall: SYS_WRITE, SYS_EXIT 。

创建 usr/rust/src/syscall.rs

x17 寄存器保存了用户产生的系统调用的编号,x10~x13 寄存器保存了系统调用的参数。用户态产生系统调用后会进入内核态,在内核态中对产生的 syscall 进行处理:

tf.sepc += 4 这一行的作用是主动跳过当前指令,具体原因在 4. Trap 中提及过。

usr/rust/src 目录下创建 lib.rs, lang_items, io.rs

上面的内容与我们在编写内核时的基本一致,这里不重复介绍。

usr/rust/src/io.rs 的内容可以直接从内核的 io.rs 复制过来,但是需要将 use bbl::sbi 改为 use super::syscall ,然后修改 putchar 函数为:

复制 riscv32-os.jsonusr/rust 目录下,删去 "pre-link-args": { "ld.lld": ["-Tsrc/boot/linker.ld"] }

最后编写 Makefile :

usr 目录下执行 make ,得到 usr/rust/build/main 二进制文件,这就是我们需要的用户程序。接下来我们就将把该程序链接进内核。

在内核的 Makefile 中将用户程序作为环境变量加入:

同时在 init.rs 中加入以下代码:

至此我们已经成功将用户程序放入内核,接下来我们需要在内核中解析用户程序并执行。

ELF 文件格式概述

ELF(Executable and linking format) 文件格式是 Linux 系统下的一种常用目标文件(object file)格式,有三种主要类型:

  • 用于执行的 可执行文件(executable file) ,用于提供程序的进程映像,加载的内存执行。 这也是本章使用的文件类型。

  • 用于连接的 可重定位文件(relocatable file) ,可与其它目标文件一起创建可执行文件和共享目标文件。

  • 共享目标文件(shared object file) ,连接器可将它与其它可重定位文件和共享目标文件连接成其它的目标文件,动态连接器又可将它与可执行文件和其它共享目标文件结合起来创建一个进程映像。

加入以下代码:

ELF header 在文件开始处描述了整个文件的组织,其文件头包含了整个执行文件的控制结构。 program header 描述与程序执行直接相关的目标文件结构信息,用来在文件中定位各个段的映像,同时包含其他一些用来为程序创建进程映像所必需的信息。根据 ELF header 和 program header 的结构描述,我们就可以完成对 ELF 格式文件的加载。

用户页表

首先加入一下内容:

在创建用户进程时,需要为用户创建一个页表。因此在 Thread 结构体中增加变量:

同时在 new_idlenew_kernel 中分别增加 proc 的初始化: proc: None

MemorySet::new_kern 能够一个新的页表,同时映射内核地址,以便产生中断异常的时候能够访问到正确的地址。接下来只需将数据从 ELF 中读取并写入页表即可,具体细节写于注释中:

用户进程的创建和内核线程没有太大的区别,但是多了对 ELF 文件的处理,以及为用户创建堆栈。

在从内核态切换至用户态时,需要恢复所有寄存器,同时调用 sret 切换状态。这和我们之前实现的 __trapret 功能是一致的。所以这里可以利用之前完成的函数。首先在 ContextContent 结构体中加入 TrapFrame 变量,然后由于需要通过 sret 进入用户态,所以我们利用之前写的 trap/trap.asm/__trapret 进行中断返回:

注意这里和 new_kernel_thread 不同的地方。在 switch.asm 的最后执行了 ret ,所以会跳转至 ra 保存的地址。对于内核线程,他会跳转至 entry 线程;对用户进程,他会跳转至 __trapret ,经过 RESTORE_ALL 宏恢复寄存器之后,执行 sret 进入用户态,执行用户程序。

Anyway ,用户进程算是实现完了。最后,创建一个用户进程并加入线程池,可以执行 make run 跑一下看看结果吧:

Last updated

Was this helpful?