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 改为:

fn remap_kernel(dtb: usize) {
    let offset = - ( KERNEL_OFFSET as isize - MEMORY_OFFSET as isize);

    use crate::memory_set::{ MemorySet, handler::Linear, attr::MemoryAttr };
    let mut memset = MemorySet::new();
    memset.push(
        stext as usize,
        etext as usize,
        MemoryAttr::new().set_execute().set_readonly(),
        Linear::new(offset),
    );
    memset.push(
        srodata as usize,
        erodata as usize,
        MemoryAttr::new().set_readonly(),
        Linear::new(offset),
    );
    memset.push(
        sdata as usize,
        edata as usize,
        MemoryAttr::new(),
        Linear::new(offset),
    );
    memset.push(
        bootstack as usize,
        bootstacktop as usize,
        MemoryAttr::new(),
        Linear::new(offset),
    );
    memset.push(
        sbss as usize,
        ebss as usize,
        MemoryAttr::new(),
        Linear::new(offset),
    );
    memset.push(
        dtb as usize,
        dtb as usize + MAX_DTB_SIZE,
        MemoryAttr::new(),
        Linear::new(offset),
    );
    unsafe{
        memset.activate();
    }
}

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

wohaocaia

实现用户进程

用户程序

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

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

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

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

#![no_std]
#![no_main]

#[macro_use]
extern crate rust;

#[no_mangle]
pub fn main() {
    for i in 0..100 {
        println!("Hello, world!");
        for j in 0..1000 {
        }
    }
    println!("Hello, world!");
}

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

创建 usr/rust/src/syscall.rs

#[inline(always)]
fn sys_call(
    syscall_id: SyscallId,
    arg0: usize,
    arg1: usize,
    arg2: usize,
    arg3: usize,
) -> i32 {
    let id = syscall_id as usize;
    let mut ret: i32;
    unsafe {
        asm!("ecall"
            : "={x10}" (ret)
            : "{x17}" (id), "{x10}" (arg0), "{x11}" (arg1), "{x12}" (arg2), "{x13}" (arg3)
            : "memory"
            : "volatile");
    }
    ret
}

pub fn sys_write(ch : u8) -> i32 {
    sys_call(SyscallId::Write, ch as usize, 0, 0, 0)
}

pub fn sys_exit(code: usize) -> ! {
    sys_call(SyscallId::Exit, code, 0, 0, 0);
    loop{}
}

enum SyscallId {
    Write = 64,
    Exit = 93,
}

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

// in process/mod.rs

pub fn exit(code: usize) {
    CPU.exit(code);
}

// in interrupt.rs

pub const SYS_WRITE: usize = 64;
pub const SYS_EXIT: usize = 93;

fn syscall(tf: &mut TrapFrame) {
    tf.sepc += 4;
    match tf.x[17] {
        SYS_WRITE => {
            print!("{}", tf.x[10] as u8 as char);
        },
        SYS_EXIT => {
            println!("exit!");
            use crate::process::exit;
            exit(tf.x[10]);
        },
        _ => {
            println!("unknown user syscall !");
        }
    };
}

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

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

// in usr/rust/src/lang_items.rs

use core::panic::PanicInfo;
use core::alloc::Layout;
use crate::syscall::*;

#[linkage = "weak"] // 弱链接,弱外部有 main 函数,则不使用该 main
#[no_mangle]
fn main() {
    panic!("No main() linked");
}

use crate::ALLOCATOR;
fn init_heap() {
    const HEAP_SIZE: usize = 0x1000;
    static mut HEAP: [u8; HEAP_SIZE] = [0; HEAP_SIZE];
    unsafe {
        ALLOCATOR.lock().init(HEAP.as_ptr() as usize, HEAP_SIZE);
    }
}

#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    let location = info.location().unwrap();
    let message = info.message().unwrap();
    println!(
        "\nPANIC in {} at line {} \n\t{}",
        location.file(),
        location.line(),
        message
    );

    loop {}
}

#[no_mangle]
pub extern "C" fn _start(_argc: isize, _argv: *const *const u8) -> ! {
    init_heap();
    main();
    sys_exit(0)
}

#[no_mangle]
pub extern fn abort() {
    panic!("abort");
}

#[lang = "oom"]
fn oom(_: Layout) -> ! {
    panic!("out of memory");
}

#[lang = "eh_personality"]
fn eh_personality() {}


// in usr/rust/src/lib.rs

#![no_std]
#![feature(asm)]
#![feature(alloc)]
#![feature(lang_items)]
#![feature(panic_info_message)]
#![feature(linkage)]
#![feature(compiler_builtins_lib)]

extern crate alloc;

#[macro_use]
pub mod io;

pub mod lang_items;
pub mod syscall;


use buddy_system_allocator::LockedHeap;

#[global_allocator]
static ALLOCATOR: LockedHeap = LockedHeap::empty();

// in usr/rust/Cargo.toml

[dependencies]
buddy_system_allocator = "0.1"

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

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

pub fn putchar(ch: char) {
    syscall::sys_write(ch as u8);
}

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

最后编写 Makefile :

# in usr/Makefile

out_dir ?= build
out_img ?= rcore32.img

cargo_args := --target riscv32-os.json
rust_src_dir := rust/src/bin
rust_bin_path := rust/target/riscv32-os/debug
rust_bins := $(patsubst $(rust_src_dir)/%.rs, $(rust_bin_path)/%, $(wildcard $(rust_src_dir)/*.rs))

.PHONY: all clean rust

all : rust

rust :
    @echo Building rust user program
    @cd rust && cargo xbuild $(cargo_args)
    @rm -rf $(out_dir)/rust && mkdir -p $(out_dir)/rust
    @echo $(out_dir)
    @echo $(rust_bins)
    @cp -r $(rust_bins) $(out_dir)/rust

clean :
    @rm -rf $(out_dir)
    @cd rust && cargo clean
    @rm -f $(out_img)

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

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

usr_path := usr/build/rust
export SFSIMG = $(usr_path)/main

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

global_asm!(concat!(
    r#"
    .section .data
    .global _user_img_start
    .global _user_img_end
_user_img_start:
    .incbin ""#,
    env!("SFSIMG"),
    r#""
_user_img_end:
"#
));

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

ELF 文件格式概述

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

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

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

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

加入以下代码:

// in Cargo.toml

[dependencies]
xmas-elf = "0.6"

// in process/structs.rs

use xmas_elf::{
    header,
    program::{ Flags, SegmentData, Type },
    ElfFile,
};

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

用户页表

首先加入一下内容:

// in process/structs.rs
use crate::memory_set::{ MemorySet, handler::ByFrame, attr::MemoryAttr};
use crate::memory::frame_allocator::alloc_frames;
use crate::consts::*;
use crate::process::{ Tid, ExitCode };
use alloc::{ sync::Arc, boxed::Box };
use alloc::alloc::{ alloc, dealloc, Layout };
use riscv::register::satp;
use core::str;

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

// in process/structs.rs
pub struct Process {
    vm: Arc<MemorySet>,
}

pub struct Thread {
    pub context: Context, // 线程相关的上下文
    pub kstack: KernelStack, // 线程对应的内核栈
    pub proc: Option<Arc<Process>>,
}

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

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

// in process/structs.rs

trait ElfExt {
    fn make_memory_set(&self) -> MemorySet;
}

impl ElfExt for ElfFile<'_> {
    fn make_memory_set(&self) -> MemorySet {
        println!("creating MemorySet from ELF");
        let mut ms = MemorySet::new_kern(); // 创建自带内核地址空间的虚拟存储系统

        for ph in self.program_iter() { // 枚举文件中的程序段
            if ph.get_type() != Ok(Type::Load) {
                continue;
            }
            // 获取程序段的大小和起始地址(虚拟的)
            let virt_addr = ph.virtual_addr() as usize;
            let mem_size = ph.mem_size() as usize;
            // 将数据读取为 u8 的数组
            let data = match ph.get_data(self).unwrap() {
                SegmentData::Undefined(data) => data,
                _ => unreachable!(),
            };

            // Get target slice
            let target = {  // 可以看到,这里的 virt_addr 是根据文件中的虚拟地址得到的,所以 target 应该仅用于 with 函数中
                println!("virt_addr {:#x}, mem_size {:#x}", virt_addr, mem_size);
                ms.push(
                    virt_addr,
                    virt_addr + mem_size,
                    ph.flags().to_attr(),
                    ByFrame::new(),
                );
                unsafe { ::core::slice::from_raw_parts_mut(virt_addr as *mut u8, mem_size) }
            };
            // Copy data
            unsafe {
                ms.with(|| {    // with 函数的作用是,将当前这个未激活页表激活并执行一个函数,然后切换回原来的页表
                    if data.len() != 0 {
                        target[..data.len()].copy_from_slice(data);
                    }
                    target[data.len()..].iter_mut().for_each(|x| *x = 0);
                });
            }
        }
        ms
    }
}

trait ToMemoryAttr {
    fn to_attr(&self) -> MemoryAttr;
}

impl ToMemoryAttr for Flags {
    fn to_attr(&self) -> MemoryAttr {   // 将文件中各个段的读写权限转换为页表权限
        let mut flags = MemoryAttr::new().set_user();
        if self.is_execute() {
            flags = flags.set_execute();
        }
        flags
    }
}

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

// in consts.rs

pub const USER_STACK_OFFSET: usize = 0x80000000 - USER_STACK_SIZE;

pub const USER_STACK_SIZE: usize = 0x10000;

// in process/structs.rs

pub unsafe fn new_user(data: &[u8]) -> Box<Thread>
    {
        let elf = ElfFile::new(data).expect("failed to read elf");

        // Check ELF type
        match elf.header.pt2.type_().as_type() {
            header::Type::Executable => {println!("it really a elf");},
            header::Type::SharedObject => {},
            _ => panic!("ELF is not executable or shared object"),
        }

        // entry_point 代表程序入口在文件中的具体位置
        let entry_addr = elf.header.pt2.entry_point() as usize;
        println!("entry: {:#x}", entry_addr);
        let mut vm = elf.make_memory_set(); // 为这个 elf 文件创建一个新的虚存系统,其中包含内核的地址空间和elf文件中程序的地址空间
        let mut ustack_top = {  // 创建用户栈
            let (ustack_buttom, ustack_top) = (USER_STACK_OFFSET, USER_STACK_OFFSET + USER_STACK_SIZE);
            let paddr = alloc_frames(USER_STACK_SIZE / PAGE_SIZE).unwrap().start_address().as_usize();
            vm.push(    // 创建一个内核栈之后还需要将这个内核栈装入虚存系统。
                ustack_buttom,
                ustack_top,
                MemoryAttr::new().set_user(),
                ByFrame::new(),
            );
            ustack_top
        };

        let kstack = KernelStack::new();    // 为用户程序创建内核栈。用于线程切换
        Box::new(Thread{    // 注意下面创建上下文使用的是哪个栈
            context: Context::new_user_thread(entry_addr, ustack_top, kstack.top(), vm.token()),
            kstack: kstack,
            proc: Some(Arc::new(Process{
                vm: Arc::new(vm),
            })),
        })
    }

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

//in context.rs
#[repr(C)]
struct ContextContent {
    ra: usize, // 返回地址
    satp: usize, // 二级页表所在位置
    s: [usize; 12], // 被调用者保存的寄存器
    tf: TrapFrame, // 中断帧
}

extern "C" {
    fn __trapret();
}

impl ContextContent {
    fn new_user_thread(entry: usize, ustack_top: usize, satp: usize) -> Self {
        ContextContent{
            ra: __trapret as usize,
            satp,
            s: [0;12],
            tf: {
                let mut tf: TrapFrame = unsafe { zeroed() };
                tf.x[2] = ustack_top;   // 栈顶 sp
                tf.sepc = entry;   // sepc 在调用 sret 之后将被被赋值给 PC
                tf.sstatus = sstatus::read();
                tf.sstatus.set_spie(true);
                tf.sstatus.set_sie(false);
                tf.sstatus.set_spp(sstatus::SPP::User);   // 代表 sret 之后的特权级为U
                tf
            },
        }
    }
}

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

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

// in process/mod.rs

extern "C" {
    fn _user_img_start();
    fn _user_img_end();
}

pub fn init() {
    println!("+------ now to initialize process ------+");
    let scheduler = Scheduler::new(1);
    let thread_pool = ThreadPool::new(100, scheduler);
    CPU.init(Thread::new_idle(), Box::new(thread_pool));
    ...
    let data = unsafe{
        ::core::slice::from_raw_parts(
            _user_img_start as *const u8,
            _user_img_end as usize - _user_img_start as usize,
        )
    };
    let user = unsafe{ Thread::new_user(data) };
    CPU.add_thread(user);
    CPU.run();
}

Last updated