Operating System Principles and Kernel Development
Operating System Principles and Kernel Development
Development
Core Responsibilities and Functions of an Operating System
Operating systems mediate between application software and physical hardware resources (software layer in red,
hardware in green, and the kernel core in blue). An operating system (OS) is system software that manages
computer hardware and software resources and provides common services for computer programs
1 . It acts as an intermediary, ensuring that multiple applications can share the CPU, memory, and I/O
devices safely and efficiently. At a high level, the core functions of an OS include resource management,
abstraction of hardware details for convenience, and providing a stable execution environment for
applications 2 3 :
• Resource Allocation and Isolation: The OS controls how CPU time, memory space, and I/O
devices are allocated to different programs so that no single application monopolizes resources. It
schedules tasks (processes) to share the processor and partitions memory among programs. By
doing so, it prevents conflicts and isolates applications from one another’s faults or security issues
2 . For example, the OS might preempt a running program to give another a turn on the CPU, or
deny a process access to memory that it does not own, thus protecting programs from each other.
1
instance, an application can read a file using a simple API call, while the OS handles the low-level disk
commands. This abstraction often employs virtualization techniques; for example, virtual memory
gives programs the illusion of a large, contiguous memory space even if physical RAM is limited 3 .
• Common Services for Applications: Operating systems provide a suite of common services and
utilities that programs rely on 4 . These include managing files on disk, communicating over
networks, and interfacing with user input/output devices. By offering standard system calls and
libraries, the OS lets applications perform operations (e.g. creating a file or launching a new process)
in a hardware-independent way. This ensures software portability: an application can run on
different hardware platforms as long as the OS provides the expected API and services.
In summary, the OS’s primary role is to be the “maestro” of the computer’s resources, orchestrating
hardware access and enabling multiple programs and users to coexist. It balances performance (by
efficiently scheduling and caching), isolation (by enforcing access permissions and memory protection), and
convenience (by presenting high-level abstractions like files and sockets instead of disk sectors or network
packets) 2 4 . All other duties of an OS – process scheduling, memory management, device control, etc. –
stem from these core goals.
• Monolithic vs. Microkernel OS: This classification refers to kernel architecture (discussed in detail in
the next section). In a monolithic-kernel OS, the entire operating system (device drivers, file system,
network stack, etc.) runs in a single large kernel address space, which can offer high performance
but lower fault isolation. In a microkernel OS, only a minimal core (scheduling, basic memory
management, inter-process communication) runs in kernel mode, and many services run as regular
processes in user space – improving modularity and fault isolation at some cost to performance 5 .
For example, traditional Unix/Linux kernels are monolithic, whereas MINIX 3 and QNX use
microkernel designs. (Hybrid approaches also exist, combining elements of both.)
• Single-Tasking vs. Multitasking: Early operating systems (like DOS) were single-tasking, meaning
they could only run one program at a time. Modern OSes are multitasking, enabling multiple
processes to run concurrently by rapidly switching the CPU among them (time-sharing) 6 .
Multitasking may be cooperative (programs yield control voluntarily) or preemptive (the OS
scheduler forcibly switches tasks). Virtually all contemporary OSes (Windows, Linux, macOS, etc.)
support preemptive multitasking, where the OS scheduler allocates CPU slices to each ready process
to give the illusion of parallelism.
• Single-User vs. Multi-User: Some systems are designed for a single user (e.g. early PC or mobile
OSes where one user owns all processes), whereas others are multi-user, supporting multiple user
accounts and sessions simultaneously. Multi-user OS (like Unix/Linux, or mainframe OSes)
implement security mechanisms to isolate users’ processes and data from each other. They often
allow remote logins and can time-share the system among several users at once. Single-user OSes
2
(like classic single-user versions of Windows or Android) don’t need to distinguish resources among
different user identities as strongly (though they still separate the OS from applications).
• Real-Time Operating Systems (RTOS): Real-time OSes are designed for deterministic performance,
guaranteeing that critical tasks are executed within strict timing constraints. In a hard real-time OS,
missing a deadline (even rarely) is considered a system failure – these are used in environments like
industrial control, avionics, or medical devices. In soft real-time systems, occasional deadline misses
are tolerable but undesired (e.g. multimedia or audio systems) 7 . Real-time OSes achieve
predictability by using special scheduling algorithms (e.g. rate-monotonic or earliest-deadline-first)
and often avoiding features that introduce unpredictability (such as virtual memory paging). They
prioritize meeting timing guarantees over maximizing throughput. For example, VxWorks,
FreeRTOS, or real-time variants of Linux (with PREEMPT_RT) can schedule tasks so that high-priority
operations happen within a known maximum latency.
• Distributed and Networked OS: A distributed operating system spreads its functions across multiple
networked computers, providing the illusion of a single coherent system. These OSes coordinate
resources and processes across machines (for example, a cluster OS or a cloud OS that manages a
cluster of servers) 8 . They often facilitate inter-process communication over the network and
handle issues like data replication and consistency. In contrast, a traditional standalone OS manages
the resources of one machine. There are also network operating systems which are somewhat
simpler, providing features for network connectivity and remote resource access (e.g., older Novell
NetWare or Windows Server acting primarily as a file/print server) but not a unified distributed
environment.
• Embedded Operating Systems: Embedded OS are specialized for devices with specific functions and
often limited resources (microcontrollers, IoT gadgets, appliances, vehicles, etc.). They tend to be
compact and efficient, sometimes tailored to run a single application or a specific set of tasks.
Often, they dispense with features not needed for their purpose (for example, an embedded OS
might not include multi-user support or complex GUIs). Some embedded OSes are real-time as well.
Examples include embedded Linux (like OpenWrt or Android’s low-level OS layer), FreeRTOS, or
Zephyr. Embedded OSes can be very small (a tiny kernel might be under 10 KB) 9 and are
optimized for reliability and predictability in simple roles (e.g. managing a microwave oven’s control
system).
These classifications are not mutually exclusive – for instance, an OS can be both multi-user and real-time (e.g.,
a real-time Unix variant), or an embedded OS might use a microkernel design. Overall, the type of operating
system dictates its internal design choices and the use-cases it is suitable for (e.g. general-purpose desktop
OS vs. real-time control system vs. distributed cluster OS).
• Monolithic Kernels: In a monolithic design, the kernel is a single unified program running in
supervisor (kernel) mode, with all OS services integrated into one address space 5 . This means
3
the process scheduler, memory manager, device drivers, file system, network stack, and other
subsystems all execute in kernel space. The advantage is efficiency: since everything is in one space,
calls between OS components are simply function calls (no user-kernel context switch overhead) and
can share data easily. Monolithic kernels often yield high performance and have straightforward
access to hardware. Classic Unix, Linux, Windows 9x, and early Mac OS kernels are monolithic.
However, this design has downsides: the large codebase running with full privileges can make the
system less stable and secure – a bug in any component (even in a driver) can crash the entire
system or be exploited to compromise the kernel 10 . Monolithic kernels can also become hard to
maintain or extend as they grow (changing one part might affect others). Modern monolithic
kernels mitigate some issues by being modular (supporting loadable modules; see below) and by
careful architecture, but they remain a single trust domain.
• Microkernels: A microkernel takes the opposite approach by minimizing what runs in kernel
mode. The kernel is kept as small as possible – typically only fundamental mechanisms like low-level
memory management (e.g. defining address spaces), CPU scheduling, and inter-process
communication (IPC) primitives are in the kernel 5 . All other services (device drivers, file systems,
network protocols, etc.) run as user-space processes (sometimes called servers). These user-space
OS servers communicate with each other and with the tiny kernel via message-passing IPC. The
main benefits are modularity, maintainability, and fault isolation: if a device driver running in
user space crashes, it doesn’t necessarily bring down the whole system, and it can potentially be
restarted independently 11 . Microkernels also enforce a more structured separation of concerns
and can be more secure (the kernel has a smaller attack surface, and components run with least
privilege) 12 13 . Examples of microkernel-based OS include QNX, MINIX 3, Genode, and (in some
implementations) L4 and its successors. The classic critique of microkernels was performance:
because services are in user space, operations that would be function calls in a monolithic kernel
become IPC messages and context switches, which are slower. For instance, reading a file might
involve the process sending a message to a file-system server and waiting for a reply, incurring
multiple user-kernel transitions. Early microkernels in the 1990s (e.g. Mach) suffered from these
overheads, but modern designs and faster hardware have narrowed the performance gap. Still,
monolithic kernels tend to have an edge in raw throughput, whereas microkernels excel in reliability
and flexibility.
• Hybrid Kernels: Many modern commercial OS kernels are “hybrid”, adopting a mix of both
monolithic and microkernel ideas. For example, Windows NT/XP/7/10 and Apple’s XNU (macOS/
iOS) are often categorized as hybrid kernels. In a hybrid design, the kernel is not as minimalist as a
true microkernel – it may include some drivers or core OS services in kernel space for performance –
but it still tries to maintain a modular structure and may run certain components in user space.
Windows, for instance, runs user-mode subsystems for various APIs, but the kernel (the NTOS core)
still includes memory management, scheduling, device driver framework, etc., and all run in kernel
mode. The XNU kernel combines a microkernel (Mach) with a monolithic BSD kernel and I/O kit
drivers in one address space. Essentially, hybrid kernels aim to get most of the speed of monolithic
kernels while incorporating the stability of microkernels for certain components. They separate
the system into modules and servers where convenient, but not with the rigidity of a pure
microkernel. Most kernels today “do not fit exactly into one category” 14 ; they are designed
pragmatically, using microkernel-like message passing or services for some things and monolithic
integration for others.
4
• Exokernels and Library OS: An exokernel is an extreme design that strips the kernel down to just
secure resource multiplexing, with no high-level abstractions at all 15 . The exokernel’s philosophy
is to avoid imposing any policy or abstraction on applications: it simply arbitrates access to raw
hardware (CPU, memory, disk blocks, etc.) securely, and leaves everything else to user-level library
operating systems (libOS). Essentially, conventional OS concepts (like files, virtual memory,
processes) would be implemented in user-space libraries linked into applications, not in the kernel.
The exokernel just ensures safety (no two apps use the same hardware resource without
coordination). This can yield performance benefits and extreme flexibility, as applications can tailor
the libOS to their needs. For example, one libOS might implement a custom file system optimized for
a particular workload, while another libOS might forgo files entirely. The drawback is complexity in
application design and potentially duplicating OS logic in many programs. Exokernels remain a
research idea (with prototypes like MIT’s ExOS); they demonstrate the separation of mechanism
(kernel’s job) from policy (left to user space). A related concept is the library operating system or
unikernel, where the OS services are library code within the application, often running on a
hypervisor. Unikernels (like MirageOS or IncludeOS) minimize the kernel to the point that the entire
app+OS can be one specialized unit – improving security by removing unnecessary code and only
including what the application needs 16 .
• Layered and Modular Design: Orthogonal to the above categories, kernels can be structured in
layers or modules internally. Early theoretical designs (e.g., Dijkstra’s THE operating system) were
layered, with strict hierarchical layers (hardware at bottom, then a layer for memory management,
then a layer for processes, etc., up to user interface at top). Each layer only uses functions of the
layer below it. While pure layering is inflexible, it aids understanding and separation of concerns. In
practice, most kernels use some layering but also break the strict rules when efficiency demands.
Modularity is highly valued: code is divided into modules with well-defined interfaces. A monolithic
kernel can still be modular internally (and modern ones are). Many support loadable kernel
modules – pieces of code (often device drivers or filesystem handlers) that can be added to or
removed from the running kernel as needed. This provides extensibility without rebooting. For
instance, the Linux kernel is monolithic but supports loadable modules (with commands like
insmod / modprobe to load drivers on the fly) 17 18 . This modular approach means a minimal
base kernel can be kept in memory, and extras are loaded only when required, helping both
flexibility and security (unused code can be left out). We will discuss modules more later, but it’s
worth noting that even microkernels benefit from modular design (their servers are modules
running in user space). Thus, “monolithic vs micro” is about where code runs, while “modular vs
integrated” is about how the code is organized – and modern kernels try to be modular whether they
run components in kernel or user space.
In summary, kernel architecture influences an OS’s performance, reliability, and complexity. Monolithic
kernels favor speed via direct internal calls, microkernels favor robustness via isolation, hybrids take a
middle road, and exokernels push abstraction out of the kernel entirely. Regardless of type, kernels must
handle the same fundamental tasks – scheduling, memory, I/O, etc. – but they do so with different
structuring philosophies.
Process Management
One of the OS’s most important jobs is process management – controlling the lifecycle of processes and
threads, and scheduling them to run on the CPU. A process is essentially a program in execution, including
5
the program’s code and its current activity or state 19 . The OS must handle creating processes, executing
them concurrently, and cleaning them up when done, all while ensuring fair sharing of the CPU and
isolation among processes.
Processes and Threads: A process is an independent unit with its own address space (memory), resources,
and at least one thread of execution. In many OSes, a process may contain multiple threads. A thread is the
smallest schedulable unit of execution – it executes sequential instructions and has its own CPU register
state and stack, but threads within the same process share the process’s memory and resources. In other
words, a process is like a container for resources (memory mappings, open files, etc.), whereas a thread is
an entity that the OS scheduler actually runs on the CPU 20 . Threads allow parallelism within a process
(useful for multi-core CPUs and structuring programs logically). Because threads of the same process share
memory, they can communicate quickly by reading/writing common data, but they also need
synchronization to avoid conflicts (addressed later). Processes, by contrast, are isolated; one process cannot
directly access another’s memory (thanks to memory management hardware and OS enforcement) without
using IPC mechanisms.
Process Lifecycle: Operating systems typically define a set of process states to conceptualize what each
process is doing. Common states include New (or Created) – a process being initialized; Ready – loaded in
memory and waiting for CPU time; Running – currently executing on a CPU; Blocked (or Waiting) – not
ready to run until some event occurs (like an I/O completion); and Terminated – finished execution and
awaiting cleanup. A process will transition through these states during its life. For example, when a process
needs to read from disk, the OS issues the I/O and blocks the process (it moves to Waiting state) until the
disk operation completes, at which point the process becomes Ready again. The OS keeps queues for these
states (e.g., a ready queue of all runnable processes). Context switching is the mechanism by which the
OS switches the CPU from running one process (or thread) to another, pausing the first and resuming
the second. During a context switch, the OS saves the state of the currently running process (its CPU
register values, program counter, etc.) and restores the saved state of the next process to run 21 22 . This
enables multiprogramming: multiple processes appear to run simultaneously by rapidly context-switching
the CPU among them. Context switching, however, has overhead (saving/loading registers, flushing caches,
etc.), so good OS design tries to minimize unnecessary switches or at least make them efficient 23 .
6
Typical process state diagram (simplified): processes transition between states such as “Running” (using the CPU),
“Waiting/Blocked” (idle until an event happens, often waiting for I/O), and “Ready” (runnable, waiting for CPU
scheduling). The OS scheduler moves a process from Ready to Running (dispatch), and from Running to Waiting or
Ready depending on events and time-slicing.
Scheduling Algorithms: The scheduler is the OS component that decides which process (or thread) runs
next on the CPU. Scheduling policies are designed to optimize criteria like CPU utilization, throughput,
response time, or fairness. There are many scheduling algorithms, each with trade-offs:
• First-Come, First-Served (FCFS): The simplest strategy – processes are executed in the order they arrive
(no preemption). Easy to implement, but short tasks can get stuck behind long ones (convoy effect).
• Round-Robin (RR): Each process in the ready queue gets a small time slice (quantum) of CPU in turn. If
a process’s quantum expires and it’s still running, it is preempted and placed back in the ready
queue, and the next process gets CPU. Round-robin ensures fair sharing and is simple; it’s
commonly used in time-sharing systems for its responsiveness. The choice of time quantum is
important (too short causes many context switches; too long degrades to FCFS).
• Priority Scheduling: Each process is assigned a priority, and the scheduler always picks the highest-
priority ready process to run. Priorities can be static or dynamic. This can be preemptive (if a new
process with higher priority arrives, it preempts the current running one) or non-preemptive. Priority
scheduling can ensure important tasks get CPU time first, but can lead to starvation of low-priority
tasks (mitigated by techniques like aging, which gradually increases the priority of waiting
processes).
• Shortest Job First (SJF) / Shortest Remaining Time: SJF selects the process with the smallest estimated
run-time to completion next. It can be optimal for average waiting time in batch systems (provably
so, if all jobs arrive together), but requires knowing or estimating job lengths. A preemptive variant,
shortest remaining time first (SRTF), will preempt if a new job arrives with a remaining time smaller
7
than the current job’s remaining time. SJF/SRTF minimize wait time but are susceptible to starvation
of long jobs if short jobs keep arriving.
• Multilevel Queue & Multilevel Feedback Queue (MLFQ): These algorithms partition processes into
different queues (by priority or type), and each queue can have its own scheduling policy. MLFQ is a
common and versatile approach used in many general-purpose OSes. Processes move between
queues based on their behavior and age. For example, a process that uses a full time slice (CPU-
intensive) may be moved to a lower-priority queue, whereas I/O-bound or interactive tasks that often
block quickly stay in high-priority queues. This way, interactive processes get fast response, and
long-running CPU-bound jobs get CPU in lower queues without starving everything else 24 25 .
MLFQ effectively combines priority scheduling with round-robin and implements aging by promoting
processes that wait too long. Windows and Linux use variants of feedback scheduling (Linux’s
Completely Fair Scheduler is a complex variant that tries to proportionally share CPU based on
weights).
• Real-Time Schedulers: In systems that require real-time guarantees, specialized scheduling like Rate
Monotonic (fixed priority) or Earliest Deadline First (dynamic priority) are used. These ensure
that tasks meet deadlines if the system is schedulable. Real-time tasks might be scheduled ahead of
normal tasks and have to be carefully analyzed for worst-case execution time.
Modern OS schedulers often use a combination of strategies to handle different workload types. For
instance, Windows uses a round-robin within priority classes and adjusts priorities dynamically (favoring I/
O-bound threads), while Unix/Linux historically used priority-based scheduling with feedback adjustments,
and newer Linux kernels use CFS (which attempts to allocate CPU in proportion to weights, essentially
achieving fairness by tracking virtual runtime for each process). The key point is the OS aims to keep the CPU
busy and responsive: context-switching occurs typically at a frequency of a few milliseconds to tens of
milliseconds in interactive systems, enabled by hardware timer interrupts that let the OS preempt running
tasks.
Process Creation and Termination: The OS provides system calls to create new processes – for example,
Unix has the fork() system call to clone a process, and exec() to load a new program into a process,
while Windows has CreateProcess() . When a new process is created, the OS allocates memory for it,
initializes its PCB (Process Control Block, the data structure with process information), and places it in the
ready queue. Processes can create child processes, forming a tree or hierarchy (which the OS tracks for
ownership and sometimes for resource quotas). When a process finishes (by completing execution or being
killed), the OS terminates it: it frees the process’s memory, closes its open files, releases other resources,
and notifies any waiting processes (for instance, a parent process may wait for child completion). The exit
status is recorded, and the process’s PCB is removed (after final accounting). Proper process termination is
crucial to avoid resource leaks – many OSes implement a notion of a zombie process or defunct process
that has terminated but whose parent hasn’t yet collected its exit status (the OS keeps minimal info for it
until the parent does so).
Threads Management: In addition to processes, many OSes have explicit support for threads (also called
lightweight processes). The OS may schedule threads rather than processes, especially on multi-core systems
where threads from the same process can run truly in parallel on different CPUs. Threads can be created
and destroyed via APIs (pthreads in POSIX, CreateThread in Windows, etc.). Some systems also support
user-level threads (threads managed in user space by a runtime library) which might be invisible to the
8
kernel – these need special handling to integrate with the kernel’s scheduling (for example, many-to-one or
many-to-many threading models). Modern kernels typically use a one-to-one model (each user thread = one
kernel schedulable entity). The OS must manage thread synchronization and context switching similarly to
processes, though a thread context switch is a bit lighter than a process switch since the memory space
remains the same (no need to change page tables, which means fewer cache/TLB flushes) 26 27 . In
practice, switching between threads of the same process can be faster than switching between different
processes.
Overall, process management ensures controlled, concurrent execution of programs. The OS tries to
maximize CPU utilization and throughput while minimizing response time and respecting priorities. It also
isolates processes (each process runs as if it has the machine to itself, except for deliberate IPC), which is
fundamental for both reliability and security.
Memory Management
Memory management is the functionality of the OS that handles allocation of memory (RAM) to
processes and the virtualization of the address space. Because memory is a limited resource, the OS must
allocate it efficiently, isolate processes’ memory from each other, and, when needed, use secondary storage
to extend usable memory (virtual memory). Key concepts in memory management include virtual
memory, paging, segmentation, and allocation strategies.
Virtual Memory and Address Spaces: Modern operating systems implement virtual memory, which means
each process operates under the illusion of a large, private memory address space, even if the physical RAM
is much smaller. The OS, with hardware support (the MMU – Memory Management Unit), maps the virtual
addresses that a process uses to physical addresses in RAM. By doing so, it achieves several goals:
isolation (one process cannot “see” or corrupt another’s memory since it has its own mapping),
convenience (each process can start at a standard address like 0 without collisions), and efficient use of
RAM via techniques like paging. Moreover, virtual memory allows the OS to overcommit memory –
running programs whose total memory demand exceeds actual physical memory by keeping portions of
data on disk and only loading into RAM what is needed at the moment 28 29 . If a process tries to access
an address that is not currently in physical memory, the hardware triggers a page fault and the OS can
respond by loading the needed data from disk (this is a core part of demand paging).
Paging: Paging is a widely-used virtual memory scheme where the address space is divided into fixed-
size blocks. Physical memory is divided into frames (e.g. 4 KB each), and virtual memory is divided into
pages of the same size. The OS maintains a page table for each process, which maps page numbers
(virtual) to frame numbers (physical) 28 . When a process accesses a virtual address, the MMU translates it
by looking up the page table: if the page is present in RAM, it gives the corresponding frame and forms the
physical address; if not, a page fault occurs and the OS must load the page from disk (perhaps evicting
another page from RAM to make space, a process known as page replacement). Paging provides
flexibility and eliminates external fragmentation: any free frame can be used for any page, since all are
equal-sized. It does introduce a level of indirection, but hardware like a TLB (Translation Lookaside Buffer)
caches recent page table entries to speed up address translation. Paging also underpins processes’ isolation
– a process cannot reference memory that isn’t in its page table. When context-switching, the CPU switches
to a new page table, instantly changing the memory view to that of the next process.
9
Segmentation: Segmentation is another (older) approach to memory management where a process’s
address space is divided into variable-sized segments based on logical divisions (for example, code
segment, data segment, stack segment). Each segment is a contiguous range of addresses, and segments
can be sized according to the program’s needs. The hardware would translate a virtual address consisting of
a segment identifier and an offset into a physical address (with a segment table providing base addresses
and lengths). Segmentation is more reflective of how programs are structured (and allows protection
settings per segment), but it can suffer from external fragmentation (as segments vary in size, memory
holes can develop). Some systems combined segmentation with paging (e.g. x86 architecture in the past:
segments which are further paged) to get benefits of both. Pure segmentation is not common in modern
general OSes (most have moved to paging), but it’s conceptually useful and was used in systems like
MULTICS and early Intel x86 OS modes.
Virtual Memory Implementation: Most contemporary OSes implement demand paging: pages are loaded
into memory only when a process actually accesses them (demanded). When memory runs low, the OS will
choose some page in RAM to evict (swap out) to disk (to a space called the swap area or pagefile), freeing
that frame for another use. The policy for choosing which page to evict is a significant aspect of OS design –
common algorithms include LRU (Least Recently Used) or approximations thereof, Optimal (theoretically,
if future accesses were known), FIFO with second chance (clock algorithm), etc. The goal is to guess which
pages won’t be needed soon to minimize page fault rate. Good memory management provides the illusion
that each process has a large, contiguous memory, even if behind the scenes its pages may be scattered in
RAM or on disk.
Memory Protection: The OS relies on hardware support (privileged CPU modes and MMU) to enforce that a
process can only access its own address space. If a process attempts to access an address not mapped (or
violate permissions, e.g. writing to a read-only page or executing data), the hardware traps to the OS
(segmentation fault or general protection fault), and the OS typically terminates the process or delivers a
signal/exception. This mechanism is crucial for security and stability – a buggy or malicious program can’t
corrupt the kernel or other processes’ memory. Additionally, OSes can use paging for features like copy-on-
write (to efficiently fork processes by initially sharing pages until a write occurs) and memory-mapped files
(treating file contents as if they were memory arrays).
Memory Allocation and Management: Within the OS, there are strategies for how to assign physical
memory to processes. When a process starts, it needs an initial allocation of memory (for code, data, heap,
stack). As it runs, it may request more (e.g. via malloc in C, which invokes system calls like brk/sbrk or
mmap to get more pages from the OS). The OS must decide where in physical memory to put these
allocations. In older systems with contiguous allocation, algorithms like first-fit, best-fit, worst-fit were
used to allocate memory regions, which could lead to fragmentation. Paging largely simplifies allocation
(any free frame works), but the OS still keeps track of free frames and possibly uses a buddy allocator or
other allocators for efficiency. In kernel space, the OS also dynamically allocates memory for its own data
structures – kernels often use specialized allocators (like Linux’s slab/slub allocator for objects, or a buddy
system for page frames).
If segmentation or other contiguous allocation is used, the OS must deal with external fragmentation
(free memory scattered in small blocks). Paging avoids external fragmentation at the cost of internal
fragmentation (if a process doesn’t use all bytes in the last page, that unused portion is wasted, though
this is typically small). Some systems used memory compaction routines to defragment memory, but with
paging this is generally unnecessary.
10
Swapping and Overcommit: In extreme cases, if the system is overloaded with memory demands, the OS
might swap out entire processes (old approach: swap the whole process memory to disk) or more
commonly, aggressively page out not-recently-used pages. Linux and others also have the concept of
overcommit policy – whether the OS allows more virtual memory to be allocated than there is backing
store. If overcommit is too large and memory really runs out (including swap), it may lead to the OS’s OOM
(Out-Of-Memory) killer terminating some process to free memory.
In summary, memory management provides each process with a clean, contiguous logical memory space
and handles the behind-the-scenes work of mapping that to actual RAM. It ensures protection and efficient
use of memory. Thanks to virtual memory, the OS can run big programs on limited physical memory by
storing inactive portions on disk transparently 29 . The combination of hardware (MMU, page tables) and
OS software (allocators, pager, replacement algorithms) achieves this crucial feat, making memory appear
abundant and safe for all processes.
File System Abstraction: From the user and application perspective, the OS presents storage as a hierarchy
of directories containing files (a hierarchical file system). A file is a named sequence of bytes (or records)
stored on disk, and the OS provides operations to create, open, read, write, and delete files, as well as to
create and navigate directories. This is a key abstraction – programs deal with human-meaningful names
and a directory tree, rather than disk block numbers. The OS keeps metadata about each file: attributes
like size, modification time, permissions, and pointers to the file’s data blocks on the disk 31 . Directories
are special files that list name-to-file metadata mappings (for instance, in UNIX, a directory contains a list of
filenames and their corresponding inode numbers). The file system interface is typically consistent across
different physical devices – whether the file is on an SSD, HDD, or even a network share, the operations
(open, read, write) are the same. This is enabled by the OS’s internal layering: many OSes use a Virtual File
System (VFS) layer that provides a common API and allows multiple file system implementations (FAT, NTFS,
ext4, etc.) to coexist 32 . The VFS routes file operations to the correct specific file system driver that handles
the on-disk format.
File System Implementation: Under the hood, file systems vary in design. They must decide how to
allocate space for files, how to organize directories, and how to keep track of free space and bad blocks.
Some common structures include:
• Contiguous allocation: Simplest conceptually – each file occupies a contiguous range of disk blocks.
Fast sequential access, but fragmentation and difficulty growing files are problems.
• Indexed allocation (e.g., inodes): Many modern file systems (Unix-like) use an index structure per
file. In ext2/ext3/ext4, for example, each file has an inode that contains pointers to data blocks
(some directly, and indirect pointers for large files) 33 . This allows files to be non-contiguous on disk
while still finding blocks via the index. NTFS uses a somewhat similar concept (the Master File Table
stores file metadata and block pointers).
11
• Linked allocation: Storing each file’s blocks as a linked list (each block has pointer to next). Simple
but random access is slow. Seldom used except perhaps for file allocation table approach (FAT is like
a table of links).
• FAT (File Allocation Table): Used in DOS/FAT file systems, a central table maintains linked-list chains
of blocks for each file. Simple and widely supported but not as robust for large disks.
• B-trees / B+trees: Many newer file systems (NTFS, APFS, XFS, ReiserFS) use tree data structures to
index file blocks or directories (which allows efficient lookup and sorted order). For example, NTFS
uses a B-tree for its MFT, and APFS uses B-trees for almost everything (allocations, directories, etc.).
• Journaling: Modern file systems often include a journal (intent log) to improve reliability. The
journal records changes about to be made (like “write these blocks to file X”) so that if a crash occurs
mid-operation, the system can replay or rollback partial operations to keep the file system
consistent. Ext4, NTFS, HFS+, etc. are journaling file systems. This protects against corruption due to
power loss at inconvenient times.
• Directories: Usually implemented either as special files containing name-to-inode mappings (as in
Unix) or some database structure (NTFS uses B-tree of filenames). The OS must support operations
to create/delete directories and list directory contents.
Disk and Storage Management: The OS interacts with storage hardware (HDDs, SSDs, etc.) through device
drivers and schedules low-level operations. For hard disks in particular, disk scheduling algorithms were
important to minimize seek time (the time to move the disk’s read/write head). Traditional disk scheduling
methods include:
• FIFO (First-In-First-Out): Serve requests in order of arrival – fair but can be suboptimal for seeks.
• SSTF (Shortest Seek Time First): Pick the request whose target location is closest to the current head
position 34 . This reduces average seek time but risks starvation of requests that are far away if new
nearer requests keep coming.
• SCAN (Elevator algorithm): The disk head moves in one direction (e.g. inward) servicing all requests in
its path, then reverses direction. This ensures a bounded wait (it’s like an elevator going up and
down, servicing all floors in order). Variants like C-SCAN (circular SCAN) only go one way and jump
back to start, treating the cylinder list as circular, which gives more uniform wait times.
• LOOK/C-LOOK: Similar to SCAN but only go as far as the last request in each direction, then reverse
(don’t go to the extreme end if not needed).
These algorithms aimed to optimize mechanical disks, where seek time and rotational latency dominate.
With SSDs (solid state drives), which have near-constant access time (no seek), scheduling is less of an
issue – in fact, parallelism and wear leveling are bigger concerns (often handled by the SSD’s firmware). Still,
OSs may try to optimize I/O by merging adjacent requests or prioritizing reads over writes depending on
scenarios.
12
The OS also manages buffer caches for disk I/O: portions of memory are used to cache recently used disk
blocks to speed up subsequent accesses (since RAM is faster than disk). Reading/writing from cache can
significantly improve performance and also allows the OS to lazy-write to disk (write-back caching), grouping
multiple small writes into one or delaying writes to when the disk is free – though this must be balanced
with reliability (unwritten data can be lost on power failure, which is why journals or flush-to-disk on
important events are used).
Data Access and Device Abstraction: The OS provides device-independent access to storage via
abstractions like files and sometimes block devices. Applications usually don’t care what physical drive their
file is on; they just use file paths. The OS’s device drivers handle the specifics of communicating with
hardware (e.g., sending SCSI or SATA commands). Many OSes also abstract the disk as a series of logical
blocks and have a layer that could allow multiple file systems or multiple volumes. For example, a Linux
system might have /dev/sda1 as a block device for a partition, and the ext4 file system is mounted on it.
The OS handles mapping file operations to specific device operations via the file system driver and device
driver.
In addition, OS storage management includes space allocation on disks (it keeps track of which blocks are
free or used via free space bitmaps or lists), quota management (limiting how much disk space a user or
group can use), and potentially encryption or compression at the file system level (some OSes provide
encrypted file systems or compress files transparently).
Overall, the OS file system module turns the raw storage hardware into the familiar hierarchy of files and
directories, manages the integrity and reliability of data (through consistency checks, journaling, etc.), and
tries to optimize performance (through caching and scheduling) 35 36 . It provides uniform access so that
programs need not worry about disk geometry or device peculiarities – they just see logical files. This
significantly enhances productivity and portability of software.
Device Drivers: A device driver is a special OS component (often a module) that knows how to interact with
a specific hardware device (or class of devices). It serves as the translator between the OS’s abstract I/O
commands and the device’s hardware protocol 37 . For example, the OS might issue a generic “write
block to disk” request; the disk driver translates that into the exact sequence of commands over SATA or
NVMe to perform the action. Drivers typically run in kernel mode (so they can access hardware registers
and memory), and they are often the most hardware-specific part of the OS. The design goal of drivers is to
abstract the device – the OS core issues high-level operations and the driver handles the low-level details
38 . This means higher OS layers (and applications) don’t need to know how a particular network card
sends packets or which SSD model is present; they just call a generic interface. Most OSes have a large
number of drivers to support different hardware. For instance, Windows or Linux includes drivers for
various graphics cards, USB devices, printers, etc. Drivers typically register themselves with the OS (for
example, to bind to a device ID or a port). They handle initialization (setting up the device), I/O requests
(executing read/write operations or control commands), and interrupt servicing for their device. Because
13
drivers run with high privilege and are complex (and often provided by hardware vendors), they are a
common source of OS crashes or vulnerabilities if they misbehave. Efforts like driver sandboxing or moving
drivers to user space (in microkernels or via user-space driver frameworks) aim to mitigate that, but in
monolithic kernels drivers are part of the kernel.
Interrupt Handling: Hardware devices often signal the OS that something has happened (e.g., “data is
ready” or “I/O completed”) via interrupts. An interrupt is a hardware signal that causes the CPU to stop its
current execution and jump to an interrupt service routine (ISR) in the OS 39 . The OS sets up interrupt
handlers (part of drivers or kernel) for various interrupt lines or messages. When a device raises an
interrupt, the CPU switches to kernel mode and executes the corresponding handler. For example, a timer
interrupt occurs at regular intervals to drive the scheduler; a disk controller might interrupt when a disk
read is finished; a network card interrupts when a packet arrives. The interrupt handler will quickly service
the device: often this means reading or writing some data to the device’s registers or memory (to transfer
the next chunk, or to retrieve input) and acknowledging the interrupt to the hardware. Because interrupts
can happen asynchronously and at potentially high frequency, ISRs are typically designed to be short and
efficient – they may defer heavy work to later. A common pattern is a split between a top half (the
immediate ISR) and a bottom half (deferred work). The top half handles the urgent part (stop the interrupt,
grab data into a buffer), then schedules a bottom-half mechanism (like a tasklet, deferred procedure call,
or interrupt thread) to do more processing outside the interrupt context (where it’s safer to block or take
locks). Interrupt-driven I/O allows the OS to avoid busy-waiting – instead of the CPU constantly polling a
device to see if it’s done, the device interrupts when it needs attention 40 . This greatly improves efficiency
for most devices (the exception being some very high-speed or special cases where polling might be used,
but generally interrupts are preferred).
DMA (Direct Memory Access): Many devices use DMA hardware to transfer data directly between device
and memory, with minimal CPU intervention. For example, to read a disk sector, the OS driver programs the
disk controller with the memory address to put data into, then the disk reads from platter and via DMA
writes the data into RAM. The CPU is free during the transfer, and when it’s done the device interrupts to
signal completion. This is crucial for high-speed I/O, as it offloads copy work from the CPU. The OS must set
up DMA and ensure the memory region is locked (not paged out) during transfer. Modern systems also
have I/O Memory Management Units (IOMMUs) to help devices do DMA securely (so a device can’t DMA
into memory it shouldn’t).
Buffering: The OS often uses buffers in memory to smooth out differences in speed or data transfer sizes
between devices and applications 41 . For instance, when you write data to a file using write() , the OS
typically copies your data into a kernel buffer cache and returns from the system call quickly, then later
actually writes to disk from the buffer. This buffering improves performance by allowing I/O to be
aggregated and by not making processes wait for slow devices. Similarly, for input, the OS might buffer
data from a device (like keystrokes from a keyboard, or incoming network packets) so that the data is ready
when the application asks for it, and to handle bursts that come faster than an app can read. Double
buffering is used in some cases (e.g., one buffer is being filled while another is being emptied) to further
improve throughput. Buffering can also adapt between devices that produce data in one size chunk and
consumers that need a different size – for example, a network might receive 1500-byte packets, but an
application might read 1 byte at a time; the OS will buffer the packet and feed the app byte by byte.
Spooling is a related concept: for devices like printers that can only handle one job at a time, the OS uses a
spooling area (usually on disk) to queue print jobs and feeds the printer from this spool, so that apps can
finish “printing” quickly and move on while the OS handles the actual device output in the background.
14
Buffers also help deal with speed mismatches: a fast producer and slow consumer (or vice versa) 41 . For
example, writing to a network might be faster than the network can send – a buffer in the OS holds the data
until the NIC can transmit each packet (this is essentially how TCP send buffers work). Or a slow disk write
might be buffered so the CPU can go back to other tasks. Buffering, however, introduces the need for cache
management (ensuring data consistency between buffers and actual device data) and possibly additional
copies (which OS designers try to minimize by techniques like zero-copy I/O or memory mapping files).
I/O Scheduling and Coordination: The OS may also schedule requests to other devices (not just disks). For
instance, a USB controller might have multiple pending transfers; the OS ensures fairness or priority among
them. For network devices, the OS network stack often has to decide packet scheduling or queueing if
multiple sockets send data – basic OS implementations use FIFO per socket, but advanced QoS (Quality of
Service) mechanisms can prioritize certain traffic. In general, the OS tries to avoid bottlenecks by balancing I/
O across devices, and thundering herd issues by waking up only tasks that have work ready, etc.
Device Independence and Naming: The OS often provides a unified interface to devices. For example, in
Unix-like systems, almost everything is accessible as a file – device files in /dev allow programs to use
regular read/write calls on devices. This device independence is facilitated by drivers behind the scenes.
Another aspect is plug and play: modern OSes can detect devices (via mechanisms like PCI or USB
enumeration) and automatically load appropriate drivers, configuring device settings. This is part of I/O
management – e.g., when you plug in a USB flash drive, the OS detects it, assigns it a driver, possibly
mounts a file system, and it becomes available.
Interrupt Prioritization and Concurrency: Because many devices can interrupt and operate concurrently,
OSs often have an interrupt priority system or disable interrupts when needed to protect critical sections
in handlers. Many kernels treat device interrupts with higher priority than normal code, which is necessary
for responsiveness but complicates concurrency (so OS must guard shared data structures accessed by
interrupts vs threads, usually via spinlocks or similar in the kernel).
In essence, I/O management is the glue between software and hardware devices: the OS encapsulates
device operations in drivers, uses interrupts and DMA to handle asynchronous transfer efficiently, and
uses buffering and caching to optimize throughput and accommodate speed differences 37 42 . It
ensures that reading from a file or sending data over a network is (for the programmer) as simple as calling
a read/write function, even though under the hood it may involve complex device protocols and timing
details.
15
Basic IPC Mechanisms: Common IPC facilities include:
• Pipes: A pipe is a simple byte stream communication channel between processes. In Unix, a pipe
(created by the pipe() system call) produces a read end and write end; one process writes into
the pipe and another reads from it (like a buffered FIFO). Pipes are often used between related
processes (e.g., parent-child or between shell pipelines). They are unidirectional (for full duplex, you
use two pipes). Named pipes (FIFOs) extend this to unrelated processes via a name in the file system.
Pipes provide synchronization implicitly (read will block if pipe empty, write will block if pipe buffer
full).
• Message Queues: A message queue allows processes to send discrete messages (packets of data) to
each other via the OS. The OS maintains the queue, and processes can append a message or retrieve
the next message. This preserves message boundaries (unlike a pipe’s raw byte stream) 44 . Many
OSes have APIs for message queues (e.g., POSIX message queues, System V message queues). They
can support features like message priorities.
• Shared Memory: In shared memory IPC, the OS maps a region of memory into the address spaces
of multiple processes, allowing those processes to read/write the same memory directly 45 . This is
the fastest form of IPC (no data copying by the kernel after setup) because once set up,
communication is just memory accesses. However, it doesn’t provide built-in synchronization –
processes must use synchronization primitives (like semaphores or mutexes) to coordinate access to
the shared data to avoid race conditions. Shared memory is common for high-performance needs;
for example, large image data might be put in a shared memory segment rather than copied
through pipes. The OS ensures the memory is mapped into each process’s page table. Typically, one
process will create a shared segment and others attach to it by an identifier.
• Sockets: Originally developed for network communication, sockets are an IPC mechanism that can
also be used locally (Unix domain sockets). Sockets allow two processes to send streams of data to
each other, identified by addresses (port numbers, etc.). For local IPC, Unix domain sockets appear as
a file path and provide bidirectional byte-stream or datagram communication, similar to network
sockets but without the network overhead 46 47 . They are used, for instance, for client-server
communication on the same machine (e.g., X11 or Wayland GUI clients talk to the display server via a
Unix socket). Network sockets (TCP/UDP) allow IPC between processes on different machines,
making them extremely powerful (though they involve network protocols).
• Signals/Software Interrupts: A signal is a limited IPC mechanism for sending a simple notification
to a process (mainly in Unix-like OSes). One process can send a signal (like SIGTERM or SIGUSR1) to
another (if permitted), which interrupts the target process similar to a software interrupt and causes
a signal handler to run or terminates the process if not handled 40 . Signals carry minimal
information (just the type of signal, except modern POSIX real-time signals can carry a small integer
or pointer). They’re useful for control (telling a process to terminate, or to reload config, etc.) but not
for bulk data transfer.
• Semaphores, Mutexes, and Condition Variables: These are synchronization primitives (more on
them below in the synchronization section) rather than data exchange mechanisms, but they are a
form of IPC in that they coordinate execution between processes/threads. For example, a semaphore
in OS (using System V semaphores or POSIX named semaphores) can be used by two processes to
16
signal events (one does a semaphore post, the other does a wait to block until the event). These don’t
transfer data per se, but they synchronize access to shared resources or timing of operations.
• Remote Procedure Calls (RPC) and Higher-level IPC: In distributed systems or even local ones, an
abstraction called RPC allows a program in one process to invoke a procedure in another process
(possibly on another machine) as if it were a local call. The OS (or middleware) handles packaging up
the call arguments, sending the request, getting the result, and returning it to the caller.
Technologies like DCOM, CORBA, SunRPC, ONC RPC, gRPC, or microkernel message-passing can be
seen as implementations of RPC or similar message exchange patterns. Locally, some OSes have
optimizations for RPC (e.g., Windows has LPC – Local Procedure Call – used for client-server in OS
components). RPC typically uses one of the basic IPC mechanisms under the hood (sockets, message
queues, etc.) but adds structure (like specifying which function to call, marshalling data).
The OS typically provides these IPC mechanisms via system calls or APIs. For instance, POSIX provides
pipe() , shm_open() for shared memory, mq_open() for message queues, socket() for sockets,
etc., and SysV IPC has its own set of calls ( shmget , semget , msgget , etc.).
Synchronization Primitives: When multiple processes or threads share data or coordinate, they need
synchronization to avoid conflicts (race conditions) and to implement correct sequencing (e.g., one process
must wait for a resource to be available). The OS (or sometimes user-level libraries, but often relying on OS
support) provides synchronization primitives such as:
• Mutex (Mutual Exclusion) Locks: A mutex is a binary lock that ensures only one thread/process can
own it at a time, thus serializing access to a resource. A thread locks (acquires) the mutex before
entering a critical section and unlocks when done; any other thread trying to lock while it’s held will
block until it’s released. Mutexes prevent data races by allowing only one thread in a code section at
once. Many OSes implement mutexes that work across processes as well (POSIX named mutexes, or
using futex under the hood on Linux, etc.).
• Condition Variables: Often used with mutexes, a condition variable allows threads to wait for a
condition to be true (releasing a mutex while waiting, and reacquiring when awakened). One thread
waits on the condition (goes to sleep until signaled), another thread signals the condition (wake one
or all waiting threads) once the desired condition (state change) occurs. This is useful for complex
sync patterns (like producer/consumer: consumer waits for “buffer not empty”, producer signals
when it puts something in buffer).
• Readers-Writers Locks: A variation of locks that allows multiple readers to access a resource
concurrently (shared lock) but an exclusive lock for writers (only one writer, and no readers
concurrently). The OS may provide this to optimize for scenarios with many reads and few writes.
17
• Spinlocks: A lock where a thread waits in a loop (“spins”) checking until it becomes free. Spinlocks
avoid context switches and are used when wait times are expected to be very short or in interrupt
contexts where you cannot sleep. They are primarily an internal kernel synchronization tool in
many OSes (for instance, inside the OS to protect data structures at high IRQL in Windows or in Linux
interrupts). User code typically uses blocking locks instead (since spinning wastes CPU time if the
wait is long).
These primitives can be used between threads of one process or across processes if the OS supports
placing them in shared memory. For example, POSIX shared memory can be combined with POSIX mutex
(process-shared attribute) so that two processes can lock a shared mutex.
The OS scheduler and IPC facilities must work together with synchronization primitives to avoid problems
like priority inversion (when a high-priority task waits on a lock held by a low-priority task — some OSes
implement priority inheritance for mutexes to mitigate this).
IPC and Microkernels: It’s worth noting that in microkernel OSes, IPC becomes even more central – since
drivers and services run in user space, almost every OS service request is done via IPC message passing.
Thus microkernels require very efficient IPC mechanisms (e.g., L4 microkernel was famed for optimizing IPC
to only a few instructions). In these systems, synchronous IPC (rendezvous messaging) might be used – a
send can optionally block until a reply is received (making it like a function call). In general OS design, IPC
can be synchronous (blocking send/receive) or asynchronous (non-blocking send, maybe with callbacks or
polling for reply) 49 . Synchronization primitives themselves can be built on IPC or vice versa (for example, a
semaphore can be seen as a kind of message queue of “tokens”).
In summary, IPC mechanisms allow processes to cooperate by sharing data or sending messages, while
synchronization primitives coordinate their execution order and access to shared resources 43 50 .
The OS ensures that these communications respect process boundaries and security (e.g., usually only
related processes or those with permission can use certain IPC channels, and all IPC calls go through the
kernel which can validate access). Effective IPC design in the OS leads to powerful modular software:
consider how a web browser spawns multiple processes and uses IPC to coordinate them (rendering
engines, GPU process, network process, etc.) or how system services (like print spooler, window server)
communicate with client apps – all that is enabled by the IPC provided by the operating system.
Dual Mode Operation (User Mode vs Kernel Mode): Modern CPUs provide at least two execution modes:
a privileged mode (kernel/supervisor) and a restricted mode (user). The OS uses this hardware feature
to protect itself and critical operations. The kernel runs in privileged mode where it can execute any
instruction and access any memory or device, whereas user programs run in user mode where certain
instructions (like those that interact with hardware or manage memory) are forbidden and memory
access is limited to that process’s space. If a user-mode program tries to perform a privileged operation
18
(e.g., directly access disk controller or modify interrupt settings), the hardware traps to the OS – essentially
the CPU prevents it and hands control to the OS. This mechanism is crucial: it means user programs cannot
corrupt the system or other programs directly; they must request service via safe interfaces (system
calls) which switch the CPU to kernel mode temporarily to perform the operation on the program’s behalf
51 . When in kernel mode, the OS has full access to hardware, but it (ideally) only stays in kernel mode for
short durations to do necessary work, then returns to user mode. This separation is the foundation of OS
security: for example, memory management leverages it by not mapping kernel memory in user mode or
marking those pages as supervisor-only, so a user-mode program can’t read or write kernel data.
Additionally, many CPUs have multiple rings or privilege levels (x86 has 4 rings, though most OSes use only
ring 0 for kernel and ring 3 for user). Some systems run drivers or less-trusted OS components in an
intermediate ring for more isolation.
System Calls and Privilege Transfer: The controlled way to go from user mode to kernel mode is via
system calls (or exceptions/interrupts). A system call is typically implemented by a special CPU instruction
(like syscall on x86_64 or a software interrupt like INT 0x80 on older x86) that causes a trap into kernel
mode at a predefined entry point. The OS verifies the parameters, does the requested operation (if
permitted), then returns to user mode. This design ensures that user code cannot just jump into arbitrary
kernel code – it can only enter through defined gateways (the system call table) and the CPU switch
enforces the mode change properly 52 53 . Thus, the kernel maintains control over sensitive operations.
Memory Protection and Privilege Levels: As mentioned under memory management, the OS uses the
hardware MMU to enforce memory protection. Each memory page can be marked as user-accessible or
kernel-only. When in user mode, any access to a kernel-only page causes a fault. This keeps the kernel’s
code and data (and other processes’ memory) safe from user program mistakes or malice. Similarly, CPUs
enforce that certain registers or instructions (like those controlling devices, or halting the CPU, etc.) can only
be executed in kernel mode. This layered defense means a user program can’t, for example, program the
DMA controller to overwrite another program’s memory – because that programming requires privileged
instructions or memory-mapped registers that are not accessible in user mode.
User Accounts and Authentication: Beyond the kernel/user separation, OSes implement user identities
and permissions. In multi-user systems, each process runs under a user account with an associated
identity (UID in Unix, security token in Windows that includes user SID and group SIDs, etc.). The OS
authenticates users typically at login (using passwords, keys, etc.) and then associates credentials with their
sessions/processes. All resource accesses can then be checked against these credentials. For instance, when
you try to open a file, the OS checks “does this user have read permission on that file?” based on the file’s
access control list or mode bits.
Access Control: Operating systems use mechanisms like Access Control Lists (ACLs) or capability-based
security to decide whether a user (or process) can perform an action on an object (file, device, IPC
endpoint, etc.). An ACL is essentially a list of permissions attached to an object, listing which users or groups
can do what (read, write, execute, etc.) 54 . A simpler form (in Unix) are permission bits (rwx for owner,
group, others). Windows uses ACLs extensively (every file, registry key, etc. has an ACL specifying allowed
and denied actions for various principals) 54 . The OS is responsible for checking these on each access and
enforcing them. Capabilities are an alternative approach: rather than checking global ACLs, a process
might hold a token or reference that encodes its permission to an object (like a file handle is a kind of
capability). Some research and security-oriented OSes (and languages like in distributed systems) use
capabilities to ensure that possessing a reference is proof of access rights, and you can only operate on
19
objects you have a reference for. In practice, mainstream OSes implement most access control via ACLs and
user/group identities.
Security Domains and Sandbox: The OS can create restricted environments or sandboxes for untrusted
programs. A sandbox is a mechanism to run code with limited rights and prevent it from affecting the rest
of the system 55 56 . For example, mobile app sandboxes: each app on iOS or Android runs as a separate
user or in a cgroup/namespace that isolates its files and limits what system calls it can make. Techniques
include namespace isolation (as in Linux containers, where a process might have an isolated view of the
filesystem or process list), seccomp filters (Linux’s Secure Computing mode can restrict the set of system
calls a process is allowed to use), or virtualization-based sandboxes (like running code in a lightweight VM).
The idea is to prevent a malicious or compromised program from doing harm – it shouldn’t be able to read
random files, open network connections, or execute arbitrary system calls to escalate privileges. Browsers,
for instance, sandbox web content rendering processes so that even if compromised by malicious
JavaScript, the process cannot directly access the user’s files or devices. The OS provides the features to
implement this: separate user IDs, chroot/jail mechanisms, namespace isolation, seccomp, etc. Sandboxing
can be seen as an extension of the OS’s normal isolation – usually taken further by removing or filtering
available system APIs to the sandboxed code 56 .
Principle of Least Privilege: Well-designed OS security follows this principle – components (and users/
programs) should have only the privileges necessary to perform their function, and no more 57 . That’s
why we have unprivileged user accounts for daily use, and separate admin or root privileges for when
needed. The OS typically has a way to escalate privilege in controlled manner (e.g., the sudo command or
UAC prompts in Windows allow a user to run something with admin rights after confirmation). Internally,
OS kernel code might also isolate parts – e.g., device drivers might be restricted to only their device’s
registers, not everything (some microkernels do this by running drivers in user mode). Newer CPU features
like virtualization and enclaves (Intel SGX, ARM TrustZone) also aim to create secure domains inside even
the OS, but that’s more advanced.
Protections Against Exploits: OSes implement various protections to make it harder for malicious code to
exploit vulnerabilities. For example, ASLR (Address Space Layout Randomization) randomizes memory
addresses of key areas (stack, libraries, heap) to make buffer overflow exploits harder to predict 58 . DEP/
NX (No-eXecute) bit marks memory regions as non-executable to prevent code injection on the stack or
heap. Stack canaries and control-flow integrity checks help detect corruption of return addresses or
function pointers 59 . While these are low-level mechanisms, they are part of the OS or compiler’s
contribution to overall security. The OS also regularly needs to be updated for security patches, and open-
source OS communities review code to catch vulnerabilities (though vulnerabilities still occur).
Security Model – Authentication and Auditing: OS security also involves authenticating users (passwords,
keys, biometric login) and maintaining audit logs of security-related events (login attempts, file access by
whom, etc.) for accountability 60 . The OS usually has a trusted computing base (TCB) which includes the
kernel and core security services. It’s important that the TCB be as small and bug-free as possible, since
compromise there undermines everything. That’s partly why microkernels were argued to be more secure
(smaller kernel). On the other hand, more code in user space can mean more attack surface in other ways.
In summary, the OS implements a protection framework that defends the integrity and confidentiality of
resources. By running code in user mode with limited privileges, using hardware enforcement for memory
and instruction control, and layering checks like ACLs for resource access, the OS creates a safe
20
environment where multiple users and programs can operate without interfering improperly 61 62 . When
configured correctly, the OS ensures that you can’t just peek or poke into another process’s memory, or read a
file you don’t have access to, or install a driver without permission. Security and protection are ongoing
challenges as new attack methods emerge, but these foundational principles (isolation, least privilege,
controlled sharing) remain at the heart of OS design.
Firmware and Bootloader: On power-up, the machine’s firmware (BIOS on older PCs, or UEFI on modern
systems, or boot ROM on embedded devices) runs first. The firmware performs a Power-On Self-Test
(POST) to check basic hardware (CPU, memory, simple devices) 63 . Then it looks for a bootloader on some
boot device (e.g., the first sector of a disk, or an EFI partition file) 64 . A bootloader is a small program
whose job is to find the OS kernel, load it into memory, and transfer execution to it 65 66 . Examples:
GRUB is a common bootloader for Linux/Unix, LILO was an older Linux loader, Windows Boot Manager for
Windows, and U-Boot or others for embedded. The bootloader may provide a menu to choose OS or kernel
configurations. It typically operates in a very constrained environment (some may have drivers for simple
filesystem access to fetch the kernel from disk). The bootloader often also initializes some hardware to a
usable state (especially in BIOS times where it had to switch to protected mode on x86, etc.). In UEFI, the
boot manager is more advanced and can directly load OS images as well.
The end result is the bootloader loads the kernel image (and possibly an initial RAM disk) into memory
and then jumps into the kernel’s entry point. At that moment, the CPU is executing the OS kernel code.
Kernel Initialization: Once the kernel starts (now running in kernel mode), it needs to set up the system to
the point it can run processes. Key steps in kernel init typically include:
• Hardware Initialization: The kernel probes or initializes hardware devices. This may involve setting
up the memory management (creating page tables for virtual memory), initializing interrupt vectors
(so that interrupts will be handled by the OS handlers), configuring timers, and discovering devices
(PCI bus enumeration, etc.). Modern kernels often have a list of drivers and will initialize those
needed for devices present. For example, enabling the disk controller, initializing the network
interface (but usually networking comes up a bit later in the boot sequence when services start), etc.
On some systems, firmware already enumerated devices (especially with ACPI/UEFI), so the OS just
reads tables of device info.
• Memory and Process Structures: The kernel sets up its internal data structures – process table,
memory manager structures (free frame list, etc.), I/O subsystem structures (buffer caches, file
tables). It also typically reserves some memory for the kernel (non-pageable memory for core
structures, etc.) and establishes the kernel heap for dynamic allocations inside the kernel.
21
• Start Core Services/Threads: Many OS kernels start some kernel threads or routines to handle
background work. For instance, the OS might start an “idle” process (the one that runs when nothing
else is runnable), disk I/O daemons (e.g., flush dirty buffers to disk periodically), or the swapper. In
Linux, after initialization, the kernel starts the init process (in user mode) as PID 1. In Windows,
the kernel (NTOS) will start the System process (which hosts kernel threads), and then launch the
Session Manager ([Link]) in user mode, which in turn starts [Link] (Windows subsystem
process) and [Link] , etc. Different OSes have different specifics, but generally the kernel
will spawn the first user-mode process once it’s ready 67 .
• Enabling Interrupts and Scheduling: Initially, the kernel may boot with interrupts disabled (to
avoid being interrupted during sensitive setup). Eventually, it enables hardware interrupts so devices
can start signalling (and thus maybe some drivers will start asynchronous operations). It also
initializes the scheduler and might start scheduling timer interrupts. At some point, the kernel will
make a call to start the scheduler which allows processes (or threads) to be scheduled.
• Mounting the File System: The OS needs to mount the root filesystem so that it can access files (like
system programs and configuration). Often the kernel is passed the location of the root filesystem
(or uses a compiled default). It will mount that (which may involve calling the appropriate file system
driver to read disk structures). Prior to that, some systems use an initial RAM disk (initrd or
initramfs) – a temporary minimal filesystem loaded by the bootloader into memory – which the
kernel mounts and uses to load essential drivers (especially for disks) that are needed to mount the
real root filesystem. After those drivers are loaded, the OS switches to the real root filesystem.
The Initial Process (Init): In Unix-like OSes, a special process with PID 1, historically called init, is started by
the kernel. This is the first user-space process. Init is responsible for spawning all other user processes and
services. It reads configuration (like /etc/inittab or systemd units) and starts background daemons,
sets up terminals, etc. 68 69 . In modern Linux, the traditional sysvinit has largely been replaced by
systemd (which is still essentially the init process, PID 1, but it handles service management in a more
complex way). In any case, once init is running, the kernel becomes largely an event handler and resource
manager, and init (and the processes it starts) perform the higher-level initialization like starting
networking, launching a login prompt or GUI, etc.
In Windows, after kernel initialization, there is a chain: the kernel starts [Link] (Session Manager)
which sets up the session and virtual memory, then that starts [Link] (Client/Server Runtime, which
handles console and GUI windows for that session) and [Link] (which presents the login screen).
After login, the system starts the [Link] (Service Control Manager to start background services)
and the user’s shell (like [Link]). The concepts are analogous: there is a sequence of user-mode
processes launched to initialize the user environment and system services.
1. Firmware stage: BIOS/UEFI runs, tests hardware, and finds a bootable device.
2. Bootloader stage: The bootloader (like GRUB) is loaded from disk. It may present a menu, then
loads the OS kernel (and maybe an initrd image) into memory, sets up kernel parameters, and jumps
to kernel entry.
22
3. Kernel startup: The kernel runs its initialization: sets up CPU (protected mode, paging enabled),
initializes devices and drivers, mounts root filesystem, etc. It then creates the first user-mode
process.
4. System initialization in user space: The init process (or equivalent) runs startup scripts or
service managers to launch system daemons (like network service, printing service, etc.), set the
hostname, and get the system into a running state. It then typically waits to reap orphaned zombies
(child processes) and keep the system running. Eventually, user login is enabled (getty or graphical
login).
5. User session startup: When a user logs in, another set of processes starts (like the user’s shell or
desktop).
After all this, the system is fully operational and the OS runs normally, managing processes, memory, I/O,
etc., until shutdown. On shutdown, a reverse process happens: the OS (often via the init system) will stop
services, unmount filesystems, and finally either halt the CPU or reboot.
Bootstrapping is a critical process because any failure means the OS never comes up. That’s why many OS
have fallback mechanisms (like recovery modes, safe mode, etc.). The term “bootstrap” literally comes from
“pulling oneself up by the bootstraps” – the system starts from almost nothing (just firmware) and loads
increasingly complex code until the full OS is running.
System Call Interface: A system call is similar to a function call, but instead of staying in user space, it traps
into the kernel. Each OS defines a set of system calls (e.g., POSIX defines calls like open , read , write ,
fork , exec , etc., and Windows has its own (mostly undocumented) native calls, usually accessed via
Win32 API). When a process executes a system call instruction, the CPU switches to kernel mode and jumps
to a predefined handler for that specific call number 52 . The OS then performs validation (e.g., are the
arguments pointers to valid memory in the calling process?), performs the requested operation in kernel
mode, and then returns a result (and perhaps data) to the user process, switching the CPU back to user
mode. Throughout this, the OS ensures isolation and security – the process cannot access data structures
not meant for it, as the only way to get results is through the return values or output parameters that the
OS explicitly provides.
23
• File Management: open or close file descriptors, read/write files, seek to a position, delete or
rename files, get file info (stat), etc. Also directory-related calls (create or remove directory, list
directory entries via readdir ) fall here.
• Device and I/O: though files and devices are often treated uniformly (Unix mantra “everything is a
file”), there are system calls for device-specific I/O or control operations (e.g., ioctl in Unix which
sends arbitrary control commands to devices, or WriteFile on Windows which can also handle
devices). There are also calls to allocate or free I/O buffers, manage device drivers, etc., usually
restricted to the OS or drivers themselves.
• Memory Management: system calls to request more memory (e.g., brk/sbrk which expands the
data segment, or mmap to map files or allocate anonymous memory) and to free or advise about
memory ( munmap , madvise ). Typically user-level memory allocation (malloc/free) uses these
under the hood. There are also calls to change memory protection on pages ( mprotect ).
• Information and Miscellaneous: Calls to get system information (number of CPUs, OS version),
calls to get time ( gettimeofday or clock_gettime ), set user or group IDs ( setuid ), etc. Also
calls to manipulate OS constructs like mount file systems, chmod to change file permissions,
kill to send signals, etc.
All these are entry points into the kernel. From a program’s perspective, using a system call is often as simple
as calling a function provided by the standard library. For instance, when you call C library function
open("[Link]", O_RDONLY) , that function will internally prepare registers and invoke the
appropriate system call (e.g., on Linux, the open system call number and arguments via syscall CPU
instruction). The OS then executes and returns the file descriptor number. The reason system calls are
needed is because those operations require kernel privileges (opening a file means manipulating kernel file
tables and disk access, which is protected; creating a process means telling the kernel to schedule a new
program, etc.).
Abstractions Provided: System calls define the core API of the operating system – they are the boundary
between user space and kernel. The abstractions the OS provides through them include things like:
• Files and File Descriptors: Instead of dealing with disk sectors, programs use file descriptors (or
handles) as an abstract representation of an open file or device stream. They read/write bytes, and
the OS figures out which disk blocks or device operations that corresponds to. This is a powerful
abstraction that makes I/O device-independent.
• Processes and Threads: Rather than the program having to know how to load code into memory
and start it on a CPU (which would be impossible from userland anyway), the OS abstracts that as a
simple operation (fork/exec or CreateProcess) that, from the perspective of the caller, returns an ID
24
of the new process. The OS handles all the low-level work and then schedules the new process to
run.
• Sockets and Networking: The OS abstracts network communication (with all its complexity of
protocols) into socket calls that let programs send/receive data over network connections without
handling the network card hardware or implementing TCP/IP – the OS does that. This greatly
simplifies application development.
• Synchronization Objects: As described earlier, OS provides mutexes, etc., often integrated with
system calls (like futex in Linux provides fast locking or WaitForSingleObject on Windows to
wait on a mutex or other object). These allow threads to coordinate in a straightforward manner
rather than manually disabling interrupts or something (which user programs cannot do in any
case).
• High-level Resource Management: The OS often offers abstractions like “a thread pool” (in some
managed environments) or job objects in Windows, timer facilities (sleep for X seconds is essentially
an abstraction over hardware timers and scheduling), etc.
Crucially, the design of the system call interface and abstractions determines how portable it is for
applications to run on different OSes. POSIX is a standard that defines a common set of system calls/APIs
for Unix-like OSes; programs written to POSIX can often compile on any Unix (Linux, macOS, *BSD) with
minimal changes. Windows has its own API (Win32) which is different. Some OSes strive to be compatible
(e.g., Linux can provide a compatibility layer for some Unix variants, Windows can emulate POSIX via SUA or
WSL).
Only Entry to Kernel: It bears repeating that system calls (and interrupts/exceptions) are the only
sanctioned way to transition from user mode to kernel mode. This is by design for security. A user program
cannot just jump to an arbitrary kernel address to execute a routine – the CPU won’t allow that without a
mode switch. Even with system calls, the OS often validates which call is being made and the parameters
(the call number provided by user has to be one the OS supports; if you provide an invalid one, OS returns
error). Thus, system calls are guarded gates – they ensure the request is safe and legitimate before the
kernel carries it out 51 71 .
Libraries and APIs: Most programs don’t invoke system calls directly with low-level instructions; instead,
they use language libraries or frameworks. For example, the C standard library ( libc ) provides functions
like fopen, printf, malloc, etc., which internally use system calls (open, write, brk, etc.) but present a nicer
interface. Similarly, high-level languages (Java, Python, etc.) have runtime libraries or interpreters that
ultimately call down to OS functions when doing I/O or thread management. The separation is: API refers to
the interface the programmer sees (which could be at a higher level than system calls), while ABI
(Application Binary Interface) includes the actual low-level system call interface (calling conventions,
numbers, etc.). The OS defines an ABI for system calls so that user programs compiled for that OS can issue
the correct traps. For instance, Linux on x86_64 has specific registers for passing arguments to a system call
and uses the syscall instruction, whereas on ARM it’s different.
Examples in practice: Opening a file might involve multiple layers: fopen() (C library) -> open()
(system call) -> kernel code for open: check permissions, find file inode, allocate a file descriptor, etc. Writing
to a file: fwrite() -> maybe buffered in C library -> eventually write() system call -> kernel code
25
copies data from user buffer to kernel buffer and schedules disk I/O. Creating a new process in Unix:
fork() system call -> kernel duplicates the process’s memory and resources (using copy-on-write
optimizations) and creates a new process entry -> returns in both processes (0 in child, child PID in parent).
Then the child often calls exec() to load a new program. This replaces its memory with a new program
image (kernel does this: frees old memory, sets up new memory from the executable, etc.). Finally, both
parent and child (or new program) continue. All these steps rely on the kernel’s behind-the-scenes work
orchestrated through system call requests.
In summary, the OS exposes clean, simplified abstractions of messy hardware details and complex
tasks through its system call interface 72 36 . This makes application programming much easier and safer,
and it centralizes control (the OS remains in charge of actual hardware and resource management). The
design of these abstractions – files, processes, sockets, etc. – is one of the great accomplishments of OS
development, making it possible to write software without needing to reinvent low-level wheels for each
program.
Loadable Kernel Modules: A primary mechanism for OS extensibility is the use of loadable kernel
modules (LKM). These are pieces of code that can be dynamically loaded into (or unloaded from) the
running kernel on demand 73 . Common examples of modules are device drivers, filesystems, or network
protocol implementations. For instance, when you plug in a new USB device, the OS might load the
appropriate driver module for that device. This avoids having every possible driver permanently compiled
into the kernel (which would make it bloated and require reboot to add support). In Linux, modules are
commonly used; one can run lsmod to see loaded modules and modprobe to insert or remove them
74 . Windows has a similar concept with device driver binaries that are loaded (with .sys files), though they
are often loaded at boot or on device arrival. The key is that the kernel defines interfaces for certain
subsystems (e.g., an interface that a network driver must implement, or a filesystem interface) and the
module adheres to that. When loaded, the module’s code becomes part of the kernel and can interact with
kernel functions and structures. The OS typically provides a module loader service that handles relocating
the module’s object code, resolving symbols (so modules can call core kernel functions), and initializing the
module. Modules often have an init function and an exit function (cleanup) that the loader will call on load/
unload.
Benefits of Modules: Modules provide extensibility (one can add new functionality, like a driver for a new
hardware device, without rebuilding the whole OS) and upgradeability (you can patch or replace a part of
the kernel by unloading an old module and loading a new one, in some cases). They also help with
maintainability – the code for a module can be developed somewhat independently, as long as it conforms
to the kernel’s module interface. In monolithic kernels, modular design compensates for some of the all-in-
one nature by segmenting pieces and allowing optional loading. As an example, Linux is monolithic but
modular: most distributions ship a single big kernel that can dynamically load drivers as needed, so you get
flexibility similar to microkernel where drivers are not all in core by default 18 . Modules also allow for policy
26
to be separated from mechanism in some cases – e.g., a kernel might have a generic framework for packet
filtering, and loadable modules implement specific firewall policies or protocols.
Hot-Plugging and Drivers: Extensibility is crucial for hardware support. Modern OS kernels support hot-
plugging of devices (USB, PCI Express hotplug, etc.). When a new device is detected at runtime, the OS can
dynamically load the corresponding driver module and bind it to the device. Without loadable modules,
either the drivers would have to all be pre-loaded (wasting memory for unused ones) or you couldn’t use
new devices without reboot. Similarly, if a device is removed, its driver can potentially be unloaded to free
memory. Some kernel subsystems even allow loading new schedulers or new security modules at runtime
(Linux’s Pluggable Scheduler modules or Linux Security Modules framework where e.g., SELinux or
AppArmor can be loaded as modules).
Kernel Module Safety: One downside of modules is that, since they run in kernel space, a buggy module
can crash the system or introduce vulnerabilities. It’s effectively like plugging in code into the core OS live.
Some research OSes or microkernels run drivers in user space to mitigate that, but mainstream ones rely
on careful testing of modules. Some OSes mark the kernel as “tainted” if a proprietary or unverified module
is loaded (Linux does this) 75 , indicating that if you report a bug, developers know an out-of-tree module
was present possibly causing issues.
Other Forms of Extensibility: Beyond loadable modules, OS design can allow extensibility via:
• Plug-in Interfaces: Even in user space, OS services might have plug-in mechanisms. For example,
the OS may allow filesystem plugins (for new filesystem types) or authentication providers that
can be added without kernel changes (some OSes keep certain things in user space – e.g., on Linux,
filesystems can even be implemented in userland via FUSE – Filesystem in Userspace, which is an
extensibility approach).
• Microkernel Servers: In microkernels, adding a new service (like a new device support or OS
feature) often means running a new server process. That’s a form of extensibility: you don’t change
the kernel, you add a server. For example, to support a new networking protocol, you could start a
new user-space service in a microkernel system.
• Scripting and Policies: Some OS components allow dynamic policy changes via scripts or
configuration. For instance, the scheduling policy might be tunable, or one could load a new
scheduling class module (Solaris had scheduler modules for real-time vs time-sharing scheduling).
Linux’s completely fair scheduler has pluggable modules for different scheduling classes. Windows
allows filter drivers to be attached to device stacks at runtime, which extend or modify behavior of
device I/O (commonly used for things like antivirus file system filters or encryption layers).
• Runtime Patch and Update Systems: Although not common, there are systems for applying live
patches to kernels (Linux has LivePatch, Windows has hotpatch mechanisms) where one can replace
or patch a function in a running kernel without a reboot. This is more about reliability/extensibility
for fixes rather than adding new features, but it’s related.
Monolithic vs. Modular vs. Microkernel Recap: As mentioned earlier, monolithic kernels with modules and
microkernels with servers both aim to make the OS flexible. A monolithic-but-modular kernel can incorporate
changes quickly via modules 73 , whereas a microkernel can start/stop user-space OS servers. The trade-off
27
is performance vs isolation. But both agree that a structured, modular approach is preferable to a tangled
codebase.
Examples: In Linux, almost all drivers (except core ones for boot like maybe disk controllers needed early)
are modules. Even some core features like the firewall (netfilter) or certain scheduler features, or
filesystems (ext4, XFS, etc.) are modules that can be loaded as needed. In Windows, drivers are typically
modular (you add a device driver by installing its .sys file, which the system will load into the kernel at
runtime when needed). Windows also has a concept of “subsystems” for its environment emulations (POSIX
subsystem, etc.) which are more user-space subsystems, but the kernel’s design is somewhat modular
internally (with executive, HAL, etc., though not loadable modules in the same way for those core parts, but
drivers yes).
Extensible OS Research: There have been research OSes aiming at even more extensibility, like allowing
user programs to inject code into the kernel safely (e.g., SPIN OS allowed safe extensions via Modula-3
language safety, or VINO OS allowed kernel plugins with transaction rollback in case of failure). The idea
was to let applications optimize the OS for their needs (for instance, a database might want a custom
paging strategy). In practice, mainstream OSes don’t allow arbitrary application-supplied kernel extensions
for security reasons, but they might provide hooks or high-level extension interfaces (like eBPF in Linux,
which allows loading safe bytecode into the kernel for things like custom packet filtering, tracing, etc.,
without risking stability – it’s an interesting modern extensibility mechanism).
User-Level Extensibility: The OS also supports extensibility at the user level: for example, you can install
new programs, libraries, etc., without altering the kernel. The system call interface remains stable, and as
long as new programs use that, they run on the OS. This kind of forward/backward compatibility is an OS
design consideration too (stable ABIs, etc., to allow an ecosystem of software to extend the capabilities of
the system).
In conclusion, modularization and extensibility ensure that an OS can adapt to new hardware and
requirements over time without a complete redesign. By dividing the OS into components and allowing
some of them to be added or updated dynamically, the OS achieves flexibility. This is crucial given the vast
array of devices and continual development in computing – no OS can be “one static blob” that never
changes or grows. The modular approach, whether via loadable kernel modules or clearly defined internal
interfaces, is what allows operating systems like Linux, Windows, and others to support thousands of
different devices and features in a maintainable way 73 . It also allows administrators and developers to
customize their systems by adding or removing functionality as needed, which is a powerful feature in both
server and embedded contexts.
Sources: 1 2 3 13 5 7 19 20 21 22 37 42 43 48 55 67 51 70 73
28
1 2 3 4 6 7 8 9 12 13 16 39 40 54 57 58 59 61 62 Operating system - Wikipedia
[Link]
29
74 75 Loadable kernel module - Wikipedia
[Link]
30