English
An operating system is the layer of software that manages a computer’s hardware on behalf of every program running on it. That description is accurate but does not convey why operating systems is one of the foundational subjects in computer science. The more telling description is what the operating system actually does: it constructs, for each running program, the convincing illusion that the program has the machine to itself — its own CPU, its own memory, its own disk — when the reality is that dozens or hundreds of programs share these resources simultaneously, none of them trusting each other, all of them capable of failing at any moment. Creating and maintaining this illusion, while keeping the programs isolated from each other and the system as a whole from catastrophic failure, is the operating system’s work.
The concepts invented to solve this problem — virtualization, isolation, scheduling, synchronization, crash recovery — have turned out to be the foundation of every subsequent layer of systems software. Databases solve the persistence and concurrency problems that file systems solved before them, using the same conceptual moves. Distributed systems solve the failure and coordination problems that operating systems solved within a single machine, now extended across a network. Cloud computing virtualizes operating systems the same way operating systems virtualize hardware. The OS concepts are not one curriculum topic among many; they are the intellectual substrate of the entire systems axis.
The three organizing problems of the subject are virtualization — constructing the illusion of dedicated resources from shared ones; concurrency — maintaining correctness when multiple processes interact with shared state without trusting each other; and persistence — managing the boundary between volatile memory and durable storage across crashes and failures. These are not merely chapter titles. They are the structure that organizes every mechanism an operating system implements, and understanding them as problems before studying the mechanisms as solutions is what distinguishes conceptual mastery from memorized vocabulary.
Prerequisites: Computer organization and architecture (§4.1) — page tables, interrupts, privileged instructions, and memory hierarchy are used throughout. Programming (§2.1) — you need to be able to read and write C. Algorithms and data structures (§2.6) — scheduling, memory allocation, and file system structures draw on this material.
How the Operating System Was Invented
The problem that operating systems solve is not ancient. It emerged when computers became capable of running multiple programs and when the cost of a computer became high enough that leaving it idle during a program’s I/O wait was economically unacceptable.
In the 1950s, computers ran one program at a time, loaded from punched cards, with the entire machine dedicated to that program for its duration. When the program waited for input or output — which was slow, relative to the CPU — the CPU sat idle. The earliest operating-systems functions were batch monitors: programs that automatically loaded the next job from a stack when the previous one finished, eliminating the manual handoff. The resident monitor concept — leaving a small program resident in memory to coordinate job sequencing — was operational by the late 1950s. IBM’s OS/360, announced in 1964, was the first attempt to write a single operating system for an entire family of machines and a vast range of applications. It was the project documented by Frederick Brooks in The Mythical Man-Month: late, expensive, and shipped with bugs that required years to correct. The scale of OS/360 revealed that software management was itself a problem that required systematic treatment.
Multiprogramming arrived when it became clear that I/O waits were fundamentally the bottleneck. If multiple programs are loaded into memory simultaneously, the CPU can switch to another program while the current one waits for I/O, keeping the CPU busy. This required the operating system to manage the CPU — scheduling which program runs when — and memory — ensuring that programs do not overwrite each other. Memory protection hardware was added to architectures to enforce isolation between programs; privileged instruction modes were added to prevent programs from directly accessing hardware that might compromise the isolation. The concept of the process — a program in execution, with its own address space, register state, and execution context — emerged from multiprogramming’s requirements.
The operating system that proved most influential was Unix, written by Ken Thompson and Dennis Ritchie at Bell Labs between 1969 and 1973. Unix was not the first general-purpose operating system — Multics, which Bell Labs had left, was more ambitious. But Unix embodied a design philosophy that Multics did not: everything is a file. Network connections, process metadata, device interfaces, inter-process communication channels — Unix presented all of these through the same read/write file interface, creating a unified abstraction that made programs composable in ways that previous systems had not achieved. The pipe, invented by Douglas McIlroy and added to Unix in 1973, connected the output of one program to the input of another through a file-like abstraction, enabling the construction of complex tools from simple ones. The Unix philosophy — small programs that do one thing well, connected through text streams — became a design principle whose influence outlasted Unix itself.
Unix was also written in C, a decision that proved transformative. Previous operating systems had been written in assembly language, tying them to specific hardware. An operating system in C could, in principle, be ported to any hardware that had a C compiler. The combination of a clean design philosophy and a portable implementation language made Unix the dominant operating system for workstations and servers through the 1980s and early 1990s, and its descendants — Linux, macOS, iOS, Android, the BSDs — run the overwhelming majority of computing infrastructure today.
The architecture debate of the 1980s and 1990s was between monolithic kernels and microkernels. A monolithic kernel — the structure of Linux and traditional Unix — runs all kernel services in a single privileged address space: the scheduler, memory manager, file systems, device drivers, and networking stack all run together in kernel mode, where a bug in any component can corrupt any other. A microkernel — the structure of Mach, L4, and ultimately seL4 — runs only the most privileged services (address space management, inter-process communication, basic scheduling) in kernel mode, with device drivers, file systems, and other services running as user-space processes. A bug in a microkernel user-space driver cannot corrupt the kernel; it can only crash the driver process, which can be restarted. In January 1992, Andrew Tanenbaum posted a message to Usenet arguing that monolithic kernels like Linux were obsolete engineering, that microkernels were demonstrably superior, and that Linux would be irrelevant within five years. Linus Torvalds replied that microkernels were academic toys and that Linux’s monolithic design was an engineering pragmatism that actually worked. The exchange, widely known as the Tanenbaum-Torvalds debate, was the most public airing of a genuine architectural disagreement in operating systems. Thirty years later, Linux runs the world’s servers, cloud infrastructure, Android devices, and most supercomputers. Microkernels run in embedded systems and formally verified kernels. The debate illuminated a genuine design tradeoff that neither side resolved.
The security crisis in operating systems writing came gradually and then all at once. C, which gave Unix its portability, is a language with no memory safety. Buffer overflows — writing past the end of an array into adjacent memory — can overwrite return addresses, turning program bugs into code execution vulnerabilities. Aleph One’s 1996 paper “Smashing the Stack for Fun and Profit” demonstrated systematically how buffer overflows could be exploited. By the 2000s, memory safety vulnerabilities accounted for the majority of reported security flaws in operating systems. The mitigations accumulated over decades: stack canaries (random values placed before return addresses, checked on function return), ASLR (address space layout randomization, randomizing where code and data are loaded to make exploitation harder), non-executable stacks (marking the stack memory as non-executable so injected shellcode cannot run), control-flow integrity (restricting indirect branches to valid targets). Each mitigation raised the cost of exploitation without eliminating the underlying vulnerability — the programs still had the bugs, they were just harder to exploit. The fundamental response to this crisis is language-level memory safety: writing operating systems code in Rust, which enforces memory safety at compile time, eliminating entire classes of vulnerabilities. The Rust-for-Linux project, incorporated into the mainline Linux kernel in 2022, is the most significant architectural shift in Linux in decades.
The formally verified operating system was demonstrated in 2009 when Gerwin Klein and colleagues at NICTA announced the verification of seL4, a complete working microkernel proven correct in Isabelle/HOL. The proof establishes that the seL4 C implementation correctly implements its Haskell specification, and that the specification has the required security properties. The result rules out the entire class of memory-safety bugs and functional correctness bugs in the verified code. seL4 has since been deployed in safety-critical aerospace systems and in other contexts where correctness guarantees justify the verification cost. The seL4 proof demonstrated that formal verification of a complete operating system was possible — not just a research exercise but an engineering achievement deployable in production. The implications for how high-assurance systems are built are still being worked out.
The contemporary operating system runs in an environment that the founders of Unix could not have anticipated. Containers — lightweight isolation mechanisms built on Linux namespaces and cgroups — allow multiple independent application environments to share a kernel, providing much of the isolation of virtual machines at a fraction of the overhead. Kubernetes orchestrates thousands of containers across hundreds of machines. eBPF (extended Berkeley Packet Filter) allows sandboxed programs to run inside the kernel itself, implementing networking, observability, and security policies without modifying the kernel or loading untrusted kernel modules. io_uring provides asynchronous I/O with near-zero overhead by using shared memory rings between kernel and userspace instead of system calls. Each of these represents the operating system adapting to new workloads — cloud-native applications, high-performance networking, observability at scale — while maintaining the foundational abstractions of virtualization, concurrency, and persistence.
Virtualization, Concurrency, and Persistence
Virtualization: Manufacturing the Illusion
Process virtualization begins with the CPU. Each process believes it has the CPU to itself; the operating system’s scheduler manages the illusion. A context switch saves the current process’s register state (including the program counter, stack pointer, and general-purpose registers) and restores the saved state of the next process to run. From the process’s perspective, nothing happened; from the hardware’s perspective, a different set of registers is in use. The scheduler decides which process to run next according to a policy: FIFO (first in, first out), round-robin (time-slice each process in turn), shortest job first (run the process expected to complete soonest), multi-level feedback queues (lower the priority of processes that use their time slice, raise the priority of processes that yield before their slice expires). No scheduling policy is universally optimal — short-process favoritism helps interactive responsiveness but can starve long-running compute jobs — and the scheduler in a real kernel is a carefully tuned compromise between competing requirements.
Virtual memory gives each process its own address space — the illusion that memory addresses from 0 to the address space limit are the process’s own. The hardware provides a Memory Management Unit (MMU) that translates virtual addresses to physical addresses on every memory access, using a page table maintained by the kernel. A page table maps each virtual page (typically 4KB) to a physical frame or marks it as not present. When the process accesses an unmapped address, the hardware triggers a page fault; the kernel’s page fault handler allocates a physical frame, updates the page table, and resumes the process, which never observes the interruption. Pages can be evicted to disk when physical memory is scarce and reloaded on demand — demand paging — making the effective address space larger than physical memory. The page table also enforces isolation: each process’s page table maps only frames that belong to that process; accessing another process’s memory triggers a protection fault. This is what makes “segmentation fault” a program-level error rather than a hardware failure: the process tried to access a virtual address for which it has no permission.
The kernel itself is protected from user processes by the hardware’s privilege levels. User-mode code cannot directly execute hardware instructions that could compromise isolation — disabling interrupts, modifying page tables, directly reading hardware registers. These privileged instructions are only available in kernel mode. When a process wants to perform a privileged operation — open a file, allocate memory, send a network packet — it makes a system call, which transitions to kernel mode, executes the kernel code for that operation, and transitions back. The system call interface is the formal boundary between user space and kernel space, and designing that interface well — what operations to expose, at what granularity, with what semantics — is one of the enduring design challenges of operating system construction.
Concurrency: The Problem Without a Clean Solution
When multiple processes or threads operate on shared state, and when any of them can be preempted at any point, correctness requires that shared state be accessed only in controlled ways. The basic problem is the race condition: two threads read the value of a counter, each increments it, each writes the result back. The final value should reflect two increments; the actual value may reflect only one, because both threads read the same value before either write completes. The increment-counter race is illustrative but trivially fixable. Races in real systems are rarely so obvious, and detecting them requires reasoning about all possible interleavings of concurrent operations — a combinatorial problem that grows exponentially with the number of threads and shared variables.
Locks (mutexes) are the basic synchronization primitive: at most one thread can hold a lock at a time. Code that accesses shared state acquires the lock, does its work, and releases the lock; other threads that want the lock wait until it is released. Locks solve the race condition but introduce deadlock: if thread A holds lock L1 and waits for L2, and thread B holds lock L2 and waits for L1, neither can proceed. Deadlock is prevented by acquiring locks in a consistent order — but consistent ordering is a global constraint that is difficult to maintain in a large codebase. It is also prevented by using lock hierarchies, timeout-based acquisition, or lock-free algorithms, each with its own tradeoffs.
Condition variables address the case where a thread must wait for a condition that depends on another thread’s action. A producer-consumer queue, for instance, requires the consumer to wait when the queue is empty. A condition variable pairs with a mutex: the consumer holds the mutex, checks the queue, and if empty, atomically releases the mutex and blocks; when the producer adds an item, it signals the condition variable, waking the consumer, which reacquires the mutex and rechecks the condition. The pattern — hold mutex, check condition, wait if false, recheck on wake — is the standard idiom for condition synchronization, and variants of it appear throughout operating systems code.
Semaphores, monitors, message-passing, and transactional memory are alternative primitives, each with different expressiveness and performance characteristics. Lock-free data structures — queues, stacks, and other structures that can be accessed concurrently without locking, using compare-and-swap atomic operations — eliminate the possibility of deadlock but introduce their own complexities: ABA problems (a location is read as value A, changes to B, changes back to A, and a CAS incorrectly succeeds), memory ordering issues on weakly-ordered architectures, and algorithm complexity. The reality of concurrency is that no single primitive handles all cases cleanly, and concurrent programming remains one of the hardest activities in software engineering — not because the concepts are obscure but because the problem admits no clean solutions.
Persistence: The Border Between Volatile and Durable
Files outlast processes. The file system creates and maintains this durability — translating the logical structure of files and directories into blocks on a physical disk, and doing so correctly even when power fails at any moment. The challenge is atomicity: many file system operations that appear simple at the API level (renaming a file, appending to a file, creating a new file) involve multiple disk writes, and if power fails between those writes, the disk may be left in an inconsistent state.
The early Unix file system had no protection against this: a crash during a multi-step update could leave the file system corrupted, requiring the user to run fsck, the file system consistency checker, to find and repair the damage. fsck was effective but slow — on large file systems, it could take hours — and the increasing size of disks made it increasingly impractical.
Journaling (write-ahead logging) addressed this by maintaining a log of pending operations. Before writing the actual data, the file system writes a journal entry describing the operation. If a crash occurs, recovery replays the journal to complete or undo the pending operation, restoring consistency. The key property is that each journal write is atomic — it either fully completes or has no effect — and if the crash occurs before the journal is complete, recovery ignores the partial journal entry; if the crash occurs after the journal is complete but before the data is written, recovery completes the operation. The journaling file system — ext3 and ext4 on Linux, NTFS on Windows, APFS on Apple — dominates production systems today because it provides crash consistency without the prohibitive recovery time of fsck.
Copy-on-write (COW) file systems — Btrfs, ZFS — take a different approach: never modify data in place. Instead of writing new data over old, they write to new blocks and atomically update the file system’s metadata to point to the new blocks. If a crash occurs, the old metadata still points to the old data, which is fully consistent. The new data is orphaned — neither old nor new metadata points to it — but orphaned blocks are cleaned up by the file system’s garbage collection. COW file systems provide additional features beyond crash consistency: snapshots (preserving the old metadata pointers to create a point-in-time view), deduplication (multiple files sharing blocks with identical content), and checksumming (verifying data integrity on every read).
The storage hierarchy below the file system has been transformed by solid-state storage. Traditional spinning hard disks had dramatically different performance characteristics for sequential and random access: sequential reads and writes performed at hundreds of megabytes per second, while random access required seeking — physically moving the read head — adding milliseconds of latency per operation. File system algorithms were designed to minimize seeking: fragmentation (distributing a file across non-contiguous blocks) was a serious performance problem. SSDs have no mechanical parts; their random-access latency is measured in microseconds, not milliseconds. Many file system optimization strategies designed for spinning disks are counterproductive on SSDs, and modern file systems have had to reconsider decades of accumulated optimization.
What Studying This Changes
Operating systems changes how practitioners see every program they write.
The first change is systems thinking: reading code as a sequence of interactions with the operating system rather than as an abstract computation. Every memory allocation goes through the kernel or a user-space allocator that eventually does. Every file read goes through the page cache or blocks waiting for disk. Every lock acquisition may block the thread and cause the scheduler to run. Every system call crosses the user-kernel boundary. This visibility — seeing the OS events behind the code — changes which questions practitioners ask when debugging and which changes they consider when optimizing.
The second change is the internalization of failure as the default. Operating systems trains the disposition to ask, for any operation: what happens if this fails? What happens if this process is killed mid-operation? What happens if the disk fills up? What happens if two of these happen simultaneously? This is not paranoia but the recognition that production systems encounter all of these conditions, and that code written without considering them fails unpredictably in production. The disposition, once internalized, shapes how code is designed: error paths are not afterthoughts but first-class concerns.
The third change is the recognition of concurrency’s pervasiveness. Even programs that nominally run single-threaded interact with the operating system’s concurrency: the kernel is preemptive, signal handlers run at unpredictable times, and hardware-level concurrency affects memory ordering. The instinct to ask “what happens when two things hit this simultaneously?” is the operating systems instinct, and it identifies bugs that remain invisible to programmers who reason only about sequential execution.
The fourth change is the ability to diagnose performance problems at the system level. CPU-bound versus I/O-bound is a distinction with practical consequences: I/O-bound programs benefit from more threads (more work can proceed while some threads wait for I/O) but CPU-bound programs do not (adding threads adds scheduling overhead without adding CPU capacity). Knowing which regime a program is in — from operating systems understanding — is a prerequisite for selecting the correct optimization strategy.
Resources
Books and Texts
Remzi and Andrea Arpaci-Dusseau’s Operating Systems: Three Easy Pieces (OSTEP, free at ostep.org) is the canonical entry. Its three-part structure — virtualization, concurrency, persistence — is the conceptual organization of the subject, not just a table of contents. The writing is unusually accessible for systems literature, explaining mechanisms in the context of the problems they solve rather than in isolation. The book is updated regularly and free online; there is no reason to study from an older or inferior substitute. Every chapter is worth reading; the persistence section is particularly strong on crash consistency and file system internals.
The hands-on partner to OSTEP is the xv6 codebase with MIT’s 6.S081 lab assignments (free at pdos.csail.mit.edu/6.S081). xv6 is a teaching Unix kernel — approximately six thousand lines of C, complete enough to boot and run real programs, simple enough to read in its entirety over a few weeks. The labs (implementing copy-on-write fork, implementing a user-space thread library, adding page-fault-based lazy allocation, implementing a log-structured portion of the file system) require actually building the mechanisms described in OSTEP. The labs are not optional supplements; they are the primary way the material is learned. Reading OSTEP without the labs produces conceptual familiarity without operational understanding. The labs without OSTEP are plausible but slower. Together they are the standard contemporary introduction.
Robert Love’s Linux Kernel Development (3rd ed., Addison-Wesley, 2010) bridges from xv6 to the Linux kernel. It explains why the Linux kernel looks the way it does — which design decisions were deliberate and which are historical artifacts — and covers the kernel’s major subsystems with the depth that the xv6 simplifications suppress. The 2010 date means it predates containers, io_uring, eBPF, and the Rust integration; supplement it with LWN for these developments.
Brendan Gregg’s Systems Performance (2nd ed., Addison-Wesley, 2020) covers performance analysis at the operating systems level. Where OSTEP explains how mechanisms work, Gregg explains how to determine whether they are working correctly in production — which tools to use, which metrics to examine, what the numbers mean. It is the right book for practitioners who need to diagnose performance problems in real systems.
| Book | Role | Type |
|---|---|---|
| Arpaci-Dusseau, Operating Systems: Three Easy Pieces (free) | Canonical entry; conceptual spine | Entry |
| MIT 6.S081 + xv6 codebase (free) | Hands-on partner; necessary, not optional | Practice |
| Robert Love, Linux Kernel Development (3rd ed.) | Bridge from teaching kernel to production kernel | Depth |
| Brendan Gregg, Systems Performance (2nd ed.) | Performance analysis at the systems level | Depth |
| Stevens & Rago, Advanced Programming in the UNIX Environment | Unix system programming reference | Reference |
| Kerrisk, The Linux Programming Interface (paid) | Linux system programming reference | Reference |
| Tanenbaum & Bos, Modern Operating Systems | Comprehensive reference; broader than OSTEP | Reference |
Courses and Lectures
MIT 6.S081 (Operating System Engineering, free with full lecture videos, labs, and reading materials) is the standard university operating systems course at the graduate level, using OSTEP and xv6. The lectures by Frans Kaashoek and Robert Morris, available on YouTube, provide the context that reading alone does not: how the instructors think about the problems, which tradeoffs they find interesting, and where the xv6 design makes deliberate simplifications relative to production kernels. The lab assignments are described above.
Berkeley CS 162 (Operating Systems and System Programming, free lectures on YouTube) covers similar material from a more applications-oriented perspective, with more coverage of threads and synchronization at the user level. The Berkeley approach is complementary to MIT’s and worth viewing alongside.
| Course | Platform | Type |
|---|---|---|
| MIT 6.S081 Operating System Engineering (free) | MIT / YouTube | Entry |
| Berkeley CS 162 Operating Systems (free) | YouTube | Entry |
Practice, Tools, and Current Sources
The xv6 labs are the primary hands-on resource. Beyond them, several tools for observing operating system behavior in real kernels are essential.
strace traces system calls made by a running program, showing exactly what interactions it has with the kernel and how long each takes. Running strace -T -e trace=file ls shows every file-related system call that ls makes, with timing. This makes the boundary between user space and kernel space concrete and observable.
perf (Linux, free) profiles CPU performance counters, including context switches, cache misses, page faults, and system call frequency. Running perf stat on a program shows how much of its time is in user space versus kernel, how many page faults it triggered, and how many context switches occurred — the operating systems footprint of the program.
Brendan Gregg’s BPF Performance Tools (the book and associated bpftrace scripts, some free online) provide eBPF-based observability. eBPF allows small programs to run inside the kernel, tracing events (system calls, scheduler decisions, memory allocation) with near-zero overhead. Tools like execsnoop (tracing process creation), opensnoop (tracing file opens), and biolatency (histogram of disk I/O latency) make internal kernel behavior observable without modifying the kernel.
LWN.net is the essential ongoing resource for Linux development. The weekly edition and the archives constitute the living documentation of how the Linux kernel evolves, and reading it is how working kernel developers stay current.
| Resource | Platform | Type |
|---|---|---|
| strace (free) | Linux | Practice |
| Linux perf for OS tracing (free) | Linux kernel tools | Practice |
| Brendan Gregg BPF scripts and bpftrace (free) | GitHub / brendangregg.com | Practice |
| Linux Kernel Documentation (free) | docs.kernel.org | Reference |
| man7.org Linux man-pages (free) | man7.org | Reference |
| OSDev Wiki (free) | wiki.osdev.org | Auxiliary |
| LWN.net (free access after delay) | lwn.net | Reference |
Traps
| Trap | Why it misleads | Better response |
|---|---|---|
| Skipping the labs | Operating systems is the subject where reading without implementation produces the least learning of any CS topic. Understanding how a page table is structured is qualitatively different from implementing the page table walk in xv6 and debugging why the memory-mapped file test fails at 2am. The concepts are not hard; the hard part is making them work in code, and that hard part is the learning. | Treat the xv6 labs as the primary curriculum, not the reading. Set aside at minimum forty hours for the labs across the semester. When a lab takes longer than expected, that is the signal that the concept is being learned. When a lab takes less time than expected, that is usually the signal that it was simplified enough that the learning did not happen. |
| Memorizing mechanisms without understanding problems | Page tables, schedulers, semaphores, and journal blocks are mechanisms that solve specific problems. Students who memorize the mechanisms without internalizing the problems they solve have acquired vocabulary without understanding — they can name the solution but cannot recognize when a new situation requires it. This is the most common way OS courses produce graduates who cannot apply what they studied. | For every mechanism studied, identify the problem it solves and ask what happens without it. What goes wrong without virtual memory protection? Without atomic commit in the file system? Without the scheduler’s preemption? The problem-first framing is what OSTEP practices throughout, and following that framing in study produces the understanding that mechanism-first study misses. |
| Treating the textbook as complete | OSTEP was last substantially revised in the early 2020s; the Linux kernel development that Robert Love describes is from 2010. Containers, eBPF, io_uring, persistent memory, and the Rust-in-the-kernel work are not in these books. These are not peripheral developments — they represent the contemporary practice of operating systems engineering. | Read LWN regularly. Browse the documentation for eBPF, io_uring, and Linux namespaces. The textbook provides the conceptual foundation; the contemporary material shows how that foundation is being applied. Treating the textbook as the totality of the subject produces practitioners who understand 2010-era systems but struggle with 2025-era infrastructure. |
| Underestimating concurrency’s difficulty | Every operating systems curriculum covers locks, condition variables, and deadlock. Fewer convey that concurrent programming is genuinely one of the hardest activities in software engineering — not because the primitives are obscure, but because reasoning about all possible interleavings of concurrent operations is fundamentally complex and does not get easier with experience. Learners who finish an OS course believing they understand concurrency have often understood the basics without the difficulty. | Implement a concurrent data structure — a concurrent hash table or a work-stealing queue — from scratch and test it under load. The testing will find bugs; debugging them will teach more about concurrency than any lecture. Then read about the bugs that caused production incidents at major technology companies: the race conditions in the Linux memory management subsystem, the deadlocks in Apple’s kernel, the memory ordering bugs in early Java implementations. The incidents make the difficulty concrete. |
| Missing the security dimension | Operating systems security is not a separate topic that appears in the last chapter — it is a dimension of every topic. Virtual memory provides isolation that attackers try to break. The scheduler creates timing channels that attackers can exploit. File system permissions are the primary access control mechanism that protects user data. Buffer overflows in kernel code are exploitable. Most OS courses treat security as an add-on, and most students absorb this framing. | Read the Aleph One paper (“Smashing the Stack for Fun and Profit,” 1996) during the virtual memory unit, not after. Understand ASLR and stack canaries as responses to concrete attack techniques, not as standalone features. When studying the file system, understand discretionary access control and its limits. The security framing should be concurrent with the mechanism study, not deferred to the end. |
| Cloud-native implies OS-agnostic | Many practitioners who work primarily with Kubernetes, Docker, and cloud services conclude that the underlying OS is abstracted away and that OS knowledge is therefore unnecessary. This conclusion is wrong in both directions: the cloud infrastructure is built on Linux and depends on its mechanisms (namespaces, cgroups, eBPF, the networking stack), and the failure modes of cloud systems are typically OS failure modes one or two layers removed. Understanding why a container is OOM-killed requires understanding the kernel’s memory management. Understanding a latency spike requires understanding kernel scheduling. | Trace one production incident in a cloud environment all the way to its kernel cause. Use strace, perf, or eBPF tooling to observe what the kernel is doing during the problematic period. The tracing will reveal that the OS is not absent — it is everywhere, just less visible than when running directly on bare metal. The exercise makes OS knowledge concretely applicable to the environment most contemporary practitioners actually work in. |
中文
操作系统是一层软件,它代表运行在计算机上的每个程序来管理硬件。这个描述准确,但并不能说明为什么操作系统是计算机科学中的基础主题之一。更能说明问题的描述,是操作系统实际做的事情:它为每个正在运行的程序构造一种令人信服的幻觉,让程序以为自己独占整台机器——拥有自己的 CPU、自己的内存、自己的磁盘——而现实是,几十个甚至几百个程序同时共享这些资源,彼此并不信任,并且每个程序都可能在任何时刻失败。在保持程序彼此隔离、并防止整个系统发生灾难性失败的同时,创造并维持这种幻觉,就是操作系统的工作。
为解决这个问题而发明的概念——虚拟化、隔离、调度、同步、崩溃恢复——后来成为所有系统软件上层的基础。数据库解决持久化和并发问题,而文件系统早已在自己的领域中解决过类似问题,并使用了相同的概念动作。分布式系统解决失败和协调问题,而操作系统早已在单机内部解决过这些问题,只是如今它们被扩展到网络之上。云计算虚拟化操作系统,正如操作系统虚拟化硬件。操作系统概念不是众多课程主题中的一个;它们是整个系统轴的智识底层。
这门学科有三个组织性问题:虚拟化——从共享资源中构造出专用资源的幻觉;并发——当多个进程在互不信任的情况下与共享状态交互时,维持正确性;持久化——在崩溃和失败中,管理易失内存与持久存储之间的边界。这些不只是章节标题。它们组织着操作系统实现的每一种机制;在学习机制作为解决方案之前,先把它们理解为问题,正是概念性掌握与词汇记忆之间的区别。
前置知识:计算机组成与体系结构(§4.1)——页表、中断、特权指令和内存层级会贯穿使用。编程(§2.1)——你需要能够读写 C。算法与数据结构(§2.6)——调度、内存分配和文件系统结构都会用到这些内容。
操作系统是如何被发明出来的
操作系统解决的问题并不古老。它是在计算机变得能够运行多个程序,并且计算机成本高到不能接受程序等待 I/O 时让机器闲置之后,才出现的。
在 1950 年代,计算机一次只运行一个程序,程序从穿孔卡片加载,并在运行期间独占整台机器。当程序等待输入或输出时——相对于 CPU 来说,I/O 很慢——CPU 就会闲置。最早的操作系统功能是 batch monitors:当前一个任务结束后,自动从任务堆中加载下一个任务,消除人工交接。resident monitor 概念——把一个小程序常驻内存,用来协调任务顺序——到 1950 年代后期已经投入运行。IBM 的 OS/360 于 1964 年发布,是第一次尝试为整个机器家族和大量应用写一个统一操作系统。Frederick Brooks 在 The Mythical Man-Month 中记录的正是这个项目:延误、昂贵,并且发布时带有需要多年修复的 bug。OS/360 的规模揭示出,软件管理本身就是一个需要系统处理的问题。
Multiprogramming 的到来,是因为人们意识到 I/O 等待才是根本瓶颈。如果多个程序同时加载进内存,那么当当前程序等待 I/O 时,CPU 可以切换到另一个程序,从而保持忙碌。这要求操作系统管理 CPU——调度哪个程序何时运行——也管理内存——确保程序不会彼此覆盖。架构中加入了内存保护硬件,以强制程序之间的隔离;也加入了特权指令模式,以防止程序直接访问可能破坏隔离的硬件。进程这个概念——一个正在执行的程序,带有自己的地址空间、寄存器状态和执行上下文——正是从 multiprogramming 的要求中产生的。
影响最深的操作系统是 Unix,由 Ken Thompson 和 Dennis Ritchie 于 1969 到 1973 年间在 Bell Labs 编写。Unix 不是第一个通用操作系统——Bell Labs 曾退出的 Multics 更有野心。但 Unix 体现了一种 Multics 没有体现的设计哲学:一切皆文件。网络连接、进程元数据、设备接口、进程间通信通道——Unix 都通过同一个 read/write 文件接口呈现,从而创造出一种统一抽象,使程序能够以前所未有的方式组合。pipe 由 Douglas McIlroy 发明,并于 1973 年加入 Unix,它通过一种类似文件的抽象,把一个程序的输出连接到另一个程序的输入,使人可以从简单工具构造复杂工具。Unix 哲学——小程序做好一件事,并通过文本流连接——成为一种设计原则,其影响力远远超过 Unix 本身。
Unix 也用 C 编写,这个决定后来证明具有变革意义。此前的操作系统用汇编语言编写,因此绑定到特定硬件上。用 C 写的操作系统,原则上可以移植到任何拥有 C 编译器的硬件上。清晰设计哲学与可移植实现语言的结合,使 Unix 在 1980 年代和 1990 年代早期成为工作站和服务器上的主导操作系统;而它的后代——Linux、macOS、iOS、Android、各类 BSD——今天运行着绝大多数计算基础设施。
1980 年代和 1990 年代的架构争论,发生在宏内核与微内核之间。宏内核——Linux 和传统 Unix 的结构——把所有内核服务都运行在同一个特权地址空间中:调度器、内存管理器、文件系统、设备驱动和网络栈都一起运行在 kernel mode 中,其中任何组件的 bug 都可能破坏其他组件。微内核——Mach、L4 以及最终 seL4 的结构——只把最特权的服务(地址空间管理、进程间通信、基础调度)放在 kernel mode 中,而把设备驱动、文件系统和其他服务作为用户空间进程运行。微内核中的用户空间驱动 bug 无法破坏内核;它只能使驱动进程崩溃,而这个进程可以被重启。1992 年 1 月,Andrew Tanenbaum 在 Usenet 上发帖,主张 Linux 这样的宏内核是过时工程,微内核明显更优,而 Linux 五年内就会无关紧要。Linus Torvalds 回应说,微内核是学术玩具,而 Linux 的宏内核设计是一种实际有效的工程实用主义。这场广为人知的 Tanenbaum-Torvalds debate,是操作系统中一次真正架构分歧的公开展示。三十年后,Linux 运行着世界上的服务器、云基础设施、Android 设备和多数超级计算机。微内核运行在嵌入式系统和形式化验证内核中。这场争论照亮了一种真实设计权衡,而双方都没有彻底解决它。
操作系统写作中的安全危机,是逐渐到来、然后突然爆发的。C 给了 Unix 可移植性,但 C 不是内存安全语言。Buffer overflows——写越数组末尾,进入相邻内存——可以覆盖返回地址,把程序 bug 变成代码执行漏洞。Aleph One 1996 年的论文 “Smashing the Stack for Fun and Profit” 系统展示了如何利用 buffer overflows。到 2000 年代,内存安全漏洞占据了操作系统报告安全缺陷的大多数。几十年中逐渐累积了许多缓解措施:stack canaries(在返回地址前放置随机值,并在函数返回时检查)、ASLR(address space layout randomization,随机化代码和数据加载位置,使利用更难)、non-executable stacks(把栈内存标记为不可执行,使注入的 shellcode 无法运行)、control-flow integrity(限制间接分支只能跳向有效目标)。每一种缓解都提高了利用成本,却没有消除底层漏洞——程序仍然有 bug,只是更难利用。对这场危机的根本回应是语言层面的内存安全:用 Rust 编写操作系统代码,在编译时强制内存安全,从而消除整类漏洞。Rust-for-Linux 项目于 2022 年并入 Linux mainline kernel,是 Linux 数十年来最重要的架构变化。
形式化验证操作系统在 2009 年被展示出来,当时 NICTA 的 Gerwin Klein 及其同事宣布完成 seL4 的验证。seL4 是一个完整可工作的微内核,并在 Isabelle/HOL 中被证明正确。这个证明表明,seL4 的 C 实现正确实现了其 Haskell specification,并且该 specification 具备所需安全属性。这个结果排除了已验证代码中的整类内存安全 bug 和功能正确性 bug。此后,seL4 已被部署到安全关键的航空航天系统,以及其他正确性保证足以证明验证成本合理的语境中。seL4 证明展示了一个完整操作系统的形式化验证是可能的——它不仅是研究练习,也是可以部署到生产中的工程成就。它对高保证系统应如何构建的影响,仍在展开。
当代操作系统运行在 Unix 创始者无法预料的环境中。Containers——建立在 Linux namespaces 和 cgroups 之上的轻量隔离机制——允许多个独立应用环境共享一个内核,以一小部分开销提供虚拟机的大部分隔离性。Kubernetes 在数百台机器上编排数千个 containers。eBPF(extended Berkeley Packet Filter)允许 sandboxed programs 在内核内部运行,在不修改内核、也不加载不可信 kernel modules 的情况下实现网络、observability 和安全策略。io_uring 通过在 kernel 和 userspace 之间使用共享内存 rings,而不是 system calls,提供接近零开销的异步 I/O。这些都代表操作系统正在适应新工作负载——cloud-native applications、高性能网络、大规模 observability——同时维持虚拟化、并发和持久化这些基础抽象。
虚拟化、并发与持久化
虚拟化:制造幻觉
进程虚拟化从 CPU 开始。每个进程都相信自己独占 CPU;操作系统的调度器管理这种幻觉。一次 context switch 会保存当前进程的寄存器状态(包括 program counter、stack pointer 和 general-purpose registers),并恢复下一个要运行进程的已保存状态。从进程视角看,什么都没有发生;从硬件视角看,正在使用的是另一组寄存器。调度器根据某种策略决定下一个运行哪个进程:FIFO(first in, first out)、round-robin(让每个进程轮流获得时间片)、shortest job first(运行预期最快完成的进程)、multi-level feedback queues(降低用完整个时间片进程的优先级,提高在时间片结束前主动让出的进程优先级)。没有一种调度策略是普遍最优的——偏爱短进程有助于交互响应性,但可能饿死长时间运行的计算任务——真实内核中的调度器,是在互相竞争要求之间仔细调校出的折中。
虚拟内存为每个进程提供自己的地址空间——也就是这样一种幻觉:从 0 到地址空间上限之间的内存地址都是该进程自己的。硬件提供 Memory Management Unit(MMU),在每次内存访问时使用内核维护的 page table,把 virtual addresses 翻译成 physical addresses。一个 page table 会把每个 virtual page(通常为 4KB)映射到一个 physical frame,或标记为不存在。当进程访问未映射地址时,硬件触发 page fault;内核的 page fault handler 分配一个 physical frame,更新 page table,然后恢复进程,而进程完全观察不到这次中断。当 physical memory 不足时,pages 可以被驱逐到磁盘,并在需要时重新加载——demand paging——使有效地址空间大于物理内存。Page table 也强制隔离:每个进程的 page table 只映射属于该进程的 frames;访问另一个进程的内存会触发 protection fault。这就是为什么 “segmentation fault” 是程序级错误,而不是硬件故障:进程试图访问一个自己没有权限访问的 virtual address。
内核本身通过硬件特权级与用户进程隔离。User-mode code 不能直接执行可能破坏隔离的硬件指令——禁用中断、修改 page tables、直接读取硬件寄存器。这些 privileged instructions 只能在 kernel mode 中使用。当一个进程想执行特权操作——打开文件、分配内存、发送网络包——它会发起 system call,转入 kernel mode,执行该操作对应的内核代码,然后再切回。System call interface 是 user space 和 kernel space 之间的正式边界,而设计好这个接口——暴露哪些操作、粒度多大、语义是什么——是操作系统构造中长期存在的设计挑战之一。
并发:没有干净解法的问题
当多个进程或线程操作共享状态,并且其中任何一个都可能在任意点被抢占时,正确性要求共享状态只能以受控方式访问。基本问题是 race condition:两个线程读取同一个 counter 的值,各自递增,再各自写回。最终值应当反映两次递增;实际值可能只反映一次,因为两个线程在任何写入完成前读到了同一个值。递增 counter 的竞态只是说明性例子,而且很容易修复。真实系统中的 races 很少这么明显,检测它们需要推理并发操作的所有可能交错——这是一个组合问题,会随着线程和共享变量数量指数级增长。
Locks(mutexes)是基本同步原语:同一时刻最多只有一个线程可以持有某把锁。访问共享状态的代码获取锁,完成工作,然后释放锁;其他想要这把锁的线程会等待其释放。Locks 解决了 race condition,但引入了 deadlock:如果线程 A 持有锁 L1 并等待 L2,而线程 B 持有 L2 并等待 L1,那么二者都无法继续。Deadlock 可以通过按一致顺序获取锁来预防——但一致顺序是一个全局约束,在大型代码库中很难维持。它也可以通过 lock hierarchies、timeout-based acquisition 或 lock-free algorithms 预防,而每种方法都有自己的权衡。
Condition variables 处理的是这样一种情况:某个线程必须等待另一个线程的行动所决定的条件。例如 producer-consumer queue 要求消费者在队列为空时等待。Condition variable 会和 mutex 配对:消费者持有 mutex,检查队列;如果为空,就原子地释放 mutex 并阻塞;当生产者添加一个 item 后,它 signal condition variable,唤醒消费者,消费者重新获取 mutex 并重新检查条件。这个模式——持有 mutex,检查条件,条件为假则等待,唤醒后重新检查——是条件同步的标准 idiom,其变体遍布操作系统代码。
Semaphores、monitors、message-passing 和 transactional memory 是其他同步原语,每种都有不同表达力和性能特征。Lock-free data structures——queues、stacks 和其他可以在不加锁情况下并发访问的结构,使用 compare-and-swap atomic operations——消除了 deadlock 可能性,但引入自己的复杂性:ABA problems(某个位置读到值 A,变成 B,又变回 A,CAS 因而错误成功)、弱内存序架构上的 memory ordering issues,以及算法复杂性。并发的现实是,没有单一原语能干净处理所有情况;并发编程仍然是软件工程中最困难的活动之一——不是因为概念晦涩,而是因为这个问题不存在干净解法。
持久化:易失与持久之间的边界
文件比进程活得更久。文件系统创造并维护这种持久性——把文件和目录的逻辑结构翻译成物理磁盘上的 blocks,并且即使电源在任何时刻失效,也要正确完成这件事。挑战在于原子性:许多在 API 层面看起来简单的文件系统操作(重命名文件、向文件追加、创建新文件)都涉及多次磁盘写入;如果在这些写入之间断电,磁盘可能被留在不一致状态中。
早期 Unix 文件系统对此没有保护:一次多步骤更新过程中的崩溃可能使文件系统损坏,要求用户运行 fsck,也就是 file system consistency checker,来寻找并修复损坏。fsck 有效但很慢——在大型文件系统上可能需要数小时——而磁盘不断变大,使它越来越不现实。
Journaling(write-ahead logging)通过维护待处理操作日志来处理这个问题。在写入实际数据之前,文件系统会先写一条描述该操作的 journal entry。如果发生崩溃,恢复过程会 replay journal,完成或撤销待处理操作,从而恢复一致性。关键性质是,每个 journal write 是原子的——要么完全完成,要么没有效果;如果崩溃发生在 journal 完成前,恢复会忽略部分 journal entry;如果崩溃发生在 journal 完成后、数据写入前,恢复会完成该操作。Journaling file system——Linux 上的 ext3 和 ext4,Windows 上的 NTFS,Apple 上的 APFS——主导今天的生产系统,因为它们提供 crash consistency,同时避免了 fsck 那种不可接受的恢复时间。
Copy-on-write(COW)文件系统——Btrfs、ZFS——采取不同方法:永远不原地修改数据。它们不是把新数据覆盖到旧数据上,而是写入新 blocks,并原子地更新文件系统元数据,让其指向新 blocks。如果发生崩溃,旧元数据仍然指向完全一致的旧数据。新数据会变成孤儿——旧元数据和新元数据都不指向它——但 orphaned blocks 会由文件系统垃圾回收清理。COW 文件系统除了 crash consistency 外,还提供其他特性:snapshots(保留旧元数据指针以创建某个时间点视图)、deduplication(多个文件共享内容相同的 blocks),以及 checksumming(每次读取时验证数据完整性)。
文件系统之下的存储层级已经被固态存储改变。传统旋转硬盘在顺序访问和随机访问上的性能特征差异巨大:顺序读写可以达到每秒数百 MB,而随机访问需要 seek——物理移动读头——每次操作增加毫秒级延迟。文件系统算法曾被设计为尽量减少 seeking:fragmentation(一个文件分布在不连续 blocks 上)是严重性能问题。SSD 没有机械部件;其随机访问延迟以微秒而非毫秒计。许多为旋转硬盘设计的文件系统优化策略,在 SSD 上反而会产生反效果,现代文件系统也不得不重新审视几十年累积下来的优化。
学习这一部分会改变什么
操作系统会改变实践者看待自己所写每个程序的方式。
第一个变化,是系统思维:把代码读成一系列与操作系统的交互,而不是抽象计算。每次内存分配都会经过内核,或经过最终会与内核交互的用户空间 allocator。每次文件读取都会经过 page cache,或阻塞等待磁盘。每次获取 lock 都可能阻塞线程,并导致调度器运行。每次 system call 都会跨越 user-kernel boundary。这种可见性——看见代码背后的 OS events——会改变实践者调试时提出的问题,也会改变优化时考虑的修改。
第二个变化,是把失败视为默认情况。操作系统训练一种倾向:对任何操作都问,如果它失败会发生什么?如果这个进程在操作中途被 kill 会发生什么?如果磁盘满了会发生什么?如果其中两个问题同时发生会怎样?这不是偏执,而是认识到生产系统确实会遇到所有这些条件,并且未考虑这些条件的代码会在生产中不可预测地失败。一旦内化,这种倾向会塑造代码设计:错误路径不再是事后补充,而是一等关切。
第三个变化,是认识到并发无处不在。即使名义上单线程运行的程序,也会与操作系统的并发性互动:内核是抢占式的,signal handlers 会在不可预测的时间运行,硬件层面的并发会影响 memory ordering。本能地问“如果两件事同时发生会怎样?”就是操作系统直觉,它能识别那些只按顺序执行推理的程序员看不见的 bug。
第四个变化,是具备系统层面的性能诊断能力。CPU-bound 与 I/O-bound 的区别具有实际后果:I/O-bound 程序会从更多线程中受益(一些线程等待 I/O 时,其他工作可以继续),但 CPU-bound 程序不会(增加线程只会增加调度开销,而不会增加 CPU 能力)。理解一个程序处在哪种状态——来自操作系统理解——是选择正确优化策略的前提。
资源
书籍与文本
Remzi 和 Andrea Arpaci-Dusseau 的 Operating Systems: Three Easy Pieces(OSTEP,可在 ostep.org 免费获取)是经典入口。它的三部分结构——虚拟化、并发、持久化——是这门学科的概念组织,而不只是目录。其写法在系统文献中异常易读,它会把机制放在其所解决问题的语境中解释,而不是孤立介绍。该书定期更新并免费在线提供;没有理由选择更旧或更差的替代品。每一章都值得读;持久化部分尤其擅长讲 crash consistency 和文件系统内部机制。
OSTEP 的动手搭档是 xv6 codebase 和 MIT 6.S081 lab assignments(可在 pdos.csail.mit.edu/6.S081 免费获取)。xv6 是一个教学用 Unix kernel——大约六千行 C,完整到可以启动并运行真实程序,又简单到可以在几周内完整读完。实验(实现 copy-on-write fork、实现用户空间线程库、加入基于 page fault 的 lazy allocation、实现文件系统的一部分 log-structured 机制)要求你真正构建 OSTEP 中描述的机制。这些实验不是可选补充;它们是学习这门材料的主要方式。只读 OSTEP 而不做实验,会产生概念熟悉感,但不会产生操作性理解。只做实验而不读 OSTEP 也可行,但更慢。二者结合,是当代标准入门路径。
Robert Love 的 Linux Kernel Development(第 3 版,Addison-Wesley,2010)把学习者从 xv6 过渡到 Linux kernel。它解释 Linux kernel 为什么长成现在这样——哪些设计决策是刻意的,哪些是历史遗产——并以 xv6 简化版本压制掉的深度覆盖内核主要子系统。2010 年这个时间意味着它早于 containers、io_uring、eBPF 和 Rust 集成;这些发展需要用 LWN 补充。
Brendan Gregg 的 Systems Performance(第 2 版,Addison-Wesley,2020)覆盖操作系统层面的性能分析。OSTEP 解释机制如何工作,Gregg 则解释如何在生产中判断这些机制是否正常工作——使用哪些工具,检查哪些指标,数字意味着什么。对于需要诊断真实系统性能问题的实践者,这是正确的书。
| 书籍 | 作用 | 类型 |
|---|---|---|
| Arpaci-Dusseau, Operating Systems: Three Easy Pieces(免费) | 经典入口;概念主干 | 入门 |
| MIT 6.S081 + xv6 codebase(免费) | 动手搭档;必要而非可选 | 实践 |
| Robert Love, Linux Kernel Development(第 3 版) | 从教学内核过渡到生产内核 | 深入 |
| Brendan Gregg, Systems Performance(第 2 版) | 系统层面性能分析 | 深入 |
| Stevens & Rago, Advanced Programming in the UNIX Environment | Unix 系统编程参考 | 参考 |
| Kerrisk, The Linux Programming Interface(付费) | Linux 系统编程参考 | 参考 |
| Tanenbaum & Bos, Modern Operating Systems | 综合参考;比 OSTEP 更宽 | 参考 |
课程与讲座
MIT 6.S081(Operating System Engineering,完整讲座视频、实验和阅读材料免费)是标准研究生层级大学操作系统课程,使用 OSTEP 和 xv6。Frans Kaashoek 和 Robert Morris 的讲座可以在 YouTube 上获得,它们提供了阅读本身不能提供的语境:教师如何思考这些问题,他们觉得哪些权衡有趣,以及 xv6 设计相对于生产内核做了哪些刻意简化。实验任务如上所述。
Berkeley CS 162(Operating Systems and System Programming,YouTube 上有免费讲座)从更偏应用的视角覆盖类似材料,并对用户层面的 threads 和 synchronization 有更多处理。Berkeley 方法与 MIT 方法互补,值得一并观看。
| 课程 | 平台 | 类型 |
|---|---|---|
| MIT 6.S081 Operating System Engineering(免费) | MIT / YouTube | 入门 |
| Berkeley CS 162 Operating Systems(免费) | YouTube | 入门 |
实践、工具与当前资料
xv6 实验是主要动手资源。除此之外,几个用于观察真实内核中操作系统行为的工具也很必要。
strace 会追踪运行中程序发起的 system calls,精确显示它与内核进行了哪些交互,以及每次交互耗时多久。运行 strace -T -e trace=file ls 会显示 ls 发起的每个文件相关 system call 及其耗时。这会使 user space 和 kernel space 之间的边界变得具体且可观察。
perf(Linux,免费)会分析 CPU performance counters,包括 context switches、cache misses、page faults 和 system call frequency。在程序上运行 perf stat 可以显示其多少时间在 user space、多少在 kernel 中,触发了多少 page faults,以及发生了多少 context switches——这就是该程序的操作系统足迹。
Brendan Gregg 的 BPF Performance Tools(书籍及相关 bpftrace scripts,部分免费在线)提供基于 eBPF 的 observability。eBPF 允许小程序在内核内部运行,以接近零开销追踪事件(system calls、scheduler decisions、memory allocation)。execsnoop(追踪进程创建)、opensnoop(追踪文件打开)、biolatency(磁盘 I/O 延迟直方图)等工具,可以在不修改内核的情况下观察内核内部行为。
LWN.net 是 Linux 开发最重要的持续资源。其周刊和档案构成了 Linux kernel 如何演进的活文档;阅读它,是工作中的内核开发者保持更新的方式。
| 资源 | 平台 | 类型 |
|---|---|---|
| strace(免费) | Linux | 实践 |
| Linux perf for OS tracing(免费) | Linux kernel tools | 实践 |
| Brendan Gregg BPF scripts and bpftrace(免费) | GitHub / brendangregg.com | 实践 |
| Linux Kernel Documentation(免费) | docs.kernel.org | 参考 |
| man7.org Linux man-pages(免费) | man7.org | 参考 |
| OSDev Wiki(免费) | wiki.osdev.org | 辅助 |
| LWN.net(延迟后免费访问) | lwn.net | 参考 |
陷阱
| 陷阱 | 为什么会误导 | 更好的回应 |
|---|---|---|
| 跳过实验 | 操作系统是所有 CS 主题中最不能只靠阅读学习的主题。理解 page table 结构,和在 xv6 中实现 page table walk、并在凌晨两点调试为什么 memory-mapped file 测试失败,是两件性质完全不同的事。概念并不难;难的是让它们在代码中工作,而这个困难本身就是学习。 | 把 xv6 实验当成主课程,而不是阅读的附属品。整个学期至少留出四十小时给实验。实验比预期花费更久时,说明概念正在被学习。实验比预期更快完成时,通常说明它被简化到学习没有真正发生。 |
| 记忆机制,却不理解问题 | Page tables、schedulers、semaphores 和 journal blocks 都是为解决特定问题而存在的机制。学生如果只记忆机制,却没有内化它们解决的问题,就只是获得了词汇,而不是理解——他们能说出解决方案名称,却无法识别新情境何时需要它。这是 OS 课程最常见的失败方式:培养出无法应用所学内容的毕业生。 | 学习每个机制时,都要识别它解决的问题,并问没有它会发生什么。没有 virtual memory protection 会出什么问题?文件系统没有 atomic commit 会怎样?调度器没有 preemption 会怎样?问题优先的框架,正是 OSTEP 贯穿使用的学习方式;按这种方式学习,才能得到机制优先学习所遗漏的理解。 |
| 把教材当成完整图景 | OSTEP 最近一次大幅修订是在 2020 年代早期;Robert Love 描述的 Linux kernel development 来自 2010 年。Containers、eBPF、io_uring、persistent memory 和 Rust-in-the-kernel 工作都不在这些书中。这些不是边缘发展——它们代表当代操作系统工程实践。 | 定期阅读 LWN。浏览 eBPF、io_uring 和 Linux namespaces 的文档。教材提供概念基础;当代材料展示这些基础正在如何被应用。把教材当成整个主题,会培养出理解 2010 年代系统、却难以处理 2025 年代基础设施的实践者。 |
| 低估并发的困难 | 每门操作系统课程都会讲 locks、condition variables 和 deadlock。较少课程能传达的是,并发编程确实是软件工程中最困难的活动之一——不是因为原语晦涩,而是因为推理并发操作所有可能交错本来就复杂,并且不会因为经验增加而变得简单。学完 OS 课程后以为自己理解了并发的学习者,往往只理解了基础,却没有理解困难本身。 | 从零实现一个并发数据结构——concurrent hash table 或 work-stealing queue——并在负载下测试它。测试会发现 bug;调试它们会比任何讲座都更能教会并发。然后阅读那些曾造成大型科技公司生产事故的 bug:Linux memory management subsystem 中的 race conditions,Apple kernel 中的 deadlocks,早期 Java 实现中的 memory ordering bugs。这些事故会使困难变得具体。 |
| 看不见安全维度 | 操作系统安全不是最后一章才出现的独立主题——它是每个主题的一个维度。Virtual memory 提供攻击者试图突破的隔离。调度器会创造攻击者可以利用的 timing channels。文件系统权限是保护用户数据的主要访问控制机制。Kernel code 中的 buffer overflows 可以被利用。多数 OS 课程把安全当作附加项,而多数学生也吸收了这种框架。 | 在 virtual memory 单元中就阅读 Aleph One 的论文(“Smashing the Stack for Fun and Profit,” 1996),而不是最后才读。把 ASLR 和 stack canaries 理解为对具体攻击技术的回应,而不是孤立功能。学习文件系统时,理解 discretionary access control 及其限制。安全框架应与机制学习同步,而不是推迟到最后。 |
| 以为 cloud-native 就意味着与 OS 无关 | 许多主要使用 Kubernetes、Docker 和云服务的实践者会得出结论:底层 OS 已经被抽象掉,因此 OS 知识不再必要。这个结论在两个方向上都是错的:云基础设施建立在 Linux 之上,并依赖其机制(namespaces、cgroups、eBPF、网络栈);云系统的失败模式通常就是隔了一两层的 OS 失败模式。理解一个 container 为什么被 OOM-killed,需要理解 kernel memory management。理解一次延迟尖峰,需要理解 kernel scheduling。 | 在云环境中追踪一次生产事故,一直追到其内核原因。使用 strace、perf 或 eBPF 工具,观察问题期间内核正在做什么。追踪会揭示 OS 并没有消失——它无处不在,只是相比直接运行在裸机上更不显眼。这个练习会让 OS 知识具体适用于多数当代实践者实际工作的环境。 |