编程作业

思路

重写 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
2
3
4
// 1. illegal start virtual address or port
if !start_va.aligned() || port & !0x7 != 0 || port & 0x7 == 0 {
return -1;
}

物理内存是否充足,需要判断尝试映射的内存长度与空闲物理内存的大小关系。rCore 所采用的物理内存策略是简单的栈式分配器,剩余物理内存大小等于 end - current 加上 recycled.len().

1
2
3
4
5
6
/// 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 出错。

1
2
3
4
5
6
7
8
// 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 中移除。

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 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);
}

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
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: 双页表实现下,用户态和内核态切换、不同进程切换时需要更换页表。对于单页表操作系统,不同用户线程切换时需要更换页表。