Before you try to understand these differences, you first need to understand other concepts:
1. Modular programming.
The module is a functionally complete part of the program. A module usually has the following properties:
- Interface separation and implementation.
- Initialization and deinitialization procedures. Both options are optional. The deinitialization procedure is probably missing in an environment with GC (Garbage Collector).
- The modules used by the program make a dependency graph of the acyclic graph aka (you may have heard about this - cyclic dependencies are not allowed, the dependency is initialized before the dependent module).
Modular programming is essential in building large systems. Each large core is a modular core, whether it is monolithic, hybrid or microkernel.
Sometimes modules can load and unload dynamically. Dynamic modules are an integral part of any expandable system. These can be plugins or, if we are talking about kernels, drivers that are developed and distributed separately from the kernel.
2. Safe and unsafe languages.
Safe languages very strictly determine what can happen in a program. Most importantly, they do not have the concept of a distorted program (or a meaningless program). Each program is valid and its execution always follows the language specification. Regardless of whether the program does what the programmer expects from it or not, it does not matter in this context.
Common features of safe languages:
- They use garbage collection.
- They do not have pointer arithmetic. This means that writing or reading to an arbitrary address is not allowed.
- They prevent access to the array out of range (if there is such a concept). Exceptions or similar mechanisms can be used to signal and recover from such failures.
- Links (or pointers) have only two possible states: a null link and a link to a valid object. This is guaranteed by the garbage collector. In fact, GC is a key component here. Some languages go even further and do not allow null references at all.
- Each object (used memory block) has type information assigned to it; an object can be accessed only through a link of the corresponding type, for example. you cannot access an array of integers through a string reference.
You can add more entries to this list, but the basic idea is to ensure that the program can only access valid memory areas using valid operations. Keep in mind that some unsafe languages can convey some or all of these features.
Examples of safe languages: Python, Java, a secure subset of C #.
Insecure languages determine what can and cannot be done in a program, but as a rule, almost nothing is needed to stop the programmer from doing the wrong thing. A program that violates these rules is called an incorrect program. From a language point of view, such a program is meaningless, and the language does not even try to determine its behavior, since it is usually almost impossible to do. In C terms, this behavior is undefined.
Examples of unsafe languages: Assembler, C, C ++, Pascal.
3. The hardware is unsafe and therefore must be programmed using an unsafe language.
Most hardware does not give you a secure environment. There were some processors that used to bind type information to each memory cell (see tagged architecture ), but modern ones do not, because this complicates the process, making it slower, more expensive and less general.
However, there are some features that allow you to implement secure environments in an unsafe hardware environment, such as memory protection, separate address spaces, and separation of execution modes in user mode and kernel mode (for example, supervisor mode).
The core is something that works on bare metal, and therefore most of it must be written in unsafe languages such as C and Assembly. Another reason is a safe environment for maximum productivity.
Microkernel and monolithic core
The monolithic core and its modules work in one common address space. And since everything is usually written in an unsafe language, any part of the kernel can access (and damage) memory belonging to another part of the kernel due to errors in the code. The unsafe nature of this environment makes it impossible to detect or recover from these failures and, most importantly, to predict the behavior of the kernel after such failures.
Microkernel is an attempt to overcome this limitation by moving different parts of the kernel to separate address spaces, effectively isolating them from each other, but providing a safe way to communicate with each other (usually through message passing). This separation creates a safe environment consisting of several unsafe processes, allowing the kernel to recover from the failure of some subsystems.
At the same time, a monolithic kernel can work with particles in a separate address space ( FUSE ), while nothing prevents the microkernel from supporting modules that share the address space with the main part of the kernel.
If most of the kernel runs in the same address space, it is considered a monolithic kernel. If most of them run in separate address spaces, such a kernel is considered a microkernel. If the core is somewhere in between and actively uses both approaches, you have a hybrid core.
Hybrid core
The concept of a hybrid core involves combining the best of the two worlds and was invented by Microsoft to increase sales of Windows NT in the 90s. Joke! But it is almost so. Every important part of Windows NT runs in a shared address space, so this is another example of a monolithic design.
Many people seem to use this term when describing a monolithic kernel that can dynamically load modules. This is due to the fact that in the past monolithic kernels did not support dynamic loading of the module and had to be recompiled every time a module was added to the kernel. Microkernels are not related to the dynamic loading of the module, but to the reliability of the kernel, its ability to recover from subsystem failures.
Answer: Linux is a monolithic kernel.
A monolithic kernel can be modular and can dynamically load modules. Microkernel, on the other hand, must be modular and must be able to dynamically load modules - the whole idea is to run them in a separate address space.
Microkernel is not the only way to overcome the unsafe nature of a monolithic core. Another way is to write a monolithic kernel in a safe language. One of the problems with this approach is that a safe environment must be provided either by hardware (and will be very limited), or it must be implemented in software using unsafe languages. Implementing such an environment will be extremely complex and will most likely have many errors (think of all the errors found in the JVM).
An example of this would be experimental OS Singularity .