2024开源操作系统训练营 rCore Chapter4练习
编程作业
思路
重写 sys_get_time
和 sys_task_info
首先,第一个任务:由于引入了虚拟内存后,sys_get_time
和 sys_task_info
失效了,需要进行重写。
这里解释一下为什么会失效:以 sys_get_time
为例,用户在发起系统调用时,传入的参数 _ts
是用户态下的虚拟地址,它需要借助内核的软件地址转换机制,查找任务对应的页表,将 _ts
转换为物理地址,再对该地址处的值进行填充。这里可以使用 page_table.rs
中预先实现好的 translated_byte_buffer
,它将一整个虚拟地址段翻译为一系列的物理地址段(每页一段),这样如果结构体 TimeVal
和 TaskInfo
横跨多个页面也同样适用。
注意,内核态代码中的地址仍然是虚拟地址,只不过在 rCore 中,内核态的低 256GB 为直接映射,因此好像内核代码在直接访问物理地址,其实还是虚拟地址。
实现 mmap
POSIX 标准中的 mmap
是将一个文件或其他对象的数据映射到进程的地址空间中,而本实验需实现的 mmap
则为简化版本,只是简单地向进程地址空间中插入一段虚拟内存段,而无需为其设置初始值。
在尝试进行 mmap
之前,需要检查可能发生的错误。按照手册的提示,可能的错误如下:
- start 没有按页大小对齐
port & !0x7 != 0
(port 其余位必须为0)port & 0x7 = 0
(这样的内存无意义)[start, start + len)
中存在已经被映射的页- 物理内存不足
前三个错误很容易判断,如下所示,在此不必赘述。
// 1. illegal start virtual address or port
if !start_va.aligned() || port & !0x7 != 0 || port & 0x7 == 0 {
return -1;
}
物理内存是否充足,需要判断尝试映射的内存长度与空闲物理内存的大小关系。rCore 所采用的物理内存策略是简单的栈式分配器,剩余物理内存大小等于 end - current
加上 recycled.len()
.
/// Check if the remaining physical memory is sufficient
pub fn is_mem_sufficient(_len: usize) -> bool {
let fa = FRAME_ALLOCATOR.exclusive_access();
let page_cnt = fa.end - fa.current + fa.recycled.len();
(_len + PAGE_SIZE - 1) / PAGE_SIZE <= page_cnt
}
要判断一段虚拟内存段是否存在已被映射的页面,需要遍历每个页面的起始地址,使用软件地址转换机制尝试对该虚拟地址进行“翻译”,如果成功得到对应的页表项,则说明虚拟页面已被映射,mmap
出错。
// 3. Check if trying to map mapped page
for vpn in vpn_range {
if let Some(pte) = inner.tasks[cur].memory_set.translate(vpn) {
if pte.is_valid() {
return -1;
}
}
}
检查完所有可能的错误之后,便可以开始进行实际的页面映射了。实际上,在 rCore 中,一个地址空间由若干个逻辑段(MapArea
)组成,想要将一段虚拟内存段插入地址空间中,只需要根据 sys_mmap
的参数 start, len, port 构建一个 MapArea
数据结构,再将其插入 memory_set.areas
中(页表项的插入会同时实现)。实际上,rCore 中提供了这样的接口:insert_framed_area
。
实现 munmap
与 mmap
一样,首先检查可能的错误:[start, start + len)
中存在未被映射的虚存,实现方式与 mmap
一样,在此不再介绍。
实际上,严格的 munmap
实现并不简单,需要考虑将原有的 memeort_set.areas
中的 MapArea
进行进行分割处理。在这里,为了实现的简单,假设 [start, start + len)
正好为一个完整的逻辑段,而非一个更大逻辑段的一部分(测试样例中没有这种情况)。有了这个简化,munmap
就很简单了:只需遍历 memory_set.areas
的各个逻辑段 area
,当找到指定逻辑段时,使用 area.unmap()
移除对应的页表项映射,最后将 area
从 memory_set.areas
中移除。
/// Remove framed area
pub fn remove_framed_area(&mut self, range: VPNRange) {
let mut idx = 0;
for area in self.areas.iter_mut() {
if area.vpn_range.get_start().0 == range.get_start().0
&& area.vpn_range.get_end().0 == range.get_end().0 {
area.unmap(&mut self.page_table);
break;
}
idx += 1;
}
self.areas.remove(idx);
}
代码
diff --git a/os/src/mm/frame_allocator.rs b/os/src/mm/frame_allocator.rs
index 01e62fd..e916f1e 100644
--- a/os/src/mm/frame_allocator.rs
+++ b/os/src/mm/frame_allocator.rs
@@ -2,7 +2,7 @@
//! controls all the frames in the operating system.
use super::{PhysAddr, PhysPageNum};
-use crate::config::MEMORY_END;
+use crate::config::{MEMORY_END, PAGE_SIZE};
use crate::sync::UPSafeCell;
use alloc::vec::Vec;
use core::fmt::{self, Debug, Formatter};
@@ -117,6 +117,13 @@ pub fn frame_dealloc(ppn: PhysPageNum) {
FRAME_ALLOCATOR.exclusive_access().dealloc(ppn);
}
+/// Check if the remaining physical memory is sufficient
+pub fn is_mem_sufficient(_len: usize) -> bool {
+ let fa = FRAME_ALLOCATOR.exclusive_access();
+ let page_cnt = fa.end - fa.current + fa.recycled.len();
+ (_len + PAGE_SIZE - 1) / PAGE_SIZE <= page_cnt
+}
+
#[allow(unused)]
/// a simple test for frame allocator
pub fn frame_allocator_test() {
diff --git a/os/src/mm/memory_set.rs b/os/src/mm/memory_set.rs
index 7a7b7ea..fca0b7f 100644
--- a/os/src/mm/memory_set.rs
+++ b/os/src/mm/memory_set.rs
@@ -63,6 +63,19 @@ impl MemorySet {
None,
);
}
+ /// Remove framed area
+ pub fn remove_framed_area(&mut self, range: VPNRange) {
+ let mut idx = 0;
+ for area in self.areas.iter_mut() {
+ if area.vpn_range.get_start().0 == range.get_start().0
+ && area.vpn_range.get_end().0 == range.get_end().0 {
+ area.unmap(&mut self.page_table);
+ break;
+ }
+ idx += 1;
+ }
+ 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 {
diff --git a/os/src/mm/mod.rs b/os/src/mm/mod.rs
index 06f045c..8f21283 100644
--- a/os/src/mm/mod.rs
+++ b/os/src/mm/mod.rs
@@ -12,9 +12,9 @@ mod heap_allocator;
mod memory_set;
mod page_table;
-pub use address::{PhysAddr, PhysPageNum, VirtAddr, VirtPageNum};
-use address::{StepByOne, VPNRange};
-pub use frame_allocator::{frame_alloc, FrameTracker};
+pub use address::{PhysAddr, PhysPageNum, VirtAddr, VirtPageNum, VPNRange};
+use address::StepByOne;
+pub use frame_allocator::{frame_alloc, FrameTracker, is_mem_sufficient};
pub use memory_set::remap_test;
pub use memory_set::{kernel_stack_position, MapPermission, MemorySet, KERNEL_SPACE};
pub use page_table::{translated_byte_buffer, PageTableEntry};
diff --git a/os/src/syscall/mod.rs b/os/src/syscall/mod.rs
index 4a5297d..129ed7f 100644
--- a/os/src/syscall/mod.rs
+++ b/os/src/syscall/mod.rs
@@ -30,8 +30,11 @@ mod process;
use fs::*;
use process::*;
+
+use crate::task::inc_syscall_times;
/// handle syscall exception with `syscall_id` and other arguments
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
+ inc_syscall_times(syscall_id);
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
diff --git a/os/src/syscall/process.rs b/os/src/syscall/process.rs
index e2f6662..e23a90c 100644
--- a/os/src/syscall/process.rs
+++ b/os/src/syscall/process.rs
@@ -1,9 +1,11 @@
//! Process management syscalls
+
+use core::mem::size_of;
+
use crate::{
- config::MAX_SYSCALL_NUM,
- task::{
- change_program_brk, exit_current_and_run_next, suspend_current_and_run_next, TaskStatus,
- },
+ config::MAX_SYSCALL_NUM, mm::translated_byte_buffer, task::{
+ change_program_brk, current_user_token, exit_current_and_run_next, get_scheduled_timespan, get_syscall_times, suspend_current_and_run_next, task_mmap, task_munmap, TaskStatus
+ }, timer::get_time_us
};
#[repr(C)]
@@ -43,7 +45,21 @@ pub fn sys_yield() -> isize {
/// HINT: What if [`TimeVal`] is splitted by two pages ?
pub fn sys_get_time(_ts: *mut TimeVal, _tz: usize) -> isize {
trace!("kernel: sys_get_time");
- -1
+ let _us = get_time_us();
+ let time_val = TimeVal {
+ sec: _us / 1_000_000,
+ usec: _us % 1_000_000,
+ };
+ let buffers = translated_byte_buffer(
+ current_user_token(), _ts as *const u8, size_of::<TimeVal>());
+ let mut time_val_ptr = &time_val as *const _ as *const u8;
+ for buffer in buffers {
+ unsafe {
+ time_val_ptr.copy_to(buffer.as_mut_ptr(), buffer.len());
+ time_val_ptr = time_val_ptr.add(buffer.len());
+ }
+ }
+ 0
}
/// YOUR JOB: Finish sys_task_info to pass testcases
@@ -51,19 +67,33 @@ pub fn sys_get_time(_ts: *mut TimeVal, _tz: usize) -> isize {
/// HINT: What if [`TaskInfo`] is splitted by two pages ?
pub fn sys_task_info(_ti: *mut TaskInfo) -> isize {
trace!("kernel: sys_task_info NOT IMPLEMENTED YET!");
- -1
+ let task_info = TaskInfo {
+ status: TaskStatus::Running,
+ syscall_times: get_syscall_times(),
+ time: get_scheduled_timespan(),
+ };
+ let buffers = translated_byte_buffer(
+ current_user_token(), _ti as *const u8, size_of::<TaskInfo>());
+ let mut task_info_ptr = &task_info as *const _ as *const u8;
+ for buffer in buffers {
+ unsafe {
+ task_info_ptr.copy_to(buffer.as_mut_ptr(), buffer.len());
+ task_info_ptr = task_info_ptr.add(buffer.len());
+ }
+ }
+ 0
}
// YOUR JOB: Implement mmap.
pub fn sys_mmap(_start: usize, _len: usize, _port: usize) -> isize {
- trace!("kernel: sys_mmap NOT IMPLEMENTED YET!");
- -1
+ // trace!("kernel: sys_mmap NOT IMPLEMENTED YET!");
+ task_mmap(_start, _len, _port)
}
// YOUR JOB: Implement munmap.
pub fn sys_munmap(_start: usize, _len: usize) -> isize {
- trace!("kernel: sys_munmap NOT IMPLEMENTED YET!");
- -1
+ // trace!("kernel: sys_munmap NOT IMPLEMENTED YET!");
+ task_munmap(_start, _len)
}
/// change data segment size
pub fn sys_sbrk(size: i32) -> isize {
diff --git a/os/src/task/mod.rs b/os/src/task/mod.rs
index a745df8..460b23b 100644
--- a/os/src/task/mod.rs
+++ b/os/src/task/mod.rs
@@ -14,8 +14,11 @@ mod switch;
#[allow(clippy::module_inception)]
mod task;
+use crate::config::MAX_SYSCALL_NUM;
use crate::loader::{get_app_data, get_num_app};
+use crate::mm::{is_mem_sufficient, MapPermission, VPNRange, VirtAddr, VirtPageNum};
use crate::sync::UPSafeCell;
+use crate::timer::get_time_ms;
use crate::trap::TrapContext;
use alloc::vec::Vec;
use lazy_static::*;
@@ -140,6 +143,11 @@ impl TaskManager {
let mut inner = self.inner.exclusive_access();
let current = inner.current_task;
inner.tasks[next].task_status = TaskStatus::Running;
+ // if the task is being called for the first time,
+ if inner.tasks[next].time == 0usize {
+ // then set it to the current time
+ inner.tasks[next].time = get_time_ms();
+ }
inner.current_task = next;
let current_task_cx_ptr = &mut inner.tasks[current].task_cx as *mut TaskContext;
let next_task_cx_ptr = &inner.tasks[next].task_cx as *const TaskContext;
@@ -153,6 +161,80 @@ impl TaskManager {
panic!("All applications completed!");
}
}
+
+ /// Get task's syscall times
+ pub fn get_syscall_times(&self) -> [u32; MAX_SYSCALL_NUM] {
+ let inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ inner.tasks[current].syscall_times.clone()
+ }
+
+ /// Get task's time span between current time and first scheduled time
+ pub fn get_scheduled_timespan(&self) -> usize {
+ let inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ get_time_ms() - inner.tasks[current].time
+ }
+
+ /// Increase syscall times
+ pub fn inc_syscall_times(&self, syscall_id: usize) -> bool {
+ if syscall_id >= MAX_SYSCALL_NUM {
+ return false;
+ }
+ let mut inner = self.inner.exclusive_access();
+ let current = inner.current_task;
+ inner.tasks[current].syscall_times[syscall_id] += 1;
+ true
+ }
+
+ /// Map some pages to memory_set
+ fn mmap(&self, start_va: VirtAddr, end_va: VirtAddr, port: usize) -> isize {
+ let mut inner = self.inner.exclusive_access();
+ let cur = inner.current_task;
+ let vpn_range = VPNRange::new(
+ VirtPageNum::from(start_va),
+ end_va.ceil()
+ );
+
+ // 3. Check if trying to map mapped page
+ for vpn in vpn_range {
+ if let Some(pte) = inner.tasks[cur].memory_set.translate(vpn) {
+ if pte.is_valid() {
+ return -1;
+ }
+ }
+ }
+
+ // After checking all errors may occur,
+ // push a new map_area to current memory_set
+ let perm = MapPermission::from_bits((port as u8) << 1).unwrap() | MapPermission::U;
+ inner.tasks[cur].memory_set.insert_framed_area(start_va, end_va, perm);
+ 0
+ }
+
+ /// Unmap some pages in memory set
+ fn munmap(&self, start_va: VirtAddr, end_va: VirtAddr) -> isize {
+ let mut inner = self.inner.exclusive_access();
+ let cur = inner.current_task;
+ let vpn_range = VPNRange::new(
+ VirtPageNum::from(start_va),
+ end_va.ceil(),
+ );
+
+ // 1. Check if trying to unmap unmapped page
+ for vpn in vpn_range {
+ if let Some(pte) = inner.tasks[cur].memory_set.translate(vpn) {
+ if !pte.is_valid() {
+ return -1
+ }
+ }
+ }
+
+ // After checking all errors may occur,
+ // remove some pages from current memory_set
+ inner.tasks[cur].memory_set.remove_framed_area(vpn_range);
+ 0
+ }
}
/// Run the first task in task list.
@@ -202,3 +284,46 @@ pub fn current_trap_cx() -> &'static mut TrapContext {
pub fn change_program_brk(size: i32) -> Option<usize> {
TASK_MANAGER.change_current_program_brk(size)
}
+
+/// Get current task's syscall times
+pub fn get_syscall_times() -> [u32; MAX_SYSCALL_NUM] {
+ TASK_MANAGER.get_syscall_times()
+}
+
+/// Get current task's time span between current time and first scheduled time
+pub fn get_scheduled_timespan() -> usize {
+ TASK_MANAGER.get_scheduled_timespan()
+}
+
+/// Increase current task's syscall times
+pub fn inc_syscall_times(syscall_id: usize) -> bool {
+ TASK_MANAGER.inc_syscall_times(syscall_id)
+}
+
+/// Map some pages to current task's memory_set
+pub fn task_mmap(start: usize, len: usize, port: usize) -> isize {
+ let start_va = VirtAddr::from(start);
+ // 1. illegal start virtual address or port
+ if !start_va.aligned() || port & !0x7 != 0 || port & 0x7 == 0 {
+ return -1;
+ }
+
+ // 2. Check is there sufficient physical memory
+ if !is_mem_sufficient(len) {
+ return -1;
+ }
+
+ let end_va = VirtAddr::from(start + len);
+ TASK_MANAGER.mmap(start_va, end_va, port)
+}
+
+/// Unmap some pages in current task's memory set
+pub fn task_munmap(start: usize, len: usize) -> isize {
+ let start_va = VirtAddr::from(start);
+ if !start_va.aligned() {
+ return -1;
+ }
+
+ let end_va = VirtAddr::from(start + len);
+ TASK_MANAGER.munmap(start_va, end_va)
+}
\ No newline at end of file
diff --git a/os/src/task/task.rs b/os/src/task/task.rs
index dce6981..396342d 100644
--- a/os/src/task/task.rs
+++ b/os/src/task/task.rs
@@ -1,6 +1,6 @@
//! Types related to task management
use super::TaskContext;
-use crate::config::TRAP_CONTEXT_BASE;
+use crate::config::{MAX_SYSCALL_NUM, TRAP_CONTEXT_BASE};
use crate::mm::{
kernel_stack_position, MapPermission, MemorySet, PhysPageNum, VirtAddr, KERNEL_SPACE,
};
@@ -28,6 +28,12 @@ pub struct TaskControlBlock {
/// Program break
pub program_brk: usize,
+
+ /// The syscall times
+ pub syscall_times: [u32; MAX_SYSCALL_NUM],
+
+ /// The first time the task was scheduled
+ pub time: usize,
}
impl TaskControlBlock {
@@ -63,6 +69,8 @@ impl TaskControlBlock {
base_size: user_sp,
heap_bottom: user_sp,
program_brk: user_sp,
+ syscall_times: [0u32; MAX_SYSCALL_NUM],
+ time: 0usize,
};
// prepare TrapContext in user space
let trap_cx = task_control_block.get_trap_cx();
问答作业
t1
Q: 请列举 SV39 页表页表项的组成,描述其中的标志位有何作用?
A:
上图为 SV39 分页模式下的页表项,其中 [53:10]
这 44 位是物理页号,最低的 8 位 [7:0]
则是标志位,它们的含义如下:
- 仅当 V(Valid) 位为 1 时,页表项才是合法的;
- R/W/X 分别控制索引到这个页表项的对应虚拟页面是否允许读/写/取指;
- U 控制索引到这个页表项的对应虚拟页面是否在 CPU 处于 U 特权级的情况下是否被允许访问;
- G 代表全局属性,当 G 被设置为 1 时,表示该页表项所描述的页面是全局共享的;
- A(Accessed) 记录自从页表项上的这一位被清零之后,页表项的对应虚拟页面是否被访问过;
- D(Dirty) 则记录自从页表项上的这一位被清零之后,页表项的对应虚拟页表是否被修改过。
t2
缺页指的是进程访问页面时页面不在页表中或在页表中无效的现象,此时 MMU 将会返回一个中断, 告知 os 进程内存访问出了问题。os 选择填补页表并重新执行异常指令或者杀死进程。
Q1: 请问哪些异常可能是缺页导致的?
A1: 进程访问未映射的内存页面、访问已被换出到磁盘的页面、进程尝试以不正确的权限访问页面(例如写入只读页面)。
Q2: 发生缺页时,描述相关重要寄存器的值,上次实验描述过的可以简略。
A2: 以下是与缺页相关的 CSR 寄存器的值:
scause
: 记录导致异常的原因。对于缺页异常,该寄存器的值包含异常的类型和特定的错误代码,指示发生了缺页异常。sstatus
: 记录处理器当前状态,其中 SPP 段记录当前特权等级。sepc
: 当缺页异常发生时,sepc
会保存出错指令的地址,以便在异常处理完成后能够返回到该指令重新执行。stval
: 在缺页异常情况下,stval
会存储导致缺页的虚拟地址,帮助操作系统确定是哪个页面缺失。
缺页有两个常见的原因,其一是 Lazy 策略,也就是直到内存页面被访问才实际进行页表操作。 比如,一个程序被执行时,进程的代码段理论上需要从磁盘加载到内存。但是 os 并不会马上这样做, 而是会保存 .text
段在磁盘的位置信息,在这些代码第一次被执行时才完成从磁盘的加载操作。
Q3: 这样做有哪些好处?
A3: 只在需要时加载页面,避免不必要的内存占用,允许系统将更多进程同时驻留在内存中;程序在启动时不必立即加载所有代码和数据,从而减少初始加载时间。
其实,我们的 mmap 也可以采取 Lazy 策略,比如:一个用户进程先后申请了 10G 的内存空间, 然后用了其中 1M 就直接退出了。按照现在的做法,我们显然亏大了,进行了很多没有意义的页表操作。
Q4: 处理 10G 连续的内存页面,对应的 SV39 页表大致占用多少内存 (估算数量级即可)?
A4: 略。
Q5: 请简单思考如何才能实现 Lazy 策略,缺页时又如何处理?描述合理即可,不需要考虑实现。
A5: 加载程序时并不真正将代码段加载到内存,将页表项的有效位设置为 0,访问该代码段时会触发缺页异常,进入内核后得知异常原因是访问 Lazy 页面,随后进行异常处理:将页面从磁盘加载入内存,随后返回用户态重新执行。
缺页的另一个常见原因是 swap 策略,也就是内存页面可能被换到磁盘上了,导致对应页面失效。
Q6: 此时页面失效如何表现在页表项(PTE)上?
A6: 有效位为 0.
t3
为了防范侧信道攻击,我们的 os 使用了双页表。但是传统的设计一直是单页表的,也就是说, 用户线程和对应的内核线程共用同一张页表,只不过内核对应的地址只允许在内核态访问。 (备注:这里的单/双的说法仅为自创的通俗说法,并无这个名词概念,详情见 KPTI )
Q1: 在单页表情况下,如何更换页表?
A1: 无需更换,通过标志位进行控制,可能需要将 TLB 清空。
Q2: 单页表情况下,如何控制用户态无法访问内核页面?(tips:看看上一题最后一问)
A2: 将内核页面的 U 标志位设置为 0.
Q3: 单页表有何优势?(回答合理即可)
A3: 不需要跳板代码进行用户态和内核态的切换,内核态和用户态切换的速度更快。
Q4: 双页表实现下,何时需要更换页表?假设你写一个单页表操作系统,你会选择何时更换页表(回答合理即可)?
A4: 双页表实现下,用户态和内核态切换、不同进程切换时需要更换页表。对于单页表操作系统,不同用户线程切换时需要更换页表。