Kernel Development for Programmers
Kernel Development for Programmers
Kernel development is not an easy task. This is a testament to your programming expertise: To
develop a kernel is to say that you understand how to create software that interfaces with and
manages the hardware. A kernel is designed to be a central core to the operating system - the
logic that manages the resources that the hardware has to offer.
One of the most important system resources that you need to manage is the processor or CPU -
this is in the form of allotting time for specific operations, and possibly interrupting a task or
process when it is time for another scheduled event to happen. This implies multitasking. There
is cooperative multitasking, in which the program itself calls a 'yield' function when it wants to
give up processing time to the next runnable process or task. There is preemptive multitasking,
where the system timer is used to interrupt the current process to switch to a new process: a form
of forcive switch, this more guarantees that a process can be given a chunk of time to run. There
are several scheduling algorithms used in order to find out what process will be run next. The
simplest of which is called 'Round Robin'. This is where you simply get the next process in the
list, and choose that to be runnable. A more complicated scheduler involves 'priorities', where
certain higher-priority tasks are allowed more time to run than a lower-priority task. Even more
complicated still is a Real-time scheduler. This is designed to guarantee that a certain process
will be allowed at least a set number of timer ticks to run. Ultimately, this number one resource
calculates to time.
The next most important resource in the system is fairly obvious: Memory. There are some times
where memory can be more precious than CPU time, as memory is limited, however CPU time is
not. You can either code your kernel to be memory- efficient, yet require alot of CPU, or CPU
efficient by using memory to store caches and buffers to 'remember' commonly used items
instead of looking them up. The best approach would be a combination of the two: Strive for the
best memory usage, while preserving CPU time.
The last resource that your kernel needs to manage are hardware resources. This includes
Interrupt Requests (IRQs), which are special signals that hardware devices like the keyboard and
hard disk can use to tell the CPU to execute a certain routine to handle the data that these devices
have ready. Another hardware resource is Direct Memory Access (DMA) channels. A DMA
channel allows a device to lock the memory bus and transfer it's data directly into system
memory whenever it needs to, without halting the processor's execution. This is a good way to
improve performance on a system: a DMA-enabled device can transfer data without bothering
the CPU, and then can interrupt the CPU with an IRQ, telling it that the data transfer is complete:
Soundcards and ethernet cards are known for using both IRQs and DMA channels. The third
hardware resource is in the form of an address, like memory, but it's an address on the I/O bus in
the form of a port. A device can be configured, read, or given data using it's I/O port(s). A
Device can use many I/O ports, typically in the form of ranges like ports 8 through 16 fro
example
Overview
This tutorial was created in an attempt to show you, the reader, how to set up the basics for a
kernel. This involves:
1) Setting up your development environment
2) The basics: Setting the stage for GRUB
3) Linking in other files and calling main()
4) Printing to the screen
5) Setting up a custom Global Descriptor Table (GDT)
6) Setting up a custom Interrupt Descriptor Table (IDT)
7) Setting up Interrupt Service Routines (ISRs) to handle your Interrupts and IRQs
8) Remapping the Programmable Interrupt Controllers (PICs) to new IDT entries
9) Installing and servicing IRQs
10) Managing the Programmable Interval Timer / System Clock (PIT)
11) Managing Keyboard IRQs and Keyboard Data
12) ...and the rest is up to you!
In computing, the kernel is a computer program that manages input/output requests from
software and translates them into data processing instructions for the central processing unit and
other electronic components of a computer. The kernel is a fundamental part of a modern
computer's operating system.[1]
When a computer program (in this case called a process) makes requests of the kernel, the
request is called a system call. Various kernel designs differ in how they manage system calls
(time-sharing) and resources. For example, a monolithic kernel executes all the operating system
instructions in the same address space to improve the performance of the system. A microkernel
runs most of the operating system's background process in user space,[2] to make the operating
system more modular and, therefore, easier to maintain.[3]
Contents
1 Kernel basic facilities
o 1.1 Memory management
o 1.2 Device management
o 1.3 System calls
2 Kernel design decisions
o 2.1 Issues of kernel support for protection
2.1.1 Hardware-based protection or language-based protection
o 2.2 Process cooperation
o 2.3 I/O devices management
3 Kernel-wide design approaches
o 3.1 Monolithic kernels
o 3.2 Microkernels
o 3.3 Monolithic kernels vs. microkernels
3.3.1 Performance
o 3.4 Hybrid (or) Modular kernels
o 3.5 Nanokernels
o 3.6 Exokernels
4 History of kernel development
o 4.1 Early operating system kernels
o 4.2 Time-sharing operating systems
o 4.3 Amiga
o 4.4 Unix
o 4.5 Mac OS
o 4.6 Microsoft Windows
o 4.7 Development of microkernels
5 See also
6 Notes
7 References
8 Further reading
9 External links
The Central Processing Unit. This is the most central part of a computer system, responsible for
running or executing programs. The kernel takes responsibility for deciding at any time which of
the many running programs should be allocated to the processor or processors (each of which
can usually run only one program at a time)
The computer's memory. Memory is used to store both program instructions and data. Typically,
both need to be present in memory in order for a program to execute. Often multiple programs
will want access to memory, frequently demanding more memory than the computer has
available. The kernel is responsible for deciding which memory each process can use, and
determining what to do when not enough is available.
Any Input/Output (I/O) devices present in the computer, such as keyboard, mouse, disk drives,
USB devices, printers, displays, network adapters, etc. The kernel allocates requests from
applications to perform I/O to an appropriate device (or subsection of a device, in the case of
files on a disk or windows on a display) and provides convenient methods for using the device
(typically abstracted to the point where the application does not need to know implementation
details of the device).
Key aspects necessary in resource management are the definition of an execution domain
(address space) and the protection mechanism used to mediate the accesses to the resources
within a domain.[1]
Kernels also usually provide methods for synchronization and communication between processes
called inter-process communication (IPC).
A kernel may implement these features itself, or rely on some of the processes it runs to provide
the facilities to other processes, although in this case it must provide some means of IPC to allow
processes to access the facilities provided by each other.
Finally, a kernel must provide running programs with a method to make requests to access these
facilities.
Memory management
The kernel has full access to the system's memory and must allow processes to safely access this
memory as they require it. Often the first step in doing this is virtual addressing, usually achieved
by paging and/or segmentation. Virtual addressing allows the kernel to make a given physical
address appear to be another address, the virtual address. Virtual address spaces may be different
for different processes; the memory that one process accesses at a particular (virtual) address
may be different memory from what another process accesses at the same address. This allows
every program to behave as if it is the only one (apart from the kernel) running and thus prevents
applications from crashing each other.[4]
On many systems, a program's virtual address may refer to data which is not currently in
memory. The layer of indirection provided by virtual addressing allows the operating system to
use other data stores, like a hard drive, to store what would otherwise have to remain in main
memory (RAM). As a result, operating systems can allow programs to use more memory than
the system has physically available. When a program needs data which is not currently in RAM,
the CPU signals to the kernel that this has happened, and the kernel responds by writing the
contents of an inactive memory block to disk (if necessary) and replacing it with the data
requested by the program. The program can then be resumed from the point where it was
stopped. This scheme is generally known as demand paging.
Virtual addressing also allows creation of virtual partitions of memory in two disjointed areas,
one being reserved for the kernel (kernel space) and the other for the applications (user space).
The applications are not permitted by the processor to address kernel memory, thus preventing
an application from damaging the running kernel. This fundamental partition of memory space
has contributed much to current designs of actual general-purpose kernels and is almost universal
in such systems, although some research kernels (e.g. Singularity) take other approaches.
Device management
To perform useful functions, processes need access to the peripherals connected to the computer,
which are controlled by the kernel through device drivers. A device driver is a computer program
that enables the operating system to interact with a hardware device. It provides the operating
system with information of how to control and communicate with a certain piece of hardware.
The driver is an important and vital piece to a program application. The design goal of a driver is
abstraction; the function of the driver is to translate the OS-mandated function calls
(programming calls) into device-specific calls. In theory, the device should work correctly with
the suitable driver. Device drivers are used for such things as video cards, sound cards, printers,
scanners, modems, and LAN cards. The common levels of abstraction of device drivers are:
1. On the hardware side:
Interfacing directly.
Using a high level interface (Video BIOS).
Using a lower-level device driver (file drivers using disk drivers).
Simulating work with hardware, while doing something entirely different.
For example, to show the user something on the screen, an application would make a request to
the kernel, which would forward the request to its display driver, which is then responsible for
actually plotting the character/pixel.[4]
A kernel must maintain a list of available devices. This list may be known in advance (e.g. on an
embedded system where the kernel will be rewritten if the available hardware changes),
configured by the user (typical on older PCs and on systems that are not designed for personal
use) or detected by the operating system at run time (normally called plug and play). In a plug
and play system, a device manager first performs a scan on different hardware buses, such as
Peripheral Component Interconnect (PCI) or Universal Serial Bus (USB), to detect installed
devices, then searches for the appropriate drivers.
As device management is a very OS-specific topic, these drivers are handled differently by each
kind of kernel design, but in every case, the kernel has to provide the I/O to allow drivers to
physically access their devices through some port or memory location. Very important decisions
have to be made when designing the device management system, as in some designs accesses
may involve context switches, making the operation very CPU-intensive and easily causing a
significant performance overhead.[citation needed]
System calls
In computing, a system call is how a program requests a service from an operating system's
kernel that it does not normally have permission to run. System calls provide the interface
between a process and the operating system. Most operations interacting with the system require
permissions not available to a user level process, e.g. I/O performed with a device present on the
system, or any form of communication with other processes requires the use of system calls.
A system call is a mechanism that is used by the application program to request a service from
the operating system. They use a machine-code instruction that causes the processor to change
mode. An example would be from supervisor mode to protected mode. This is where the
operating system performs actions like accessing hardware devices or the memory management
unit. Generally the operating system provides a library that sits between the operating system and
normal programs. Usually it is a C library such as Glibc or Windows API. The library handles
the low-level details of passing information to the kernel and switching to supervisor mode.
System calls include close, open, read, wait and write.
To actually perform useful work, a process must be able to access the services provided by the
kernel. This is implemented differently by each kernel, but most provide a C library or an API,
which in turn invokes the related kernel functions.[5]
The method of invoking the kernel function varies from kernel to kernel. If memory isolation is
in use, it is impossible for a user process to call the kernel directly, because that would be a
violation of the processor's access control rules. A few possibilities are:
An important consideration in the design of a kernel is the support it provides for protection from
faults (fault tolerance) and from malicious behaviors (security). These two aspects are usually
not clearly distinguished, and the adoption of this distinction in the kernel design leads to the
rejection of a hierarchical structure for protection.[1]
The mechanisms or policies provided by the kernel can be classified according to several criteria,
including: static (enforced at compile time) or dynamic (enforced at run time); pre-emptive or
post-detection; according to the protection principles they satisfy (i.e. Denning[6][7]); whether they
are hardware supported or language based; whether they are more an open mechanism or a
binding policy; and many more.
Support for hierarchical protection domains[8] is typically that of CPU modes. An efficient and
simple way to provide hardware support of capabilities is to delegate the MMU the responsibility
of checking access-rights for every memory access, a mechanism called capability-based
addressing.[9] Most commercial computer architectures lack MMU support for capabilities. An
alternative approach is to simulate capabilities using commonly supported hierarchical domains;
in this approach, each protected object must reside in an address space that the application does
not have access to; the kernel also maintains a list of capabilities in such memory. When an
application needs to access an object protected by a capability, it performs a system call and the
kernel performs the access for it. The performance cost of address space switching limits the
practicality of this approach in systems with complex interactions between objects, but it is used
in current operating systems for objects that are not accessed frequently or which are not
expected to perform quickly.[10][11] Approaches where protection mechanism are not firmware
supported but are instead simulated at higher levels (e.g. simulating capabilities by manipulating
page tables on hardware that does not have direct support), are possible, but there are
performance implications.[12] Lack of hardware support may not be an issue, however, for
systems that choose to use language-based protection.[13]
An important kernel design decision is the choice of the abstraction levels where the security
mechanisms and policies should be implemented. Kernel security mechanisms play a critical role
in supporting security at higher levels.[9][14][15][16][17]
One approach is to use firmware and kernel support for fault tolerance (see above), and build the
security policy for malicious behavior on top of that (adding features such as cryptography
mechanisms where necessary), delegating some responsibility to the compiler. Approaches that
delegate enforcement of security policy to the compiler and/or the application level are often
called language-based security.
The lack of many critical security mechanisms in current mainstream operating systems impedes
the implementation of adequate security policies at the application abstraction level.[14] In fact, a
common misconception in computer security is that any security policy can be implemented in
an application regardless of kernel support.[14]
Typical computer systems today use hardware-enforced rules about what programs are allowed
to access what data. The processor monitors the execution and stops a program that violates a
rule (e.g., a user process that is about to read or write to kernel memory, and so on). In systems
that lack support for capabilities, processes are isolated from each other by using separate
address spaces.[18] Calls from user processes into the kernel are regulated by requiring them to
use one of the above-described system call methods.
No need for separate address spaces. Switching between address spaces is a slow operation that
causes a great deal of overhead, and a lot of optimization work is currently performed in order
to prevent unnecessary switches in current operating systems. Switching is completely
unnecessary in a language-based protection system, as all code can safely operate in the same
address space.
Flexibility. Any protection scheme that can be designed to be expressed via a programming
language can be implemented using this method. Changes to the protection scheme (e.g. from a
hierarchical system to a capability-based one) do not require new hardware.
Disadvantages include:
Longer application start up time. Applications must be verified when they are started to ensure
they have been compiled by the correct compiler, or may need recompiling either from source
code or from bytecode.
Inflexible type systems. On traditional systems, applications frequently perform operations that
are not type safe. Such operations cannot be permitted in a language-based protection system,
which means that applications may need to be rewritten and may, in some cases, lose
performance.
Process cooperation
Edsger Dijkstra proved that from a logical point of view, atomic lock and unlock operations
operating on binary semaphores are sufficient primitives to express any functionality of process
cooperation.[19] However this approach is generally held to be lacking in terms of safety and
efficiency, whereas a message passing approach is more flexible.[20] A number of other
approaches (either lower- or higher-level) are available as well, with many modern kernels
providing support for systems such as shared memory and remote procedure calls.
The idea of a kernel where I/O devices are handled uniformly with other processes, as parallel
co-operating processes, was first proposed and implemented by Brinch Hansen (although similar
ideas were suggested in 1967[21][22]). In Hansen's description of this, the "common" processes are
called internal processes, while the I/O devices are called external processes.[20]
Similar to physical memory, allowing applications direct access to controller ports and registers
can cause the controller to malfunction, or system to crash. With this, depending on the
complexity of the device, some devices can get surprisingly complex to program, and use several
different controllers. Because of this, providing a more abstract interface to manage the device is
important. This interface is normally done by a Device Driver or Hardware Abstraction Layer.
Frequently, applications will require access to these devices. The Kernel must maintain the list of
these devices by querying the system for them in some way. This can be done through the BIOS,
or through one of the various system buses (such as PCI/PCIE, or USB). When an application
requests an operation on a device (Such as displaying a character), the kernel needs to send this
request to the current active video driver. The video driver, in turn, needs to carry out this
request. This is an example of Inter Process Communication (IPC).
Kernel-wide design approaches
Naturally, the above listed tasks and features can be provided in many ways that differ from each
other in design and implementation.
The principle of separation of mechanism and policy is the substantial difference between the
philosophy of micro and monolithic kernels.[23][24] Here a mechanism is the support that allows
the implementation of many different policies, while a policy is a particular "mode of operation".
For instance, a mechanism may provide for user log-in attempts to call an authorization server to
determine whether access should be granted; a policy may be for the authorization server to
request a password and check it against an encrypted password stored in a database. Because the
mechanism is generic, the policy could more easily be changed (e.g. by requiring the use of a
security token) than if the mechanism and policy were integrated in the same module.
In minimal microkernel just some very basic policies are included,[24] and its mechanisms allows
what is running on top of the kernel (the remaining part of the operating system and the other
applications) to decide which policies to adopt (as memory management, high level process
scheduling, file system management, etc.).[1][20] A monolithic kernel instead tends to include
many policies, therefore restricting the rest of the system to rely on them.
Per Brinch Hansen presented arguments in favor of separation of mechanism and policy.[1][20] The
failure to properly fulfill this separation, is one of the major causes of the lack of substantial
innovation in existing operating systems,[1] a problem common in computer architecture.[25][26][27]
The monolithic design is induced by the "kernel mode"/"user mode" architectural approach to
protection (technically called hierarchical protection domains), which is common in conventional
commercial systems;[28] in fact, every module needing protection is therefore preferably included
into the kernel.[28] This link between monolithic design and "privileged mode" can be
reconducted to the key issue of mechanism-policy separation;[1] in fact the "privileged mode"
architectural approach melts together the protection mechanism with the security policies, while
the major alternative architectural approach, capability-based addressing, clearly distinguishes
between the two, leading naturally to a microkernel design[1] (see Separation of protection and
security).
While monolithic kernels execute all of their code in the same address space (kernel space)
microkernels try to run most of their services in user space, aiming to improve maintainability
and modularity of the codebase.[3] Most kernels do not fit exactly into one of these categories, but
are rather found in between these two designs. These are called hybrid kernels. More exotic
designs such as nanokernels and exokernels are available, but are seldom used for production
systems. The Xen hypervisor, for example, is an exokernel.
Monolithic kernels
Main article: Monolithic kernel
Diagram of a monolithic kernel
In a monolithic kernel, all OS services run along with the main kernel thread, thus also residing
in the same memory area. This approach provides rich and powerful hardware access. Some
developers, such as UNIX developer Ken Thompson, maintain that it is "easier to implement a
monolithic kernel"[29] than microkernels. The main disadvantages of monolithic kernels are the
dependencies between system components — a bug in a device driver might crash the entire
system — and the fact that large kernels can become very difficult to maintain.
Monolithic kernels, which have traditionally been used by Unix-like operating systems, contain
all the operating system core functions and the device drivers (small programs that allow the
operating system to interact with hardware devices, such as disk drives, video cards and
printers). This is the traditional design of UNIX systems. A monolithic kernel is one single
program that contains all of the code necessary to perform every kernel related task. Every part
which is to be accessed by most programs which cannot be put in a library is in the kernel space:
Device drivers, Scheduler, Memory handling, File systems, Network stacks. Many system calls
are provided to applications, to allow them to access all those services. A monolithic kernel,
while initially loaded with subsystems that may not be needed can be tuned to a point where it is
as fast as or faster than the one that was specifically designed for the hardware, although more in
a general sense. Modern monolithic kernels, such as those of Linux and FreeBSD, both of which
fall into the category of Unix-like operating systems, feature the ability to load modules at
runtime, thereby allowing easy extension of the kernel's capabilities as required, while helping to
minimize the amount of code running in kernel space. In the monolithic kernel, some advantages
hinge on these points:
Most work in the monolithic kernel is done via system calls. These are interfaces, usually kept in
a tabular structure, that access some subsystem within the kernel such as disk operations.
Essentially calls are made within programs and a checked copy of the request is passed through
the system call. Hence, not far to travel at all. The monolithic Linux kernel can be made
extremely small not only because of its ability to dynamically load modules but also because of
its ease of customization. In fact, there are some versions that are small enough to fit together
with a large number of utilities and other programs on a single floppy disk and still provide a
fully functional operating system (one of the most popular of which is muLinux). This ability to
miniaturize its kernel has also led to a rapid growth in the use of Linux in embedded systems.
These types of kernels consist of the core functions of the operating system and the device
drivers with the ability to load modules at runtime. They provide rich and powerful abstractions
of the underlying hardware. They provide a small set of simple hardware abstractions and use
applications called servers to provide more functionality. This particular approach defines a
high-level virtual interface over the hardware, with a set of system calls to implement operating
system services such as process management, concurrency and memory management in several
modules that run in supervisor mode. This design has several flaws and limitations:
Coding in kernel can be challenging, in part because one cannot use common libraries (like a full-
featured libc), and because one needs to use a source-level debugger like gdb. Rebooting the
computer is often required. This is not just a problem of convenience to the developers. When
debugging is harder, and as difficulties become stronger, it becomes more likely that code will
be "buggier".
Bugs in one part of the kernel have strong side effects; since every function in the kernel has all
the privileges, a bug in one function can corrupt data structure of another, totally unrelated part
of the kernel, or of any running program.
Kernels often become very large and difficult to maintain.
Even if the modules servicing these operations are separate from the whole, the code
integration is tight and difficult to do correctly.
Since the modules run in the same address space, a bug can bring down the entire system.
Monolithic kernels are not portable; therefore, they must be rewritten for each new
architecture that the operating system is to be used on.
In the microkernel approach, the kernel itself only provides basic functionality that allows the execution
of servers, separate programs that assume former kernel functions, such as device drivers, GUI servers,
etc.
Microkernels
Main article: Microkernel
Microkernel (also abbreviated μK or uK) is the term describing an approach to Operating System
design by which the functionality of the system is moved out of the traditional "kernel", into a set
of "servers" that communicate through a "minimal" kernel, leaving as little as possible in
"system space" and as much as possible in "user space". A microkernel that is designed for a
specific platform or device is only ever going to have what it needs to operate. The microkernel
approach consists of defining a simple abstraction over the hardware, with a set of primitives or
system calls to implement minimal OS services such as memory management, multitasking, and
inter-process communication. Other services, including those normally provided by the kernel,
such as networking, are implemented in user-space programs, referred to as servers.
Microkernels are easier to maintain than monolithic kernels, but the large number of system calls
and context switches might slow down the system because they typically generate more
overhead than plain function calls.
Only parts which really require being in a privileged mode are in kernel space: IPC (Inter-
Process Communication), Basic scheduler, or scheduling primitives, Basic memory handling,
Basic I/O primitives. Many critical parts are now running in user space: The complete scheduler,
Memory handling, File systems, and Network stacks. Micro kernels were invented as a reaction
to traditional "monolithic" kernel design, whereby all system functionality was put in a one static
program running in a special "system" mode of the processor. In the microkernel, only the most
fundamental of tasks are performed such as being able to access some (not necessarily all) of the
hardware, manage memory and coordinate message passing between the processes. Some
systems that use micro kernels are QNX and the HURD. In the case of QNX and Hurd user
sessions can be entire snapshots of the system itself or views as it is referred to. The very essence
of the microkernel architecture illustrates some of its advantages:
Most micro kernels use a message passing system of some sort to handle requests from one
server to another. The message passing system generally operates on a port basis with the
microkernel. As an example, if a request for more memory is sent, a port is opened with the
microkernel and the request sent through. Once within the microkernel, the steps are similar to
system calls. The rationale was that it would bring modularity in the system architecture, which
would entail a cleaner system, easier to debug or dynamically modify, customizable to users'
needs, and more performing. They are part of the operating systems like AIX, BeOS, Hurd,
Mach, Mac OS X, MINIX, QNX. Etc. Although micro kernels are very small by themselves, in
combination with all their required auxiliary code they are, in fact, often larger than monolithic
kernels. Advocates of monolithic kernels also point out that the two-tiered structure of
microkernel systems, in which most of the operating system does not interact directly with the
hardware, creates a not-insignificant cost in terms of system efficiency. These types of kernels
normally provide only the minimal services such as defining memory address spaces, Inter-
process communication (IPC) and the process management. The other functions such as running
the hardware processes are not handled directly by micro kernels. Proponents of micro kernels
point out those monolithic kernels have the disadvantage that an error in the kernel can cause the
entire system to crash. However, with a microkernel, if a kernel process crashes, it is still
possible to prevent a crash of the system as a whole by merely restarting the service that caused
the error.
Other services provided by the kernel such as networking are implemented in user-space
programs referred to as servers. Servers allow the operating system to be modified by simply
starting and stopping programs. For a machine without networking support, for instance, the
networking server is not started. The task of moving in and out of the kernel to move data
between the various applications and servers creates overhead which is detrimental to the
efficiency of micro kernels in comparison with monolithic kernels.
A microkernel allows the implementation of the remaining part of the operating system as a
normal application program written in a high-level language, and the use of different operating
systems on top of the same unchanged kernel.[20] It is also possible to dynamically switch among
operating systems and to have more than one active simultaneously.[20]
As the computer kernel grows, a number of problems become evident. One of the most obvious
is that the memory footprint increases. This is mitigated to some degree by perfecting the virtual
memory system, but not all computer architectures have virtual memory support.[30] To reduce
the kernel's footprint, extensive editing has to be performed to carefully remove unneeded code,
which can be very difficult with non-obvious interdependencies between parts of a kernel with
millions of lines of code.
By the early 1990s, due to the various shortcomings of monolithic kernels versus microkernels,
monolithic kernels were considered obsolete by virtually all operating system researchers. As a
result, the design of Linux as a monolithic kernel rather than a microkernel was the topic of a
famous debate between Linus Torvalds and Andrew Tanenbaum.[31] There is merit on both sides
of the argument presented in the Tanenbaum–Torvalds debate.
Performance
Monolithic kernels are designed to have all of their code in the same address space (kernel
space), which some developers argue is necessary to increase the performance of the system.[32]
Some developers also maintain that monolithic systems are extremely efficient if well-written.[32]
The monolithic model tends to be more efficient[citation needed] through the use of shared kernel
memory, rather than the slower IPC system of microkernel designs, which is typically based on
message passing.[citation needed]
The performance of microkernels constructed in the 1980s the year in which it started and early
1990s was poor.[33][34] Studies that empirically measured the performance of these microkernels
did not analyze the reasons of such inefficiency.[33] The explanations of this data were left to
"folklore", with the assumption that they were due to the increased frequency of switches from
"kernel-mode" to "user-mode",[33] to the increased frequency of inter-process communication[33]
and to the increased frequency of context switches.[33]
In fact, as guessed in 1995, the reasons for the poor performance of microkernels might as well
have been: (1) an actual inefficiency of the whole microkernel approach, (2) the particular
concepts implemented in those microkernels, and (3) the particular implementation of those
concepts.[33] Therefore it remained to be studied if the solution to build an efficient microkernel
was, unlike previous attempts, to apply the correct construction techniques.[33]
On the other end, the hierarchical protection domains architecture that leads to the design of a
monolithic kernel[28] has a significant performance drawback each time there's an interaction
between different levels of protection (i.e. when a process has to manipulate a data structure both
in 'user mode' and 'supervisor mode'), since this requires message copying by value.[35]
By the mid-1990s, most researchers had abandoned the belief that careful tuning could reduce
this overhead dramatically,[citation needed] but recently, newer microkernels, optimized for
performance, such as L4[36] and K42 have addressed these problems.[verification needed]
The hybrid kernel approach combines the speed and simpler design of a monolithic kernel with the
modularity and execution safety of a microkernel.
Hybrid (or) Modular kernels
Main article: Hybrid kernel
Hybrid kernels are used in most commercial operating systems such as Microsoft Windows NT,
2000, XP, Vista, 7 and 8. Apple Inc's own Mac OS X uses a hybrid kernel called XNU which is
based upon code from Carnegie Mellon's Mach kernel and FreeBSD's monolithic kernel. They
are similar to micro kernels, except they include some additional code in kernel-space to increase
performance. These kernels represent a compromise that was implemented by some developers
before it was demonstrated that pure micro kernels can provide high performance. These types of
kernels are extensions of micro kernels with some properties of monolithic kernels. Unlike
monolithic kernels, these types of kernels are unable to load modules at runtime on their own.
Hybrid kernels are micro kernels that have some "non-essential" code in kernel-space in order
for the code to run more quickly than it would were it to be in user-space. Hybrid kernels are a
compromise between the monolithic and microkernel designs. This implies running some
services (such as the network stack or the filesystem) in kernel space to reduce the performance
overhead of a traditional microkernel, but still running kernel code (such as device drivers) as
servers in user space.
Many traditionally monolithic kernels are now at least adding (if not actively exploiting) the
module capability. The most well known of these kernels is the Linux kernel. The modular
kernel essentially can have parts of it that are built into the core kernel binary or binaries that
load into memory on demand. It is important to note that a code tainted module has the potential
to destabilize a running kernel. Many people become confused on this point when discussing
micro kernels. It is possible to write a driver for a microkernel in a completely separate memory
space and test it before "going" live. When a kernel module is loaded, it accesses the monolithic
portion's memory space by adding to it what it needs, therefore, opening the doorway to possible
pollution. A few advantages to the modular (or) Hybrid kernel are:
Faster development time for drivers that can operate from within modules. No reboot required
for testing (provided the kernel is not destabilized).
On demand capability versus spending time recompiling a whole kernel for things like new
drivers or subsystems.
Faster integration of third party technology (related to development but pertinent unto itself
nonetheless).
Modules, generally, communicate with the kernel using a module interface of some sort. The
interface is generalized (although particular to a given operating system) so it is not always
possible to use modules. Often the device drivers may need more flexibility than the module
interface affords. Essentially, it is two system calls and often the safety checks that only have to
be done once in the monolithic kernel now may be done twice. Some of the disadvantages of the
modular approach are:
With more interfaces to pass through, the possibility of increased bugs exists (which implies
more security holes).
Maintaining modules can be confusing for some administrators when dealing with problems like
symbol differences.
Nanokernels
Main article: Nanokernel
A nanokernel delegates virtually all services — including even the most basic ones like interrupt
controllers or the timer — to device drivers to make the kernel memory requirement even
smaller than a traditional microkernel.[37]
Exokernels
Main article: Exokernel
Exokernels are a still experimental approach to operating system design. They differ from the
other types of kernels in that their functionality is limited to the protection and multiplexing of
the raw hardware, providing no hardware abstractions on top of which to develop applications.
This separation of hardware protection from hardware management enables application
developers to determine how to make the most efficient use of the available hardware for each
specific program.
Exokernels in themselves are extremely small. However, they are accompanied by library
operating systems, providing application developers with the functionalities of a conventional
operating system. A major advantage of exokernel-based systems is that they can incorporate
multiple library operating systems, each exporting a different API, for example one for high level
UI development and one for real-time control.
Strictly speaking, an operating system (and thus, a kernel) is not required to run a computer.
Programs can be directly loaded and executed on the "bare metal" machine, provided that the
authors of those programs are willing to work without any hardware abstraction or operating
system support. Most early computers operated this way during the 1950s and early 1960s,
which were reset and reloaded between the execution of different programs. Eventually, small
ancillary programs such as program loaders and debuggers were left in memory between runs, or
loaded from ROM. As these were developed, they formed the basis of what became early
operating system kernels. The "bare metal" approach is still used today on some video game
consoles and embedded systems,[38] but in general, newer computers use modern operating
systems and kernels.
In 1969 the RC 4000 Multiprogramming System introduced the system design philosophy of a
small nucleus "upon which operating systems for different purposes could be built in an orderly
manner",[39] what would be called the microkernel approach.
The development of time-sharing systems led to a number of problems. One was that users,
particularly at universities where the systems were being developed, seemed to want to hack the
system to get more CPU time. For this reason, security and access control became a major focus
of the Multics project in 1965.[41] Another ongoing issue was properly handling computing
resources: users spent most of their time staring at the screen and thinking instead of actually
using the resources of the computer, and a time-sharing system should give the CPU time to an
active user during these periods. Finally, the systems typically offered a memory hierarchy
several layers deep, and partitioning this expensive resource led to major developments in virtual
memory systems.
Amiga
Main article: AmigaOS
The Commodore Amiga was released in 1985, and was among the first (and certainly most
successful) home computers to feature a hybrid architecture.[citation needed] The Amiga's kernel
executive component, exec.library, uses microkernel message passing design but there are other
kernel components, like graphics.library, that had a direct access to the hardware. There is no
memory protection and the kernel is almost always running in a user mode. Only special actions
are executed in kernel mode and user mode applications can ask operating system to execute
their code in kernel mode.
Unix
Main article: Unix
A diagram of the predecessor/successor family relationship for Unix-like systems.
During the design phase of Unix, programmers decided to model every high-level device as a
file, because they believed the purpose of computation was data transformation.[42]
For instance, printers were represented as a "file" at a known location — when data was copied
to the file, it printed out. Other systems, to provide a similar functionality, tended to virtualize
devices at a lower level — that is, both devices and files would be instances of some lower level
concept. Virtualizing the system at the file level allowed users to manipulate the entire system
using their existing file management utilities and concepts, dramatically simplifying operation.
As an extension of the same paradigm, Unix allows programmers to manipulate files using a
series of small programs, using the concept of pipes, which allowed users to complete operations
in stages, feeding a file through a chain of single-purpose tools. Although the end result was the
same, using smaller programs in this way dramatically increased flexibility as well as ease of
development and use, allowing the user to modify their workflow by adding or removing a
program from the chain.
In the Unix model, the Operating System consists of two parts; first, the huge collection of utility
programs that drive most operations, the other the kernel that runs the programs.[42] Under Unix,
from a programming standpoint, the distinction between the two is fairly thin; the kernel is a
program, running in supervisor mode,[43] that acts as a program loader and supervisor for the
small utility programs making up the rest of the system, and to provide locking and I/O services
for these programs; beyond that, the kernel didn't intervene at all in user space.
Over the years the computing model changed, and Unix's treatment of everything as a file or byte
stream no longer was as universally applicable as it was before. Although a terminal could be
treated as a file or a byte stream, which is printed to or read from, the same did not seem to be
true for a graphical user interface. Networking posed another problem. Even if network
communication can be compared to file access, the low-level packet-oriented architecture dealt
with discrete chunks of data and not with whole files. As the capability of computers grew, Unix
became increasingly cluttered with code. It is also because the modularity of the Unix kernel is
extensively scalable.[44] While kernels might have had 100,000 lines of code in the seventies and
eighties, kernels of modern Unix successors like Linux have more than 13 million lines.[45]
Mac OS
Main article: History of Mac OS
Apple Computer first launched Mac OS in 1984, bundled with its Apple Macintosh personal
computer. Apple moved to a nanokernel design in Mac OS 8.6. Against this, Mac OS X is based
on Darwin, which uses a hybrid kernel called XNU, which was created combining the 4.3BSD
kernel and the Mach kernel.[47]
Microsoft Windows
Main article: History of Microsoft Windows
Microsoft Windows was first released in 1985 as an add-on to MS-DOS. Because of its
dependence on another operating system, initial releases of Windows, prior to Windows 95, were
considered an operating environment (not to be confused with an operating system). This product
line continued to evolve through the 1980s and 1990s, culminating with release of the Windows
9x series (upgrading the system's capabilities to 32-bit addressing and pre-emptive multitasking)
through the mid-1990s and ending with the release of Windows Me in 2000. Microsoft also
developed Windows NT, an operating system intended for high-end and business users. This line
started with the release of Windows NT 3.1 in 1993, and has continued through the years of 2010
with Windows 8 and Windows Server 2012.
The release of Windows XP in October 2001 brought the NT kernel version of Windows to
general users, replacing Windows 9x with a completely different operating system. The
architecture of Windows NT's kernel is considered a hybrid kernel because the kernel itself
contains tasks such as the Window Manager and the IPC Managers, with a client/server layered
subsystem model.[48]
Development of microkernels
Although Mach, developed at Carnegie Mellon University from 1985 to 1994, is the best-known
general-purpose microkernel, other microkernels have been developed with more specific aims.
The L4 microkernel family (mainly the L3 and the L4 kernel) was created to demonstrate that
microkernels are not necessarily slow.[36] Newer implementations such as Fiasco and Pistachio
are able to run Linux next to other L4 processes in separate address spaces.[49][50]