Problem set 3: WeensyOS

In this assignment, you will add new features to a tiny operating system: process memory isolation, virtual memory, and some system calls.

You may want to read Chapter 9 of the textbook. Specifically, the 64-bit x86 virtual memory architecture is described in Section 9.7; the PTE_P, PTE_W, and PTE_U bits are shown in Figure 9.23 and discussed in Section 9.7.1.

Get the code

Get our code with

$ git pull; git pull --no-rebase handout main

or, alternately

$ git pull; git pull --no-rebase https://github.com/cs61/cs61-f26-psets.git main

This will merge our Problem Set 3 code with your previous work. If you have any “conflicts” from prior problem sets, resolve them before continuing further. Run git push to save your work back to your personal repository.

Running WeensyOS

To launch WeensyOS, open a Docker container with ./cs61-run-docker, then cd pset3; make run. You should see something like this, which shows four related p-allocator processes running in parallel:

Initial WeensyOS state

The image above loops forever; in an actual run on your real computer, the bars will move to the right and stay there. Don’t worry if your image has different numbers of K’s or otherwise has different details. (If your bars run painfully slowly, edit the p-allocator.cc file and reduce the ALLOC_SLOWDOWN constant.)

The WeensyOS documentation has more information on how to build WeensyOS and how to run it in different modes, including modes that output more debugging information. Some highlights:

Initial state

WeensyOS displays the current state of physical and virtual memory. Each character represents 4 KiB of memory (i.e., a single x86-64 page). There are 2 MiB of physical memory in total. (How many pages is this?) Here are two labeled physical memory diagrams, showing what the characters mean and how memory is arranged.

Physical memory map 1

Physical memory map 2

The virtual memory display is similar, but it may contain blank spaces as well. A blank virtual memory page corresponds to an unmapped page, and when a process (or the kernel) tries to access such an address, the processor will page fault.

The handout version of WeensyOS runs four processes, 1 through 4. Each process runs a different program. The four programs are compiled from the same source code (p-allocator.cc), but for each program the compiler is told to use a different region of memory for its text and data segments. Each p-allocator asks the kernel for more heap space, one page at a time, until it runs out of room. Each process’s heap begins just above its code and global data, and ends just below its stack. The processes allocate space at different rates: Process 2 allocates space twice as quickly as Process 1, Process 3 goes three times faster, and Process 4 goes four times faster. (A random number generator is used, so the exact rates may vary.) The marching rows of numbers show how quickly the heap spaces for processes 1, 2, 3, and 4 are allocated.

Although the virtual memory display cycles between the four processes’ address spaces, in the handout code the display won’t change much from process to process, because all the address spaces are the same. This means that there is no memory isolation between processes—something you must fix!

In the virtual memory display, a character is reverse video (i.e., black foreground and colored background) if an application process is allowed to access the corresponding address. Initially, any process can modify all of physical memory, including the kernel. This means that memory is not properly isolated!

Goal

You will implement complete and correct memory isolation for WeensyOS processes. Then you'll implement full virtual memory, which will improve utilization of physical RAM.

This assignment has lots of support code, but the code you write should all go in kernel.cc.

There is no make check functionality for this pset. Instead, you should run your instance of WeensyOS and visually compare it to the images in the pset description.

Phase 1: Kernel isolation

WeensyOS processes could stomp all over the kernel’s memory if they wanted to. Better stop that! Change kernel_start, the kernel initialization function, so that kernel memory is inaccessible to applications—except for the memory holding the CGA console (the single page at CONSOLE_ADDR == 0xB8000).

When you are done, WeensyOS should look like this. In the virtual map, kernel memory is no longer reverse-video, since the user can’t access it (except for the CGA console).

Kernel isolation

Use vmiter to create memory mappings. Start from the vmiter loop in the kernel_start function.

About virtual memory iterators (vmiter)

WeensyOS memory layout

The identity-mapped kernel_pagetable

If you get stuck or confused, read the debugging notes on this page!

In addition, make sure that your sys_page_alloc system call preserves kernel isolation: Applications shouldn’t be able to use sys_page_alloc to screw up the kernel. This requires changes to the SYSCALL_PAGE_ALLOC case in syscall. Read the description of sys_page_alloc in u-lib.hh to get a feeling for the possible errors.

Phase 2: Isolated address spaces

Implement process isolation by giving each process its own independent page table. Your OS should look like this:

Per-process isolation

Each process only has permission to access its own pages, which you can tell because only its own pages are shown in reverse video.

How to implement per-process page tables in process_setup:

Note the diagram now has four pages for each process in the kernel area, starting at 0x1000. These are the four-level page tables for each process. (The colored background indicates that these pages contain kernel-private page table data, even though the pages “belong” to the process.) The first page was allocated explicitly in process_setup; the other pages were allocated by vmiter::try_map as the page table was initialized.

One common solution, shown above, leaves addresses above PROC_START_ADDR totally unmapped by default, but other designs work too. As long as a virtual address mapping has no PTE_U bit, its process isolation properties are unchanged. For instance, this solution, in which all mappings are present but accessible only to the kernel, also implements process isolation correctly:

Per-process isolation, alternate

If you create an incorrect page table, WeensyOS might crazily reboot. Don’t panic; see the debugging hints above.

About program images and segments (pgm and seg)

Phase 3: General process address spaces

So far, WeensyOS processes use identity mappings for process memory: a process code, data, stack, or heap page with virtual address X is stored in the physical page with physical address X. This is inflexible and limits utilization. Processes don’t have access to the address mapping (only the kernel does), so it should be fine for a process’s virtual address X to map to a different physical page—the process won’t be able to tell the difference. This will also enable new functionality, like running different processes with similar virtual address spaces.

Change your operating system to allocate all process data, including its code, globals, stack, and heap, using kalloc instead of direct access to the physpages array. This will turn the process page tables from subsets of an identity mapping to a more general mapping, where lots of pages have different virtual and physical addresses.

Here’s how your OS should look after this phase.

Virtual page allocation

This will complicate the code that initializes process code in process_setup. You’ll need to figure out why (hint: which page table is being used in process_setup?) and find a way around it (hint: vmiter or set_pagetable).

Phase 4: Nonsequential physical page allocation

In the handout code, kalloc always chooses the available physical page that has the lowest address. This means consecutive kalloc calls typically return consecutive physical pages. But your code should not depend on this behavior.

Change kalloc to allocate pages nonsequentially. This can be as simple as setting page_increment to 3 (or any larger odd number). The virtual address spaces should work as before, though the physical memory map will look different:

Virtual page allocation

But if you see a panic—like, maybe,

PAGE FAULT on 0xcccccccccccccccc (pid 3, read missing page, rip=0xcccccccccccccccc)!

you have a problem. Go back and think again about how to copy instructions and data from program segments into process memory.

Once you’re confident in your phase 4 solution, you can go back to setting page_increment = 1, but your code should work with any page allocation strategy.

Phase 5: Overlapping address spaces

Now the processes are isolated, which is awesome, but they’re still not taking full advantage of virtual memory. Since they are isolated, they can use the same address ranges without conflicting on the underlying data.

In this phase, change each process’s stack to grow down from address 0x300000 == MEMSIZE_VIRTUAL. Now the processes have enough heap room to use up all of physical memory!

Overlapping address spaces

If there’s no physical memory available, sys_page_alloc should return an error code to the calling process, such as -1. Do not kill the calling process! Lack of memory is a potentially recoverable problem.

Debugging WeensyOS

There are several ways to debug WeensyOS. We recommend:

Understanding memory errors

You may want to skip this section until you have completed the first few parts of the pset.

The WeensyOS memory viewer, which is defined in k-memviewer.cc, checks your memory management data structures and reports any problems it sees. The memusage::symbol_at() function chooses which symbol to display for each page and detects some errors. Here’s what some of those symbols and error messages mean.

The WeensyOS exception handler will also print messages on page faults, which indicate that process memory accesses went wrong.

Turnin

You will turn in your code by pushing your git repository to github.com/cs61/YOUR-PSET_REPO.git and updating the grading server.