Introduction
This post records the Labs in Chapter 4 of rCore Tutorial Book V3.
Labs
Lab 1: Fix sys_get_time
Problem
After use Virtual Memory, the old sys_get_time
implementation will cause a page fault.
Analysis
The old implementation of sys_get_time
directly writes the time value to the address pointed by the ts
pointer. However, the ts
pointer is a virtual address, and the kernel’s page table is different from the user’s page table in rCore.
1
2
3
4
5
6
7
8
9
10
11
12
| // os/src/syscall/process.rs
pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize {
let us = get_time_us();
unsafe {
*ts = TimeVal {
sec: us / 1_000_000,
usec: us % 1_000_000,
};
}
0
}
|
We can translate the virtual address to a physical address and then write the time value to the physical address.
Solution
For easy implementation, we can move TimeVal
to the os/src/timer.rs
file.
1
2
3
4
5
6
7
8
| // os/src/timer.rs
#[repr(C)]
#[derive(Debug)]
pub struct TimeVal {
pub sec: usize,
pub usec: usize,
}
|
Then, we can implement a function to translate the virtual address to a physical address. We translate the vrirtual page number to a physical page number and then calculate the physical address by adding the page offset.
1
2
3
4
5
6
7
8
9
10
11
12
| // os/src/mm/page_table.rs
pub fn translated_time_val(token: usize, ptr: *const usize) -> &'static mut TimeVal {
let page_table = PageTable::from_token(token);
let va = VirtAddr::from(ptr as usize);
let vpn = va.floor();
let ppn = page_table.translate(vpn).unwrap().ppn();
let pa: PhysAddr = ppn.into();
unsafe {
((pa.0 + va.page_offset()) as *mut TimeVal).as_mut().unwrap()
}
}
|
Finally, we can modify the sys_get_time
function to use the translated_time_val
function to get the physical address of the TimeVal
struct.
1
2
3
4
5
6
7
8
9
10
11
| // os/src/syscall/process.rs
pub fn sys_get_time(ts: *mut TimeVal, _tz: usize) -> isize {
let us = get_time_us();
let ts = translated_time_val(current_user_token(), ts as *const usize);
*ts = TimeVal {
sec: us / 1_000_000,
usec: us % 1_000_000,
};
0
}
|
This problem is solved, we can test it by run make run TEST=1
.
Lab 2: Implement mmap
and munmap
System Calls
Problem
In Linux, mmap
is primarily used for mapping files into memory, but for the purpose of this experiment, its functionality is simplified to only request memory.
Please implement the mmap
and munmap
system calls. The definition of mmap
is as follows:
1
| fn sys_mmap(start: usize, len: usize, prot: usize) -> isize
|
System call ID: 222
Requests physical memory of size len
bytes (it is not required to specify the actual physical memory location; any available block will suffice) and maps it to the virtual memory starting at start
with memory page attributes defined by prot
.
Parameters:
start
: The starting address of the virtual memory to be mapped, which must be page-aligned.len
: The length in bytes of the mapping, which can be 0.prot
: The memory protection flags:- Bit 0 indicates read permission.
- Bit 1 indicates write permission.
- Bit 2 indicates execute permission.
- Other bits are invalid and must be 0.
Return Value: Returns 0
on success, or -1
in case of an error.
Notes:
- To simplify, the target virtual memory region must be page-aligned. The length
len
can be rounded up to the nearest page size. Memory page reclamation in case of allocation failure is not considered.
Possible Errors:
start
is not aligned to the page size.prot & !0x7 != 0
(other bits of prot
must be 0).prot & 0x7 = 0
(such memory has no meaning).- The range
[start, start + len)
contains already mapped pages. - Insufficient physical memory.
The definition of munmap
is as follows:
1
| fn sys_munmap(start: usize, len: usize) -> isize
|
- System call ID: 215
- Unmaps the virtual memory in the range
[start, start + len)
. - Parameters and Return Value: Refer to the
mmap
system call for parameter descriptions and return values. - Notes:
- To simplify, memory recovery and reclamation in case of parameter errors are not considered.
- Possible Errors:
- The range
[start, start + len)
contains unmapped virtual memory.
TIPS: Pay attention to the semantics of the prot
parameter, as it differs significantly from the kernel-defined MapPermission
!
Analysis
Actually, most key points are given in the problem description. A fun fact is that the prot
parameter can be converted to the MapPermission
enum kust by shifting a bit. The definitions of MapPermission
and MmapProtection
are as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| // os/src/mm/memory_set.rs
bitflags! {
pub struct MapPermission: u8 {
const R = 1 << 1;
const W = 1 << 2;
const X = 1 << 3;
const U = 1 << 4;
}
pub struct MmapProtection: u8 {
const R = 1 << 0;
const W = 1 << 1;
const X = 1 << 2;
}
}
|
So we can convert the prot
parameter to MapPermission
by:
1
| MapPermission::from_bits_truncate(prot.bits << 1) | MapPermission::U
|
Solution
At first, we shoud change map
and unmap
functions in PageTable
, in the past, we use assert to check the validity of the page table entry, but now we should return a boolean value to indicate the success of the operation.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // os/src/mm/page_table.rs
impl PageTable {
#[allow(unused)]
pub fn map(&mut self, vpn: VirtPageNum, ppn: PhysPageNum, flags: PTEFlags) -> bool {
let pte = self.find_pte_create(vpn).unwrap();
if pte.is_valid() {
return false;
}
*pte = PageTableEntry::new(ppn, flags | PTEFlags::V);
true
}
#[allow(unused)]
pub fn unmap(&mut self, vpn: VirtPageNum) -> bool {
let pte = self.find_pte_create(vpn).unwrap();
if !pte.is_valid() {
return false;
}
*pte = PageTableEntry::empty();
true
}
}
|
For the same reason, we should change the map_one
, unmap_one
, map
, and unmap
functions in MapArea
. We can unmap
the pages that have been mapped when an error occurs in the map
function.
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
| // os/src/mm/memory_set.rs
impl MapArea {
pub fn map_one(&mut self, page_table: &mut PageTable, vpn: VirtPageNum) -> bool {
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) -> bool {
match self.map_type {
MapType::Framed => {
self.data_frames.remove(&vpn);
}
_ => {}
}
page_table.unmap(vpn)
}
pub fn map(&mut self, page_table: &mut PageTable) -> bool {
if let Some(err_vpn) = self
.vpn_range
.into_iter()
.find(|&vpn| !self.map_one(page_table, vpn))
{
VPNRange::new(self.vpn_range.get_start(), err_vpn)
.into_iter()
.for_each(|vpn| {
self.unmap_one(page_table, vpn);
});
false
} else {
true
}
}
#[allow(unused)]
pub fn unmap(&mut self, page_table: &mut PageTable) -> bool {
for vpn in self.vpn_range {
if !self.unmap_one(page_table, vpn) {
return false;
}
}
true
}
}
|
Then, we can implement the mmap
and munmap
functions in the MemorySet
struct. The mmap
function will push a new MapArea
to the MemorySet
and map the pages in the MapArea
. The munmap
function will unmap the pages in the range [start_va, end_va)
. The definitions of MmapProtection
have been given above in Analysis.
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
| // os/src/mm/memory_set.rs
impl MemorySet {
pub fn mmap(&mut self, start_va: VirtAddr, end_va: VirtAddr, prot: MmapProtection) -> bool {
self.push(
MapArea::new(
start_va,
end_va,
MapType::Framed,
MapPermission::from_bits_truncate(prot.bits << 1) | MapPermission::U,
),
None,
)
}
pub fn munmap(&mut self, start_va: VirtAddr, end_va: VirtAddr) -> bool {
for vpn in VPNRange::new(start_va.floor(), end_va.ceil()) {
let mut vpn_result = false;
for i in 0..self.areas.len() {
if self.areas[i].unmap_one(&mut self.page_table, vpn) {
vpn_result = true;
}
}
if !vpn_result {
return false;
}
}
true
}
}
|
In the TaskControlBlock
struct, we can implement the mmap
and munmap
functions to call the mmap
and munmap
functions in the MemorySet
struct. The validation of the prot
parameter is also implemented in the mmap
function, and the start_va
should be page-aligned.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| // os/src/task/tasks.rs
impl TaskControlBlock {
pub fn mmap(&mut self, start: usize, len: usize, prot: usize) -> bool {
if prot & !7 != 0 || prot & 7 == 0 {
return false;
}
let start_va = VirtAddr::from(start);
let end_va = VirtAddr::from(start + len);
if !start_va.aligned() {
return false;
}
self.memory_set.mmap(start_va, end_va, MmapProtection::from_bits_truncate(prot as u8))
}
pub fn munmap(&mut self, start: usize, len: usize) -> bool {
let start_va = VirtAddr::from(start);
let end_va = VirtAddr::from(start + len);
self.memory_set.munmap(start_va, end_va)
}
}
|
Then we should expose these functions for syscalls subsystem to use.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| // os/src/task/mod.rs
impl TaskManager {
fn current_mmap(&self, start: usize, len: usize, prot: usize) -> bool {
let mut inner = self.inner.exclusive_access();
let cur = inner.current_task;
inner.tasks[cur].mmap(start, len, prot)
}
fn current_munmap(&self, start: usize, len: usize) -> bool {
let mut inner = self.inner.exclusive_access();
let cur = inner.current_task;
inner.tasks[cur].munmap(start, len)
}
}
pub fn current_mmap(start: usize, len: usize, prot: usize) -> bool {
TASK_MANAGER.current_mmap(start, len, prot)
}
pub fn current_munmap(start:usize, len: usize) -> bool {
TASK_MANAGER.current_munmap(start, len)
}
|
Finally, we can implement the sys_mmap
and sys_munmap
functions in the syscall
module for reacting to the system calls.
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
| // os/src/syscall/mod.rs
const SYSCALL_MMAP: usize = 222;
const SYSCALL_MUNMAP: usize = 215;
pub fn syscall(syscall_id: usize, args: [usize; 3]) -> isize {
match syscall_id {
SYSCALL_WRITE => sys_write(args[0], args[1] as *const u8, args[2]),
SYSCALL_EXIT => sys_exit(args[0] as i32),
SYSCALL_YIELD => sys_yield(),
SYSCALL_GET_TIME => sys_get_time(args[0] as *mut TimeVal, args[1]),
SYSCALL_MMAP => sys_mmap(args[0], args[1], args[2]),
SYSCALL_MUNMAP => sys_munmap(args[0], args[1]),
_ => panic!("Unsupported syscall_id: {}", syscall_id),
}
}
// os/src/syscall/process.rs
pub fn sys_mmap(start: usize, len: usize, prot: usize) -> isize {
if current_mmap(start, len, prot) {
0
} else {
-1
}
}
pub fn sys_munmap(start: usize, len: usize) -> isize {
if current_munmap(start, len) {
0
} else {
-1
}
}
|
This problem is solved, we can test it by run make run TEST=2
.
Conclusion
In this chapter, I have learned about the memory management solutions in modern operating systems and CPU architectures. I got to know the basic concepts of virtual memory, page tables, and memory protection, and deeply understood the computer is composed of layers of abstraction. I also learned how to implement the mmap
and munmap
system calls in rCore, which is a good practice for understanding the memory management mechanism in the operating systems. I will continue to learn the following chapters in future.