【总体思路】
- 编译:应用程序和内核独立编译,合并为一个镜像
- 编译:不同应用程序可采用统一的起始地址
- 构造:系统调用服务,进程的管理与初始化
- 构造:建立基于页表机制的虚存空间
- 运行:特权级切换,进程与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
被封装成两个不同的API
,wait
和waitpid
-
第 10 - 21 行:
wait
表示等待任意一个子进程结束- 第 12 行:根据
sys_waitpid
的约定它需要传的pid
参数为-1
表示等待任意一个子进程。 - 第 13 - 15 行:当
sys_waitpid
返回值为-2
,即要等待的子进程存在但它却尚未退出的时候,我们调用yield_
主动交出CPU
使用权,待下次CPU
使用权被内核交还给它的时候再次调用sys_waitpid
查看要等待的子进程是否退出。这样做可以减小CPU
资源的浪费。 - 第 16 - 18 行:返回已经推出的
pid
。
- 第 12 行:根据
-
第 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
文件主要执行流程如下所示:
- 通过
sys_read
获取字符串(即文件名) - 通过
sys_fork
创建子进程 - 在子进程中通过
sys_exec
创建新应用的进程 - 在父进程中通过
sys_waitpid
等待子进程结束 - 跳转到第一步循环执行
// 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 行:声明
line
为String
类型。 -
第 23 行:打印
>>
提示符 -
第 24 行:清空
buff
-
第 25 - 66 行:为
shell
控制行的循环窗口。-
第 26 行:
getchar
通过sys_read
获取一个字符。 -
第 28 行:如果这个字符是回车换行(
0x0a
,0x0d
),也就是最后一个字符,则执行接下来的条件判断。- 第 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
也需要弹出最后一个字符。
- 第 52 行:如果当前
-
第 60 行:如果用户输入了一个其他字符
- 第 61 - 63 行::会被视为用户的正常输入,我们直接将它打印在屏幕上并加入到
line
中。
- 第 61 - 63 行::会被视为用户的正常输入,我们直接将它打印在屏幕上并加入到
-
shell
应用主要是维护shell
这个命令行。通过对用户输入的信息进行解析,把对应输入的应用当成一个新的进程来执行。
应用程序设计小结:当内核初始化完毕之后,它会从可执行文件
initproc
中加载并执行用户初始程序initproc
,而用户初始程序initproc
中又会fork
并exec
来运行shell
程序user_shell
。这两个应用虽然都是在CPU
的U
特权级执行的,但是相比其他应用,它们要更加底层和基础。原则上应该将它们作为一个组件打包在操作系统中。但这里为了实现更加简单,我们并不将它们和其他应用进行区分。
【内核程序设计】
【应用的链接与加载 -> 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 行:获取(需要的应用切片位置开始,下一个应用开始)位置之间的地址,也就是代码段。
- 第 20 行:获取
-
第 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
向量组。
- 第 44 - 46 行:依据每个应用的结尾
由于每个应用之间有
'\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
。
- 第 16 行:
-
第 24 - 45 行:为
PidAllocator
实现了三个方法,new
、alloc
、dealloc
。- 第 22 - 27 行:通过
new
方法创建一个进程标识符分配器,创建PID
为0
,创建recycled
向量表。 - 第 28 - 35 行:
PidAllocator::alloc
将会分配出去一个将usize
包装之后的PidHandle
。- 第 29 行:从
recycled
中pop
出一个进程ID
。 - 第 30 行:
pop
成功则直接返回这个进程ID
作为新分配的进程ID
。 - 第 32 行:如果没有
pop
成功则将当前current
值加一,增加分配器的总进程数量。 - 第 33 行:创建一个
current-1
的值作为新的进程ID
分配出去。
- 第 29 行:从
- 第 36 - 44 行:
PidAllocator::dealloc
会检查pid
是否合法后将其回收,也就是加入到recycled
中。- 第 37 行:用
current
来判断pid
是否合法,也就是小于current
。 - 第 38 行:在
recycled
中查找pid
是否存在。 - 第 39 行:将释放的
pid
重新加入到recycled
中。
- 第 37 行:用
- 第 22 - 27 行:通过
-
第 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
实现了三个方法,new
、push_on_top
、get_top
。- 第 80 - 89 行:为
KernelStack
实现了new
方法。依据pid
创建对应的内核栈。- 第 81 行:由于
pid_handle
是结构体,所以先获取其里面的pid
。 - 第 82 行:依据
kernel_stack_position
函数计算对应pid
的栈顶和栈底。 - 第 83 - 87 行:在当前地址空间
KERNEL_SPACE
中插入一个Framed
方式映射到物理内存的逻辑段,该逻辑段就是相应pid
的内核地址空间。 - 第 88 行:将
pid
放到KernelStack
中。
- 第 81 行:由于
- 第 92 - 102 行:
push_on_top
方法可以将一个类型为T
的变量压入内核栈顶并返回其裸指针,这也是一个泛型函数。 - 第 103 - 106 行:
get_top
方法通过kernel_stack_position
获取对应pid
的内核栈栈顶。
- 第 80 - 89 行:为
-
第 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>
可以提供互斥从而避免数据竞争。
- 第 20 - 22 行:在初始化之后就不再变化的作为一个字段直接放在任务控制块中。这里将进程标识符
-
第 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
的时候能找到执行程序。 - 第 84 行:我们解析应用的
-
第 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 行:
getpid
以usize
的形式返回当前进程的进程标识符。
-
-
第 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
实现了new
、add
、fetch
这三个方法。调度算法来看,这里用到的就是最简单的 RR 算法。- 第 19 - 23 行:使用
new
方法创建一个双端队列。 - 第 25 - 27 行:使用
add
方法将一个任务加入队尾。 - 第 29 - 31 行:使用
fetch
方法从队头中取出一个任务来执行。
- 第 19 - 23 行:使用
- 第 34 - 38 行:全局实例
TASK_MANAGER
为TaskManager
类型。 - 第 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
控制流的任务上下文。
- 第 18 行:
- 第 23 - 39 行:为
Processor
实现四个方法,new
、get_idle_task_cx_ptr
、take_current
、current
- 第 24 - 29 行:通过
new
方法创建Processor
结构,current
为0
,初始化上下文。 - 第 30 - 32 行:返回
idle_task_cx_ptr
字段。 - 第 33 - 35 行:
Processor::take_current
可以取出当前正在执行的任务。Option::take
意味着current
字段也变为None
。 - 第 36 - 38 行:
Processor::current
返回当前执行的任务的一份拷贝。
- 第 24 - 29 行:通过
- 第 43 行:创建单个
Processor
的全局实例PROCESSOR
。 - 第 50 - 69 行:创建
run_tasks
函数,它循环调用fetch_task
直到顺利从任务管理器中取出一个任务,然后获得__switch
两个参数进行任务切换。注意在整个过程中要严格控制临界区。- 第 53 行:尝试通过
fetch_task
函数从任务管理器中取出一个任务。 - 第 54 行:获取
idle
状态的上下文。 - 第 57 行:获取下一个应用的上下文。
- 第 58 行:设置进程状态为
Running
。 - 第 59 行:需要手动回收对即将执行任务的任务控制块的借用标记,使得后续我们仍可以访问该任务控制块。这里我们不能依赖编译器在
if let
块结尾时的自动回收,因为中间我们会在自动回收之前调用__switch
,这将导致我们在实际上已经结束访问却没有进行回收的情况下切换到下一个任务,最终可能违反UPSafeCell
的借用约定而使得内核报错退出。 - 第 61 行:将
Processor
的current
设为将要运行的task
。 - 第 63 行:手动回收
PROCESSOR
的借用标记。 - 第 65 行:通过
__switch
方法进行进程的切换。(__switch
函数主要做的事情就是将依据传入的两个参数,将当前的状态(14个寄存器)保存到第一个参数中,再将第二个参数中所保存的状态(14个寄存器)恢复到当前寄存器中。当返回的时候就会从下一个应用所传入的ra
字段加载运行。也就顺利成章的完成了任务的切换。)
- 第 53 行:尝试通过
- 第 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
返回之后的位置,也即开启了下一轮的调度循环。
- 第 99 行:获取
进程管理核心数据结构小结:
Processor
处理器管理结构主要管理CPU
当前进程的执行,到底执行哪一个进程。而执行哪一个进程依据TaskManager
任务管理器里面双端队列里面是否有需要执行的进程。如果有则将idle
控制流与进行的上下文进行交换执行进程。当一个应用执行完后交出CPU
使用权时,进入内核后它会调用schedule
函数来切换到idle
控制流并开启新一轮的任务调度。
【进程管理机制的设计实现】
【进程管理机制实现概述】
- 创建初始进程:创建第一个用户态进程
initproc
- 进程生成机制:介绍进程相关的系统调用
sys_fork
/sys_exec
- 进程调度机制:进程主动/被动切换
- 进程资源回收机制:调用
sys_exit
退出或进程终止后保存其退出码 - 进程资源回收机制:父进程通过
sys_waitpid
收集该进程的信息并回收其资源 - 字符输入机制:通过**
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
的效果是会继续执行这个任务。
- 第 37 行:通过
-
第 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
触发调度及任务切换,由于我们再也不会回到该进程的执行过程中,因此无需关心任务上下文的保存。
- 第 56 行:我们调用
-
第 87 - 95 行:基于
lazy_static
在运行时初始化初始进程的进程控制块INITPROC
。也就是调用TaskControlBlock::new
来创建一个进程控制块,它需要传入ELF
可执行文件的数据切片作为参数,这可以通过加载器loader
子模块提供的get_app_data_by_name
接口查找initproc
的ELF
执行文件数据来获得。 -
第 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
中我们也正是通过这个返回值来判断要执行的应用是否存在。
- 第 61 行:获取当前应用的
-
第 74 - 107 行:实现了
sys_waitpid
函数。sys_waitpid
是一个立即返回的系统调用,它的返回值语义是:如果当前的进程不存在一个进程
ID
为pid
(pid
== -1 或pid
> 0)的子进程,则返回-1
。如果存在一个进程
ID
为pid
的僵尸子进程,则正常回收并返回子进程的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
返回。
- 第 80 - 87 行:判断
【进程的生成机制】
【fork 系统调用的实现 -> os/src/mm/memory_set.rs】
- 建立新页表,复制父进程地址空间的内容
- 创建新的陷入上下文
- 创建新的应用内核栈
- 创建任务上下文
- 建立父子关系
- 设置
0
为fork
返回码
在实现 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 行:返回新创建的地址空间。
- 第 224 行:我们通过
-
第 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
。
- 第 172 行:通过
【系统调用后重新获取 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_handler
中syscall
返回之后再设置为sys_fork
的返回值,这里我们返回子进程的PID
。
- 第 55 行:在
进程的
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 行:手动查页表将输入的字符正确的写入到应用地址空间。
- 第 28 行:判断是否来自
【进程资源回收机制】
【进程的退出】
应用调用 sys_exit
系统调用主动退出或者出错由内核终止之后,会在内核中调用exit_current_and_run_next
函数退出当前进程并切换到下一个进程。具体代码参考os/src/syscall/process.rs
。
【父进程回收子进程资源】
父进程通过 sys_waitpid
系统调用来回收子进程的资源并收集它的一些信息。具体代码参考os/src/syscall/process.rs
。
进程OS小结:
Process OS
在Ankylosauridae OS
的基础上进行了修改,增加了进程的管理和初始化,主要是将原来的任务结构体修改成了进程结构体,增加了父子进程之间的关系、进程码、退出状态等。同时也增加了shell
界面。
Process OS
新增了sys_fork
和sys_exec
两个系统调用,使得可以通过进程创建子进程。初始进程由内核手动创建,进程之间的调度通过双端队列来进行。
由于进程的出现,应用之间有了一定的联系,并不像
Ankylosauridae OS
将应用按照顺序一个一个的执行。应用之间的执行因为进程的加入可以通过队列按照时间片执行。与time-sharing OS
一样,但内部的管理因为地址空间的划分又保证了安全性。