Assume we are on an hart with M, S and U modes support and we currently are in S mode with virtual address enabled and satp
pointing to the current PTE table (the kernel PTE table).
Now assume we want to start a process in U mode, then I first allocate all the required pages and populate the new process PTE table (by manually translating supervisor virtual address to physical ones). However before we switch to U mode with sret
we first need to load the address of the process PTE table because in U mode satp
won't be accessible.
But when we update satp
with the user PTE table we won't be able to access our kernel code because satp
immediately starts translating virtual address, and we will end to access only process memory and not kernel memory since now satp
doesn't point anymore to the kernel PTE table.
We should also not be able to access the next instruction because pc
holds a virtual address managed by the old PTE table, therefore is no more valid because now satp
points to the new PTE table.
The only solution I have found is to temporarily turn-off kernel virtual memory, manually translate kernel virtual addresses to store kernel contents, jump to a page pointed by the new PTE table, load the new process PTE table in satp
thus entering in user process virtual memory and then issue sret
to enter U mode.
What is the correct way to enable virtual memory for a user process without messing with kernel virtual memory?
The same problem holds when we want to manage interrupts in S mode that are issued in U mode. If the supervisor Interrupt Vector Table is stored in kernel virtual memory instead of physical one then when a user process causes an interrupt the MMU would use the virtual address to find the Interrupt Vector Table by using satp
, but satp
now points to the user process PTE table that doesn't map the IVT.
Even in this case, the only solution I have found is to temporarily disable virtual memory in supervisor mode in order to find location of the kernel PTE table that should be stored at a well-known physical address in order to be found without MMU.
There are solutions that doesn't rely on disabling kernel virtual memory? Why RiscV specifications don't added a new CSR to manage addresses translation only in user mode?
But when we update satp with the user PTE table we won't be able to access our kernel code because satp immediately starts translating virtual address, and we will end to access only process memory and not kernel memory since now satp doesn't point anymore to the kernel PTE table.
...
What is the correct way to enable virtual memory for a user process without messing with kernel virtual memory?
...
I can think of two approaches:
Map kernel pages into the page tables for each user process, with the U
bit in the PTEs for kernel pages cleared. This allows your kernel running in S-mode to access these pages (when dealing with an exception or interrupt) and triggers a page fault if the process running in U-mode tries to do the same. Setting the SUM
bit also lets the kernel access process pages.
The G
(Global) bit can be set for kernel PTEs so kernel page mappings are maintained in the TLB between context switches, improving performance.
This is what is done in the Linux Kernel and a port of egos-2000
Have separate page tables for the kernel. But keep a single trampoline page that is mapped into both the kernel and process page tables and holds the code that modifies satp
and switches between privilege modes. The kernel jumps to that trampoline when switching to a user process, and exceptions & interrupts start at some code in the trampoline that points satp
to kernel page tables before jumping to the respective handlers.
This saves space (less page table mappings) but makes context switching a bit more expensive.
The xv6 OS folks have chosen this approach.