SoLDMG wrote:
linguofreak wrote:
Certainly monolithic kernels are less than ideal, but currently available CPUs aren't great for building microkernels on, plus the memory model assumed by Unix and C (as well as by Windows nowadays) does not lend itself well to microkernels.
Care to elaborate so I won't be surprised later on? I'm assuming you're talking about paging and taskswitching, right?
Yeah, pretty much. Microkernels tend to implement the functionality traditionally provided by monolithic kernels with server processes (because of the memory model provided by most CPUs), but the thing is that many of those functionalities are better provided by a shared library. The difference between kernel-type shared libraries and userland shared libraries is that userland shared libraries only need access to the data areas of the tasks that call them, whereas kernel-type shared libraries have their own data that the code calling the library has to be prevented from accessing directly.
The solution to this in monolithic kernel systems on traditional hardware is to map the kernel-type libraries and their private data into an area that only kernel-type libraries can access. This protects their private data from userland code without significantly slowing down access to the libraries, but has the disadvantage of not protecting the libraries' private data areas from other kernel-type libraries. To protect the kernel-type libraries from each other on a traditional CPU, you have to move them into separate server processes and have other programs access their functionality via IPC, which slows things down. For some things (disk and user I/O), this may be acceptable, as there are often considerable latencies involved in waiting for the hardware or the user anyways. In these cases userland implementations of the relevant functionality often already exist for Linux.
An architecture that I think would work better for a microkernel than anything that currently exists would be something like this:
Start with Intel segmentation. Instead of having each segment descriptor specify a base in a paged address space defined elsewhere, have it specify a page directory so that the segment is its own paged address space.
The global descriptor table (or a set of global descriptor tables defined by a global descriptor directory) contains a descriptor for every segment that exists on the system. Programs cannot load selectors pointing into the GDT directly.
The local descriptor table, rather than containing a list of descriptors in the same format as the GDT, as on Intel systems, is an indirection layer on top of the GDT. When a program performs a segment load, the CPU uses the selector as an index into the LDT. An LDT descriptor consists of a selector pointing into the GDT and possibly some permission bits. The CPU uses the selector in the LDT descriptor to find the appropriate segment descriptor in the GDT and loads the segment register in question with information from the GDT descriptor (such as the page directory for that segment).
The GDT descriptor for each segment contains a field associating an LDT with that segment. At any given moment, there are two LDTs active, one for the current code segment, and one for the current stack / thread local data segment. Using a selector with the most significant bit cleared to load a segment register causes the low order bits of the selector to be used as an index into the LDT for the current code segment, using a selector with the most significant bit set causes the LDT for the current stack segment to be used.
If a program needs to call a library that has private data (lets say that the library is a device driver, and the private data has to do with multiplexing access to the device), it makes a far call to the library's code segment. This causes the library's LDT to be loaded as the code LDT. The segment containing the library's private data is mapped in the library's LDT, allowing the library to load that segment to access its private data. The library does its work, then invalidates the segment register that it used to access its private data segment and makes a far return to the original program's code segment. This causes the program's LDT to be loaded. The program's LDT does not have the library's private data segment mapped, which prevent the program from accessing the library's private data.