rCore Notes: Labs in Chapter 4

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.

Licensed under CC BY-NC-SA 4.0
Built with Hugo
Theme Stack designed by Jimmy