An operating system includes the parts responsible for basic use and administration, such as the kernel, device drivers, boot loader, command shell, and basic utilities, with the kernel as its innermost core providing foundational services, managing hardware, and distributing system resources through subsystems such as interrupt handlers, the scheduler, memory management, networking, and interprocess communication.
Hardware execution is split into two states:
- Kernel-space: An elevated state with a protected memory space and full hardware access, running in kernel mode.
- User-space: A restricted state where normal applications run in user mode with access to only a subset of machine resources.
Applications interact with the kernel through system calls, usually by invoking C library functions that request kernel work on their behalf. Hardware interacts with the kernel through interrupts, which stop the processor’s current work and run a specific interrupt handler.
At any moment, a processor is doing one of three things:
- Executing user code in a process in user-space.
- Executing on behalf of a specific process in kernel-space, which is process context.
- Handling an asynchronous interrupt in kernel-space, which is interrupt context and has no associated process.
History
- Unix originated in 1969 at Bell Laboratories from the terminated Multics project, evolving into a family of operating systems built around a shared application programming interface (API) and design decisions.
- The operating system was rewritten in C in 1973, an unprecedented architectural step that guaranteed broad hardware portability across diverse platforms.
- Berkeley Software Distributions (BSD) expanded the system by adding virtual memory, demand paging, job control, and TCP/IP networking.
- Modern Unix implementations are highly scalable general-purpose systems featuring preemptive multitasking, multithreading, and shared libraries with demand loading.
- Core characteristics underpinning Unix’s stability and elegance include:
- Simplicity: Implements a streamlined design utilizing hundreds, rather than thousands, of basic system calls.
- Unified file model: Almost everything is represented as a file, simplifying device and data manipulation into a core set of system calls.
- C programming: The kernel and system utilities are written in C, ensuring cross-architecture portability and developer accessibility.
- Process creation: Extremely fast process creation time leveraging the unique
fork()system call. - Interprocess Communication (IPC): Simple and robust IPC primitives allow single-purpose programs to be strung together to accomplish complex tasks.
- Clean layering: Strict architectural separation between policy and mechanism.
- Linux was developed in 1991 by Linus Torvalds for the Intel 80386 microprocessor as a free Unix-like system.
- The system implements the Unix API (as defined by POSIX and the Single Unix Specification) but is a wholly independent implementation, not a direct descendant of Unix source code.
- The Linux kernel is open-source software licensed under the GNU General Public License (GPL) version 2.0, mandating that distributed modifications include available source code.
- The term “Linux” strictly refers to the kernel itself, whereas a functional Linux system is an amalgamation of the kernel, C library, toolchain, basic utilities, and user interfaces.
Monolithic vs. Microkernel
Operating system kernels generally adhere to one of two primary structural schools of design:
- Monolithic kernels: The entire kernel executes as a single, large process running entirely in a single address space.
- Internal communication utilizes direct C function invocation, yielding extreme simplicity and high performance.
- Typically compiled and stored on disk as a single static binary image.
- Microkernels: Kernel functionality is highly partitioned into separate, distinct processes called servers, with most servers relegated to unprivileged user-space.
- Direct function invocation is impossible; communication relies on Interprocess Communication (IPC) mechanisms and message passing built into the system.
- This model is modular and prevents a failure in one server from crashing others, but incurs significant latency and throughput penalties due to IPC overhead and frequent kernel-to-user context switches.
Linux firmly employs a monolithic kernel design, running entirely in kernel mode in a single address space, but pragmatically incorporates the most advantageous features of the microkernel model.
Characteristics distinguishing the Linux kernel from classic monolithic Unix designs include:
- Modular design: Capable of dynamically loading and unloading separate kernel modules into the kernel image on demand.
- Kernel preemption: The Linux kernel can preempt tasks actively executing inside the kernel itself.
- Symmetrical Multiprocessor (SMP) support: Native support for SMP architectures, a feature absent in most traditional Unix implementations.
- Unified thread model: The kernel treats all processes identically without a strict internal thread architecture; threads are simply standard processes that happen to share resources.
- Object-oriented device model: Features device classes, hot-pluggable events, and a user-space device filesystem (
sysfs).
Development Lifecycle
Linux kernels are distributed in two flavors: stable kernels for production use and development kernels for rapid experimental change.
Kernel versions follow the form :
- Major and minor: Identify the kernel series, such as
2.6. - Minor release number: Even numbers indicate stable kernels, while odd numbers indicate development kernels.
- Revision number: Increases with regular releases that add bug fixes, drivers, and features.
- Stable version: An optional fourth number for important backported bug fixes.
Historically, kernel development moved from experimental implementation to feature freeze, then code freeze, and finally a stabilized release. The 2.6 series changed this model by continuing the same series instead of branching into 2.7, folding smaller development cycles into each 2.6 revision.
The Linux kernel is developed collaboratively over the Internet by hackers, commercial developers, and hardware maintainers. Peer review, patch submission, and architectural discussion largely revolve around the Linux Kernel Mailing List (lkml).
Kernel Source Tree Structure
The kernel source tree is organized into logical subsystems under the root directory:
arch: Architecture-specific source code.block: Block I/O layer.crypto: Cryptographic API.drivers: Device drivers.firmware: Device firmware required by certain drivers.fs: The Virtual Filesystem (VFS) and individual filesystems.include: Kernel headers.init: Kernel boot and initialization code.ipc: Interprocess communication code.kernel: Core subsystems, including the scheduler.lib: Helper routines.mm: Memory management subsystem and the virtual memory layer.net: Networking subsystem.security: Linux Security Module.sound: Sound subsystem.usr: Early user-space code used for initramfs.
The root directory also contains key project files such as COPYING for the GNU GPL v2 license, CREDITS for major contributors, MAINTAINERS for subsystem ownership, and Makefile for the base build rules. Familiarity with this layout helps when configuring the kernel and selecting the subsystems and drivers to compile.
Configuring and Building the Kernel
The Linux source code is available, so you can tailor it before compiling and enable support only for the specific features and drivers you want, making configuration a required step before building the kernel.
Configuration
Since the kernel supports a wide range of features and hardware, configuration is substantial, with features and drivers controlled by CONFIG_ options and the selected state stored in a .config file at the root of the source tree.
Configuration options come in a few basic types:
- Boolean:
yesorno, which enables or disables a feature directly in the kernel. - Tristate:
yes,no, ormodule, which allows code to be built as a loadable module. - String or integer: A value exposed internally as a preprocessor macro rather than a build toggle.
Distribution kernels, such as those shipped by Ubuntu or Fedora, are precompiled with a broad set of features enabled and many drivers built as modules.
Several tools are available for configuration:
make config: Interactive text-based command-line utility.make menuconfig:ncurses-based interface.make gconfig:gtk+-based graphical interface.make defconfig: Generates a default configuration for the target architecture.make oldconfig: Updates an existing configuration for a newer source tree.
Build
Once the kernel is configured, make builds it while automatically tracking dependencies. Parallel builds can be sped up with make -jn, where n is the number of jobs, and tools such as distcc or ccache can reduce build time further. A successful build produces the kernel image, its loadable modules, and related files such as System.map, which maps memory addresses to function and variable names.
Installing the Kernel
Kernel installation depends on the target architecture and boot loader, so the exact steps vary by system. Always keep one or two known-safe kernels available in case a newly installed kernel fails.
Module installation is architecture-independent and handled by make modules_install, which copies compiled modules into /lib/modules. Once the kernel image, modules, and supporting files are installed, the system is ready to boot and run the new kernel.
A Different Beast
The Linux kernel differs from a normal user-space application in several important ways. These differences do not necessarily make kernel development harder, but they do make it a different kind of programming environment.
- The kernel has no access to the standard C library or standard C headers.
- The kernel is written in GNU C.
- The kernel lacks the memory protection available to user-space.
- The kernel cannot easily use floating-point operations.
- The kernel has a small fixed-size per-process stack.
- Synchronization and concurrency are major concerns because the kernel is asynchronous, preemptive, and SMP-aware.
- Portability is important.
No glibc
The kernel is not linked against the standard C library or any other external library. Size and speed are the main reasons, and even a reduced libc would still be too large and inefficient for kernel use.
Many familiar libc routines are reimplemented inside the kernel:
- Common string functions live in
lib/string.cand - are used through headers such as
<linux/string.h>. printk()plays the role ofprintf(),- also takes log-level prefixes such as
KERN_ERRso messages can be routed appropriately.
Kernel code also uses only headers from the kernel source tree:
- Base headers live under
include/. - Architecture-specific headers live under
arch/<architecture>/include/asm/ - and are included with the
asm/prefix, such as<asm/ioctl.h>.
GNU C
The kernel uses ISO C99 and GNU C extensions, requiring gcc version 3.2 or later, with 4.4 recommended.
- Inline functions eliminate call overhead by inserting the function body directly at the call site.
- They are declared
static inlineinside header files to ensure type safety and prevent the creation of an exported function. - Increased memory use and instruction cache footprint limit them to small, time-critical functions.
- They are declared
- Inline assembly uses the
asm()compiler directive to embed assembly instructions directly within C functions for low-level fast paths. - Branch annotations optimize conditional branches through compiler directives.
likely()andunlikely()tell the compiler to optimize branches expected to be taken most often.- Incorrect branch hints incur a performance penalty.
No Memory Protection
When a user-space program performs an illegal memory access, the kernel can trap the fault, send SIGSEGV, and kill the process. Illegal memory access in the kernel is far more serious, because it results in an oops, a major kernel error.
Kernel memory is also not pageable, so every byte consumed by the kernel is one less byte of available physical memory. That makes memory bugs and unnecessary allocation far more costly than they are in user-space.
No Easy Use of Floating Point
Floating-point support is easy in user-space because the kernel handles the transition and state management. The kernel itself does not have that luxury, so floating-point use usually requires manually saving and restoring floating-point registers, along with other architecture-specific work. In practice, floating-point operations are avoided inside the kernel except in rare cases.
Small, Fixed-Size Stack
User-space programs can often get away with large stack allocations because their stacks can grow dynamically. The kernel stack is small and fixed in size, so large on-stack structures and arrays are dangerous.
Exact stack size varies by architecture. On x86, it is configurable at build time and can be 4KB or 8KB. Historically, kernel stacks were two pages, commonly 8KB on 32-bit systems and 16KB on 64-bit systems. Each process gets its own kernel stack.
Synchronization
The kernel is highly susceptible to race conditions, so shared resources must be protected with synchronization primitives such as spinlocks and semaphores.
Concurrent execution arises from several sources:
- Preemptive multitasking, which allows the scheduler to suspend and resume processes.
- Symmetric multiprocessing (SMP), which allows multiple processors to execute kernel code simultaneously.
- Asynchronous interrupts, which can change execution flow during active resource access.
- Kernel preemption, which allows higher-priority tasks to interrupt active kernel code.
Portability
Portability ensures that architecture-independent kernel code can compile and run across different systems. That requires isolating architecture-dependent logic, remaining endian-neutral, staying 64-bit clean, and avoiding hardcoded assumptions about word or page size.
These synchronization and portability constraints shape how kernel subsystems are designed and how kernel code must be written.