编程作业

思路

重写 sys_get_timesys_task_info

首先,第一个任务:由于引入了虚拟内存后,sys_get_timesys_task_info 失效了,需要进行重写。

这里解释一下为什么会失效:以 sys_get_time 为例,用户在发起系统调用时,传入的参数 _ts 是用户态下的虚拟地址,它需要借助内核的软件地址转换机制,查找任务对应的页表,将 _ts 转换为物理地址,再对该地址处的值进行填充。这里可以使用 page_table.rs 中预先实现好的 translated_byte_buffer,它将一整个虚拟地址段翻译为一系列的物理地址段(每页一段),这样如果结构体 TimeValTaskInfo 横跨多个页面也同样适用。

注意,内核态代码中的地址仍然是虚拟地址,只不过在 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() 移除对应的页表项映射,最后将 areamemory_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: 双页表实现下,用户态和内核态切换、不同进程切换时需要更换页表。对于单页表操作系统,不同用户线程切换时需要更换页表。