I have not encountered devices that are so sensitive to timing that my microkernel (GitHub:
managarm) cannot handle them. Most modern devices require you to set up structures describing operations in system memory before submitting those operation - the submission itself is only a single port or MMIO write and thus not sensitive to timing.
nullplan wrote:Another problem that was already discussed before is the issue of process priorities. In a µK, you often have one server that handles PCI (enumerates devices and provides access to config space), and then servers to handle the devices on the bus. And how are you going to prioritize these processes relative to each other? Many choices, easy to get wrong.
My personal experience here is that priorities without priority inheritance just do not work in situations like that and decrease performance and latency.
---
My two cents on the OP's question:
In general, microkernel designs tend to be more difficult because they have to do strictly more than monolithic kernel: In addition to everything that a monolithic kernel has to do, they need to take communication into account. So, while it is not necessarily the case that a single aspect will be much more complex than in a monolithic kernel, everything will be a bit more complex.
Furthermore, a main difficulty that I often encounter is how to make things fast. Thinking about how the VFS/FS interaction
can work is a matter of minutes. Designing it in a way that is fast takes a lot of time.
---
As the OP asked for one particular difficulty: I found race-free handling of IRQs in userspace drivers to be somewhat complicated. I have scrapped at least 3 or so designs before I came up with the current one in managarm. It is difficult because the way that IRQ handling usually works (call a handler, do some work, perform EOI) does not work in an asynchronous design. Of course, monolithic kernels do not encounter this problem: they can just run handlers synchronously (and maybe delegate additional work to a thread after EOI). managarm however, cannot easily do that: it asynchronously delegates the handling to a userspace thread. If you're curious how it works, I'll scratch the design below but it is somewhat technical.
There are many complications: handling differs based on the trigger mode (level- vs. edge-triggered), IRQs can be shared, in some situations drivers might be unable to determine if an IRQ originated from their device (!) (mostly after a device reset), mask and unmask operations can race against IRQ delivery, devices need to be able to asynchronously ACK and NAK (!) IRQs etc. The way this is now solved in managarm involves managing multiple sequence numbers that are matched against each other to determine if some actions (like mask IRQ, unmask IRQ, notify userspace) need to be taken: the sequence number of the hardware IRQ (incremented in each ISR) and, derived from that, the sequence number of the IRQ that was seen last by the driver as well as the sequence number of the IRQ that the driver
tried to ACK/NAK last. The latter might differ from the sequence number that was actually ACKed/NAKed last because another IRQ might have happened in the case of shared IRQs (in which case a NAK must be ignored but a ACK must still unmask the IRQ). While this interface sounds complicated, it's quite nice to userspace - most of the complexity is handled in the kernel and drivers only have to do
Code: Select all
uint64_t seq = 0;
// [...]
seq = wait_for_irq(some_irq_capability, seq);
if(from_my_device()) {
ack_irq(seq);
// Handle the IRQ.
}else{
nak_irq(seq);
}
Using this system, managarm can reliably (i) handle IRQs asynchronously in all situations, (2) detect IRQs that are NAKed by all drivers and provide debugging information if that happens and (3) detect if drivers fail to either ACK or NAK IRQs in time which would either decrease I/O performance or lead to system lockup (if IRQs are not ACKed at all).