Discussions on more advanced topics such as monolithic vs micro-kernels, transactional memory models, and paging vs segmentation should go here. Use this forum to expand and improve the wiki!
I like to share the following design idea. What do you think about it?
It allows switching between a monolithic and a micro kernel architecture at link time. The design is basically the (Remote) Proxy pattern [GHJV p207].
The two figures below demonstrate how it would work:
ModuleX and ModuleY are what would be specific services in a micro kernel architecture (e.g. memory management, filesystem, or drivers).
The box Kernel represents other parts of the kernel that communicate with these modules. These parts can be the basic services found in a mirco kernel, other modules, or the api layer.
Each module is split in an interface IModule* and an implementation Module*Impl. Interaction occurs only via the functions defined in the interface. The implementation part implements these functions.
In the monolithic variant all modules are linked within the kernel image.
The micro kernel variant contains two extra parts per module:
ProxyX and ProxyY implement respectively IModuleX and IModuleY by passing messages to the corresponding message service.
MessageServiceX and MessageServiceY drive the module implementation by calling functions in the modules' interfaces. Both the proxies and the message services are easy to implement and small.
Now the kernel, ModuleX, and ModuleY are in separate images and communicate via messages.
Some considerations:
Although I used UML and inheritance to explain the idea, you can easily use C (or some other non-object oriented language). Select some functions which define the interface of a module, make sure that the modules and the kernel interact only via these functions and not via global data, and let the linker replace the function symbols with either the poxy implementation or with the actual implementation.
The traditional proxy pattern relies on late binding (i.e. virtual methods). If you use C++ and don't like the extra level of indirection, you can use templates or a macro to select the concrete type.
Separate user level (ring 3) code from privileged (ring 0) code.
Some uses:
Start to develop a monolithic kernel; once you've implemented message passing and module loading switch to a micro kernel.
Have the flexibility of a micro kernel during development. When you have a stable set of modules and discover that a monolithic kernel is better for a certain production environment, you can switch to a monolithic kernel.
Apply the design to a modular kernel--micro kernel combination. The computer administrator can decide which modules should be in kernel space and which ones should be in userspace at module load time.
A problem:
What to do with the API that applications use to interact with the OS?
Do all applications have to be recompiled when the kernel architecture is switched?
Do you add an extra layer of indirection? (e.g. an int21 function that repacks the parameters and dispatches messages or a message service that calls functions in the monolithic kernel.)
I look forward to see your comments and criticism and I hope the design is useful to someone.
One thought I had for drivers which absolutely need performance (E.G. graphics drivers) was to run them in kernel mode, and repoint their call vectors. Since all processes interact with the kernel through user mode call vectors (Which are located up in kernel address space - but user readable), which are stored in a table pointed to by %fs, I could just repoint these vectors to be very fast (no context switch) calls that directly enter the kernel. This has two advantages - firstly, high speed drivers can bypass the normal system call mechanism, and secondly, theres no need for a context switch whenever an app calls the driver, or when an interrupt does so.
The driver is still logically a process, but it's running at ring 0, and it has all the capabilities (and responsibilities) that come with that. Another advantage is that it can still be run in ring 3 if the user wishes, without a recompile (It wouldn't be possible to transition between rings while running though).
Owen wrote:One thought I had for drivers which absolutely need performance (E.G. graphics drivers) was to run them in kernel mode.
If you're still using traditional IPC for interacting with your graphics drivers, what benefit (performance wise) would they have for being in the kernel?
Interrupt servicing doesn't need a context switch and IPC with them doesn't need a context switch. Yes, you have to switch privilege rings, but thats still orders of magnitude less expensive than a full TLB invalidation.
My IPC design needs no copying in any case, and only needs a system call if the buffer is full (writing) or empty (reading).
(I map 1 or more pages as a ringbuffer per direction and have both processes manage it without kernel intervention. Yes, theoretically you could screw up the other process' position pointer if you wanted, but the code at the other end should check for this anyway - and the supplied code which it's reccomended is used does)