CEUC301 · Unit 1 of 9
Operating System
Design, Processes & Threads
Every operating system is a pile of compromises, made explicit. This chapter gives you the vocabulary to name those compromises — design goals, kernel architecture, process/thread control blocks, switching cost, and threading models — and one running example small enough to compute by hand.
The spine. Throughout this chapter we follow one small program: WriteWell,
a text editor, running as a single process P with two threads — T_ui,
which reads keystrokes and repaints the window, and T_save, which periodically writes the
document to disk and runs the spell-checker. We'll build P on Linux and on Windows, trace the
exact same keystroke through three different kernel architectures, read its real control-block fields on both
operating systems, and compute what it actually costs the CPU to switch between T_ui and
T_save. Small enough to hold in your head; real enough that every number is one you could
reproduce with a stopwatch and a kernel source tree.
Design Goals
Six things an OS is judged on, and why you can never max out all six at once.
Ask someone to describe a "good" operating system and they'll say it's fast. That's true, but incomplete enough to be dangerous. An OS is good for a purpose: the same design that makes a pacemaker's OS good (never fail, ever) would make a gaming laptop's OS terrible (nothing would ever run fast). Before you can judge whether Linux's choices or Windows's choices are "right," you need a vocabulary for what each choice is being traded against.
The six goals
Every OS design decision — down to something as small as how WriteWell saves a
file — is a vote for some of these six goals at the expense of others.
| Goal | Definition | Typically traded against |
|---|---|---|
| Performance | Throughput (work done per second) and latency (time to finish one unit of work), measured, not felt. | Security checks, portability layers — anything that adds instructions between a request and its answer. |
| Scalability | The design keeps its performance and correctness promises as load grows — more cores, more processes, more users. | Simplicity; scaling usually means more bookkeeping (locks, partitioning, distributed state). |
| Reliability | The system keeps delivering correct service despite accidental faults — a bug, a power blip, a full disk. | Performance (checkpoints, redundant writes) and simplicity (recovery logic). |
| Security | The system keeps delivering correct service despite adversarial faults — someone actively trying to break it. | Performance almost always; isolation and checking cost cycles. |
| Portability | The same design (or the same code) runs across different hardware or OS versions with minimal change. | Performance; abstraction layers add a hop between your code and the metal. |
| Interoperability | The system exchanges data and services meaningfully with other systems — different OS, different vendor. | Simplicity and sometimes security (standard formats are also well-studied attack surfaces). |
Reliability ≠ Security
Students conflate these constantly, and it costs marks under CO1. Reliability is about surviving Murphy: nobody meant for the disk to fill up. Security is about surviving an adversary: somebody meant for your buffer to overflow. The mechanisms overlap — isolation helps both — but the threat model is different, and an exam question that says "evaluate the reliability of this design against a malicious input" is really asking about security.
Why you can't max out all six
Design goals aren't a checklist you tick off independently; they sit in tension. Three tensions come up constantly enough to name:
- Performance vs. security. Every bounds check, every permission lookup, every sandbox boundary is CPU cycles spent not doing the user's work. A kernel that trusted every request would be blazingly fast and catastrophically insecure.
- Performance vs. portability. A hardware abstraction layer (HAL) lets the same OS run on different CPUs by inserting an indirection between your code and the hardware. The indirection is exactly what costs the performance.
- Scalability and reliability often help each other — the same redundancy (replicated state, multiple workers) that lets a system scale out also gives it something to fail over to. This is the one pairing on this list that isn't purely a trade-off, and it's worth remembering precisely because the other pairs train you to expect conflict everywhere.
Worked example · how should WriteWell save your file?
design reasoning, not arithmeticT_save needs to write the document to disk. Two designs are on the
table. Design A: open the target file and overwrite it directly. Design B:
write to a temporary file, then atomically rename it over the target. Which goals does each design win, and
which does WriteWell's team actually choose?Design A is one open() + one write(). Design B adds a
second file, an fsync(), and a rename() — a few
hundred microseconds more, dwarfed by the disk write itself. Small edge to A.
If the process is killed, the machine loses power, or the disk fills up mid-write, Design A leaves a half-written file — the previous, complete draft is gone and the new one is corrupt. Design B never exposes a half-written file at the target path: the old file is untouched until the rename, and a rename within the same filesystem is atomic on both Linux and Windows. Large edge to B.
This is the twist. "Atomic rename" is not implemented identically everywhere. On Linux,
rename() silently and atomically replaces an existing destination file. On Windows,
the equivalent (MoveFileEx with MOVEFILE_REPLACE_EXISTING,
or ReplaceFile for extra care with attributes) has more edge cases around open
handles and requires the WriteWell team to write OS-specific code either way. Design B is safer but costs
some of the portability that Design A got for free.
Scalability is a wash — neither design changes how the app behaves under more documents or more users. Security tilts slightly toward B: a reader who opens the file mid-save either sees the complete old version or the complete new one, never a half-written one that might contain partially-flushed sensitive content. Interoperability is unaffected; the file format itself doesn't change.
Figure 1 · the same six goals, two designs
WriteWell's autosave: two designs, six goals
1 = weak · 5 = strongPitfall
Listing all six goals as independent bullet points, with no discussion of what was given up, is the single most common way to lose marks on a "justify this design choice" question. A justification names the goal you sacrificed, not just the goal you gained.
Practice 1.1
- An OS is being chosen for a pacemaker's embedded controller. Rank the six goals for this deployment and
justify your top two.
Show solution
Reliability and security dominate, and in that order. A pacemaker that fails "gracefully" is still a failure with a heartbeat attached to it, so reliability (surviving hardware faults, brownouts, radiation-induced bit flips) outranks everything. Security is a close second now that pacemakers are wirelessly programmable — an adversarial firmware update is a real threat model, not a hypothetical one. Performance matters only insofar as it must meet a hard real-time deadline (Unit 9 covers this); beyond that deadline, more speed buys nothing. Portability and interoperability are nearly irrelevant — the device runs on one known chip for its entire service life.
- Now rank the same six goals for a high-frequency trading system that must react to market data in
microseconds.
Show solution
Performance dominates so heavily that some trading systems deliberately weaken portability (hand-tuned for one exact CPU model, sometimes one exact silicon stepping) and even reliability in the "survive-any-fault" sense — they'd rather crash fast and restart than pay for the redundancy that would let them limp on. Security still matters (this is still money) but is often pushed to the network edge rather than paid for on every hot-path instruction. This is a useful contrast with the pacemaker: same six goals, almost the opposite ranking.
- WriteWell's team disables the spell-checker's sandbox to shave 40 ms off startup time. Which goal was
traded for which, and what's the concrete risk?
Show solution
Security was traded for performance. The concrete risk: the spell-checker parses dictionary and document content that could, in principle, be crafted to exploit a parser bug. Sandboxed, a compromised spell-checker thread is contained to its own restricted resources; unsandboxed, it runs with the same privileges as
T_uiandT_save, so a single parser bug becomes a compromise of the whole process — including the document being edited and whatever files that process's user can access. - Synthesis. Explain, with a concrete OS mechanism, why scalability and reliability can
reinforce each other even though performance and security almost never do.
Show solution
Take replicated storage: keeping three copies of a file across three machines is originally a reliability mechanism (one disk dying doesn't lose data). The same three copies let you serve three times the read traffic by routing requests to whichever replica is least busy — a scalability win, for free, from a mechanism you built for a different reason. Performance and security don't get this kind of accident: a security check is extra work on the same critical path as the performance you're trying to protect, so there's no mechanism that happens to serve both at once the way redundancy serves both scalability and reliability.
Kernel Architectures
Same request, same disk, three different distances between them.
When T_save calls write(), that request has to pass through
filesystem code, then a device driver, before a single byte reaches the disk. Where that code lives
— all bundled into one privileged program, or split across several separately-protected ones — is
the single biggest structural decision an OS designer makes, and it decides almost everything else in this
course: how fast a syscall is, how much a driver bug can damage, how portable the design is to new hardware.
Three ways to organise the same code
Picture a company that has to handle "write this file." A monolithic kernel is one department that does everything in-house — filesystem, drivers, memory manager — all reporting to the same manager, talking to each other with direct function calls. A microkernel keeps only the absolute essentials in-house (scheduling, basic memory protection, message delivery) and outsources everything else — the filesystem, the device drivers — to separate contractors who can only be reached by sending a message and waiting for a reply. A hybrid kernel keeps almost everything in-house like the monolithic design, for speed, but organises it into cleanly separated internal modules so a change to one doesn't require rebuilding the whole thing.
| Architecture | Filesystem & drivers run in… | Examples |
|---|---|---|
| Monolithic | Kernel space, same address space as the scheduler & memory manager. | Linux, traditional UNIX, classic BSD |
| Microkernel | User space, as separate processes reached only via IPC. | MINIX 3, QNX, seL4, L4 family |
| Hybrid | Kernel space, but organised as loadable, layered internal modules. | Windows NT kernel, XNU (macOS) |
Formal picture: what actually crosses a boundary
Two costs distinguish these designs, and it's worth being precise about which is which:
- Mode transitions. A trap from user mode into kernel mode (and back) — needed for any syscall, in every architecture. Cheap: on modern x86, tens to low hundreds of cycles.
- Address-space (context) switches. Needed only when execution must move into a different process's address space — which happens on every IPC hop between separate servers in a microkernel, but not when a monolithic or hybrid kernel calls between its own internal functions, because those functions all live in the one kernel address space. Section 1.4 puts an exact number of microseconds on this.
This is the whole trade-off in one sentence: microkernels pay address-space switches for isolation; monolithic and hybrid kernels pay potential blast radius for speed.
Figure 2 · tracing T_save's write() across three architectures
Same call, three kernels
click to switch architecturewrite() traps once into the kernel;
the VFS layer, the filesystem driver, and the block driver are all ordinary function calls inside that same
kernel address space, so there isn't a second address-space switch to pay for.Depth · the microkernel performance story isn't settled the way textbooks imply
Early microkernels (1980s Mach) really were slow — their IPC cost was measured in the thousands of cycles, which is where "microkernels are slow" entered folklore. Modern microkernel design closed most of that gap: seL4's fast-path IPC runs at roughly 10–20% above the theoretical hardware floor for a kernel entry, address-space switch, and kernel exit — independent measurements put competing microkernels anywhere from about 2× to an order of magnitude slower than seL4 on the same operation, which tells you the "microkernel tax" is now mostly an implementation-quality question, not a law of physics. The isolation benefit (a crashed or malicious filesystem server can be restarted without taking the kernel down) is real and unconditional; the performance penalty is real but has shrunk a great deal since the architecture argument was first made.
Pitfall
A loadable kernel module (LKM) is a monolithic-kernel feature — it lets you add code to Linux's kernel address space at runtime without recompiling it. That code still runs in kernel mode, with full kernel privileges, sharing the one address space with everything else. It is not a "piece of microkernel design bolted onto Linux," and confusing the two is a common exam trap: modularity of the build is not the same thing as modularity of protection.
Practice 1.2
- QNX is used to run the infotainment and instrument-cluster software in some cars, where a crashed
graphics driver must not be able to take down the rest of the system. Explain why a microkernel is a
deliberate fit here, in terms of the goals from 1.1.
Show solution
Reliability and, to a lesser extent, security dominate this deployment (a stalled instrument cluster is a safety issue, not an inconvenience). In QNX's microkernel design, the graphics driver is a separate user-space server; if it crashes, the microkernel and the other servers (the ones actually driving the engine-management display or warning lights) are unaffected and the driver can be restarted. In a monolithic design, a bug in that same driver runs in kernel mode and can corrupt kernel memory used by everything else. The performance cost of the extra IPC hops is judged acceptable because the workload isn't latency-critical at microsecond scale.
- Give one reason Linux, despite decades of microkernel research existing, remains monolithic for
general-purpose desktop and server use.
Show solution
Raw syscall and I/O throughput matters enormously for general-purpose workloads (web servers, databases, compilers), and a monolithic design avoids paying an address-space switch on every filesystem or network operation. Linux also mitigates the isolation downside with loadable kernel modules, kernel-space sandboxing technologies (eBPF verification, for instance), and aggressive testing/fuzzing of driver code — reducing (not eliminating) the blast-radius risk without paying the microkernel's per-call cost.
- Interpretation. A vendor claims "our hybrid kernel gives us microkernel-level isolation
with monolithic-level speed." Is this a fair description of what a hybrid kernel like the Windows NT kernel
actually does?
Show solution
Not quite, and this is a common marketing overstatement. A hybrid kernel's core services (I/O manager, memory manager, the filesystem driver stack) still run in one shared kernel address space — a bug in a kernel-mode filesystem filter driver can still corrupt kernel memory, exactly as in a monolithic design. What the hybrid design actually buys is engineering modularity (drivers are separately loadable, layered, easier to develop and reason about) and the option to push some services to user mode when isolation is worth the cost — not the hard, mechanism-enforced isolation a true microkernel gives every component by default.
Process & Thread Control Blocks
What the kernel has to remember so a paused thread can resume as if nothing happened.
The scheduler is about to take T_ui off the CPU and put T_save
on. A moment later it will put T_ui back — and T_ui needs to
resume exactly where it left off, mid-instruction if necessary, with no idea that time passed at all. Something
has to remember precisely enough for that illusion to hold.
The bookmark, and the note pinned to it
Think of pausing a thread like being interrupted mid-recipe. A bookmark alone (which step you were on) isn't enough — you also need a note of what's in each mixing bowl, which oven you were using, and where the ingredients are. That note is what a control block is: enough state, saved outside the CPU, to resume an execution exactly where it stopped.
An OS keeps two levels of this note. The Process Control Block (PCB) holds what's shared by an entire process — the "kitchen" everyone in it uses. The Thread Control Block (TCB) holds what's private to one thread of execution within that process — one cook's exact position in the recipe.
Formal contents
PCB — one per process, shared by all its threads
- Process ID (PID) and parent PID
- Process state (running / ready / waiting / zombie)
- Address space: pointers to page tables, code/data/heap layout
- Open file table, working directory
- Signal handler table
- Credentials / security context (user ID, access token)
- List of the process's threads
TCB — one per thread, private to that thread
- Thread ID (TID)
- Thread state (running / ready / blocked)
- Saved program counter (PC) and stack pointer (SP)
- Saved general-purpose registers
- Its own stack (a region within the shared address space)
- Scheduling priority
- Thread-local storage (TLS) pointer
- Back-pointer to the owning process's PCB
The one field that's easy to get backwards
Only the register state of the thread that's currently running is genuinely "live" — sitting in the actual CPU registers, not yet written anywhere. Every other thread's TCB already holds an accurate saved copy from the last time it was switched out. A context switch therefore does one save (the outgoing thread's live registers → its TCB) and one restore (the incoming thread's TCB → the CPU registers) — not N saves for N threads.
Figure 3 · PCB and two TCBs for process P (WriteWell)
What the kernel actually holds right now
one instant, frozenT_save, the thread that isn't running, has
a meaningfully "saved" PC and SP; T_ui's are wherever the CPU's registers physically
are right now.The real implementations diverge from the textbook picture
The PCB/TCB split above is the conceptual model every textbook teaches. Real kernels implement it two genuinely different ways, and the difference is worth knowing because it's a favourite "explain the internals" exam question.
| Concept | Linux | Windows |
|---|---|---|
| Underlying structure | A single type, task_struct, for every
schedulable entity — there is no separate TCB type at all. |
EPROCESS (process) and ETHREAD (thread) are
genuinely different structure types. |
| Where the PCB-equivalent lives | The fields that would be "process-level" simply are
shared between several task_structs, by pointer. |
Embedded KPROCESS inside EPROCESS, holding
the address-space root (DirectoryTableBase, loaded into CR3) and the process's
thread list. |
| Where the TCB-equivalent lives | The rest of the same task_struct:
its own kernel stack, saved registers, scheduling entity. |
Embedded KTHREAD inside ETHREAD, holding
the kernel stack, scheduling info, and a link back into EPROCESS.ThreadListHead. |
| What makes two threads "the same process" | They were created with
clone() using CLONE_VM (share mm_struct),
CLONE_FILES (share the file table), and CLONE_SIGHAND —
sharing is a per-resource choice at creation time. |
Their ETHREADs are linked into the same
EPROCESS.ThreadListHead — sharing is structural, not a creation-time flag. |
Depth · why Linux made this choice
Linux's clone() system call is the single primitive behind both
fork() (a new process: none of CLONE_VM/FILES/SIGHAND set, so
nothing is shared) and thread creation in a pthreads library (all three set, so almost everything is shared).
Rather than build two schedulable-entity types, Linux built one and made "is this a thread or a process"
a question about which resources two entities happen to share — a smaller kernel, at the cost of the
clean conceptual PCB/TCB split not existing anywhere as literal code.
Pitfall
Don't answer "what does a TCB contain?" by describing a struct you'd find in the Linux source, and don't answer "does Linux have a TCB?" with a flat yes or no. The PCB/TCB split is a model for reasoning about what must be saved and what can be shared; Linux and Windows are both valid, different, real implementations of that same model.
Practice 1.3
- A process has three threads. At this instant, one is RUNNING, one is READY, and one is BLOCKED on disk
I/O. For each, say whether its PC and SP are "live in the CPU" or "saved in its TCB."
Show solution
Only the RUNNING thread's PC and SP are live in the CPU registers. Both the READY thread and the BLOCKED thread have their PC and SP saved in their own TCBs — from the kernel's point of view, "not currently executing" is the only fact that matters for where the register state lives, regardless of why it isn't executing.
- Variation. Contrast what
fork()duplicates versus whatpthread_create()shares, in terms of theCLONE_*flags.Show solution
fork()callsclone()with none ofCLONE_VM,CLONE_FILES, orCLONE_SIGHANDset (the child gets copy-on-write copies of the address space and its own duplicated file descriptor table, though the underlying open file objects are still shared).pthread_create(), underneath, callsclone()withCLONE_VM | CLONE_FILES | CLONE_SIGHAND | CLONE_THREAD— the new task shares the address space, file table, and signal handlers outright, which is precisely what makes it "a thread" rather than "a process" in the textbook sense. - Interpretation. A crash dump shows two Linux
task_structs with differentpidvalues but the samemmpointer. What does that tell you, and what would you expect theirtgidvalues to be?Show solution
They're two threads of the same process — sharing an
mm_structmeans they share the address space, which is exactly the definition of "same process, different thread" in the PCB/TCB model. Theirtgid(thread group ID, the value user space sees as "the PID") should be identical, even though each has its own distinctpidfor the kernel's internal scheduling purposes. - Synthesis. In a microkernel (1.2), the filesystem server is itself a user-space process
that might run several of its own worker threads. Are those workers' TCBs "part of the operating system"
the same way a monolithic kernel's TCBs are?
Show solution
Not in the same sense. In a monolithic kernel, every TCB (Linux's
task_struct) is kernel data, maintained and trusted by the kernel itself. In a microkernel, the filesystem server's worker threads have TCBs maintained by whatever user-space threading library the server uses — the microkernel only knows about the server process's own kernel-level schedulable entities (however many the microkernel itself schedules for that server), not about threads the server manages internally. This is precisely the kernel-level vs. user-level distinction that Section 1.5 makes precise.
Switching Overhead & Performance Evaluation
Every context switch is free CPU time you don't get to spend on either thread.
Getting interrupted mid-task costs more than the interruption itself — you also lose the moment it
takes to reload what you were doing. A CPU has exactly the same problem when the scheduler switches it from
T_ui to T_save, just measured in microseconds instead of minutes.
The question this section answers precisely: how much, and when does it start to matter?
Direct cost and indirect cost
A switch has two kinds of cost, and only one of them shows up if you just count instructions.
A thread switch (T_ui → T_save, same process) pays
only the direct cost: registers and stack pointer change, but the page tables underneath don't, so every entry
already sitting in the TLB is still valid. A process switch pays direct cost and indirect cost:
the new process's page table root has to be loaded, which (on most hardware, most of the time) invalidates the
TLB, so the first several memory accesses after the switch each cost a full page-table walk instead of one fast
lookup.
Depth · not every process switch flushes the TLB anymore
Hardware with Process-Context Identifiers (PCID) on x86, paired with kernel support (mainline Linux since roughly 4.14), can tag TLB entries with the ID of the address space that created them. Two processes' entries can then coexist in the TLB without a flush on every switch — the indirect cost above is a worst case that applies to hardware or configurations without this tagging, not a law every system obeys.
Worked numbers
Published measurements of context-switch cost vary by roughly an order of magnitude depending on hardware and workload — direct costs in the low single-digit microseconds are typical on modern x86, with indirect costs from tens to hundreds of microseconds depending on how much of the working set has to be re-cached. The figures below sit inside that published range and are what we'll use for every calculation in this section.
| Component | Thread switch | Process switch |
|---|---|---|
| Register / PC / SP save-restore + scheduler bookkeeping | 1.8 µs | 1.8 µs |
| Address-space root reload | — | 0.4 µs |
| TLB re-warm (30 pages × ~50 ns/page) | — | 1.5 µs |
| Total | 1.8 µs | 3.7 µs |
A process switch costs roughly 2× a thread switch here — and the gap grows with the working set, since only the process switch's indirect term scales with it (Practice 1.4 asks you to show this).
Figure 4 · does the overhead actually matter?
Overhead as a fraction of CPU time, vs. scheduling quantum
drag the sliderPitfall
"Context switch cost" is not a single constant your kernel has memorised — the indirect term is a function of the incoming thread's working set, which changes call to call. Quoting one microsecond figure as "the cost of a context switch on Linux," full stop, is the kind of overclaim that loses marks on a CO2 question asking you to evaluate a scheduling design.
Callback to 1.1
This is the performance-vs-responsiveness tension from Section 1.1 made numeric: a shorter quantum makes the system feel more responsive (every thread gets the CPU back sooner) but spends a larger fraction of that CPU on switching rather than working. There's no quantum that's simultaneously "as short as possible" and "zero overhead" — only a quantum chosen for where the workload sits on that curve.
Practice 1.4
- Using the same per-switch costs, compute the overhead percentage for both switch types at a 5 ms
quantum.
Show solution
overhead% = cost ÷ (cost + q). For q = 5000 µs: thread switch = 1.8 ÷ 5001.8 ≈ 0.036%; process switch = 3.7 ÷ 5003.7 ≈ 0.074%. Both are still small, but note the process-switch figure is already double the thread-switch figure at every quantum — the ratio between them doesn't depend on q at all, only their absolute values do.
- Variation. Suppose
T_save's working set right after a switch-in is 100 pages instead of 30. Recompute the process switch's total cost and its ratio to a thread switch.Show solution
Indirect cost = 100 × 0.05 µs = 5.0 µs. Total process switch = 1.8 + 0.4 + 5.0 = 7.2 µs, which is 4.0× the 1.8 µs thread switch — double the earlier 2× ratio, entirely because the indirect term is the only one that scales with working set. This is exactly why "process switches cost roughly 2× a thread switch" is a working-set-dependent statement, not a hardware constant.
- Interpretation. A profiler reports that a server spends 4% of its CPU time "in context
switch overhead" at a 200 µs quantum. Is that figure alarming on its own?
Show solution
Not on its own — it matches almost exactly what this section's numbers predict for process switches at a pathologically short 200 µs quantum (1.82% for thread switches, and up to several times that for process switches with a larger working set), so the right response isn't "the kernel is broken," it's "this quantum is too short for this workload." The fix under consideration should be the scheduling quantum or the process/thread mix, not the context switch implementation itself.
- Synthesis. Explain, using 1.2's vocabulary, why a microkernel's IPC-heavy design is
especially sensitive to the numbers in this section.
Show solution
Every IPC round trip between two separate microkernel servers is, at the hardware level, an address-space switch — it pays the process-switch cost from this section's table, indirect term included, not the cheaper thread-switch cost. A monolithic kernel's internal function calls between "filesystem code" and "driver code" pay neither, because they never leave one address space. This is exactly the mechanism behind seL4's engineering effort in 1.2: shrinking that per-IPC direct-plus-indirect cost is the whole game for making a microkernel design competitive.
Kernel-Level vs. User-Level Threads
Does the kernel know your thread exists? The answer changes what happens when it blocks.
Imagine a manager who only tracks "departments," never individual staff. If one worker in a department is on
a long phone call, does the manager notice the other three workers in that department are free to keep working
— or does the manager just see "that department is busy" and route no new work to any of them? Whether an
OS's scheduler can see T_ui and T_save as two separate,
independently-schedulable things, or only as one thing called "process P," is exactly this question.
Two ways to implement "a thread"
A kernel-level thread (KLT), the 1:1 model, is created with a syscall and gets its own kernel-visible TCB (Section 1.3) — the kernel schedules it directly, exactly like any other thread. A user-level thread (ULT), the N:1 model, is implemented entirely by a library running in user space: the library keeps its own tiny scheduler, its own per-thread stacks and saved registers, and multiplexes many ULTs onto a single kernel-level entity that the OS is the only one aware of. From the kernel's point of view, an N:1 process with fifty user-level threads looks exactly like a process with one thread.
| Model | Kernel sees… | Examples |
|---|---|---|
| 1:1 (kernel-level) | Every thread, individually. | Linux NPTL (pthreads), Windows threads — the default on both OSes today. |
| N:1 (user-level) | One entity per process, no matter how many threads the app thinks it has. | Early Java "green threads" on Solaris; any cooperative coroutine library. |
| M:N (hybrid) | M kernel entities, onto which a user-space library schedules N>M threads. | Old Solaris (through Solaris 8) and early FreeBSD — both abandoned it (below). |
The trade-off that actually matters
User-level threads are cheap: creating one, or switching between two, never involves a syscall or the context-switch costs from Section 1.4 — it's a library-managed stack swap, often under a hundred nanoseconds. Kernel-level threads are expensive to create and switch by comparison, but they buy two things a user-level thread can never get on its own: genuine parallelism across multiple cores (the kernel can put two KLTs on two cores at once; two ULTs multiplexed onto one KLT never run simultaneously, no matter how many cores the machine has), and independence when one thread blocks — which the rest of this section makes concrete.
Figure 5 · T_save's autosave blocks — who else stalls?
T_save occasionally saves to a slow, cloud-synced folder; that write blocks for
250 ms while it happens — well past the ~100 ms threshold at which a delay reads as a
perceptible stall rather than instant response. Step through what each threading model does with that
250 ms.
Step through the timeline
step 1 of 51:1 — kernel-level threads
N:1 — user-level threads
Run the numbers on a full hour of typing: autosave fires every 30 s (120 times/hour), and suppose about
one save in six hits the slow, cloud-synced path. Under 1:1, none of that reaches T_ui
— the kernel keeps scheduling it independently the whole time. Under N:1, the cumulative freeze is
20 slow saves × 250 ms ≈ 5.0 seconds of frozen keystrokes, every hour, entirely
invisible to any profiler that only measures CPU time, because the CPU isn't busy during a block — it's
the user who's waiting.
Depth · why M:N looked ideal on paper and got abandoned in practice
M:N promises the best of both: cheap user-level switching most of the time, with enough kernel-level threads underneath to get real parallelism and to avoid the total-blocking problem, since the library can migrate a ULT to a different KLT if one blocks. Solaris shipped exactly this and retired it in Solaris 9 (2002) in favour of a plain 1:1 model; FreeBSD tried the same thing starting 2003 and abandoned it by 2006. Both post-mortems point at the same root cause: a two-level scheduler is a genuinely harder engineering problem — deciding how many KLTs to keep runnable, handling signals correctly, avoiding priority inversion between the two scheduling levels — and the complexity cost consistently outweighed the performance win once 1:1 implementations themselves got fast. Modern high-concurrency runtimes (Go's goroutines are the well-known current example) resurrect the M:N idea but sidestep the classic blocking problem by having the runtime intercept blocking syscalls itself and hand that kernel thread to other work — a more disciplined version of the same trick, built with three decades of hindsight.
Pitfall — two different things are both called "kernel thread"
A kernel-level thread (this section) is a threading model: a user-visible thread
that happens to be directly scheduled by the kernel. A kernel thread is something else
entirely: an OS-internal worker that executes only kernel code and belongs to no user process at all —
Linux's kworker and ksoftirqd threads, for instance, visible in
ps output but never running a single line of application code. Exam questions exploit
this overlap deliberately; read "kernel thread" in context before answering.
Pitfall
"User-level threads are strictly worse, use kernel threads always" is an overcorrection. For workloads with enormous numbers of short-lived, mostly-cooperating tasks — and no genuine need for one to keep running while another blocks on the same core — user-level threads (or coroutines, which are the same idea under a different name) are dramatically cheaper and often the right choice. The 1:1 model wins specifically when independent blocking and multicore parallelism both matter, which is common, but not universal.
Practice 1.5
- A process has one KLT hosting four ULTs. One ULT calls a blocking
read(). What happens to the other three?Show solution
Under the plain N:1 model, all three block along with it — the kernel only knows about the single KLT, and that KLT is now waiting in the kernel for the read to complete, so none of the process's user-level threads can run, regardless of what they were doing.
- Variation. Same setup, but the runtime is M:N with two KLTs backing those four ULTs. Does
the answer change?
Show solution
Only partially. The ULT that made the blocking call takes its KLT down with it, but the other KLT is still schedulable, so up to one of the remaining three ULTs can keep running if the runtime has migrated it there. The other two are still stuck waiting for a free KLT — M:N reduces the blast radius of one blocking call, it doesn't eliminate it, which is part of why the engineering complexity in the depth box above was judged not worth it.
- Interpretation. A four-core machine runs a process with eight user-level threads
multiplexed onto one kernel thread, all doing CPU-bound work with no blocking calls at all. Why is this
design guaranteed to leave three cores idle no matter how the user-level scheduler is tuned?
Show solution
The kernel can only place kernel-visible schedulable entities onto cores, and this process exposes exactly one. No amount of cleverness in the user-space scheduler changes how many kernel-visible entities exist; it can only decide which of the eight ULTs currently occupies that one KLT. True parallelism requires enough KLTs for the cores you want to use — a purely N:1 design is architecturally capped at using one core, permanently.
- Synthesis. Connect this section back to 1.3: what has to be true about a threading
library's internal data structures for it to implement ULTs at all, given that the kernel provides no TCB
for them?
Show solution
The library must maintain its own control-block equivalent for each ULT entirely in user memory — a saved program counter, stack pointer, register set, and stack region, precisely mirroring the TCB fields from 1.3, just outside the kernel's view and outside its protection. This is also exactly why a crash or infinite loop in the user-space scheduler can wedge every ULT in the process with no kernel-level recourse: there's no kernel-level record of them to fall back on.
Cheat Sheet & Self-Test
Everything above, compressed to what you'd want on the way into an exam.
1.1 Design goals
Six: performance, scalability, reliability, security, portability, interoperability.
Reliability = survives accidents. Security = survives adversaries.
Tensions: performance vs. security; performance vs. portability.
Rare ally: scalability + reliability, via redundancy.
1.2 Kernel architectures
Monolithic (Linux): services in kernel space, function calls, fast, one blast radius.
Microkernel (MINIX/QNX/seL4): services as user processes, IPC, isolated, address-space switch per hop.
Hybrid (Windows NT, XNU): kernel-space, modular internally, monolithic-like cost.
LKM ≠ microkernel — still kernel-mode code.
1.3 PCB & TCB
PCB (shared): PID, address space, open files, signal handlers, credentials.
TCB (private): TID, saved PC/SP, registers, own stack, priority, TLS.
Linux: one type, task_struct; sharing set by clone() flags.
Windows: EPROCESS/KPROCESS vs. ETHREAD/KTHREAD, genuinely separate types.
1.4 Switching overhead
cost = direct + indirect. Indirect (TLB/cache) applies to process switches only.
Illustrative: thread switch ≈ 1.8 µs; process switch ≈ 3.7 µs (~2×, grows with working set).
Overhead % = cost ÷ (cost + quantum) — shrinks fast as quantum grows.
1.5 Threading models
1:1 (kernel-level): kernel sees every thread; true parallelism; independent blocking.
N:1 (user-level): kernel sees one entity; one block stalls everything; cheap to switch.
M:N: best of both on paper; Solaris/FreeBSD both tried and abandoned it (2002 / 2006).
Mixed self-test
Deliberately not grouped by section — your exam won't be either.
- Two Linux
task_structs share the samemmpointer. Same process or different processes?Show solution
Same process, different threads of it — sharing an
mm_structis precisely what "same address space" means, which is the definition of two threads belonging to one process. (1.3) - A process exposes exactly one kernel-schedulable entity, internally running 20 user-level threads doing
pure CPU-bound work, on an 8-core machine. How many cores does it actually use, at most?
Show solution
One. The kernel can only place kernel-visible entities on cores; with only one such entity, no arrangement of the 20 user-level threads changes that. (1.5)
- Name one pair of design goals that reinforce each other rather than trade off, and the mechanism that
causes it.
Show solution
Scalability and reliability, via redundancy: replicating data or workers for fault tolerance also lets you spread load across the replicas, improving scalability as a side effect of a reliability mechanism. (1.1)
- True or false, and justify in one sentence: "a thread switch has zero indirect cost, full stop."
Show solution
False as stated. It's true that a thread switch's TLB entries stay valid, since the address space doesn't change — that's the specific indirect cost this chapter's worked numbers model, and it really is absent. But the incoming thread's data and code footprint can still differ from the outgoing thread's, so cache misses from that footprint change are still possible even within one process; "zero indirect cost" is only precisely true for the TLB term, not for every cache effect. (1.4)
- Which of the three kernel architectures pays a full address-space switch on essentially every driver
call, and why?
Show solution
Microkernel — drivers and the filesystem run as separate user-space processes, so reaching them from another process (or from the kernel) requires IPC, and IPC between distinct processes is, at the hardware level, an address-space switch. (1.2, 1.4)
- What Windows structure is embedded inside
EPROCESSto hold scheduling-relevant information about the process?Show solution
KPROCESS. The analogous embedding on the thread side isKTHREADinsideETHREAD. (1.3) - A project report claims "our OS is secure, reliable, fast, portable, scalable, and interoperable," with no
further discussion. What's the flaw, independent of whether each claim happens to be true?
Show solution
It names no trade-off — every real design buys some of these goals by spending others, and a claim that lists all six with nothing sacrificed either hasn't been stress-tested or isn't being honestly reported. A credible design claim says which goal was the priority and which goal paid for it. (1.1)
- Why did Solaris retire its M:N threading model in favour of 1:1, starting with Solaris 9 in 2002?
Show solution
Not primarily performance — it was engineering complexity. A two-level scheduler has to decide how many kernel threads to keep runnable, handle signal delivery correctly across both levels, and avoid priority inversion between them; once 1:1 implementations became fast enough on their own, that complexity cost stopped being worth paying. (1.5)
Further reading
- Stallings, Operating Systems: Internals and Design Principles, 9th ed., Ch. 2 & 4. This course's primary text; closest match to this chapter's design-goals framing and PCB/TCB terminology.
- Silberschatz, Galvin & Gagne, Operating System Concepts, 10th ed., Ch. 3 & 4. The clearest textbook PCB/TCB diagrams; its classic word-processor multithreading example is the ancestor of this chapter's WriteWell spine.
- Tanenbaum & Bos, Modern Operating Systems, 5th ed., Ch. 1. The strongest historical treatment of monolithic vs. microkernel vs. hybrid design.
- The seL4 Foundation, "Performance," sel4.systems/performance.html. Primary source for the current microkernel IPC-cost figures used in Section 1.2's depth box.
- Oracle, "Multithreading in the Solaris Operating Environment" (Solaris 9 whitepaper). Primary source for the M:N-to-1:1 threading history in Section 1.5.
Before this chapter — your syllabus lists these as bridge/self-study topics, and this chapter assumes them: the instruction cycle and hardware-level context switching (Stallings, Computer Organization and Architecture, 10th ed., pp. 168–210), what an OS is for in the first place, and the memory hierarchy (same text, pp. 290–320). If Section 1.4's talk of TLBs and cache re-warming felt unfamiliar rather than just fast, that's the gap to fill before continuing.
Where this goes next — Unit 2, CPU Scheduling and Performance Evaluation, picks up
exactly where 1.4 stopped: given that every switch has a cost, which thread runs next? WriteWell's
T_ui and T_save come back as the running example for FCFS, SJF,
Round Robin, and priority scheduling, with the same kind of worked, verified numbers.