Microkernels are easier to build than Monolithic kernels

All off topic discussions go here. Everything from the funny thing your cat did to your favorite tv shows. Non-programming computer questions are ok too.
User avatar
AndrewAPrice
Member
Member
Posts: 2297
Joined: Mon Jun 05, 2006 11:00 pm
Location: USA (and Australia)

Microkernels are easier to build than Monolithic kernels

Post by AndrewAPrice »

My OS is built around a microkernel. In past iterations, it was monolithic before I rebooted the effort into a microkernel. I'm going to argue that building an OS around a microkernel is less work than a monolithic kernel.
  • It forces you to write modular code - by separating the kernel and each service.
    (For a complicated project like an OS, writing modular code is a good thing.)
  • You can write your services in a higher level language.
    (My kernel is in pure C and requires no runtime/standard library. I ported libcxx and I can write my services in C++ and use the full standard library such as std::thread, std::filesystem, std::map, lambdas, global constructors, etc. To do this in the kernel, it would mean maintaining two libcxx ports. C++ is just an example, you could implement a service in Rust, Go, Python - whatever is most natural to you - and only maintain one runtime.)
  • There's less code in the kernel. The bulk of your OS is actually the services.
    (Less code has to be written in the lower level language without standard libraries.)
  • The kernel code can be cleaner by having fewer special cases.
    (For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.)
  • Grub's module support makes loading your initial set of services/drivers easy.
    (This solves the chicken and egg problem of "How do I load my first services/drivers before I have a file system service and a disk driver?")
Change my mind.
My OS is Perception.
User avatar
Demindiro
Member
Member
Posts: 96
Joined: Fri Jun 11, 2021 6:02 am
Libera.chat IRC: demindiro
Location: Belgium
Contact:

Re: Microkernels are easier to build than Monolithic kernels

Post by Demindiro »

AndrewAPrice wrote:My OS is built around a microkernel. In past iterations, it was monolithic before I rebooted the effort into a microkernel. I'm going to argue that building an OS around a microkernel is less work than a monolithic kernel.
I don't think a microkernel is easier to build/write around than a monolithic kernel perse, but as you say:
  • It forces you to write modular code - by separating the kernel and each service.
    (For a complicated project like an OS, writing modular code is a good thing.)
It does force you to write code in a (more) modular way, which will help in the long run as you naturally try to reuse existing interfaces instead of adding more and more.
  • You can write your services in a higher level language.
    (My kernel is in pure C and requires no runtime/standard library. I ported libcxx and I can write my services in C++ and use the full standard library such as std::thread, std::filesystem, std::map, lambdas, global constructors, etc. To do this in the kernel, it would mean maintaining two libcxx ports. C++ is just an example, you could implement a service in Rust, Go, Python - whatever is most natural to you - and only maintain one runtime.)
  • There's less code in the kernel. The bulk of your OS is actually the services.
    (Less code has to be written in the lower level language without standard libraries.)
I don't think this is a property unique to microkernels. IIRC there are modules for Linux that allow you to run Lua in the kernel (ZFS uses it for some things apparently).
  • The kernel code can be cleaner by having fewer special cases.
    (For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.)
Fully agree. It should also make it easier to harden the kernel.

I do technically have kernel threads though. I found it easier to implement the "idle task" that way (+ I run some setup code with a kernel thread). The only real differences between user and kernel threads in my kernel is that the latter never return to userspace and don't have a parent process.
  • Grub's module support makes loading your initial set of services/drivers easy.
    (This solves the chicken and egg problem of "How do I load my first services/drivers before I have a file system service and a disk driver?")
I also used grub's modules initially but found that it got unwieldy as more and more services got added. Instead, I made a very easy to parse filesystem and make grub load that instead.
My OS is Norost B (website, Github, sourcehut)
My filesystem is NRFS (Github, sourcehut)
thewrongchristian
Member
Member
Posts: 422
Joined: Tue Apr 03, 2018 2:44 am

Re: Microkernels are easier to build than Monolithic kernels

Post by thewrongchristian »

AndrewAPrice wrote:My OS is built around a microkernel. In past iterations, it was monolithic before I rebooted the effort into a microkernel. I'm going to argue that building an OS around a microkernel is less work than a monolithic kernel.
  • It forces you to write modular code - by separating the kernel and each service.
    (For a complicated project like an OS, writing modular code is a good thing.)
  • You can write your services in a higher level language.
    (My kernel is in pure C and requires no runtime/standard library. I ported libcxx and I can write my services in C++ and use the full standard library such as std::thread, std::filesystem, std::map, lambdas, global constructors, etc. To do this in the kernel, it would mean maintaining two libcxx ports. C++ is just an example, you could implement a service in Rust, Go, Python - whatever is most natural to you - and only maintain one runtime.)
  • There's less code in the kernel. The bulk of your OS is actually the services.
    (Less code has to be written in the lower level language without standard libraries.)
  • The kernel code can be cleaner by having fewer special cases.
    (For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.)
  • Grub's module support makes loading your initial set of services/drivers easy.
    (This solves the chicken and egg problem of "How do I load my first services/drivers before I have a file system service and a disk driver?")
Change my mind.
Wouldn't dream of changing your mind.

While basically monolithic, my kernel won't be getting a loadable module interface, as I hope to provide user level device and filesystem drivers, largely for the reasons you state.

I'm not sure I'd go with a full microkernel though. I think I'll have the VFS, process and virtual memory management in the kernel, putting all that in userspace just seems unnecessary overhead.
User avatar
eekee
Member
Member
Posts: 872
Joined: Mon May 22, 2017 5:56 am
Location: Kerbin
Discord: eekee
Contact:

Re: Microkernels are easier to build than Monolithic kernels

Post by eekee »

I've been thinking I'd be happier with a microkernel, if not necessarily a strict one, and this has just-about convinced me. I was always going to modularize anyway, but hadn't really considered the others. I'd certainly prefer to write services in the full userspace environment, and having fewer special cases appeals very much.
Demindiro wrote:I also used grub's modules initially but found that it got unwieldy as more and more services got added. Instead, I made a very easy to parse filesystem and make grub load that instead.
Plan 9 does this, though its initrd is built into the kernel image. I'd definitely do it the same way.
Kaph — a modular OS intended to be easy and fun to administer and code for.
"May wisdom, fun, and the greater good shine forth in all your work." — Leo Brodie
nullplan
Member
Member
Posts: 1760
Joined: Wed Aug 30, 2017 8:24 am

Re: Microkernels are easier to build than Monolithic kernels

Post by nullplan »

AndrewAPrice wrote:Change my mind.
Gladly. My kernel will be monolithic, because some of the benefits of microkernels do turn out to be spurious at closer inspection (e.g. added security can be defeated with DMA unless an IOMMU is employed).

Microkernels force you to turn any driver interface into a message stream, and to serialize all requests down that stream and deserialize the answers (and the driver is running that same interface in reverse). All of that packaging is unnecessary. It also presents a bottleneck, and an open communication problem. n processes communicate with m driver threads. How do you ensure balance?

In a monolithic kernel, there is no need to have the driver in a different process, and so system calls can be handled right in system call context. n processes access the same hardware at the same time (serialized by a lock if need be). No weird mapping in the middle. Interrupts can be handled in interrupt context because all the driver data is right there. It is typically not advisable, and a kernel thread would be better, but it is possible.
AndrewAPrice wrote:It forces you to write modular code - by separating the kernel and each service.
(For a complicated project like an OS, writing modular code is a good thing.)
It also forces you to repeat a lot of code, because code cannot be shared between drivers (except possibly in shared libraries, but that is a whole other can of worms). Need to deserialize requests to a disk driver? Your AHCI, ATA, and floppy drivers likely contain the same loop, the same thread pool stuff, and the same switch in the middle there, only the precise method of posting the request to the hardware is different.

Modularity is good, but repeating yourself is bad. See how this works? It is a balancing act.
AndrewAPrice wrote:The kernel code can be cleaner by having fewer special cases.
(For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.)
Why would kernel threads be special cases? Except for the fact that they don't have to reload CR3 unless the task they replaced was exiting, which is a tiny architecture-dependent detail. I do it like Linux: Kernel threads are just normal tasks for the scheduler, they just never return to user space. All the accounting and scheduling still works exactly the same.
AndrewAPrice wrote:Grub's module support makes loading your initial set of services/drivers easy.
(This solves the chicken and egg problem of "How do I load my first services/drivers before I have a file system service and a disk driver?")
Well, that's not an argument for microkernels. Thanks to Linux (I will note, a monolithic kernel) most open source boot loaders have the ability to load some kind of file alongside the kernel. Monolithic kernels use those for the initial RAM disk. It allows for things like an encrypted root file system, or a network root, or what have you.

So I only have to concede two points here: Yes, you can write your drivers in a different language, and yes, there is less code in the kernel. Though I will note that I am trying to reduce kernel-code anyway, and I'm not doing so with a microkernel approach. I just want to push more things into user space. For example, I have no plans to support a virtual terminal. Debug messages go through the serial port, and I will leave the framebuffer entirely alone. User space can handle that however it wants. That incidentally also means that I don't have a font renderer, however simple, in my kernel. Also nothing that translates key presses into scan codes. A virtual terminal application can be written (it would take control of the frame buffer and the keyboard event device, and communicate with its children by way of a pseudo-terminal interface), and it can be written in whatever language should please the author. The kernel's job is to abstract the hardware, i.e. to provide a consistent interface to different hardware implementations (e.g. the virtual terminal should not have to know if the keyboard is on USB or PS/2) but not necessarily go further than that.
Carpe diem!
rdos
Member
Member
Posts: 3265
Joined: Wed Oct 01, 2008 1:55 pm

Re: Microkernels are easier to build than Monolithic kernels

Post by rdos »

AndrewAPrice wrote:It forces you to write modular code - by separating the kernel and each service.
No need to go microkernel for that. You don't need to create a huge kernel "blob" just because you can. My kernel is less than 64k (by design), and the bulk of the OS is contained in drivers. Drivers are not separate processes, rather they live in the normal kernel environment. However, they are protected by their own code & data selector. This is a bit of middle ground between a monolithic kernel and a microkernel. I don't need to serialize things, but I'm still somewhat in a different "address space". The code is certainly modular & separated.
AndrewAPrice wrote: You can write your services in a higher level language.
Agreed. You also have a larger address space in a 32-bit environment.
AndrewAPrice wrote: There's less code in the kernel. The bulk of your OS is actually the services.
Same thing here, but not a microkernel.
AndrewAPrice wrote: For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.
A decent OS should have kernel threads. Not having kernel threads indicates you cannot run several tasks at the same time in kernel, and this is a very poor design.
AndrewAPrice wrote: Grub's module support makes loading your initial set of services/drivers easy.
I have a special tool to create the "OS binary". Grub simply cannot do it, and I'm not always booting with Grub.
rdos
Member
Member
Posts: 3265
Joined: Wed Oct 01, 2008 1:55 pm

Re: Microkernels are easier to build than Monolithic kernels

Post by rdos »

nullplan wrote:Why would kernel threads be special cases? Except for the fact that they don't have to reload CR3 unless the task they replaced was exiting, which is a tiny architecture-dependent detail. I do it like Linux: Kernel threads are just normal tasks for the scheduler, they just never return to user space. All the accounting and scheduling still works exactly the same.
I think not having kernel threads means you have a huge lock over your whole kernel code. Might be clean, but certainly not a good idea. Old Unix systems were like this, and when somebody tells me they don't have kernel threads, it indicates to me they have the "huge kernel lock". :-)
nexos
Member
Member
Posts: 1078
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Microkernels are easier to build than Monolithic kernels

Post by nexos »

AndrewAPrice wrote:It forces you to write modular code - by separating the kernel and each service.
You can do the same easily in monolithic kernels, with a little planning. Even when writing a microkernel system, you have to plan out how to componentize the system.
AndrewAPrice wrote:You can write your services in a higher level language.
If you care about performance, than you won't do that :wink:
Besides, that's possible in monolithic kernels as well. NetBSD allow Lua modules to be loaded. Linux is preparing to allow Rust in the kernel.
AndrewAPrice wrote:There's less code in the kernel. The bulk of your OS is actually the services.
Not sure how this is an advantage.
AndrewAPrice wrote:(Less code has to be written in the lower level language without standard libraries.)
You'll need some sort of kernel C library either way around.
AndrewAPrice wrote:The kernel code can be cleaner by having fewer special cases.
I have to disagree strongly here. For example, you now have to think about the cases in a microkernel where the kernel and user process manager must interact. You must have the kernel and user memory manager interact some how. This creates extra cases.
And in a fast microkernel, the codebase probably won't be very clean. Look at L4.
AndrewAPrice wrote:(For example, I have no need for kernel threads. Everything the kernel does (pass a message, start a thread, etc.) is fast enough to be done in a syscall that can return immediately. So my scheduler, interrupt return code, etc. can be cleaner.)
What about scheduler load balancing? Or thread prioritizing? Or page zeroing? Or page swapping? You should do those in threads for throughput's sake.

Besides, kernel and user threads are extremely similar. It should only add a little code to your kernel so you can differentiate, primarily code to prevent CR3 from re-loaded, preventing the TSS from being set, ensuring the segments regs point to kernel code / data, etc. That's it.
AndrewAPrice wrote:Grub's module support makes loading your initial set of services/drivers easy.
That's a problem monolithic kernel writers have spilled much ink over as well :wink:

Now, I personally am writing a microkernel. But I'm doing that because it's better, not because it's simpler.

Of course, you are entitled to your own opinions. If you think writing a microkernel is easier, than great! I wouldn't ever want to change your mind.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
nullplan
Member
Member
Posts: 1760
Joined: Wed Aug 30, 2017 8:24 am

Re: Microkernels are easier to build than Monolithic kernels

Post by nullplan »

rdos wrote:I think not having kernel threads means you have a huge lock over your whole kernel code. Might be clean, but certainly not a good idea. Old Unix systems were like this, and when somebody tells me they don't have kernel threads, it indicates to me they have the "huge kernel lock". :-)
I have no idea what you mean. I have kernel threads, they just aren't a special case in the scheduler. Not only do I have kernel threads, I also have an interruptible kernel (i.e. the interrupt flag is enabled in syscalls after all the necessary parts of the transition to kernel mode have happened). And I certainly have no big kernel lock. I do think about what global state I have and how to protect it during queries and updates.
Carpe diem!
rdos
Member
Member
Posts: 3265
Joined: Wed Oct 01, 2008 1:55 pm

Re: Microkernels are easier to build than Monolithic kernels

Post by rdos »

nullplan wrote:
rdos wrote:I think not having kernel threads means you have a huge lock over your whole kernel code. Might be clean, but certainly not a good idea. Old Unix systems were like this, and when somebody tells me they don't have kernel threads, it indicates to me they have the "huge kernel lock". :-)
I have no idea what you mean. I have kernel threads, they just aren't a special case in the scheduler. Not only do I have kernel threads, I also have an interruptible kernel (i.e. the interrupt flag is enabled in syscalls after all the necessary parts of the transition to kernel mode have happened). And I certainly have no big kernel lock. I do think about what global state I have and how to protect it during queries and updates.
If you have kernel threads, then you don't have the problem. :-)

Of course, kernel threads should just be like normal threads. They just don't return to user level. They can run in the system process (if you have one).
User avatar
Demindiro
Member
Member
Posts: 96
Joined: Fri Jun 11, 2021 6:02 am
Libera.chat IRC: demindiro
Location: Belgium
Contact:

Re: Microkernels are easier to build than Monolithic kernels

Post by Demindiro »

nullplan wrote: My kernel will be monolithic, because some of the benefits of microkernels do turn out to be spurious at closer inspection (e.g. added security can be defeated with DMA unless an IOMMU is employed).
It is true that if a driver has direct access to hardware the security of paging is moot, but some drivers don't need to access hardware directly. E.g. USB device drivers don't work directly with physical addresses. They only tell the host controller they want to read/write data and the controller driver provides buffers as needed. Another example would be filesystem drivers, which usually rely on some sort of disk driver to read & write data to a device.
Microkernels force you to turn any driver interface into a message stream, and to serialize all requests down that stream and deserialize the answers (and the driver is running that same interface in reverse). All of that packaging is unnecessary.
You can avoid a lot of that serialization overhead by using shared memory. A message is then only a small or empty packet to notify the other side that data is ready.
It also presents a bottleneck, and an open communication problem. n processes communicate with m driver threads. How do you ensure balance?
I use ring queues for this. An (IMO big) advantage of ring queues is that batch sizes scale automatically: if there isn't much work, a thread will process one packet at a time and have relatively low latency. If there is much work, batch sizes increase and while latency increases, there is also much less overhead from switching between processes, increasing throughput.
AndrewAPrice wrote:It forces you to write modular code - by separating the kernel and each service.
(For a complicated project like an OS, writing modular code is a good thing.)
It also forces you to repeat a lot of code, because code cannot be shared between drivers (except possibly in shared libraries, but that is a whole other can of worms). Need to deserialize requests to a disk driver? Your AHCI, ATA, and floppy drivers likely contain the same loop, the same thread pool stuff, and the same switch in the middle there, only the precise method of posting the request to the hardware is different.

Modularity is good, but repeating yourself is bad. See how this works? It is a balancing act.
You can put duplicate functionality in libraries. I have a library "driver_utils" as well as a bunch of other libraries with commonly used code.

At most, the compiler links in roughly the same code for each binary, though if that is a concern you can always resort to dynamically loaded libraries.
rdos wrote: I think not having kernel threads means you have a huge lock over your whole kernel code. Might be clean, but certainly not a good idea. Old Unix systems were like this, and when somebody tells me they don't have kernel threads, it indicates to me they have the "huge kernel lock".
Not having (explicit) kernel threads does not mean you need a big kernel lock. I only added kernel threads fairly recently and at no point did I use a big lock anywhere (Rust helps a lot here with enforcing thread safety).
nexos wrote:
AndrewAPrice wrote:It forces you to write modular code - by separating the kernel and each service.
You can do the same easily in monolithic kernels, with a little planning. Even when writing a microkernel system, you have to plan out how to componentize the system.
The keyword here is "forces". While you certainly can plan a good layout for a monolithic kernel, the barrier for adding a quick hack is much higher in a microkernel and helps doing "the right thing" from the get-go IME
AndrewAPrice wrote:You can write your services in a higher level language.
If you care about performance, than you won't do that :wink:
Depends on how much performance you need. Even Python is likely fast enough for e.g. a keyboard driver.
AndrewAPrice wrote:There's less code in the kernel. The bulk of your OS is actually the services.
Not sure how this is an advantage.
At least personally, I find it easier to reason about smaller codebases. While the total amount of code may not be less in a microkernel there is a clearer distinction between the kernel and a service, which makes it easier for me to focus.
AndrewAPrice wrote:The kernel code can be cleaner by having fewer special cases.
I have to disagree strongly here. For example, you now have to think about the cases in a microkernel where the kernel and user process manager must interact. You must have the kernel and user memory manager interact some how. This creates extra cases.
And in a fast microkernel, the codebase probably won't be very clean. Look at L4.
In a microkernel there are much less of those cases, because generally you have a limited API that does a well-defined thing.

Also, you don't necessarily need to put the memory manager in userspace. My kernel does not have any dependency on user programs.
My OS is Norost B (website, Github, sourcehut)
My filesystem is NRFS (Github, sourcehut)
nexos
Member
Member
Posts: 1078
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Microkernels are easier to build than Monolithic kernels

Post by nexos »

nullplan wrote:(e.g. added security can be defeated with DMA unless an IOMMU is employed).
I think you answered your own problem :wink:
nullplan wrote:Microkernels force you to turn any driver interface into a message stream, and to serialize all requests down that stream and deserialize the answers (and the driver is running that same interface in reverse). All of that packaging is unnecessary.
You could do what I am going to do and have the kernel work with an unserialized form. Then you won't have any "extra packaging", and all copying can be deferred with CoW (unless [obviously] the buffer is small enough that CoW would be counterproductive).
nullplan wrote:n processes communicate with m driver threads. How do you ensure balance?
You implement a thread pool mechanism. The thread pool needn't be too fancy.
nullplan wrote:n processes access the same hardware at the same time (serialized by a lock if need be).
You probably shouldn't carry on two different conversations with hardware at once. That is destined to not fare very well.

The m requesters communicating with n server threads "problem" in microkernels isn't a problem when you really think about it. In a monolithic kernel, two threads attempting to read from a file at the same time would only be a problem if they were running on the same CPU. At that, it still wouldn't be a big problem really as only one thread can run at a given moment on a single CPU no matter what. Once you prioritize requests, the problem disappears.
nullplan wrote:It also forces you to repeat a lot of code, because code cannot be shared between drivers (except possibly in shared libraries, but that is a whole other can of worms).
If you don't want shared libs, use static ones.
Demindiro wrote:The keyword here is "forces". While you certainly can plan a good layout for a monolithic kernel, the barrier for adding a quick hack is much higher in a microkernel and helps doing "the right thing" from the get-go IME
True, but if someone is going to not plan out base structure, surely they won't plan other things. Hence I still don't see that as an advantage.
Demindiro wrote:Depends on how much performance you need. Even Python is likely fast enough for e.g. a keyboard driver.
Keyboard latency should be kept to a minimum in every way possible!

And besides, you could still write a driver in Python in a monolithic system. You'd just need an embedded interpreter in the kernel, or you JIT it in the build process. Look at Lua in NetBSD.
Demindiro wrote:In a microkernel there are much less of those cases, because generally you have a limited API that does a well-defined thing
I dunno, I've still thought of plenty of edge cases in my design, such as boot graphics, boot process loading, process management, and memory management.
Demindiro wrote:Also, you don't necessarily need to put the memory manager in userspace. My kernel does not have any dependency on user programs.
The bulk of my MM will be in-kernel as well, but actually doing things like swapping and memory-mapped files still requires some sort of user-space interception.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
nullplan
Member
Member
Posts: 1760
Joined: Wed Aug 30, 2017 8:24 am

Re: Microkernels are easier to build than Monolithic kernels

Post by nullplan »

nexos wrote:
nullplan wrote:(e.g. added security can be defeated with DMA unless an IOMMU is employed).
I think you answered your own problem :wink:
One claim of microkernels was always that they are better secured against faulty or malicious drivers (if it is faulty enough, the distinction starts to disappear). And now the solution is supposed to be another piece of hardware. Forgive my skepticism. IOMMU only helps on chipsets that have them, and only if you have a driver/infrastructure for them in your kernel, and only within its page granularity. And most IOMMUs I am aware of have pages of 64kB.
nexos wrote:
nullplan wrote:Microkernels force you to turn any driver interface into a message stream, and to serialize all requests down that stream and deserialize the answers (and the driver is running that same interface in reverse). All of that packaging is unnecessary.
You could do what I am going to do and have the kernel work with an unserialized form. Then you won't have any "extra packaging", and all copying can be deferred with CoW (unless [obviously] the buffer is small enough that CoW would be counterproductive).
What are you talking about? One example of what I was talking about: An application wants to read 20 bytes from a file. It issues the call

Code: Select all

read(fd, buf, 20);
In a monolithic kernel, that call works its way down the stack, through the FD abstraction layer, the VFS, the file system, and the volume drivers, and finally ends up in the page cache for the drive, where it results in a request to the page cache to load page 12345 from the drive and notify the caller. Depending on implementation, that may be the only time the request has to be serialized in any form. Or if you skip the I/O scheduler then not even then; you just update the page cache right out of the system call context. Once the page is updated, the appropriate 20 bytes are copied to the user.

In a microkernel, the kernel sees the above call. Since read() is an I/O operation, it first has to package that request up and send it to the I/O manager. Which unpacks it and sends it to the VFS. Where it results in a request to the appropriate FS driver. Where it results in at least one request for a page from the appropriate volume driver. Where the request are changed only slightly before being sent to the page cache. It is not always the same request, but they are requests that result from the original desire of the application to read 20 bytes from a file.

Each time, a message has to be constructed and then sent to another process, because none of these things are in the same process, and nor are they in the kernel. All of the things that in a monolithic kernel are just simple or indirect function calls become remote procedure calls in a microkernel, and each time you have to turn the request into some kind of structure and send it down a message stream.
nexos wrote:
nullplan wrote:n processes access the same hardware at the same time (serialized by a lock if need be).
You probably shouldn't carry on two different conversations with hardware at once. That is destined to not fare very well.
Depends on the hardware. Obviously, some hardware, like bulk-only USB sticks, only support one transaction at a time. Others, like UAS drives, support multiple transactions at a time and only require serialization at enqueue and dequeue time.
nexos wrote:The m requesters communicating with n server threads "problem" in microkernels isn't a problem when you really think about it. In a monolithic kernel, two threads attempting to read from a file at the same time would only be a problem if they were running on the same CPU. At that, it still wouldn't be a big problem really as only one thread can run at a given moment on a single CPU no matter what. Once you prioritize requests, the problem disappears.
I have no idea what you are on about. There are many problems with m:n communication channels. Balance is only the first issue (is it even an issue? Shouldn't you want to keep the number of active threads low in the name of saving the planet?). How do you ensure non-starvation? Is fairness a goal? If so, how do you achieve it?

In a monolithic kernel, all that is really needed is a good scheduler and a good lock implementation (and optionally, a good I/O scheduler). In a microkernel, you suddenly also need a good m:n message queue implementation.
nexos wrote:
nullplan wrote:It also forces you to repeat a lot of code, because code cannot be shared between drivers (except possibly in shared libraries, but that is a whole other can of worms).
If you don't want shared libs, use static ones.
My opinions on shared libraries are well publicized elsewhere. Besides, they wouldn't help if you actually took advantage of the other benefit of microkernels, that you can build your drivers in different languages, because I guarantee you that Rust and Go and Python have their own thread-pool implementations and do not share code with libcxx.

Indeed, busysbox is an attempt at shared libraries at their simplest: The idea is to put all of the code in one binary and next to nothing into its data section (that's why their global variables are dynamically allocated). The result is that all processes running busybox share text because it is the same binary. A side effect is that the resulting binary is smaller than all implemented programs in separate files would be because of synergy effects. Basically the same thing we have with microkernels and monolithic kernels. Now you attempt to recapture the lost synergy effects in a library
nexos wrote:And besides, you could still write a driver in Python in a monolithic system. You'd just need an embedded interpreter in the kernel, or you JIT it in the build process. Look at Lua in NetBSD.
That thought did occur to me after sending my earlier reply. You can also write single drivers in a different compiled language, so long as that language can be made to interface with the main kernel language. Because linking object files together is exactly what a linker does. But the main thrust of the point, in my opinion, is less that you have a free choice of language, and more that you can write the code in the way of a userspace program, and take advantage of all the features that offers. Or to say it more laconically: Hosted environment, not freestanding.
Carpe diem!
nexos
Member
Member
Posts: 1078
Joined: Tue Feb 18, 2020 3:29 pm
Libera.chat IRC: nexos

Re: Microkernels are easier to build than Monolithic kernels

Post by nexos »

nullplan wrote:In a microkernel, the kernel sees the above call. Since read() is an I/O operation, it first has to package that request up and send it to the I/O manager. Which unpacks it and sends it to the VFS. Where it results in a request to the appropriate FS driver. Where it results in at least one request for a page from the appropriate volume driver. Where the request are changed only slightly before being sent to the page cache. It is not always the same request, but they are requests that result from the original desire of the application to read 20 bytes from a file.
What I meant was that you make the kernel work with an unserialized form of the message. Then, passing it down the layers becomes a lot faster. True, not as fast as a monolithic system, but still pretty close.

And you don't need that many services for an I/O request. You really only need to send a message to the VFS, which in my case, will have "trusted" FSes linked in the same process and only untrusted ones outside of the process. That eliminates one message. Than you send a message to volume manager, which contains the buffer cache right there.

2 messages. That's not too many.
nullplan wrote:I have no idea what you are on about.
I thought your problem was that m:n communication poses a bottleneck. I addressed that aspect in my above post. But, I'll address your other problems.
nullplan wrote:Shouldn't you want to keep the number of active threads low in the name of saving the planet?
Yes, I would only have one thread per CPU in a server.
nullplan wrote:How do you ensure non-starvation? Is fairness a goal? If so, how do you achieve it?
Starvation? Ideally, most server requests should get in and out quickly, meaning that we avoid any sort of starvation. E.g., if we are reading a sector from disk, after dispatching the DMA request, we start running the next task until the IRQ fires.

If a server request takes a long time to complete, than the server designer should re-evaluate their design.

As for fairness, you could implement prioritization of incoming requests. That could make things more complex, but it would prevent priority inversion.
nullplan wrote:In a monolithic kernel, all that is really needed is a good scheduler and a good lock implementation (and optionally, a good I/O scheduler). In a microkernel, you suddenly also need a good m:n message queue implementation.
Exactly my point. Microkernel's aren't simpler to write. They're better, but not simpler.
"How did you do this?"
"It's very simple — you read the protocol and write the code." - Bill Joy
Projects: NexNix | libnex | nnpkg
vvaltchev
Member
Member
Posts: 274
Joined: Fri May 11, 2018 6:51 am

Re: Microkernels are easier to build than Monolithic kernels

Post by vvaltchev »

AndrewAPrice wrote: Change my mind
I won't even try :-) Microkernels are what the mainstream opinion (professors, researchers, most developers here etc.) considers better, so go with it.

I can share my opinion though, hoping to not end up in an endless (and hatred) discussion. I really don't have time for that, guys.
I completely agree with Linus Torvalds' on the topic: despite the dislike of many people, the industry has proven him right. Most of the so-called "microkernel" real-world operating systems are not truly microkernel and avoid message passing & context switching at all cost. The overhead of message-passing is excessive and a well-written monolithic kernel can always outperform a complex micro-kernel OS both in terms of latency and throughput. Even if in a microkernel architecture the kernel itself will be simpler, the overall complexity will grow significantly compared to a monolithic implementation. In particular, if you try to care about performance and start doing all sort of "tricks" to speed up the insanely-slow native implementation. The increased stability of microkernels in case a module (service) crashes, that is so-much praised by Prof. Tanenbaum, is an overrated IMHO: you cannot really get the work done on a microkernel even if a small part of it crashes and gets restarted: that would cause an insane amount of side-effect failures on userspace. Therefore, the whole OS has to be extremely well tested and work flawlessly, no matter the architecture. Partial or total crashes cannot be realistically tolerated. Therefore, if that's the case, why even bothering with a complex microkernel system? If everything needs to be close to perfect and work flawlessly, why not implementing it in the simplest way possible? Also, with the addition of loadable modules and user-space filesystems (see FUSE), a "monolithic" kernel like Linux can be fairly flexible.
Tilck, a Tiny Linux-Compatible Kernel: https://github.com/vvaltchev/tilck
Post Reply