February 7, 2022
In this lab, we are going to cover allocating pages of physical memory for user process just at the moment the process needs them (i.e., allocating them on-demand).
osv
uses an address space and a list of memory regions
(i.e., segments) to track information about a virtual address space. A
virtual address space ranges from virtual address
[0, 0xffffffffffffffff]
on a 64-bit machine. A memory
region tracks a contiguous region of memory within an address space. A
memory region belongs to only one address space, an address space can
have many memory regions. The kernel sets up memory regions for each
process and use them to track valid memory ranges for a process to
access. Each user process has a memory region for code, stack, and heap
on start up. More details can be found here.
In addition to managing each process’s virtual address space, the
kernel is also responsible for setting up the address translation table
(page table) which maps a virtual address to a physical address. Each
virtual address space has its own page table, which is why when two
processes access the same virtual address, they access different data.
osv
uses struct vpmap
to represent the page
table. include/kernel/vpmap.h
defines a list of operations
on the page table. For example vpmap_map
maps a virtual
page to a physical page.
You will be interacting with all these abstractions in this lab.
To pull any new changes and tests for this lab, run the command
$ git pull upstream master
and merge. After the merge, double check that your code still passes tests for previous labs.
In the stack_setup
function in
kernel/proc.c
currently sets up each new process with a
page of stack memory. Great! But what if a process wants to use more
stack? One option is to allocate more physical pages for stack on start
up and map them into the process’s address space. But if a process
doesn’t actually use all of its stack, the allocated physical pages are
wasted while the process executes.
To reduce this waste, a common technique is to allocate physical
pages for additional stack pages on demand. For this part of the lab,
you will extend osv
to do on-demand stack growth. This
means that the physical memory of the additional stack page is
allocated at run-time. Whenever a user application issues an instruction
that reads or writes to the user stack (e.g., creating a stack frame,
accessing local variables), we grow the stack as needed. For this lab,
you should limit your overall stack size to USTACK_PAGES
(10) pages total.
To implement on-demand stack growth, you will need to understand how to handle page faults. A page fault is a hardware exception that occurs when a process accesses a virtual memory page without a valid page table mapping, or with a valid mapping, but where the process does not have permission to perform the operation.
On a page fault, the process will trap into the kernel and trigger the page fault handler. If the fault address is within a valid memory region and has the proper permission, the page fault handler should allocate a physical page, map it to the process’s page table, and resume process execution (i.e., return). Note that a write on a read only memory permission is not a valid access and the calling process should terminate.
osv’s page fault handler is
kernel/pgfault.c:handle_page_fault
.
void
(vaddr_t fault_addr, int present, int write, int user) {
handle_page_faultif (user) {
(&user_pgfault, 1);
__sync_add_and_fetch}
// turn on interrupt now that we have the fault address
(INTR_ON); intr_set_level
fault_addr
is the address that was attempted to be
accessedpresent
is set if it was a page protection issue (fault
address has a corresponding physical page mapped, but permission is
off). This is not set if the page is not present.write
is set if the fault occurred on a write.user
is set if the fault occurred in user mode.To support stack growth, you should first expand the range of stack
memory region so any address within [10 pages below
USTACK_UPPERBOUND
, USTACK_UPPERBOUND
] is
valid. Currently stack_setup
pmem_alloc
)as_map_memregion
). Note that
as_map_memregion
takes the address of the start of the
region as the second argument, which will be the lowest address within
the region.vpmap_map
). Again this function takes the address of the
start of a page, which will be the lowest address within the page.You will want to modify step (2.) to allow 10 stack pages. Step (1.)
should remain unchanged, as you want to allocate additional pages only
on demand (i.e., as part of the page fault handler). Step (3.) is
conceptually the same, but you will need to make sure it is mapping the
first (the highest) page in the stack. You should ignore the
/* Your Code Here. */
comment in stack_setup
for now—a different extension of stack behavior may be part of lab
5.
Your next step will be to implement the page fault handler. To avoid
information leaking, you need to memset
the allocated
physical page to 0s. Note that you cannot directly access physical
memory, so you need to translate the physical address to a kernel
virtual address using kmap_p2v(paddr)
before you do the
memset
. memset
is defined in
include/lib/string.h
and implemented in
lib/string.c
.
For this part, the reference solution modified
kernel/proc.c
and kernel/pgfault.c
.
After you have set up page fault handler to handle on-demand stack
growth, you will now support dynamic heap growth. Heap growth differs in
that a process has to explicitly request for more virtual address to be
allocated to its heap. A process that needs more memory at runtime can
call sbrk
(set program break) to grow its heap size. The
kernel implementation of sbrk
is in sys_sbrk
in syscall.c
. The common use case is the situation where a
user library routine, malloc
in C or new
in
C++, calls sbrk
whenever the application asks to allocate
memory that cannot fit on the current heap (e.g., if the heap is
completely allocated due to prior calls to malloc
).
If a user application wants to increase the heap size by
n
bytes, it calls sbrk(n)
.
sbrk(n)
returns the OLD limit. The user application can
also decrease the amount of the heap by passing negative values to
sbrk
. Generally, the user library asks sbrk
to
provide more space than immediately needed, to reduce the number of
system calls.
When a user process is first spawned, its heap memory region is
initialized to size 0, so the first call to malloc
always
calls sbrk
. osv
needs to track how much memory
has been allocated to each process’s heap, and also extend/decrease size
of heap based on the process’s request. To do so, you need to implement
(kernel/mm/vm.c:memregion_extend
).
Once you have properly set up the memory region range for dynamic
heap growth, you can handle page fault from heap address similar to how
you handle on-demand stack growth. osv
internally allocates
and frees user memory at page granularity, but a process can call
sbrk
to allocate/deallocate memory at byte granularity. The
OS does this to be portable, since an application cannot depend on the
machine adopting a specific page size.
In user space, we have provided an implementation of
malloc
and free
(in lib/malloc.c
)
that is going to use sbrk
. After the implementation of
sbrk
is done, user-level applications should be able to
call malloc
and free
.
For this part, the reference solution modified
kernel/mm/vm.c
, kernel/syscall.c
, and
kernel/pgfault.c
.
After you implement the system calls described above, test your lab 4
code by either running individual tests in osv
shell (i.e.,
make qemu
and then enter the name of the test file in
user/lab4
without the .c
), or run
python3 test.py 4
to run all the tests.
When debugging, I would recommend running individual tests in
osv
. test.py
depends on observing
TEST-NAME passed
in the output from osv
, and
the presence of debugging print statements can interfere with this (as
output from various processes can get mixed together). It’s also
important, therefore, that you disable any print statements before
submission to Gradescope.
You will submit your work for this project via Gradescope.
Gradescope lets you submit via GitHub, which is probably the easiest
method. All you’ll need to do is connect your GitHub account (the
Gradescope submission page has a button for this) and select the
repository and branch you wish to submit. Alternatively, you can create
a zip file of the osv-w22
directory and upload that. The
the arch
, include
and kernel
directories from your submission will be used.
When you submit, the autograder will compile your code and run the test cases. The Gradescope autograder will run each test multiple times.
Although you are allowed submit your answers as many times as you like, you should not treat Gradescope as your only debugging tool. Many people may submit their projects near the deadline, and thus it will Gradescope take longer to process the requests. You may not get feedback in a timely manner to help you debug problems.
This lab will be graded out of 90 points, as shown in the table below. Comments explaining your approach can help earn partial credit if there are tests that don’t pass. Poor coding style can lose points, so make sure to submit clean, well-organized code.
Test | Points |
---|---|
bad-mem-access |
10 |
grow-stack |
15 |
grow-stack-edgecase |
10 |
malloc-test |
10 |
sbrk-small |
15 |
sbrk-large |
15 |
sbrk-decrement |
15 |