Process OS

在这里插入图片描述

【总体思路】

  • 编译:应用程序和内核独立编译,合并为一个镜像
  • 编译:不同应用程序可采用统一的起始地址
  • 构造:系统调用服务,进程的管理与初始化
  • 构造:建立基于页表机制的虚存空间
  • 运行:特权级切换,进程与OS相互切换
  • 运行:切换地址空间,跨地址空间访问数据

【代码架构】

├── bootloader
│   ├── rustsbi-k210.bin
│   └── rustsbi-qemu.bin
├── LICENSE
├── os
│   ├── build.rs(修改:基于应用名的应用构建器)
│   ├── Cargo.toml
│   ├── Makefile
│   └── src
│       ├── config.rs
│       ├── console.rs
│       ├── entry.asm
│       ├── lang_items.rs
│       ├── link_app.S
│       ├── linker-k210.ld
│       ├── linker-qemu.ld
│       ├── loader.rs(修改:基于应用名的应用加载器)
│       ├── main.rs(修改)
│       ├── mm(修改:为了支持本章的系统调用对此模块做若干增强)
│       │   ├── address.rs
│       │   ├── frame_allocator.rs
│       │   ├── heap_allocator.rs
│       │   ├── memory_set.rs
│       │   ├── mod.rs
│       │   └── page_table.rs
│       ├── sbi.rs
│       ├── sync
│       │   ├── mod.rs
│       │   └── up.rs
│       ├── syscall
│       │   ├── fs.rs(修改:新增 sys_read)
│       │   ├── mod.rs(修改:新的系统调用的分发处理)
│       │   └── process.rs(修改:新增 sys_getpid/fork/exec/waitpid)
│       ├── task
│       │   ├── context.rs
│       │   ├── manager.rs(新增:任务管理器,为上一章任务管理器功能的一部分)
│       │   ├── mod.rs(修改:调整原来的接口实现以支持进程)
│       │   ├── pid.rs(新增:进程标识符和内核栈的 Rust 抽象)
│       │   ├── processor.rs(新增:处理器管理结构 ``Processor`` ,为上一章任务管理器功能的一部分)
│       │   ├── switch.rs
│       │   ├── switch.S
│       │   └── task.rs(修改:支持进程管理机制的任务控制块)
│       ├── timer.rs
│       └── trap
│           ├── context.rs
│           ├── mod.rs(修改:对于系统调用的实现进行修改以支持进程系统调用)
│           └── trap.S
├── README.md
├── rust-toolchain
├── tools
│   ├── kflash.py
│   ├── LICENSE
│   ├── package.json
│   ├── README.rst
│   └── setup.py
└── user(对于用户库 user_lib 进行修改,替换了一套新的测例)
├── Cargo.toml
├── Makefile
└── src
    ├── bin
    │   ├── exit.rs
    │   ├── fantastic_text.rs
    │   ├── forktest2.rs
    │   ├── forktest.rs
    │   ├── forktest_simple.rs
    │   ├── forktree.rs
    │   ├── hello_world.rs
    │   ├── initproc.rs
    │   ├── matrix.rs
    │   ├── sleep.rs
    │   ├── sleep_simple.rs
    │   ├── stack_overflow.rs
    │   ├── user_shell.rs
    │   ├── usertests.rs
    │   └── yield.rs
    ├── console.rs
    ├── lang_items.rs
    ├── lib.rs
    ├── linker.ld
    └── syscall.rs

【应用程序设计】

【新增进程管理系统调用 -> user/src/lib.rs】

// user/src/syscall.rs
/// 功能:当前进程 fork 出来一个子进程。
/// 返回值:对于子进程返回 0,对于当前进程则返回子进程的 PID
/// syscall ID:220
pub fn sys_fork() -> isize {
    syscall(SYSCALL_FORK, [0, 0, 0])
}

/// 功能:将当前进程的地址空间清空并加载一个特定的可执行文件,返回用户态后开始它的执行。
/// 参数:path 给出了要加载的可执行文件的名字;
/// 返回值:如果出错的话(如找不到名字相符的可执行文件)则返回 -1,否则不应该返回。
/// syscall ID:221
pub fn sys_exec(path: &str, args: &[*const u8]) -> isize {
    syscall(
        SYSCALL_EXEC,
        [path.as_ptr() as usize, args.as_ptr() as usize, 0],
    )
}

/// 功能:当前进程等待一个子进程变为僵尸进程,回收其全部资源并收集其返回值。
/// 参数:pid 表示要等待的子进程的进程 ID,如果为 -1 的话表示等待任意一个子进程;
/// exit_code 表示保存子进程返回值的地址,如果这个地址为 0 的话表示不必保存。
/// 返回值:如果要等待的子进程不存在则返回 -1;否则如果要等待的子进程均未结束则返回 -2;
/// 否则返回结束的子进程的进程 ID。
/// syscall ID:260
pub fn sys_waitpid(pid: isize, exit_code: *mut i32) -> isize {
    syscall(SYSCALL_WAITPID, [pid as usize, exit_code as usize, 0])
}

user/src/lib.rs中主要对一些系统调用进行了外部的封装,新增的函数如下所示:

// user/src/lib.rs
pub fn fork() -> isize {
    sys_fork()
}

pub fn exec(path: &str, args: &[*const u8]) -> isize {
    sys_exec(path, args)
}

pub fn wait(exit_code: &mut i32) -> isize {
    loop {
        match sys_waitpid(-1, exit_code as *mut _) {
            -2 => {
                sys_yield();
            }
            n => {
                return n;
            }
        }
    }
}

pub fn waitpid(pid: usize, exit_code: &mut i32) -> isize {
    loop {
        match sys_waitpid(pid as isize, exit_code as *mut _) {
            -2 => {
                sys_yield();
            }
            n => {
                return n;
            }
        }
    }
}
  • 第 2 - 4 行: sys_fork 被封装成 fork

  • 第 6 - 8 行: sys_exec 被封装成 exec

    这里值得一提的是 sys_waitpid 被封装成两个不同的 APIwaitwaitpid

  • 第 10 - 21 行:wait 表示等待任意一个子进程结束

    • 第 12 行:根据 sys_waitpid 的约定它需要传的 pid 参数为 -1表示等待任意一个子进程。
    • 第 13 - 15 行:当 sys_waitpid 返回值为 -2 ,即要等待的子进程存在但它却尚未退出的时候,我们调用 yield_ 主动交出 CPU 使用权,待下次 CPU 使用权被内核交还给它的时候再次调用 sys_waitpid 查看要等待的子进程是否退出。这样做可以减小 CPU 资源的浪费。
    • 第 16 - 18 行:返回已经推出的pid
  • 第 23 - 34 行:waitpid表示等待一个输入pid的子进程结束。流程与上述一样,只不过将sys_waitpid传入的参数进行了修改。

【用户初始程序 initproc -> user/src/bin/ch5b_initproc.rs】

user/src/bin/ch5b_initproc.rs为用户初始程序

// user/src/bin/ch5b_initproc.rs
#![no_std]
#![no_main]

#[macro_use]
extern crate user_lib;

use user_lib::{exec, fork, wait, yield_};

#[no_mangle]
fn main() -> i32 {
    if fork() == 0 {
        exec("ch5b_user_shell\0", &[0 as *const u8]);
    } else {
        loop {
            let mut exit_code: i32 = 0;
            let pid = wait(&mut exit_code);
            if pid == -1 {
                yield_();
                continue;
            }
            println!(
                "[initproc] Released a zombie process, pid={}, exit_code={}",
                pid, exit_code,
            );
        }
    }
    0
}
  • 第 12 行:通过fork()创建子进程
  • 第 13 行:fork 返回值为 0 的分支,表示子进程,此行直接通过 exec 执行 shell 程序 user_shell ,注意我们需要在字符串末尾手动加入 \0 ,因为 Rust 在将这些字符串连接到只读数据段的时候不会插入 \0
  • 第 15 行:开始则为返回值不为 0 的分支,表示调用 fork 的用户初始程序 initproc 自身。
  • 第 17 行:可以看到它在不断循环调用 wait 来等待那些被移交到它下面的子进程并回收它们占据的资源。
  • 第 18 - 21 行:如果回收失败的话,就 yield_ 交出 CPU 资源并在下次轮到它执行的时候再回收看看。这也可以看出,用户初始程序 initproc 对于资源的回收并不算及时,但是对于已经退出的僵尸进程,用户初始程序 initproc 最终总能够成功回收它们的资源。
  • 第 22 - 25 行:如果回收成功的话则会打印一条报告信息给出被回收子进程的 pid 值和返回值;

个人理解:initproc主要的功能是创建shell进程,同时作为初始进程类似于1号进程,其也担当了对于僵尸进程进行资源的释放。

【应用 shell -> user/src/bin/ch5b_user_shell.rs】

user/src/bin/ch5b_user_shell.rs该应用文件为新创建的应用shell文件主要执行流程如下所示:

  1. 通过sys_read获取字符串(即文件名)
  2. 通过sys_fork创建子进程
  3. 在子进程中通过sys_exec创建新应用的进程
  4. 在父进程中通过sys_waitpid等待子进程结束
  5. 跳转到第一步循环执行
// user/src/bin/ch5b_user_shell.rs
#![no_std]
#![no_main]

extern crate alloc;

#[macro_use]
extern crate user_lib;

const LF: u8 = 0x0au8;
const CR: u8 = 0x0du8;
const DL: u8 = 0x7fu8;
const BS: u8 = 0x08u8;

use alloc::string::String;
use user_lib::console::getchar;
use user_lib::{exec, flush, fork, waitpid};

#[no_mangle]
pub fn main() -> i32 {
    println!("Rust user shell");
    let mut line: String = String::new();
    print!(">> ");
    flush();
    loop {
        let c = getchar();
        match c {
            LF | CR => {
                print!("\n");
                if !line.is_empty() {
                    line.push('\0');
                    let pid = fork();
                    if pid == 0 {
                        // child process
                        if exec(line.as_str(), &[0 as *const u8]) == -1 {
                            println!("Error when executing!");
                            return -4;
                        }
                        unreachable!();
                    } else {
                        let mut exit_code: i32 = 0;
                        let exit_pid = waitpid(pid as usize, &mut exit_code);
                        assert_eq!(pid, exit_pid);
                        println!("Shell: Process {} exited with code {}", pid, exit_code);
                    }
                    line.clear();
                }
                print!(">> ");
                flush();
            }
            BS | DL => {
                if !line.is_empty() {
                    print!("{}", BS as char);
                    print!(" ");
                    print!("{}", BS as char);
                    flush();
                    line.pop();
                }
            }
            _ => {
                print!("{}", c as char);
                flush();
                line.push(c as char);
            }
        }
    }
}
  • 第 21 行:打印Rust user shell提示符

  • 第 22 行:声明lineString类型。

  • 第 23 行:打印>>提示符

  • 第 24 行:清空buff

  • 第 25 - 66 行:为shell控制行的循环窗口。

    • 第 26 行:getchar通过sys_read获取一个字符。

    • 第 28 行:如果这个字符是回车换行0x0a0x0d),也就是最后一个字符,则执行接下来的条件判断。

      • 第 29 行:打印回车换行
      • 第 30 行:如果目前的line也就是存储输入数据的buff不为空,代表输入完全
      • 第 32 行:通过fork()创建子进程
      • 第 33 行:通过返回的结果区分子进程和父进程
      • 第 35 行:通过exec()对子进程进行实例化,如果返回值为 -1 则说明在应用管理器中找不到名字相同的应用,此时子进程就直接打印错误信息并退出。反之 exec 则根本不会返回,而是开始执行目标应用。
      • 第 36 - 37 行:实例化失败后给出失败提示符并返回错误码-4
      • 第 39 行:unreachable! 只是panic! 的简写,带有固定的特定消息。如果exec正常执行是跳不到这里的。
      • 第 40 行:通过pid确定这里是父进程。
      • 第 41 - 44 行:父进程通过waitpid等待子进程的返回,如果成功则返回0。并打印退出码为0和相应的pid

      由于子进程是从 user_shell 进程中 fork 出来的,它们除了 fork 的返回值不同之外均相同,自然也可以看到一个和user_shell 进程维护的版本相同的字符串 line

      所以这里if-else判断相当于执行两个单独的进程,父进程的返回值走else,子进程的返回值走if

      • 第 46 行:将line清空。
      • 第 48 行:打印>>提示符
      • 第 49 行:清空buff
    • 第 51 行:如果这个字符是 BS (Backspace)退格键(ASCII=8)或者是**DEL (Delete)删除键**(ASCII=127)

      • 第 52 行:如果当前line不为空
      • 第 53 - 55 行:输入一个特殊的退格字节 BS 来实现将屏幕上当前行的最后一个字符用空格替换掉。
      • 第 56 行:清空buff
      • 第 57 行:user_shell 进程内维护的 line 也需要弹出最后一个字符。
    • 第 60 行:如果用户输入了一个其他字符

      • 第 61 - 63 行::会被视为用户的正常输入,我们直接将它打印在屏幕上并加入到 line 中。

shell应用主要是维护shell这个命令行。通过对用户输入的信息进行解析,把对应输入的应用当成一个新的进程来执行。

应用程序设计小结:当内核初始化完毕之后,它会从可执行文件 initproc 中加载并执行用户初始程序 initproc,而用户初始程序 initproc中又会 forkexec 来运行shell程序 user_shell 。这两个应用虽然都是在 CPUU 特权级执行的,但是相比其他应用,它们要更加底层和基础。原则上应该将它们作为一个组件打包在操作系统中。但这里为了实现更加简单,我们并不将它们和其他应用进行区分。

在这里插入图片描述

【内核程序设计】

【应用的链接与加载 -> os/src/loader.rs】

在编译操作系统的过程中,会通过build.rs生成 link_app.S 文件

# os/src/link_app.S
	.align 3
    .section .data
    .global _num_app
_num_app:
    .quad 19            #应用程序个数
	......
	
	.global _app_names
_app_names:             #app0的名字
    .string "ch2b_bad_address"          
	......
	
	.section .data
    .global app_0_start
    .global app_0_end
    .align 3
app_0_start:            #app0的开始位置
    .incbin "../user/build/elf/ch2b_bad_address.elf"
app_0_end:              #app0的结束位置

	.section .data
    .global app_1_start
    .global app_1_end
    .align 3
app_1_start:            #app0的开始位置
    .incbin "../user/build/elf/ch2b_bad_instructions.elf"
app_1_end:              #app0的结束位置

Ⅰ:在编译完user目录下的应用之后会在/user/build/elf/目录下生成对应应用的elf格式bin文件。

Ⅱ:在编译操作系统的时候会依据build.rs生成link_app.S的汇编文件,该文件链接到每一个app编译后所对应的目标文件上,并可以通过应用名称进行加载。

Ⅲ:在加载器 loader.rs 中,分析 link_app.S 中的内容,并用一个全局可见的 只读 向量 APP_NAMES 来按照顺序将所有应用的名字保存在内存中,为通过 exec 系统调用创建新进程做好了前期准备。

// os/src/loader.rs
//! Loading user applications into memory

use alloc::vec::Vec;
use lazy_static::*;

/// Get the total number of applications.
pub fn get_num_app() -> usize {
    extern "C" {
        fn _num_app();
    }
    unsafe { (_num_app as usize as *const usize).read_volatile() }
}

/// get applications data
pub fn get_app_data(app_id: usize) -> &'static [u8] {
    extern "C" {
        fn _num_app();
    }
    let num_app_ptr = _num_app as usize as *const usize;
    let num_app = get_num_app();
    let app_start = unsafe { core::slice::from_raw_parts(num_app_ptr.add(1), num_app + 1) };
    assert!(app_id < num_app);
    unsafe {
        core::slice::from_raw_parts(
            app_start[app_id] as *const u8,
            app_start[app_id + 1] - app_start[app_id],
        )
    }
}

lazy_static! {
    /// A global read-only vector for saving app names
    static ref APP_NAMES: Vec<&'static str> = {
        let num_app = get_num_app();
        extern "C" {
            fn _app_names();
        }
        let mut start = _app_names as usize as *const u8;
        let mut v = Vec::new();
        unsafe {
            for _ in 0..num_app {
                let mut end = start;
                while end.read_volatile() != b'\0' {
                    end = end.add(1);
                }
                let slice = core::slice::from_raw_parts(start, end as usize - start as usize);
                let str = core::str::from_utf8(slice).unwrap();
                v.push(str);
                start = end.add(1);
            }
        }
        v
    };
}

/// Get elf data by app name
pub fn get_app_data_by_name(name: &str) -> Option<&'static [u8]> {
    let num_app = get_num_app();
    (0..num_app)
        .find(|&i| APP_NAMES[i] == name)
        .map(get_app_data)
}

/// Print all of app names during kernel initialization
pub fn list_apps() {
    println!("/**** APPS ****");
    for app in APP_NAMES.iter() {
        println!("{}", app);
    }
    println!("**************/");
}
  • 第 8 - 13 行:获取link_app.S文件中从_num_app这个标识符的第一行.quad 12,也就是应用数量。

  • 第 16 - 30 行:获取对应应用的数据

    • 第 20 行:获取_num_app也就是应用起始地址。
    • 第 21 行:获取总体应用数量。
    • 第 22 行:获取所有应用的切片。
    • 第 24 - 28 行:获取(需要的应用切片位置开始,下一个应用开始)位置之间的地址,也就是代码段。
  • 第 32 - 55 行:用一个全局可见的 只读 向量 APP_NAMES 来按照顺序将所有应用的名字保存在内存中。

    • 第 35 行:获取总体的应用数量。
    • 第 39 行:获取起始应用地址。
    • 第 40 行:创建返回的向量组。
    • 第 41 - 52 行:对依据link_app.S文件中的顺序将一个个应用加载进APP_NAMES向量组中。
      • 第 44 - 46 行:依据每个应用的结尾'\0'结束符来获取每个应用的大小。
      • 第 47 行:获取应用地址的切片。
      • 第 48 行:将切片进行类型转换。
      • 第 49 行:将转换后的切片push进这个向量组中。
      • 第 50 行:通过将start的地址加一转到下一个应用的首地址,因为这些应用在link_app.S中是连续存放的。
      • 第 53 行:返回最终的v向量组。

    由于每个应用之间有'\0'进行分割,所以可以依据此将每个应用提取出来。

  • 第 58 - 63 行:依据传入的app_name获取对应app_name的应用,也就是找到其所对应的elf文件。

    • 第 59 行:首先获取应用的整个数量。
    • 第 60 行:通过迭代器,闭包等方式在APP_NAMES中找到对应的app段并返回出去。
  • 第 66 - 72 行:通过迭代器列出来所有的app名称

【进程管理的核心数据结构】

【数据结构的关系】

  • 进程标识符 PidHandle 以及内核栈 KernelStack
  • 任务控制块 TaskControlBlock
  • 任务管理器 TaskManager
  • 处理器管理结构 Processor

【进程标识符和内核栈 -> os/src/task/pid.rs】

  • PidHandle代表了进程的pid
  • PidAllocator对进程的pid进行管理也就是管理PidHandle
  • 应用的内核栈标识符为KernelStack,该标识符存储的为相应应用的pid。通过该pid可以找到相应进程的内核栈地址空间。
//! os/src/task/pid.rs
//! Task pid implementation.
//!
//! Assign PID to the process here. At the same time, the position of the application KernelStack
//! is determined according to the PID.

use crate::config::{KERNEL_STACK_SIZE, PAGE_SIZE, TRAMPOLINE};
use crate::mm::{MapPermission, VirtAddr, KERNEL_SPACE};
use crate::sync::UPSafeCell;
use alloc::vec::Vec;
use lazy_static::*;

/// 进程标识符分配器
struct PidAllocator {
    /// A new PID to be assigned
    current: usize,
    /// Recycled PID sequence
    recycled: Vec<usize>,
}

impl PidAllocator {
    pub fn new() -> Self {
        PidAllocator {
            current: 0,
            recycled: Vec::new(),
        }
    }
    pub fn alloc(&mut self) -> PidHandle {
        if let Some(pid) = self.recycled.pop() {
            PidHandle(pid)
        } else {
            self.current += 1;
            PidHandle(self.current - 1)
        }
    }
    pub fn dealloc(&mut self, pid: usize) {
        assert!(pid < self.current);
        assert!(
            !self.recycled.iter().any(|ppid| *ppid == pid),
            "pid {} has been deallocated!",
            pid
        );
        self.recycled.push(pid);
    }
}

lazy_static! {
    /// Pid allocator instance through lazy_static!
    static ref PID_ALLOCATOR: UPSafeCell<PidAllocator> =
        unsafe { UPSafeCell::new(PidAllocator::new()) };
}

/// Abstract structure of PID
pub struct PidHandle(pub usize);

impl Drop for PidHandle {
    fn drop(&mut self) {
        //println!("drop pid {}", self.0);
        PID_ALLOCATOR.exclusive_access().dealloc(self.0);
    }
}

pub fn pid_alloc() -> PidHandle {
    PID_ALLOCATOR.exclusive_access().alloc()
}

/// Return (bottom, top) of a kernel stack in kernel space.
pub fn kernel_stack_position(app_id: usize) -> (usize, usize) {
    let top = TRAMPOLINE - app_id * (KERNEL_STACK_SIZE + PAGE_SIZE);
    let bottom = top - KERNEL_STACK_SIZE;
    (bottom, top)
}

/// 进程的内核栈标识符
pub struct KernelStack {
    pid: usize,
}

impl KernelStack {
    pub fn new(pid_handle: &PidHandle) -> Self {
        let pid = pid_handle.0;
        let (kernel_stack_bottom, kernel_stack_top) = kernel_stack_position(pid);
        KERNEL_SPACE.exclusive_access().insert_framed_area(
            kernel_stack_bottom.into(),
            kernel_stack_top.into(),
            MapPermission::R | MapPermission::W,
        );
        KernelStack { pid: pid_handle.0 }
    }
    #[allow(unused)]
    /// Push a variable of type T into the top of the KernelStack and return its raw pointer
    pub fn push_on_top<T>(&self, value: T) -> *mut T
    where
        T: Sized,
    {
        let kernel_stack_top = self.get_top();
        let ptr_mut = (kernel_stack_top - core::mem::size_of::<T>()) as *mut T;
        unsafe {
            *ptr_mut = value;
        }
        ptr_mut
    }
    pub fn get_top(&self) -> usize {
        let (_, kernel_stack_top) = kernel_stack_position(self.pid);
        kernel_stack_top
    }
}

impl Drop for KernelStack {
    fn drop(&mut self) {
        let (kernel_stack_bottom, _) = kernel_stack_position(self.pid);
        let kernel_stack_bottom_va: VirtAddr = kernel_stack_bottom.into();
        KERNEL_SPACE
            .exclusive_access()
            .remove_area_with_start_vpn(kernel_stack_bottom_va.into());
    }
}
  • 第 14 - 19 行:定义了栈式分配策略的进程标识符分配器 PidAllocator

    • 第 16 行:current代表当前进程标识符分配器最大的进程ID+1
    • 第 18 行:recycled存储了还未分配的进程ID
  • 第 24 - 45 行:为PidAllocator实现了三个方法,newallocdealloc

    • 第 22 - 27 行:通过new方法创建一个进程标识符分配器,创建PID0,创建recycled向量表。
    • 第 28 - 35 行:PidAllocator::alloc 将会分配出去一个将 usize 包装之后的 PidHandle
      • 第 29 行:从recycledpop出一个进程ID
      • 第 30 行:pop成功则直接返回这个进程ID作为新分配的进程ID
      • 第 32 行:如果没有pop成功则将当前current值加一,增加分配器的总进程数量。
      • 第 33 行:创建一个current-1的值作为新的进程ID分配出去。
    • 第 36 - 44 行:PidAllocator::dealloc 会检查pid是否合法后将其回收,也就是加入到recycled中。
      • 第 37 行:用current来判断pid是否合法,也就是小于current
      • 第 38 行:在recycled中查找pid是否存在。
      • 第 39 行:将释放的pid重新加入到recycled中。
  • 第 47 - 51 行:将进程标识符分配器进行实例化为 PID_ALLOCATOR

  • 第 54 行:同一时间存在的所有进程都有一个自己的进程标识符,它们是互不相同的整数。这里我们使用 RAII 的思想,将其抽象为一个 PidHandle 类型,当它的生命周期结束后,对应的整数会被编译器自动回收。

  • 第 56 - 61 行:为PidHandle实现了Drop Trait 来允许编译器进行自动的资源回收。

  • 第 63 - 65 行:我们将PidAllocator::alloc包装为一个全局分配进程标识符的接口 pid_alloc 提供给内核的其他子模块。

在这里插入图片描述

  • 第 68 - 72 行:kernel_stack_position函数来根据进程标识符计算内核栈在内核地址空间中的位置。返回的第一个参数为对应pid的内核栈栈底,第二个参数为栈顶。

  • 第 75 - 77 行:之前我们将每个应用的内核栈按照应用编号从小到大的顺序将它们作为逻辑段从高地址到低地址放在内核地址空间中,且两两之间保留一个守护页面使得我们能够尽可能早的发现内核栈溢出问题。从本章开始,我们将应用编号替换为进程标识符。我们可以在内核栈 KernelStack保存着它所属进程的 PID

  • 第 79 - 107 行:为KernelStack实现了三个方法,newpush_on_topget_top

    • 第 80 - 89 行:为KernelStack实现了new方法。依据pid创建对应的内核栈。
      • 第 81 行:由于pid_handle是结构体,所以先获取其里面的pid
      • 第 82 行:依据kernel_stack_position函数计算对应pid的栈顶和栈底。
      • 第 83 - 87 行:在当前地址空间KERNEL_SPACE中插入一个 Framed 方式映射到物理内存的逻辑段,该逻辑段就是相应pid的内核地址空间。
      • 第 88 行:将pid放到KernelStack中。
    • 第 92 - 102 行:push_on_top 方法可以将一个类型为 T 的变量压入内核栈顶并返回其裸指针,这也是一个泛型函数。
    • 第 103 - 106 行:get_top方法通过kernel_stack_position获取对应pid的内核栈栈顶。
  • 第 109 - 117 行:为KernelStack实现了drop方法。一旦它的生命周期结束则在内核地址空间中将对应的逻辑段删除,这也就意味着那些物理页帧被同时回收掉了。

    内核栈 KernelStack 也用到了 RAII 的思想,具体来说,实际保存它的物理页帧的生命周期与它绑定在一起,当 KernelStack 生命周期结束后,这些物理页帧也将会被编译器自动回收。

【进程控制块 -> os/src/task/task.rs】

进程抽象的对应**实现是进程控制块TaskControlBlock也简称TCB **,它是内核对进程进行管理的单位。在内核看来,它就等价于一个进程。

//! os/src/task/task.rs
//! Types related to task management & Functions for completely changing TCB

use super::TaskContext;
use super::{pid_alloc, KernelStack, PidHandle};
use crate::config::TRAP_CONTEXT;
use crate::mm::{MemorySet, PhysPageNum, VirtAddr, KERNEL_SPACE};
use crate::sync::UPSafeCell;
use crate::trap::{trap_handler, TrapContext};
use alloc::sync::{Arc, Weak};
use alloc::vec::Vec;
use core::cell::RefMut;

/// Task control block structure
/// 创建任务控制块结构体,其作用为进程控制块
/// Directly save the contents that will not change during running
pub struct TaskControlBlock {
    // immutable
    /// Process identifier
    pub pid: PidHandle,
    /// Kernel stack corresponding to PID
    pub kernel_stack: KernelStack,
    // mutable
    inner: UPSafeCell<TaskControlBlockInner>,
}

/// Structure containing more process content
///
/// Store the contents that will change during operation
/// and are wrapped by UPSafeCell to provide mutual exclusion
pub struct TaskControlBlockInner {
    /// The physical page number of the frame where the trap context is placed
    pub trap_cx_ppn: PhysPageNum,
    /// Application data can only appear in areas
    /// where the application address space is lower than base_size
    pub base_size: usize,
    /// Save task context
    pub task_cx: TaskContext,
    /// Maintain the execution status of the current process
    pub task_status: TaskStatus,
    /// Application address space
    pub memory_set: MemorySet,
    /// Parent process of the current process.
    /// Weak will not affect the reference count of the parent
    pub parent: Option<Weak<TaskControlBlock>>,
    /// A vector containing TCBs of all child processes of the current process
    pub children: Vec<Arc<TaskControlBlock>>,
    /// It is set when active exit or execution error occurs
    pub exit_code: i32,
}

/// Simple access to its internal fields
impl TaskControlBlockInner {
    /*
    pub fn get_task_cx_ptr2(&self) -> *const usize {
        &self.task_cx_ptr as *const usize
    }
    */
    pub fn get_trap_cx(&self) -> &'static mut TrapContext {
        self.trap_cx_ppn.get_mut()
    }
    pub fn get_user_token(&self) -> usize {
        self.memory_set.token()
    }
    fn get_status(&self) -> TaskStatus {
        self.task_status
    }
    pub fn is_zombie(&self) -> bool {
        self.get_status() == TaskStatus::Zombie
    }
}

impl TaskControlBlock {
    /// Get the mutex to get the RefMut TaskControlBlockInner
    pub fn inner_exclusive_access(&self) -> RefMut<'_, TaskControlBlockInner> {
        self.inner.exclusive_access()
    }

    /// Create a new process
    ///
    /// At present, it is only used for the creation of initproc
    pub fn new(elf_data: &[u8]) -> Self {
        // memory_set with elf program headers/trampoline/trap context/user stack
        let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
        let trap_cx_ppn = memory_set
            .translate(VirtAddr::from(TRAP_CONTEXT).into())
            .unwrap()
            .ppn();
        // alloc a pid and a kernel stack in kernel space
        let pid_handle = pid_alloc();
        let kernel_stack = KernelStack::new(&pid_handle);
        let kernel_stack_top = kernel_stack.get_top();
        // push a task context which goes to trap_return to the top of kernel stack
        let task_control_block = Self {
            pid: pid_handle,
            kernel_stack,
            inner: unsafe {
                UPSafeCell::new(TaskControlBlockInner {
                    trap_cx_ppn,
                    base_size: user_sp,
                    task_cx: TaskContext::goto_trap_return(kernel_stack_top),
                    task_status: TaskStatus::Ready,
                    memory_set,
                    parent: None,
                    children: Vec::new(),
                    exit_code: 0,
                })
            },
        };
        // prepare TrapContext in user space
        let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
        *trap_cx = TrapContext::app_init_context(
            entry_point,
            user_sp,
            KERNEL_SPACE.exclusive_access().token(),
            kernel_stack_top,
            trap_handler as usize,
        );
        task_control_block
    }
    /// Load a new elf to replace the original application address space and start execution
    pub fn exec(&self, elf_data: &[u8]) {
        // memory_set with elf program headers/trampoline/trap context/user stack
        let (memory_set, user_sp, entry_point) = MemorySet::from_elf(elf_data);
        let trap_cx_ppn = memory_set
            .translate(VirtAddr::from(TRAP_CONTEXT).into())
            .unwrap()
            .ppn();

        // **** access inner exclusively
        let mut inner = self.inner_exclusive_access();
        // substitute memory_set
        inner.memory_set = memory_set;
        // update trap_cx ppn
        inner.trap_cx_ppn = trap_cx_ppn;
        // initialize trap_cx
        let trap_cx = inner.get_trap_cx();
        *trap_cx = TrapContext::app_init_context(
            entry_point,
            user_sp,
            KERNEL_SPACE.exclusive_access().token(),
            self.kernel_stack.get_top(),
            trap_handler as usize,
        );
        // **** release inner automatically
    }
    /// Fork from parent to child
    pub fn fork(self: &Arc<TaskControlBlock>) -> Arc<TaskControlBlock> {
        // ---- access parent PCB exclusively
        let mut parent_inner = self.inner_exclusive_access();
        // copy user space(include trap context)
        let memory_set = MemorySet::from_existed_user(&parent_inner.memory_set);
        let trap_cx_ppn = memory_set
            .translate(VirtAddr::from(TRAP_CONTEXT).into())
            .unwrap()
            .ppn();
        // alloc a pid and a kernel stack in kernel space
        let pid_handle = pid_alloc();
        let kernel_stack = KernelStack::new(&pid_handle);
        let kernel_stack_top = kernel_stack.get_top();
        let task_control_block = Arc::new(TaskControlBlock {
            pid: pid_handle,
            kernel_stack,
            inner: unsafe {
                UPSafeCell::new(TaskControlBlockInner {
                    trap_cx_ppn,
                    base_size: parent_inner.base_size,
                    task_cx: TaskContext::goto_trap_return(kernel_stack_top),
                    task_status: TaskStatus::Ready,
                    memory_set,
                    parent: Some(Arc::downgrade(self)),
                    children: Vec::new(),
                    exit_code: 0,
                })
            },
        });
        // add child
        parent_inner.children.push(task_control_block.clone());
        // modify kernel_sp in trap_cx
        // **** access children PCB exclusively
        let trap_cx = task_control_block.inner_exclusive_access().get_trap_cx();
        trap_cx.kernel_sp = kernel_stack_top;
        // return
        task_control_block
        // ---- release parent PCB automatically
        // **** release children PCB automatically
    }
    pub fn getpid(&self) -> usize {
        self.pid.0
    }
}

#[derive(Copy, Clone, PartialEq)]
/// task status: UnInit, Ready, Running, Exited
pub enum TaskStatus {
    UnInit,
    Ready,
    Running,
    Zombie,
}
  • 第 17 - 25 行:创建任务控制块,目前其功能与进程控制块一样。

    • 第 20 - 22 行:在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符 PidHandle内核栈 KernelStack 放在其中。
    • 第 24 行:在运行过程中可能发生变化的则放在 TaskControlBlockInner 中,将它再包裹上一层 UPSafeCell<T> 放在任务控制块中。 在此使用 UPSafeCell<T> 可以提供互斥从而避免数据竞争。
  • 第 31 - 50 行:TaskControlBlockInner存储在操作期间将更改的内容。

    • 第 33 行:trap_cx_ppn 指出了应用地址空间中的 Trap 上下文被放在的物理页帧的物理页号

    • 第 36 行:base_size 的含义是应用数据仅有可能出现在应用地址空间低于 base_size 字节的区域中。借助它我们可以清楚的知道应用有多少数据驻留在内存中

    • 第 33 行:task_cx 保存任务上下文,用于任务切换。

    • 第 33 行:task_status 维护当前进程的执行状态

    • 第 33 行:memory_set 表示应用地址空间

    • 第 33 行:parent 指向当前进程的父进程(如果存在的话)。注意我们使用 Weak 而非 Arc 来包裹另一个任务控制块,因此这个智能指针将不会影响父进程的引用计数。

    • 第 33 行:children 则将当前进程的所有子进程的任务控制块Arc 智能指针的形式保存在一个向量中,这样才能够更方便的找到它们。

    • 第 33 行:当进程调用 exit 系统调用主动退出或者执行出错由内核终止的时候,它的退出码 exit_code 会被内核保存在它的任务控制块中,并等待它的父进程通过 waitpid 回收它的资源的同时也收集它的 PID 以及退出码。

      我们在维护父子进程关系的时候大量用到了智能指针 Arc/Weak ,当且仅当它的引用计数变为 0 的时候,进程控制块以及被绑定到它上面的各类资源才会被回收。

  • 第 53 - 71 行:为TaskControlBlockInner实现了各种方法,主要是对于它内部字段的快捷访问。

  • 第 73 - 191 行:为TaskControlBlock 实现了各种方法。

    • 第 75 - 77 行:实现了inner_exclusive_access方法,尝试获取互斥锁来得到 TaskControlBlockInner 的可变引用

    • 第 82 - 120 行:new 用来创建一个新的进程,目前仅用于内核中手动创建唯一一个初始进程 initproc

      • 第 84 行:我们解析应用的 ELF 执行文件得到应用地址空间 memory_set用户栈在应用地址空间中的位置 user_sp 以及应用的入口点 entry_point
      • 第 85 行:我们手动查页表找到位于应用地址空间中新创建的**Trap 上下文被实际放在哪个物理页帧**上,用来做后续的初始化。
      • 第 90 - 92 行:我们为该进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 kernel_stack_top
      • 第 94 - 109 行:我们整合之前的部分信息创建进程控制块 task_control_block
      • 第 111 行:获取进程控制块的陷入上下文。
      • 第 112 - 118 行:我们初始化位于该进程应用地址空间中的 Trap 上下文,使得第一次进入用户态的时候时候能正确跳转到应用入口点并设置好用户栈,同时也保证在 Trap 的时候用户态能正确进入内核态。
      • 第 119 行:将 task_control_block 返回。

      创建新的进程:主要是依据对应应用的ELF文件,将其中的各个段进行区分出来,同时创建pid。将所有上述的信息综合成TCB进程控制块,并填充其内部的trap_context使得Trap的时候能够进入内核态并return的时候能找到执行程序。

    • 第 122 - 146 行:exec 用来实现 exec 系统调用,即当前进程加载并执行另一个 ELF 格式可执行文件

      • 第 124 行:解析传入的ELF格式文件数据,生成对应的地址空间。

      • 第 125 行:我们手动查页表找到位于应用地址空间中新创建的**Trap 上下文被实际放在哪个物理页帧**上,用来做后续的初始化。

      • 第 131 行:获得该进程inner的使用权。

      • 第 133 行:替换地址空间,这将导致原有的地址空间生命周期结束,里面包含的全部物理页帧都会被回收。

      • 第 135 行:更新trap上下文的物理页。

      • 第 137 - 144 行:修改新的地址空间中的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化,这样才能正常实现 Trap 机制。

        exec的实现其实是依据elf文件创建一个新的地址空间并将新地址空间加入到TCB中,同时依据ELF格式文件修改原TCB的某些数据。

    • 第 148 - 187 行:fork 用来实现 fork 系统调用,即当前进程 fork 出来一个与之几乎相同的子进程。

      • 第 150 行:获取当前进程inner段的可执行权限。

      • 第 152 行:依据父进程为子进程创建一个新的地址空间。这里不是通过ELF文件获得的。

      • 第 153 行:我们手动查页表找到位于应用地址空间中新创建的**Trap 上下文被实际放在哪个物理页帧**上,用来做后续的初始化。

      • 第 158 - 160 行:我们为该进程分配 PID 以及内核栈,并记录下内核栈在内核地址空间的位置 kernel_stack_top

      • 第 161 - 176 行:我们整合之前的部分信息创建进程控制块 task_control_block 。注意第171行我们将父进程的弱引用计数放到子进程的进程控制块中。

      • 第 178 行:将子进程插入到父进程的孩子向量 children 中。

      • 第 181 - 182 行:将新分配的内核栈添加到陷入式上下文中。

        我们在子进程内核栈上压入一个初始化的任务上下文,使得内核一旦通过任务切换到该进程,就会跳转到 trap_return 来进入用户态。而在复制地址空间的时候,子进程的 Trap 上下文也是完全从父进程复制过来的,这可以保证子进程进入用户态和其父进程回到用户态的那一瞬间 CPU 的状态是完全相同的(后面我们会让它们的返回值不同从而区分两个进程)。而两个进程的应用数据由于地址空间复制的原因也是完全相同的,这是 fork 语义要求做到的。

      • 第 184 行:返回TCB

    • 第 188 - 190 行:getpidusize 的形式返回当前进程的进程标识符。

  • 第 195 - 200 行:提供了进程运行的各种状态。

在这里插入图片描述

【任务管理器 -> os/src/task/manager.rs】

在这里任务管理器自身仅负责管理所有任务(进程),并不像上一章一样维护着 CPU 当前在执行哪个任务。

//! os/src/task/manager.rs
//! Implementation of [`TaskManager`]
//! 
//! It is only used to manage processes and schedule process based on ready queue.
//! Other CPU process monitoring functions are in Processor.

use super::TaskControlBlock;
use crate::sync::UPSafeCell;
use alloc::collections::VecDeque;
use alloc::sync::Arc;
use lazy_static::*;

pub struct TaskManager {
    ready_queue: VecDeque<Arc<TaskControlBlock>>,
}

/// A simple FIFO scheduler.
impl TaskManager {
    pub fn new() -> Self {
        Self {
            ready_queue: VecDeque::new(),
        }
    }
    /// Add process back to ready queue
    pub fn add(&mut self, task: Arc<TaskControlBlock>) {
        self.ready_queue.push_back(task);
    }
    /// Take a process out of the ready queue
    pub fn fetch(&mut self) -> Option<Arc<TaskControlBlock>> {
        self.ready_queue.pop_front()
    }
}

lazy_static! {
    /// TASK_MANAGER instance through lazy_static!
    pub static ref TASK_MANAGER: UPSafeCell<TaskManager> =
        unsafe { UPSafeCell::new(TaskManager::new()) };
}

pub fn add_task(task: Arc<TaskControlBlock>) {
    TASK_MANAGER.exclusive_access().add(task);
}

pub fn fetch_task() -> Option<Arc<TaskControlBlock>> {
    TASK_MANAGER.exclusive_access().fetch()
}
  • 第 13 - 15 行:创建任务管理结构体,TaskManager 将所有的任务控制块用引用计数 Arc 智能指针包裹后放在一个双端队列 VecDeque 中。 使用智能指针的原因在于,任务控制块经常需要被放入/取出,如果直接移动任务控制块自身将会带来大量的数据拷贝开销, 而对于智能指针进行移动则没有多少开销。其次,允许任务控制块的共享引用在某些情况下能够让我们的实现更加方便。
  • 第 18 - 32 行:为TaskManager实现了newaddfetch这三个方法。调度算法来看,这里用到的就是最简单的 RR 算法。
    • 第 19 - 23 行:使用new方法创建一个双端队列。
    • 第 25 - 27 行:使用add方法将一个任务加入队尾。
    • 第 29 - 31 行:使用fetch方法从队头中取出一个任务来执行。
  • 第 34 - 38 行:全局实例 TASK_MANAGERTaskManager类型。
  • 第 40 - 42 行:将TASK_MANAGER::add进行封装为add_task提供给内核的其他子模块。
  • 第 44 - 46 行:将TASK_MANAGER::fetch进行封装为fetch_task提供给内核的其他子模块。

TaskManager主要管理所有进程的运行顺序,通过RR算法的思想进行执行。

在这里插入图片描述

【处理器管理结构 -> os/src/task/processor.rs】

处理器管理结构 Processor 负责维护从任务管理器 TaskManager 分离出去的那部分 CPU 状态

每个 Processor 都有一个 idle 控制流,它们运行在每个核各自的启动栈上,功能是尝试从任务管理器中选出一个任务来在当前核上执行。 在内核初始化完毕之后,核通过调用 run_tasks 函数来进入 idle 控制流:

//! os/src/task/processor.rs
//! Implementation of [`Processor`] and Intersection of control flow
//! Here, the continuous operation of user apps in CPU is maintained,
//! the current running state of CPU is recorded,
//! and the replacement and transfer of control flow of different applications are executed.

use super::__switch;
use super::{fetch_task, TaskStatus};
use super::{TaskContext, TaskControlBlock};
use crate::sync::UPSafeCell;
use crate::trap::TrapContext;
use alloc::sync::Arc;
use lazy_static::*;

/// Processor management structure
pub struct Processor {
    /// The task currently executing on the current processor
    current: Option<Arc<TaskControlBlock>>,
    /// The basic control flow of each core, helping to select and switch process
    idle_task_cx: TaskContext,
}

impl Processor {
    pub fn new() -> Self {
        Self {
            current: None,
            idle_task_cx: TaskContext::zero_init(),
        }
    }
    fn get_idle_task_cx_ptr(&mut self) -> *mut TaskContext {
        &mut self.idle_task_cx as *mut _
    }
    pub fn take_current(&mut self) -> Option<Arc<TaskControlBlock>> {
        self.current.take()
    }
    pub fn current(&self) -> Option<Arc<TaskControlBlock>> {
        self.current.as_ref().map(|task| Arc::clone(task))
    }
}

lazy_static! {
    /// PROCESSOR instance through lazy_static!
    pub static ref PROCESSOR: UPSafeCell<Processor> = unsafe { UPSafeCell::new(Processor::new()) };
}

/// The main part of process execution and scheduling
///
/// Loop fetch_task to get the process that needs to run,
/// and switch the process through __switch
pub fn run_tasks() {
    loop {
        let mut processor = PROCESSOR.exclusive_access();
        if let Some(task) = fetch_task() {
            let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
            // access coming task TCB exclusively
            let mut task_inner = task.inner_exclusive_access();
            let next_task_cx_ptr = &task_inner.task_cx as *const TaskContext;
            task_inner.task_status = TaskStatus::Running;
            drop(task_inner);
            // release coming task TCB manually
            processor.current = Some(task);
            // release processor manually
            drop(processor);
            unsafe {
                __switch(idle_task_cx_ptr, next_task_cx_ptr);
            }
        }
    }
}

/// Get current task through take, leaving a None in its place
pub fn take_current_task() -> Option<Arc<TaskControlBlock>> {
    PROCESSOR.exclusive_access().take_current()
}

/// Get a copy of the current task
pub fn current_task() -> Option<Arc<TaskControlBlock>> {
    PROCESSOR.exclusive_access().current()
}

/// Get token of the address space of current task
pub fn current_user_token() -> usize {
    let task = current_task().unwrap();
    let token = task.inner_exclusive_access().get_user_token();
    token
}

/// Get the mutable reference to trap context of current task
pub fn current_trap_cx() -> &'static mut TrapContext {
    current_task()
        .unwrap()
        .inner_exclusive_access()
        .get_trap_cx()
}

/// Return to idle control flow for new scheduling
pub fn schedule(switched_task_cx_ptr: *mut TaskContext) {
    let mut processor = PROCESSOR.exclusive_access();
    let idle_task_cx_ptr = processor.get_idle_task_cx_ptr();
    drop(processor);
    unsafe {
        __switch(switched_task_cx_ptr, idle_task_cx_ptr);
    }
}
  • 第 16 - 21 行:创建处理器管理结构 Processor 描述CPU 执行状态
    • 第 18 行:current 表示在当前处理器上正在执行的任务
    • 第 20 行:idle_task_cx 表示当前处理器上的 idle 控制流的任务上下文
  • 第 23 - 39 行:为Processor实现四个方法,newget_idle_task_cx_ptrtake_currentcurrent
    • 第 24 - 29 行:通过new方法创建Processor结构,current0,初始化上下文。
    • 第 30 - 32 行:返回idle_task_cx_ptr字段。
    • 第 33 - 35 行:Processor::take_current 可以取出当前正在执行的任务。 Option::take 意味着 current 字段也变为 None
    • 第 36 - 38 行:Processor::current 返回当前执行的任务的一份拷贝。
  • 第 43 行:创建单个 Processor 的全局实例 PROCESSOR
  • 第 50 - 69 行:创建run_tasks函数,它循环调用 fetch_task 直到顺利从任务管理器中取出一个任务,然后获得 __switch 两个参数进行任务切换。注意在整个过程中要严格控制临界区。
    • 第 53 行:尝试通过fetch_task函数从任务管理器中取出一个任务。
    • 第 54 行:获取idle状态的上下文。
    • 第 57 行:获取下一个应用的上下文。
    • 第 58 行:设置进程状态为Running
    • 第 59 行:需要手动回收对即将执行任务的任务控制块的借用标记,使得后续我们仍可以访问该任务控制块。这里我们不能依赖编译器在 if let 块结尾时的自动回收,因为中间我们会在自动回收之前调用 __switch ,这将导致我们在实际上已经结束访问却没有进行回收的情况下切换到下一个任务,最终可能违反 UPSafeCell 的借用约定而使得内核报错退出。
    • 第 61 行:将Processorcurrent设为将要运行的task
    • 第 63 行:手动回收 PROCESSOR 的借用标记。
    • 第 65 行:通过__switch方法进行进程的切换。(__switch函数主要做的事情就是将依据传入的两个参数,将当前的状态(14个寄存器)保存到第一个参数中,再将第二个参数中所保存的状态(14个寄存器)恢复到当前寄存器中。当返回的时候就会从下一个应用所传入的ra字段加载运行。也就顺利成章的完成了任务的切换。)
  • 第 71 - 74 行:take_current_task函数对Processor::take_current 进行了封装,可以取出当前正在执行的任务。
  • 第 77 - 79 行:current_task函数对Processor::current 进行了封装,可以取出当前正在执行的任务的拷贝。
  • 第 82 - 86 行:current_user_token函数获取当前运行的进程的地址空间(TaskControlBlockInner结构)。
  • 第 89 - 94 行:current_trap_cx函数获取当前运行的进程的上下文(TaskControlBlockInner结构)。
  • 第 97 - 104 行:当一个应用交出 CPU 使用权时,进入内核后它会调用 schedule 函数来切换到 idle 控制流并开启新一轮的任务调度。需要传入即将被切换出去的任务的 task_cx_ptr 来在合适的位置保存任务上下文。也就是将当前的上下文保存在传入进来的cx中,将idle的上下文切回来。
    • 第 99 行:获取idle_task_cx也就是idle状态的上下文。
    • 第 102 行:通过__switch汇编进行任务的切换,切换回idle控制流。切换回去之后,内核将跳转到 Processor::run__switch 返回之后的位置,也即开启了下一轮的调度循环。

进程管理核心数据结构小结:Processor处理器管理结构主要管理CPU当前进程的执行,到底执行哪一个进程。而执行哪一个进程依据TaskManager任务管理器里面双端队列里面是否有需要执行的进程。如果有则将idle控制流与进行的上下文进行交换执行进程。当一个应用执行完后交出 CPU 使用权时,进入内核后它会调用 schedule 函数来切换到 idle 控制流并开启新一轮的任务调度。

在这里插入图片描述

【进程管理机制的设计实现】

【进程管理机制实现概述】

  1. 创建初始进程:创建第一个用户态进程 initproc
  2. 进程生成机制:介绍进程相关的系统调用 sys_fork/sys_exec
  3. 进程调度机制:进程主动/被动切换
  4. 进程资源回收机制:调用sys_exit 退出或进程终止后保存其退出码
  5. 进程资源回收机制:父进程通过 sys_waitpid 收集该进程的信息并回收其资源
  6. 字符输入机制:通过**sys_read 系统调用获得字符输入**

【创建初始进程 -> os/src/task/mod.rs】

内核初始化完毕之后即会调用 task 子模块提供的 add_initproc 函数来将初始进程 initproc 加入任务管理器。

//! os/src/task/mod.rs
//! Implementation of process management mechanism
//! Here is the entry for process scheduling required by other modules
//! (such as syscall or clock interrupt).
//! By suspending or exiting the current process, you can
//! modify the process state, manage the process queue through TASK_MANAGER,
//! and switch the control flow through PROCESSOR.
//!
//! Be careful when you see [`__switch`]. Control flow around this function
//! might not be what you expect.

mod context;
mod manager;
mod pid;
mod processor;
mod switch;
#[allow(clippy::module_inception)]
mod task;

use crate::loader::get_app_data_by_name;
use alloc::sync::Arc;
use lazy_static::*;
use manager::fetch_task;
use switch::__switch;
pub use task::{TaskControlBlock, TaskStatus};

pub use context::TaskContext;
pub use manager::add_task;
pub use pid::{pid_alloc, KernelStack, PidHandle};
pub use processor::{
    current_task, current_trap_cx, current_user_token, run_tasks, schedule, take_current_task,
};

/// Make current task suspended and switch to the next task
pub fn suspend_current_and_run_next() {
    // There must be an application running.
    let task = take_current_task().unwrap();

    // ---- access current TCB exclusively
    let mut task_inner = task.inner_exclusive_access();
    let task_cx_ptr = &mut task_inner.task_cx as *mut TaskContext;
    // Change status to Ready
    task_inner.task_status = TaskStatus::Ready;
    drop(task_inner);
    // ---- release current PCB

    // push back to ready queue.
    add_task(task);
    // jump to scheduling cycle
    schedule(task_cx_ptr);
}

/// Exit current task, recycle process resources and switch to the next task
pub fn exit_current_and_run_next(exit_code: i32) {
    // take from Processor
    let task = take_current_task().unwrap();
    // **** access current TCB exclusively
    let mut inner = task.inner_exclusive_access();
    // Change status to Zombie
    inner.task_status = TaskStatus::Zombie;
    // Record exit code
    inner.exit_code = exit_code;
    // do not move to its parent but under initproc

    // ++++++ access initproc TCB exclusively
    {
        let mut initproc_inner = INITPROC.inner_exclusive_access();
        for child in inner.children.iter() {
            child.inner_exclusive_access().parent = Some(Arc::downgrade(&INITPROC));
            initproc_inner.children.push(child.clone());
        }
    }
    // ++++++ release parent PCB

    inner.children.clear();
    // deallocate user space
    inner.memory_set.recycle_data_pages();
    drop(inner);
    // **** release current PCB
    // drop task manually to maintain rc correctly
    drop(task);
    // we do not have to save task context
    let mut _unused = TaskContext::zero_init();
    schedule(&mut _unused as *mut _);
}

lazy_static! {
    /// Creation of initial process
    ///
    /// the name "initproc" may be changed to any other app name like "usertests",
    /// but we have user_shell, so we don't need to change it.
    pub static ref INITPROC: Arc<TaskControlBlock> = Arc::new(TaskControlBlock::new(
        get_app_data_by_name("ch5b_initproc").unwrap()
    ));
}

pub fn add_initproc() {
    add_task(INITPROC.clone());
}
  • 第 35 - 51 行:suspend_current_and_run_next函数可以暂停当前任务并切换到下一个任务

    • 第 37 行:通过take_current_task函数获取当前的进程。
    • 第 40 行:获取当前进程inner段的权限。
    • 第 41 行:获取当前进程的上下文。
    • 第 43 行:将当前正在执行的进程的状态改为就绪态。
    • 第 44 行:手动回收对任务的任务控制块的借用标记
    • 第 48 行:将这个任务放入任务管理器的队尾。
    • 第 49 行:调用 schedule 函数来触发调度并切换任务。注意,当仅有一个任务的时候, suspend_current_and_run_next 的效果是会继续执行这个任务。
  • 第 54 - 85 行:exit_current_and_run_next函数可以退出当前任务并切换到下一个任务,与之前比exit_current_and_run_next 带有一个退出码作为参数。当在 sys_exit 正常退出的时候,退出码由应用传到内核中;而出错退出的情况(如访存错误或非法指令异常)则是由内核指定一个特定的退出码。这个退出码会在 exit_current_and_run_next 写入当前进程的进程控制块中:

    • 第 56 行:我们调用 take_current_task 来将当前进程控制块从处理器监控 PROCESSOR 中取出而不是得到一份拷贝,这是为了正确维护进程控制块的引用计数。
    • 第 58 行:我们获得inner段的权限。
    • 第 60 行:我们将进程控制块中的状态修改为 TaskStatus::Zombie 即僵尸进程,这样它后续才能被父进程在 waitpid 系统调用的时候回收。
    • 第 62 行:我们将传入的退出码 exit_code 写入进程控制块中,后续父进程在 waitpid 的时候可以收集。
    • 第 67 - 71 行:将当前进程的所有子进程挂在初始进程 initproc 下面,其做法是遍历每个子进程,修改其父进程为初始进程,并加入初始进程的孩子向量中。
    • 第 75 行:将当前进程的孩子向量清空。
    • 第 77 行:对于当前进程占用的资源进行早期回收。只是将地址空间中的逻辑段列表 areas 清空,这将导致应用地址空间被回收,但用来存放页表的那些物理页帧此时还不会被回收。
    • 第 83 - 84 行:我们调用 schedule 触发调度及任务切换,由于我们再也不会回到该进程的执行过程中,因此无需关心任务上下文的保存。
  • 第 87 - 95 行:基于 lazy_static 在运行时初始化初始进程的进程控制块 INITPROC。也就是调用TaskControlBlock::new 来创建一个进程控制块,它需要传入 ELF 可执行文件的数据切片作为参数,这可以通过加载器 loader 子模块提供的 get_app_data_by_name 接口查找 initprocELF 执行文件数据来获得。

  • 第 97 - 99 行:创建add_initproc函数,在初始化 INITPROC 之后,就可以在 add_initproc 中调用 task 的任务管理器 manager 子模块提供的 add_task 接口,将其加入到任务管理器,然后idle执行流程就会执行这个进程。

【进程调度机制 -> os/src/syscall/process.rs】

通过调用 task 子模块提供的 suspend_current_and_run_next 函数可以暂停当前任务并切换到下一个任务(具体代码详见上面所述哈),下面给出了两种典型的使用情况:

  • 当应用调用 sys_yield 主动交出使用权
  • 本轮时间片用尽或者由于某些原因内核中的处理无法继续的时候,就会在内核中调用此函数触发调度机制并进行任务切换trap_handler中。
//! os/src/syscall/process.rs
//! Process management syscalls

use crate::loader::get_app_data_by_name;
use crate::mm::{translated_refmut, translated_str};
use crate::task::{
    add_task, current_task, current_user_token, exit_current_and_run_next,
    suspend_current_and_run_next, TaskStatus,
};
use crate::timer::get_time_us;
use alloc::sync::Arc;
use crate::config::MAX_SYSCALL_NUM;

#[repr(C)]
#[derive(Debug)]
pub struct TimeVal {
    pub sec: usize,
    pub usec: usize,
}

#[derive(Clone, Copy)]
pub struct TaskInfo {
    pub status: TaskStatus,
    pub syscall_times: [u32; MAX_SYSCALL_NUM],
    pub time: usize,
}

pub fn sys_exit(exit_code: i32) -> ! {
    debug!("[kernel] Application exited with code {}", exit_code);
    exit_current_and_run_next(exit_code);
    panic!("Unreachable in sys_exit!");
}

/// current task gives up resources for other tasks
pub fn sys_yield() -> isize {
    suspend_current_and_run_next();
    0
}

pub fn sys_getpid() -> isize {
    current_task().unwrap().pid.0 as isize
}

/// Syscall Fork which returns 0 for child process and child_pid for parent process
pub fn sys_fork() -> isize {
    let current_task = current_task().unwrap();
    let new_task = current_task.fork();
    let new_pid = new_task.pid.0;
    // modify trap context of new_task, because it returns immediately after switching
    let trap_cx = new_task.inner_exclusive_access().get_trap_cx();
    // we do not have to move to next instruction since we have done it before
    // for child process, fork returns 0
    trap_cx.x[10] = 0;
    // add new task to scheduler
    add_task(new_task);
    new_pid as isize
}

/// Syscall Exec which accepts the elf path
pub fn sys_exec(path: *const u8) -> isize {
    let token = current_user_token();
    let path = translated_str(token, path);
    if let Some(data) = get_app_data_by_name(path.as_str()) {
        let task = current_task().unwrap();
        task.exec(data);
        0
    } else {
        -1
    }
}

/// If there is not a child process whose pid is same as given, return -1.
/// Else if there is a child process but it is still running, return -2.
pub fn sys_waitpid(pid: isize, exit_code_ptr: *mut i32) -> isize {
    let task = current_task().unwrap();
    // find a child process

    // ---- access current TCB exclusively
    let mut inner = task.inner_exclusive_access();
    if !inner
        .children
        .iter()
        .any(|p| pid == -1 || pid as usize == p.getpid())
    {
        return -1;
        // ---- release current PCB
    }
    let pair = inner.children.iter().enumerate().find(|(_, p)| {
        // ++++ temporarily access child PCB lock exclusively
        p.inner_exclusive_access().is_zombie() && (pid == -1 || pid as usize == p.getpid())
        // ++++ release child PCB
    });
    if let Some((idx, _)) = pair {
        let child = inner.children.remove(idx);
        // confirm that child will be deallocated after removing from children list
        assert_eq!(Arc::strong_count(&child), 1);
        let found_pid = child.getpid();
        // ++++ temporarily access child TCB exclusively
        let exit_code = child.inner_exclusive_access().exit_code;
        // ++++ release child PCB
        *translated_refmut(inner.memory_set.token(), exit_code_ptr) = exit_code;
        found_pid as isize
    } else {
        -2
    }
    // ---- release current PCB lock automatically
}
  • 第 35 - 38 行:实现了sys_yield 函数,其内部为切换下一个进程。

  • 第 45 - 57 行:实现了sys_fork 函数。

    • 第 46 行:获取当前进程。
    • 第 47 行:通过当前进程创建子进程。
    • 第 48 行:获取新进程的pid
    • 第 50 行:获取新进程的陷入式上下文。
    • 第 53 行:将子进程的 Trap 上下文中用来存放系统调用返回值的 a0 寄存器修改为 0
    • 第 55 行:将生成的子进程通过 add_task 加入到任务管理器中。
    • 第 56 行:父进程返回新的pid

    sys_fork函数首先通过fork函数创建子进程。

    最后sys_fork通过修改新创建子进程的陷入式上下文里所对应的返回值寄存器里的值,可以使得子进程返回的值为0,而父进程直接返回新进程的pid就好了,trap_handler后续会设置该父进程的返回值寄存器为sys_fork的返回值达到返回子进程pid的操作。

  • 第 60 - 70 行:实现了sys_exec 函数。应用在 sys_exec 系统调用中传递给内核的只有一个要执行的应用名字符串在当前应用地址空间中的起始地址

    • 第 61 行:获取当前应用的token
    • 第 62 行:它调用 translated_str 找到要执行的应用名。
    • 第 63 行:试图在应用加载器提供的 get_app_data_by_name 接口中找到对应的 ELF 格式的数据。
    • 第 64 - 66 行:如果找到,就调用 TaskControlBlock::exec 替换掉地址空间并返回 0。这个返回值其实并没有意义,因为我们在替换地址空间的时候本来就对 Trap 上下文重新进行了初始化。
    • 第 67 - 69 行:如果没有找到,就不做任何事情并返回 -1。在shell程序-user_shell中我们也正是通过这个返回值来判断要执行的应用是否存在。
  • 第 74 - 107 行:实现了sys_waitpid函数。sys_waitpid 是一个立即返回的系统调用,它的返回值语义是:

    如果当前的进程不存在一个进程 IDpidpid== -1 或 pid > 0)的子进程,则返回 -1

    如果存在一个进程 IDpid 的僵尸子进程,则正常回收并返回子进程的 pid,并更新系统调用的退出码参数为 exit_code

    这里还有一个 -2 的返回值,它的含义是子进程还没退出,通知用户库 user_lib (是实际发出系统调用的地方),这样用户库看到是 -2 后,就进一步调用 sys_yield 系统调用,让当前父进程进入等待状态。

    注:在编写应用的开发者看来, 位于用户库 user_lib 中的 wait/waitpid 两个辅助函数都必定能够返回一个有意义的结果,要么是 -1,要么是一个正数 PID ,是不存在 -2 这种通过等待即可消除的中间结果的。让调用 wait/waitpid 两个辅助函数的进程等待正是在用户库 user_lib 中完成。

    • 第 80 - 87 行:判断 sys_waitpid 是否会返回 -1 ,这取决于当前进程是否有一个符合要求的子进程。当传入的 pid-1 的时候,任何一个子进程都算是符合要求;但 pid 不为 -1 的时候,则只有 PID 恰好与 pid 相同的子进程才算符合条件。
    • 第 88 - 93 行:判断符合要求的子进程中是否有僵尸进程,如果有的话还需要同时找出它在当前进程控制块子进程向量中的下标。如果找不到的话直接返回 -2 ,否则进入第 34 - 102 行的处理:
      • 第 94 行:我们将子进程从向量中移除并置于当前上下文中。
      • 第 96 行:确认这是对于该子进程控制块的唯一一次强引用,即它不会出现在某个进程的子进程向量中,更不会出现在处理器监控器或者任务管理器中。当它所在的代码块结束,这次引用变量的生命周期结束,将导致该子进程进程控制块的引用计数变为 0 ,彻底回收掉它占用的所有资源,包括:内核栈和它的 PID 还有它的应用地址空间存放页表的那些物理页帧等等。
      • 第 97 行:得到子进程的 PID 并会在最终返回。
      • 第 99 行:到了子进程的退出码。
      • 第 101 行:写入到当前进程的应用地址空间中。由于应用传递给内核的仅仅是一个指向应用地址空间中保存子进程返回值的内存区域的指针,我们还需要在 translated_refmut 中手动查页表找到应该写入到物理内存中的哪个位置,这样才能把子进程的退出码 exit_code 返回给父进程
      • 第 102 行:将子进程的 PID 返回。

【进程的生成机制】

【fork 系统调用的实现 -> os/src/mm/memory_set.rs】
  • 建立新页表,复制父进程地址空间的内容
  • 创建新的陷入上下文
  • 创建新的应用内核栈
  • 创建任务上下文
  • 建立父子关系
  • 设置0fork返回码

在实现 fork 的时候,最为关键且困难的是为子进程创建一个和父进程几乎完全相同的应用地址空间os/src/mm/memory_set.rs大体与Ankylosauridae OS相同,若有需要可以参考哈,这里说一下新增的部分 😃

//! os/src/mm/memory_set.rs
//! Implementation of [`MapArea`] and [`MemorySet`].

use super::{frame_alloc, FrameTracker};
use super::{PTEFlags, PageTable, PageTableEntry};
use super::{PhysAddr, PhysPageNum, VirtAddr, VirtPageNum};
use super::{StepByOne, VPNRange};
use crate::config::{MEMORY_END, PAGE_SIZE, TRAMPOLINE, TRAP_CONTEXT, USER_STACK_SIZE};
use crate::sync::UPSafeCell;
use alloc::collections::BTreeMap;
use alloc::sync::Arc;
use alloc::vec::Vec;
use lazy_static::*;
use riscv::register::satp;

extern "C" {
    fn stext();
    fn etext();
    fn srodata();
    fn erodata();
    fn sdata();
    fn edata();
    fn sbss_with_stack();
    fn ebss();
    fn ekernel();
    fn strampoline();
}

lazy_static! {
    /// a memory set instance through lazy_static! managing kernel space
    pub static ref KERNEL_SPACE: Arc<UPSafeCell<MemorySet>> =
        Arc::new(unsafe { UPSafeCell::new(MemorySet::new_kernel()) });
}

/// memory set structure, controls virtual-memory space
pub struct MemorySet {
    page_table: PageTable,
    areas: Vec<MapArea>,
}

impl MemorySet {
    pub fn new_bare() -> Self {
        Self {
            page_table: PageTable::new(),
            areas: Vec::new(),
        }
    }
    pub fn token(&self) -> usize {
        self.page_table.token()
    }
    /// Assume that no conflicts.
    pub fn insert_framed_area(
        &mut self,
        start_va: VirtAddr,
        end_va: VirtAddr,
        permission: MapPermission,
    ) {
        self.push(
            MapArea::new(start_va, end_va, MapType::Framed, permission),
            None,
        );
    }
    pub fn remove_area_with_start_vpn(&mut self, start_vpn: VirtPageNum) {
        if let Some((idx, area)) = self
            .areas
            .iter_mut()
            .enumerate()
            .find(|(_, area)| area.vpn_range.get_start() == start_vpn)
        {
            area.unmap(&mut self.page_table);
            self.areas.remove(idx);
        }
    }
    fn push(&mut self, mut map_area: MapArea, data: Option<&[u8]>) {
        map_area.map(&mut self.page_table);
        if let Some(data) = data {
            map_area.copy_data(&mut self.page_table, data);
        }
        self.areas.push(map_area);
    }
    /// Mention that trampoline is not collected by areas.
    fn map_trampoline(&mut self) {
        self.page_table.map(
            VirtAddr::from(TRAMPOLINE).into(),
            PhysAddr::from(strampoline as usize).into(),
            PTEFlags::R | PTEFlags::X,
        );
    }
    /// Without kernel stacks.
    pub fn new_kernel() -> Self {
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();
        // map kernel sections
        info!(".text [{:#x}, {:#x})", stext as usize, etext as usize);
        info!(".rodata [{:#x}, {:#x})", srodata as usize, erodata as usize);
        info!(".data [{:#x}, {:#x})", sdata as usize, edata as usize);
        info!(
            ".bss [{:#x}, {:#x})",
            sbss_with_stack as usize, ebss as usize
        );
        info!("mapping .text section");
        memory_set.push(
            MapArea::new(
                (stext as usize).into(),
                (etext as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::X,
            ),
            None,
        );
        info!("mapping .rodata section");
        memory_set.push(
            MapArea::new(
                (srodata as usize).into(),
                (erodata as usize).into(),
                MapType::Identical,
                MapPermission::R,
            ),
            None,
        );
        info!("mapping .data section");
        memory_set.push(
            MapArea::new(
                (sdata as usize).into(),
                (edata as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        info!("mapping .bss section");
        memory_set.push(
            MapArea::new(
                (sbss_with_stack as usize).into(),
                (ebss as usize).into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        info!("mapping physical memory");
        memory_set.push(
            MapArea::new(
                (ekernel as usize).into(),
                MEMORY_END.into(),
                MapType::Identical,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        memory_set
    }
    /// Include sections in elf and trampoline and TrapContext and user stack,
    /// also returns user_sp and entry point.
    pub fn from_elf(elf_data: &[u8]) -> (Self, usize, usize) {
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();
        // map program headers of elf, with U flag
        let elf = xmas_elf::ElfFile::new(elf_data).unwrap();
        let elf_header = elf.header;
        let magic = elf_header.pt1.magic;
        assert_eq!(magic, [0x7f, 0x45, 0x4c, 0x46], "invalid elf!");
        let ph_count = elf_header.pt2.ph_count();
        let mut max_end_vpn = VirtPageNum(0);
        for i in 0..ph_count {
            let ph = elf.program_header(i).unwrap();
            if ph.get_type().unwrap() == xmas_elf::program::Type::Load {
                let start_va: VirtAddr = (ph.virtual_addr() as usize).into();
                let end_va: VirtAddr = ((ph.virtual_addr() + ph.mem_size()) as usize).into();
                let mut map_perm = MapPermission::U;
                let ph_flags = ph.flags();
                if ph_flags.is_read() {
                    map_perm |= MapPermission::R;
                }
                if ph_flags.is_write() {
                    map_perm |= MapPermission::W;
                }
                if ph_flags.is_execute() {
                    map_perm |= MapPermission::X;
                }
                let map_area = MapArea::new(start_va, end_va, MapType::Framed, map_perm);
                max_end_vpn = map_area.vpn_range.get_end();
                memory_set.push(
                    map_area,
                    Some(&elf.input[ph.offset() as usize..(ph.offset() + ph.file_size()) as usize]),
                );
            }
        }
        // map user stack with U flags
        let max_end_va: VirtAddr = max_end_vpn.into();
        let mut user_stack_bottom: usize = max_end_va.into();
        // guard page
        user_stack_bottom += PAGE_SIZE;
        let user_stack_top = user_stack_bottom + USER_STACK_SIZE;
        memory_set.push(
            MapArea::new(
                user_stack_bottom.into(),
                user_stack_top.into(),
                MapType::Framed,
                MapPermission::R | MapPermission::W | MapPermission::U,
            ),
            None,
        );
        // map TrapContext
        memory_set.push(
            MapArea::new(
                TRAP_CONTEXT.into(),
                TRAMPOLINE.into(),
                MapType::Framed,
                MapPermission::R | MapPermission::W,
            ),
            None,
        );
        (
            memory_set,
            user_stack_top,
            elf.header.pt2.entry_point() as usize,
        )
    }
    /// Copy an identical user_space
    pub fn from_existed_user(user_space: &MemorySet) -> MemorySet {
        let mut memory_set = Self::new_bare();
        // map trampoline
        memory_set.map_trampoline();
        // copy data sections/trap_context/user_stack
        for area in user_space.areas.iter() {
            let new_area = MapArea::from_another(area);
            memory_set.push(new_area, None);
            // copy data from another space
            for vpn in area.vpn_range {
                let src_ppn = user_space.translate(vpn).unwrap().ppn();
                let dst_ppn = memory_set.translate(vpn).unwrap().ppn();
                dst_ppn
                    .get_bytes_array()
                    .copy_from_slice(src_ppn.get_bytes_array());
            }
        }
        memory_set
    }
    pub fn activate(&self) {
        let satp = self.page_table.token();
        unsafe {
            satp::write(satp);
            core::arch::asm!("sfence.vma");
        }
    }
    pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
        self.page_table.translate(vpn)
    }
    pub fn recycle_data_pages(&mut self) {
        //*self = Self::new_bare();
        self.areas.clear();
    }
}

/// map area structure, controls a contiguous piece of virtual memory
pub struct MapArea {
    vpn_range: VPNRange,
    data_frames: BTreeMap<VirtPageNum, FrameTracker>,
    map_type: MapType,
    map_perm: MapPermission,
}

impl MapArea {
    pub fn new(
        start_va: VirtAddr,
        end_va: VirtAddr,
        map_type: MapType,
        map_perm: MapPermission,
    ) -> Self {
        let start_vpn: VirtPageNum = start_va.floor();
        let end_vpn: VirtPageNum = end_va.ceil();
        Self {
            vpn_range: VPNRange::new(start_vpn, end_vpn),
            data_frames: BTreeMap::new(),
            map_type,
            map_perm,
        }
    }
    pub fn from_another(another: &MapArea) -> Self {
        Self {
            vpn_range: VPNRange::new(another.vpn_range.get_start(), another.vpn_range.get_end()),
            data_frames: BTreeMap::new(),
            map_type: another.map_type,
            map_perm: another.map_perm,
        }
    }
    pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
        let ppn: PhysPageNum;
        match self.map_type {
            MapType::Identical => {
                ppn = PhysPageNum(vpn.0);
            }
            MapType::Framed => {
                let frame = frame_alloc().unwrap();
                ppn = frame.ppn;
                self.data_frames.insert(vpn, frame);
            }
        }
        let pte_flags = PTEFlags::from_bits(self.map_perm.bits).unwrap();
        page_table.map(vpn, ppn, pte_flags);
    }

    pub fn unmap_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) {
        #[allow(clippy::single_match)]
        match self.map_type {
            MapType::Framed => {
                self.data_frames.remove(&vpn);
            }
            _ => {}
        }
        page_table.unmap(vpn);
    }
    pub fn map(&mut self, page_table: &mut PageTable) {
        for vpn in self.vpn_range {
            self.map_one(page_table, vpn);
        }
    }
    pub fn unmap(&mut self, page_table: &mut PageTable) {
        for vpn in self.vpn_range {
            self.unmap_one(page_table, vpn);
        }
    }
    /// data: start-aligned but maybe with shorter length
    /// assume that all frames were cleared before
    pub fn copy_data(&mut self, page_table: &mut PageTable, data: &[u8]) {
        assert_eq!(self.map_type, MapType::Framed);
        let mut start: usize = 0;
        let mut current_vpn = self.vpn_range.get_start();
        let len = data.len();
        loop {
            let src = &data[start..len.min(start + PAGE_SIZE)];
            let dst = &mut page_table
                .translate(current_vpn)
                .unwrap()
                .ppn()
                .get_bytes_array()[..src.len()];
            dst.copy_from_slice(src);
            start += PAGE_SIZE;
            if start >= len {
                break;
            }
            current_vpn.step();
        }
    }
}

#[derive(Copy, Clone, PartialEq, Debug)]
/// map type for memory set: identical or framed
pub enum MapType {
    Identical,
    Framed,
}

bitflags! {
    /// map permission corresponding to that in pte: `R W X U`
    pub struct MapPermission: u8 {
        const R = 1 << 1;
        const W = 1 << 2;
        const X = 1 << 3;
        const U = 1 << 4;
    }
}

#[allow(unused)]
pub fn remap_test() {
    let mut kernel_space = KERNEL_SPACE.exclusive_access();
    let mid_text: VirtAddr = ((stext as usize + etext as usize) / 2).into();
    let mid_rodata: VirtAddr = ((srodata as usize + erodata as usize) / 2).into();
    let mid_data: VirtAddr = ((sdata as usize + edata as usize) / 2).into();
    assert!(!kernel_space
        .page_table
        .translate(mid_text.floor())
        .unwrap()
        .writable());
    assert!(!kernel_space
        .page_table
        .translate(mid_rodata.floor())
        .unwrap()
        .writable());
    assert!(!kernel_space
        .page_table
        .translate(mid_data.floor())
        .unwrap()
        .executable());
    info!("remap_test passed!");
}
  • 第 223 - 241 行:MemorySet::from_existed_user 可以复制一个完全相同的地址空间

    • 第 224 行:我们通过 new_bare创建一个空的地址空间
    • 第 226 行:通过 map_trampoline 为这个地址空间映射上跳板页面,这是因为我们解析 ELF 创建地址空间的时候,并没有将跳板页作为一个单独的逻辑段插入到地址空间的逻辑段向量 areas 中,所以这里需要单独映射上。
    • 第 228 行:areas逻辑段 MapArea 的向量,遍历原地址空间中的所有逻辑段。
    • 第 229 行:通过MapArea::from_another复制这个逻辑段
    • 第 230 行:将复制之后的逻辑段插入新的地址空间,在插入的时候就已经实际分配了物理页帧了。
    • 第 232 - 238 行:我们遍历逻辑段中的每个虚拟页面,对应完成数据复制,这只需要找出两个地址空间中的虚拟页面各被映射到哪个物理页帧,就可转化为将数据从物理内存中的一个位置复制到另一个位置,使用 copy_from_slice 即可轻松实现。
    • 第 240 行:返回新创建的地址空间。
  • 第 252 - 255 行:MemorySet::recycle_data_pages 只是将地址空间中的逻辑段列表 areas 清空(即执行 Vec 向量清空),这将导致应用地址空间被回收(即进程的数据和代码对应的物理页帧都被回收),但用来存放页表的那些物理页帧此时还不会被回收(会由父进程最后回收子进程剩余的占用资源)。

  • 第 282 - 289 行:MapArea::from_another 可以从一个逻辑段复制得到一个虚拟地址区间、映射方式和权限控制均相同的逻辑段(从逻辑段复制一个新的逻辑段),不同的是由于它还没有真正被映射到物理页帧上,所以 data_frames 字段为空

    memory_set.rs在原来的基础上新增了逻辑段的复制地址空间的复制

    逻辑段的复制纯粹复制原本的内容但由于没有真正映射到物理页所有没有物理页帧。

    地址空间的复制为复制一个完全相同的地址空间,包括各个虚拟页面,逻辑段,跳板。在复制逻辑段的时候分配了实际的物理页帧了就。

【exec 系统调用的实现 -> os/src/mm/page_table.rs】

exec 系统调用使得一个进程能够加载一个新应用的 ELF 可执行文件中的代码和数据替换原有的应用地址空间中的内容,并开始执行(具体参考os/src/task/task.rs)。os/src/mm/page_table.rs页表项具体内同参考Ankylosauridae OS这里只对新增加的进行说明哈 😃

  • 回收已有应用地址空间,基于ELF 文件的全新的地址空间直接替换已有应用地址空间
  • 修改进程控制块的 Trap 上下文,将解析得到的应用入口点、用户栈位置以及一些内核的信息进行初始化
// os/src/mm/page_table.rs
//! Implementation of [`PageTableEntry`] and [`PageTable`].

use super::{frame_alloc, FrameTracker, PhysAddr, PhysPageNum, StepByOne, VirtAddr, VirtPageNum};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;
use bitflags::*;

bitflags! {
    /// page table entry flags
    pub struct PTEFlags: u8 {
        const V = 1 << 0;
        const R = 1 << 1;
        const W = 1 << 2;
        const X = 1 << 3;
        const U = 1 << 4;
        const G = 1 << 5;
        const A = 1 << 6;
        const D = 1 << 7;
    }
}

#[derive(Copy, Clone)]
#[repr(C)]
/// page table entry structure
pub struct PageTableEntry {
    pub bits: usize,
}

impl PageTableEntry {
    pub fn new(ppn: PhysPageNum, flags: PTEFlags) -> Self {
        PageTableEntry {
            bits: ppn.0 << 10 | flags.bits as usize,
        }
    }
    pub fn empty() -> Self {
        PageTableEntry { bits: 0 }
    }
    pub fn ppn(&self) -> PhysPageNum {
        (self.bits >> 10 & ((1usize << 44) - 1)).into()
    }
    pub fn flags(&self) -> PTEFlags {
        PTEFlags::from_bits(self.bits as u8).unwrap()
    }
    pub fn is_valid(&self) -> bool {
        (self.flags() & PTEFlags::V) != PTEFlags::empty()
    }
    pub fn readable(&self) -> bool {
        (self.flags() & PTEFlags::R) != PTEFlags::empty()
    }
    pub fn writable(&self) -> bool {
        (self.flags() & PTEFlags::W) != PTEFlags::empty()
    }
    pub fn executable(&self) -> bool {
        (self.flags() & PTEFlags::X) != PTEFlags::empty()
    }
}

/// page table structure
pub struct PageTable {
    root_ppn: PhysPageNum,
    frames: Vec<FrameTracker>,
}

/// Assume that it won't oom when creating/mapping.
impl PageTable {
    pub fn new() -> Self {
        let frame = frame_alloc().unwrap();
        PageTable {
            root_ppn: frame.ppn,
            frames: vec![frame],
        }
    }
    /// Temporarily used to get arguments from user space.
    pub fn from_token(satp: usize) -> Self {
        Self {
            root_ppn: PhysPageNum::from(satp & ((1usize << 44) - 1)),
            frames: Vec::new(),
        }
    }
    fn find_pte_create(&mut self, vpn: VirtPageNum) -> Option<&mut PageTableEntry> {
        let mut idxs = vpn.indexes();
        let mut ppn = self.root_ppn;
        let mut result: Option<&mut PageTableEntry> = None;
        for (i, idx) in idxs.iter_mut().enumerate() {
            let pte = &mut ppn.get_pte_array()[*idx];
            if i == 2 {
                result = Some(pte);
                break;
            }
            if !pte.is_valid() {
                let frame = frame_alloc().unwrap();
                *pte = PageTableEntry::new(frame.ppn, PTEFlags::V);
                self.frames.push(frame);
            }
            ppn = pte.ppn();
        }
        result
    }
    fn find_pte(&self, vpn: VirtPageNum) -> Option<&PageTableEntry> {
        let idxs = vpn.indexes();
        let mut ppn = self.root_ppn;
        let mut result: Option<&PageTableEntry> = None;
        for (i, idx) in idxs.iter().enumerate() {
            let pte = &ppn.get_pte_array()[*idx];
            if i == 2 {
                result = Some(pte);
                break;
            }
            if !pte.is_valid() {
                return None;
            }
            ppn = pte.ppn();
        }
        result
    }
    #[allow(unused)]
    pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) {
        let pte = self.find_pte_create(vpn).unwrap();
        assert!(!pte.is_valid(), "vpn {:?} is mapped before mapping", vpn);
        *pte = PageTableEntry::new(ppn, flags | PTEFlags::V);
    }
    #[allow(unused)]
    pub fn unmap(&mut self, vpn: VirtPageNum) {
        let pte = self.find_pte_create(vpn).unwrap();
        assert!(pte.is_valid(), "vpn {:?} is invalid before unmapping", vpn);
        *pte = PageTableEntry::empty();
    }
    pub fn translate(&self, vpn: VirtPageNum) -> Option<PageTableEntry> {
        self.find_pte(vpn).copied()
    }
    pub fn translate_va(&self, va: VirtAddr) -> Option<PhysAddr> {
        self.find_pte(va.clone().floor()).map(|pte| {
            //println!("translate_va:va = {:?}", va);
            let aligned_pa: PhysAddr = pte.ppn().into();
            //println!("translate_va:pa_align = {:?}", aligned_pa);
            let offset = va.page_offset();
            let aligned_pa_usize: usize = aligned_pa.into();
            (aligned_pa_usize + offset).into()
        })
    }
    pub fn token(&self) -> usize {
        8usize << 60 | self.root_ppn.0
    }
}

/// translate a pointer to a mutable u8 Vec through page table
pub fn translated_byte_buffer(token: usize, ptr: *const u8, len: usize) -> Vec<&'static mut [u8]> {
    let page_table = PageTable::from_token(token);
    let mut start = ptr as usize;
    let end = start + len;
    let mut v = Vec::new();
    while start < end {
        let start_va = VirtAddr::from(start);
        let mut vpn = start_va.floor();
        let ppn = page_table.translate(vpn).unwrap().ppn();
        vpn.step();
        let mut end_va: VirtAddr = vpn.into();
        end_va = end_va.min(VirtAddr::from(end));
        if end_va.page_offset() == 0 {
            v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..]);
        } else {
            v.push(&mut ppn.get_bytes_array()[start_va.page_offset()..end_va.page_offset()]);
        }
        start = end_va.into();
    }
    v
}

pub fn translated_str(token: usize, ptr: *const u8) -> String {
    let page_table = PageTable::from_token(token);
    let mut string = String::new();
    let mut va = ptr as usize;
    loop {
        let ch: u8 = *(page_table
            .translate_va(VirtAddr::from(va))
            .unwrap()
            .get_mut());
        if ch == 0 {
            break;
        } else {
            string.push(ch as char);
            va += 1;
        }
    }
    string
}

pub fn translated_refmut<T>(token: usize, ptr: *mut T) -> &'static mut T {
    //println!("into translated_refmut!");
    let page_table = PageTable::from_token(token);
    let va = ptr as usize;
    //println!("translated_refmut: before translate_va");
    page_table
        .translate_va(VirtAddr::from(va))
        .unwrap()
        .get_mut()
}
  • 第 171 - 188 行::translated_str 便可以从内核地址空间之外的某个应用的用户态地址空间中拿到一个字符串
    • 第 172 行:通过from_token 可以临时创建一个专用来手动查页表的 PageTable ,它仅有一个从传入的 satp token 中得到的多级页表根节点的物理页号,它的 frames 字段为空,也即不实际控制任何资源。也就是获得应用的用户态地址空间。
    • 第 173 行:创建存储的字符串。
    • 第 175 - 186 行:针对应用的字符串中字符的用户态虚拟地址,查页表,找到对应的物理地址,逐字节地构造字符串存到String中,直到发现一个 \0 为止。
    • 第 187 行:返回String
【系统调用后重新获取 Trap 上下文 -> os/src/trap/mod.rs】

对于系统调用 sys_exec 来说,一旦调用它之后,我们会发现 trap_handler 原来上下文中的 cx 失效了——因为它是用来访问之前地址空间中 Trap 上下文被保存在的那个物理页帧的,而现在它已经被回收掉了。因此,为了能够处理类似的这种情况,我们在 syscall 分发函数返回之后需要重新获取 cx

//! os/src/trap/mod.rs
//! Trap handling functionality
//! For rCore, we have a single trap entry point, namely `__alltraps`. At
//! initialization in [`init()`], we set the `stvec` CSR to point to it.
//!
//! All traps go through `__alltraps`, which is defined in `trap.S`. The
//! assembly language code does just enough work restore the kernel space
//! context, ensuring that Rust code safely runs, and transfers control to
//! [`trap_handler()`].
//!
//! It then calls different functionality based on what exactly the exception
//! was. For example, timer interrupts trigger task preemption, and syscalls go
//! to [`syscall()`].

mod context;

use crate::config::{TRAMPOLINE, TRAP_CONTEXT};
use crate::syscall::syscall;
use crate::task::{
    current_trap_cx, current_user_token, exit_current_and_run_next, suspend_current_and_run_next,
};
use crate::timer::set_next_trigger;
use riscv::register::{
    mtvec::TrapMode,
    scause::{self, Exception, Interrupt, Trap},
    sie, stval, stvec,
};

core::arch::global_asm!(include_str!("trap.S"));

pub fn init() {
    set_kernel_trap_entry();
}

fn set_kernel_trap_entry() {
    unsafe {
        stvec::write(trap_from_kernel as usize, TrapMode::Direct);
    }
}

fn set_user_trap_entry() {
    unsafe {
        stvec::write(TRAMPOLINE as usize, TrapMode::Direct);
    }
}

pub fn enable_timer_interrupt() {
    unsafe {
        sie::set_stimer();
    }
}

#[no_mangle]
pub fn trap_handler() -> ! {
    set_kernel_trap_entry();
    let scause = scause::read();
    let stval = stval::read();
    match scause.cause() {
        Trap::Exception(Exception::UserEnvCall) => {
            // jump to next instruction anyway
            let mut cx = current_trap_cx();
            cx.sepc += 4;
            // get system call return value
            let result = syscall(cx.x[17], [cx.x[10], cx.x[11], cx.x[12]]);
            // cx is changed during sys_exec, so we have to call it again
            cx = current_trap_cx();
            cx.x[10] = result as usize;
        }
        Trap::Exception(Exception::StoreFault)
        | Trap::Exception(Exception::StorePageFault)
        | Trap::Exception(Exception::InstructionFault)
        | Trap::Exception(Exception::InstructionPageFault)
        | Trap::Exception(Exception::LoadFault)
        | Trap::Exception(Exception::LoadPageFault) => {
            println!(
                "[kernel] {:?} in application, bad addr = {:#x}, bad instruction = {:#x}, core dumped.",
                scause.cause(),
                stval,
                current_trap_cx().sepc,
            );
            // page fault exit code
            exit_current_and_run_next(-2);
        }
        Trap::Exception(Exception::IllegalInstruction) => {
            println!("[kernel] IllegalInstruction in application, core dumped.");
            // illegal instruction exit code
            exit_current_and_run_next(-3);
        }
        Trap::Interrupt(Interrupt::SupervisorTimer) => {
            set_next_trigger();
            suspend_current_and_run_next();
        }
        _ => {
            panic!(
                "Unsupported trap {:?}, stval = {:#x}!",
                scause.cause(),
                stval
            );
        }
    }
    trap_return();
}

#[no_mangle]
pub fn trap_return() -> ! {
    set_user_trap_entry();
    let trap_cx_ptr = TRAP_CONTEXT;
    let user_satp = current_user_token();
    extern "C" {
        fn __alltraps();
        fn __restore();
    }
    let restore_va = __restore as usize - __alltraps as usize + TRAMPOLINE;
    unsafe {
        core::arch::asm!(
            "fence.i",
            "jr {restore_va}",
            restore_va = in(reg) restore_va,
            in("a0") trap_cx_ptr,
            in("a1") user_satp,
            options(noreturn)
        );
    }
}

#[no_mangle]
pub fn trap_from_kernel() -> ! {
    panic!("a trap {:?} from kernel!", scause::read().cause());
}

pub use context::TrapContext;
  • 第 54 - 102 行:为trap的处理函数。
    • 第 55 行:在 trap_return 的开始处就调用 set_kernel_trap_entry ,弱化了 S态 –> S态的 Trap 处理过程,直接 panic
    • 第 58 行:根据 scause 寄存器所保存的 Trap 的原因进行分发处理。这里我们无需手动操作这些 CSR ,而是使用 Rust 第三方库 riscv
    • 第 61 行:获取当前进程的trap上下文。
    • 第 62 行:将当前进程 Trap 上下文中的 sepc 向后移动了 4 字节,使得它回到用户态之后,会从发出系统调用的 ecall 指令的下一条指令开始执行。
    • 第 64 行:进行系统调用。
    • 第 66 行:由于在系统调用的sys_exec时上下文已经进行了改变,因为它是用来访问之前地址空间中 Trap 上下文被保存在的那个物理页帧的,而现在它已经被回收掉了。所以我们需要在 syscall 分发函数返回之后需要重新获取 cx
    • 第 67 行:父进程系统调用的返回值会在 trap_handlersyscall 返回之后再设置为 sys_fork 的返回值,这里我们返回子进程的 PID

进程的fork主要是创建一个新的进程,fork创建一个一模一样的父进程,具有TCB。进程的exec主要是对地址空间的改变,exec则对fork出来的父进程进行实例化,将其原本的地址空间进行回收并用应用的地址空间进行替代。这里需要注意的是trap_handler在调用sys_exec之后由于之前的地址空间已经被回收了,所以需要重新获取上下文。

【shell 程序 user_shell 的输入机制】

为了实现shell程序 user_shell 的输入机制,我们需要实现 sys_read 系统调用使得应用能够取得用户的键盘输入

//! os/src/syscall/fs.rs
//! File and filesystem-related syscalls

use crate::mm::translated_byte_buffer;
use crate::sbi::console_getchar;
use crate::task::{current_user_token, suspend_current_and_run_next};

const FD_STDIN: usize = 0;
const FD_STDOUT: usize = 1;

pub fn sys_write(fd: usize, buf: *const u8, len: usize) -> isize {
    match fd {
        FD_STDOUT => {
            let buffers = translated_byte_buffer(current_user_token(), buf, len);
            for buffer in buffers {
                print!("{}", core::str::from_utf8(buffer).unwrap());
            }
            len as isize
        }
        _ => {
            panic!("Unsupported fd in sys_write!");
        }
    }
}

pub fn sys_read(fd: usize, buf: *const u8, len: usize) -> isize {
    match fd {
        FD_STDIN => {
            assert_eq!(len, 1, "Only support len = 1 in sys_read!");
            let mut c: usize;
            loop {
                c = console_getchar();
                if c == 0 {
                    suspend_current_and_run_next();
                    continue;
                } else {
                    break;
                }
            }
            let ch = c as u8;
            let mut buffers = translated_byte_buffer(current_user_token(), buf, len);
            unsafe {
                buffers[0].as_mut_ptr().write_volatile(ch);
            }
            1
        }
        _ => {
            panic!("Unsupported fd in sys_read!");
        }
    }
}
  • 第 26 - 51 行:实现了sys_read系统调用,我们仅支持从标准输入 FD_STDIN 即文件描述符 0 读入,且单次读入的长度限制为 1,即每次只能读入一个字符。
    • 第 28 行:判断是否来自FD_STDIN标准输入。
    • 第 29 行:目前只支持一个字符一个字符读入。
    • 第 32 行:调用 sbi 子模块提供的从键盘获取输入的接口 console_getchar
    • 第 33 - 35 行:如果返回 0 则说明还没有输入,我们调用 suspend_current_and_run_next 暂时切换到其他进程,等下次切换回来的时候再看看是否有输入了。
    • 第 41 - 44 行:手动查页表将输入的字符正确的写入到应用地址空间。

【进程资源回收机制】

【进程的退出】

应用调用 sys_exit 系统调用主动退出或者出错由内核终止之后,会在内核中调用exit_current_and_run_next 函数退出当前进程并切换到下一个进程。具体代码参考os/src/syscall/process.rs

【父进程回收子进程资源】

父进程通过 sys_waitpid 系统调用来回收子进程的资源并收集它的一些信息。具体代码参考os/src/syscall/process.rs

进程OS小结:

Process OSAnkylosauridae OS的基础上进行了修改,增加了进程的管理和初始化,主要是将原来的任务结构体修改成了进程结构体,增加了父子进程之间的关系、进程码、退出状态等。同时也增加了shell界面。

Process OS 新增了sys_forksys_exec两个系统调用,使得可以通过进程创建子进程。初始进程由内核手动创建,进程之间的调度通过双端队列来进行。

在这里插入图片描述

由于进程的出现,应用之间有了一定的联系,并不像Ankylosauridae OS将应用按照顺序一个一个的执行。应用之间的执行因为进程的加入可以通过队列按照时间片执行。与time-sharing OS一样,但内部的管理因为地址空间的划分又保证了安全性。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值