The question is a bit long.
Recently I am working on replicating the xv6 operation system using Rust. Here is the repository and the helper repository.
My question lies in the implementation of kvm.rs.
There are 3 functions that will walk through the page table.
The common things in these function are that they traverse the page table, which can be viewed as a tree. However they will do different things during traversal.
I am wondering is it possible that I can create a shared struct to abstract the behavior of traversing the page table?
My initial thought is to create a struct, which has some common field like page table, virtual address and current level. It can have two function that user can set different behavior when encountering non-leaf page or leaf page:
And then rely on a trait implementation for the meat of the traversal:
pub trait PageTableVisitor {
// n.b. requires nightly feature try_trait_v2
// If that's no ok I'd probably just use Result
// (You can use `.ok()` to get an `Option`)
type Output: core::ops::Try;
fn check_va(&mut self, va: VirtAddr) -> Self::Output;
fn leaf(&mut self, pte: &mut PageTableEntry) -> Self::Output;
fn nonleaf(&mut self, pte: &mut PageTableEntry) -> Self::Output;
}
And then implement on the MapWalkerMut:
impl<Extra: PageTableVisitor> MapWalkerMut<'_, Extra> {
fn visit_mut(&mut self) -> Extra::Output {
let _ = self.extra.check_va(self.va)?;
let index = self.va.get_index(self.level);
let pte = &mut self.pagetable[index];
match self.level.next_level() {
None => {
self.extra.leaf(pte)
}
Some(next_level) => {
let _ = self.extra.nonleaf(pte)?;
let next_table = unsafe { &mut *(pte.addr() as *mut PageTable) };
self.pagetable = next_table;
self.level = next_level;
self.visit_mut()
}
}
}
}
If you want different return type without the Try trait, you'll have to work out something else; maybe as simple as another method to transform a Result, so you can still use ? most places.
fn visit_mut(&mut self) -> Extra::Output {
result = self._visit_mut(); // as above but with `Result` everywhere
self.extra.finalize(result)
}
I will check and try your code.
It comes to me that, is this kind of abstraction zero-cost? Since it is in kernel and I don't want any extra operation compared to what I implemented right now.
However I think with visitor pattern there will be some sort of function call overhead that is inevitable.
The code (visit_mut) will be monomorphized as if you wrote each implementation out. As for the function calls, you can mark the methods in your implementation of the trait as #[inline] or #[inline(always)] as a hint.
Just finish my code change. It works perfectly.
An extra question, you said that:
Mutable and non-mutable is hard to abstract over
Right now I am going to abstract out the map_addr_recur. So I will have to add another method visit in MapWalkerMut or should I just create another struct MapWalker which does not hold a mutable PageTable?
I would recommend the latter. You can only have one usable mutable borrow at a time (a better term would have been unique borrow), where as you can have as many shared borrowed as you like. So the difference between holding a &mut PageTable and a &PageTable can be quite large.
See also the standard library, where there's often a Iter and an IterMut, etc.