Pgtbl
- Print a page table
- A kernel page table per process
- Simplify copyin/copyinstr
This Lab simply optimizes the page table function of the system, so that the program can directly parse the pointer of the user mode when the program is in the kernel mode.
The author took about 8 hours
Print a page table
The first part is to add a function vmprint
to the system to print a given page table. This function receives a parameter pagetable
(the physical address of the root page table), and recursively traverses the entire page. table, print valid table entries.
Refer to the freewalk
function (defined in kernel/vm.c:331), traverse 512 entries each time, if the entry is valid, print the relevant information (what level , the number of items, the pte content and the physical address corresponding to the pte content), and if it is a first-level and second-level page table, continue to recurse until the third-level page table returns. The reference code is as follows:
void vmprint_helper(pagetable_t pgtbl, int level) {<!-- --> // there are 2^9 = 512 PTEs in a page table. for(int i = 0; i < 512; i ++ ){<!-- --> pte_t pte = pgtbl[i]; if(pte & PTE_V){<!-- --> for (int j = 0; j < level; j ++ ) {<!-- --> if (j) printf(" "); printf(".."); } printf("%d: pte %p pa %p\\ ", i, pte, PTE2PA(pte)); if ((pte & amp; (PTE_R|PTE_W|PTE_X)) == 0) {<!-- --> // this PTE points to a lower-level page table. uint64 child = PTE2PA(pte); vmprint_helper((pagetable_t)child, level + 1); } } } } void vmprint(pagetable_t pgtbl) {<!-- --> printf("page table %p\\ ", pgtbl); vmprint_helper(pgtbl, 1); }
A kernel page table per process
As the title, the content of the second part is to add a separate copy of the kernel page table for each process, paving the way for the next section to directly dereference the user mode pointer.
First, you need to add a field to the process structure (defined in kernel/proc.h) struct proc
to maintain a copy of the kernel page table, as shown in the following figure
Then, since we need to initialize a copy of the kernel page table for each process when allocating processes, we need to refer to the kvminit
function (defined in kernel/vm .c:66), write a function proc_kvminit
that initializes the copy of the kernel page table in the process, the code is as follows. The content of this function is basically the same as the kvminit
function. The uvmmap
is similar to the kvmmap
function (defined in kernel/vm.c:171), which maps a given virtual address and physical address range , the only difference is that the former modifies the incoming specified page table instead of just the global kernel page table.
void uvmmap(pagetable_t pagetable, uint64 va, uint64 pa, uint64 sz, int perm) {<!-- --> if(mappages(pagetable, va, sz, pa, perm) != 0) panic("uvmmap"); } /* * create a direct-map page table for the given process. */ pagetable_t proc_kvminit() {<!-- --> pagetable_t pgtbl = (pagetable_t) kalloc(); if (pgtbl == 0) return 0; memset(pgtbl, 0, PGSIZE); // uart registers uvmmap(pgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W); //virtio mmio disk interface uvmmap(pgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W); //CLINT uvmmap(pgtbl, CLINT, CLIINT, 0x10000, PTE_R | PTE_W); // PLIC uvmmap(pgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W); // map kernel text executable and read-only. uvmmap(pgtbl, KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X); // map kernel data and the physical RAM we'll make use of. uvmmap(pgtbl, (uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W); // map the trampoline for trap entry/exit to // the highest virtual address in the kernel. uvmmap(pgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X); return pgtbl; }
Then call the proc_kvminit
function defined above in the allocproc
function (defined in kernel/proc.c:95) to realize initialization during process allocation In-process copy of the kernel page table. At the same time, it is also necessary to move the initialization code segment of the page table entry corresponding to the process kernel stack in procinit to the allocproc
function, as shown below. What needs to be noted here is that the modification of the process context
field in the original code must be placed at the bottom. (I don’t know why for the time being, I will make up the reason when I know it)
... // initialize the process kernel page table p->kernel_pagetable = proc_kvminit(); if(p->kernel_pagetable == 0){<!-- --> freeproc(p); release( &p->lock); return 0; } // initialize the process kernel stack in kernel process kernel page table char *pa = kalloc(); if(pa == 0) panic("kalloc"); uint64 va = KSTACK((int) (p - proc)); uvmmap(p->kernel_pagetable, va, (uint64)pa, PGSIZE, PTE_R | PTE_W); p->kstack = va; // Set up new context to start executing at forkret, // which returns to user space. memset( &p->context, 0, sizeof(p->context)); p->context.ra = (uint64)forkret; p->context.sp = p->kstack + PGSIZE; return p;
Next, in the scheduler
function (defined in kernel/proc.c:489), when the scheduling process is executed, the kernel page table corresponding to the process is loaded into the satp register , and call sfence_vma
to refresh. After the process is executed, the page table is called to switch back to the global kernel page table. The code segment is shown below.
// load process's kernel page table and flush the TLB w_satp(MAKE_SATP(p->kernel_pagetable)); sfence_vma(); swtch( &c->context, &p->context); // load kernel page table when process done kvminithart();
Finally, the copy of the kernel page table maintained by the process needs to be released in the freeproc
function (defined in kernel/proc.c:155). It is necessary to release the kernel stack physical space maintained by the kernel page table in the process, and call the uvmunmap
function (defined in kernel/vm.c:230 ) can be. At the same time, it is also necessary to destroy the maintained kernel page table copy. Since the freewalk
function only destroys the first-level and second-level page table entries, you need to write a similar function to destroy the third-level page. Table entries, as shown below.
// Recursively free process's kernel page-table pages. void proc_freewalk(pagetable_t pagetable) {<!-- --> // there are 2^9 = 512 PTEs in a page table. for(int i = 0; i < 512; i ++ ){<!-- --> pte_t pte = pagetable[i]; if((pte & amp; PTE_V) & amp; & amp; (pte & amp; (PTE_R|PTE_W|PTE_X)) == 0){<!-- --> // this PTE points to a lower-level page table. uint64 child = PTE2PA(pte); proc_freewalk((pagetable_t)child); pagetable[i] = 0; } else if(pte & PTE_V){<!-- --> pagetable[i] = 0; } } kfree((void*)pagetable); }
The relevant code segment in the freeproc
function is as follows
// free process's kernel page table uvmunmap(p->kernel_pagetable, p->kstack, 1, 1); p->kstack = 0; proc_freewalk(p->kernel_pagetable); p->kernel_pagetable = 0;
Simplify copyin/copyinstr
What needs to be implemented in this part is to add the user space mapping of each process to the kernel page table copy maintained by the process (created in the previous section), since the virtual address of the user space starts from 0, and the virtual address of the kernel starts from higher (the document says it is PLIC, but Figure 3.3 in xv6book starts from CLIT, I don’t know why), so some virtual space is left for user space mapping (0~PLIC-1) .
We need the kernel page maintained for the process in the fork
function, exec
function, growproc
function and userinit
function The userspace mapping is added to the table, since these functions change the user mapping.
First, I defined a function uvm2ukvm
following the uvmcopy
function (defined in kernel/vm.c:384), which receives two pages One is the user process page table, the other is the kernel page table maintained in the user process, and receives the start virtual address and the end virtual address to be mapped, and copies the user space virtual address in this range to the kernel page maintained by the process table. Note that the PTE_U flag needs to be set to 0, otherwise the kernel cannot access it.
void uvm2ukvm(pagetable_t upgtbl, pagetable_t ukpgtbl, uint64 st, uint64 ed) {<!-- --> pte_t *pte_u, *pte_uk; uint64 pa, i; uint flags; for (i = st; i < ed; i + = PGSIZE) {<!-- --> if((pte_u = walk(upgtbl, i, 0)) == 0) panic("uvm2ukvm: pte_u should exist"); if((*pte_u & PTE_V) == 0) panic("uvm2ukvm: page not present"); pa = PTE2PA(*pte_u); flags = PTE_FLAGS(*pte_u); flags & amp;= (~PTE_U); if((pte_uk = walk(ukpgtbl, i, 1)) == 0) panic("uvm2ukvm: pte_uk should exist"); *pte_uk = PA2PTE(pa) | flags; } }
In the fork
function (defined in kernel/proc.c:289), call the above function and add a line of code.
... // Copy user memory from parent to child. if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){<!-- --> freeproc(np); release( &np->lock); return -1; } np->sz = p->sz; uvm2ukvm(np->pagetable, np->kernel_pagetable, 0, np->sz); ...
In the exec
function (defined in kernel/exec.c:13), just add the previous line of code in the same way.
... // Commit to the user image. oldpagetable = p->pagetable; p->pagetable = pagetable; p->sz = sz; uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, sz); p->trapframe->epc = elf.entry; // initial program counter = main p->trapframe->sp = sp; // initial stack pointer proc_freepagetable(oldpagetable, oldsz); ...
In the growproc
function, when applying for memory growth, it is necessary to judge whether the upper bound of the virtual address after growth exceeds the starting address of the PLIC, and if so, return -1, otherwise, the above function will be called to increase Copy the address range to the kernel page table maintained by the process.
... if (PGROUNDUP(sz + n) > PLIC) {<!-- --> return -1; } if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {<!-- --> return -1; } uvm2ukvm(p->pagetable, p->kernel_pagetable, sz - n, sz); ...
When the process page table is initialized for the first time in the userinit
function, it is also copied.
... // allocate one user page and copy init's instructions // and data into it. uvminit(p->pagetable, initcode, sizeof(initcode)); uvm2ukvm(p->pagetable, p->kernel_pagetable, 0, PGSIZE); ...
Finally, change the content in the copyin
function and copyinstr
function body to call the copyin_new
and copyinstr_new
functions.