The purpose of this project is to implement Copy-On-Write (COW) in the xv6 kernel.
The current implementation of the fork()
system call in xv6 copies the entire user-space memory of the parent process to the child. Additionally, this task is often unnecessary, especially when fork() is followed by an exec()
in the child, which discards the copied memory without it being used for the most part. However, if both processes (parent and child) share a page and one or both of them write to it, then a separate copy is required for each of them.
The goal of supporting Copy-On-Write (COW) during fork() is to defer the allocation and copying of physical memory pages for the child until it actually needs to use them, if indeed it needs them.
- Compiler with support for C 11 or newer
sudo apt install gcc
- You will need a RISC-V "newlib" tool chain from https://github.com/riscv/riscv-gnu-toolchain, and qemu compiled for riscv64-softmmu. Once they are installed, and in your shell search path, you can run "make qemu".
When on the xv6-project-2021 directory of the project:
make qemu
Then execute the tests cowtest
and usertests
:
cowtest
usertests
To exit from qemu
, press Ctrl+A
then X
.
- In the file spinlock.h, the structure of the
extern struct ref_spinlock
is defined, which is used to hold areference counter
for eachphysical page
, as stated in the assignment, through the integer arrayreference_count
. In the file kalloc.c, this global struct is created, namedref_c
.
Source code can be found here: kalloc.c
In the existing for loop, the initialization of the reference counter of the current iteration's page was added with the value 1 (this counter will be reset to 0 through kfree()). This action is necessary because later kfree() is called, which decrements the counter - so initialization must be done first.
When kfree() is called, firstly the reference counter of the given page is decremented. Then, a check is performed to see if this counter is equal to 0. In this case, it means that no process is using the specific page, so it can be freed; the page is returned to the list of available pages (following the code as it was initially in kfree()). If it's not 0, then no further action is needed.
The initialization of the reference counter of the specific page with the value 1 was added (lines 101-104).
Source code can be found here: trap.c
An else if(r_scause() == 15)
was added at line 68 to recognize page fault 15 - Store/AMO page fault. When page fault with code 15 occurs, it must be recognized as a valid CoW fault, meaning it is a valid page, user-level, etc. First, it checks if the virtual address (obtained by calling r_stval()) is permissible. Then, it retrieves the page table entry (PTE) of the specific page using the walk() function. It checks if the PTE obtained is valid and user-level. The physical address of the PTE is stored in the variable pa0. As mentioned in the assignment, a new page is created via kalloc() and the old page is copied to the new one via memmove(). Finally, the flags of the PTE of the old page are retrieved, the PTE is made to point to the physical address of the new page, and the PTE_W bit is set to 1. kfree() is called for the previous (initial) read-only page.
Source code can be found here: vm.c
The uvmcopy() function is modified appropriately so that new pages are not created but existing pa pages are used when mappages() is called (line 317), in order to map the physical memory pages of the parent to the child. Additionally, the PTE_W flag in both the parent's and child's PTEs should be cleared. This is achieved on lines 316 and 320 using bitwise AND. In each iteration of each page, the reference counter is incremented.
The purpose of walkaddr() is to traverse and directly retrieve the physical address from a virtual address without going through the hardware MMU, which means that no page fault has occurred. So, what we need to do is modify copyout() to use the same mechanism as page faults when encountering a CoW page. Just like in the usertrap() function in trap.c, all necessary checks and procedures are performed. That is, if the virtual addresses are permissible and do not exceed MAXVA, we obtain the PTE, perform checks if it's valid and user-level, and store the physical address of the PTE in the variable pa0. Then, it checks if the PTE is read-only, i.e., if the PTE_W bit is equal to 0 (line 388). If it is, then the entire procedure followed in usertrap() must be followed, and then the previous tasks must be carried out (in the subsequent memmove (line 411), the old page is replaced with the new one), otherwise it remains unchanged, and there is no need to create a new page, copy the old one to the new one, etc.