← All chapters
Chapter 34· 18 min read · illustrated

Case Study: Linux & Android

Every concept in this course, wired together inside the one kernel your career actually runs on

Until now we have studied operating-system ideas one at a time and mostly in the abstract: a process here, a scheduler there, page tables in their own chapter, file systems in another. That is the right way to learn — but it is not how any of it exists. In a real machine these pieces are not separate lectures; they are one program, running right now, holding your web server and your database and your editor all at once. This chapter takes everything we have built and shows it assembled inside a single, concrete, wildly successful system: Linux.

We chose Linux because you are almost certainly running on it already, whether you notice or not. The cloud instance behind your API, the container your CI spins up, the Raspberry Pi on your desk, and — through Android — the phone in your pocket are all Linux. Learning how this one kernel does processes, scheduling, memory, files, and security is not a detour into trivia; it is the moment the whole course stops being theory and becomes the thing you deploy to on Monday.

Along the way we will keep pointing backward. When we meet task_struct you will recognise the process model from Part B; when we meet CFS you will recognise the scheduler from Chapter 12; when we meet the VFS you will recognise the file-system abstraction from Part E. Then we climb one layer higher to Android, which takes this same kernel and builds an entire mobile platform on top of it, reusing Linux where it can and extending it where it must.

01

Why Linux — the OS the world actually runs on

If you had to bet on a single piece of software being underneath your code, Linux is the safe bet. It runs the overwhelming majority of web servers and cloud instances, every one of the world’s fastest supercomputers, the routers and TVs and cars quietly embedded around you, and — as the kernel inside Android — most of the smartphones on Earth. No operating system in history has ever run on such a range of machines, from a $5 microcontroller to a data centre with a hundred thousand cores.

For an engineer that ubiquity has a practical payoff: the concepts you learn here transfer almost everywhere. A `fork` on your laptop behaves like a `fork` on the production box; the scheduler that runs your test suite is the scheduler that runs the exchange matching engine. Learning Linux deeply is one of the highest-leverage investments a working developer can make, precisely because you meet the same kernel again and again for the rest of your career.

Architecturally, Linux is a modular monolithic kernel — a phrase worth unpacking. Monolithic means the core services (the scheduler, memory manager, file systems, network stack, and device drivers) all run together in one privileged kernel address space, calling each other as fast function calls rather than sending messages the way a microkernel would. Modular means that despite living in one space, the kernel can load and unload chunks of itself — loadable kernel modules, typically device drivers — at runtime, so you do not rebuild the kernel to add support for a new network card.

Monolithic kernel
All core OS services run together in one kernel address space and call each other directly — fast, but a bug in one part can take down the whole kernel.
Modular
The kernel can load and unload code (mostly drivers) at runtime via kernel modules, without a reboot or rebuild.
Open source
The full source is public under the GPL; anyone can read, audit, patch, and ship it — which is why it spread to every kind of hardware.
Userland
Everything that is not the kernel — the shell, libraries, and programs — running in user space on top of the syscall interface.

The frame for this chapter: Treat Linux as the course’s answer key. Every abstraction we defined — process, thread, scheduler, page table, VFS, namespace — is a real, named, inspectable thing here. This chapter is where the theory meets a system you can actually run commands against.

Tap to enlarge
02

A short history — from Unix to a billion devices

The story starts with Unix, written at Bell Labs around 1969 by Ken Thompson and Dennis Ritchie. Unix introduced ideas so good we still build on them — a hierarchical file system, "everything is a file", small composable tools, and a portable implementation in the new C language. But Unix became commercial and fragmented into competing, expensive versions, which left a gap: a Unix-like system anyone could freely study and run.

Two efforts filled that gap. In the 1980s Andrew Tanenbaum wrote MINIX, a small Unix-like system meant for teaching OS courses, and Richard Stallman’s GNU project began building free replacements for all the Unix userland tools — a compiler, a shell, the core utilities — but still lacked a finished kernel. Then in 1991 a Finnish student named Linus Torvalds, inspired by MINIX, announced a "just a hobby, won’t be big and professional" kernel of his own. That kernel was Linux.

The pieces fit together perfectly: Linus had a kernel and no tools; GNU had tools and no kernel. Combined under the GPL licence, they formed a complete, free operating system — which is why purists call it GNU/Linux. This is also the cleanest place to nail the distinction that confuses newcomers: the kernel is the single project Linus still coordinates, while a distribution (Debian, Ubuntu, Fedora, Arch, Android) is that kernel packaged together with a userland and a package manager into something you can install.

Ask a running system which kernel it is and which distribution wraps it.bash
$ uname -a
Linux devbox 6.8.0-45-generic #45-Ubuntu SMP x86_64 GNU/Linux
#        ^kernel version           ^same kernel, any distro

$ cat /etc/os-release | head -2
PRETTY_NAME="Ubuntu 24.04.1 LTS"   # the distribution
NAME="Ubuntu"                      # kernel + userland + apt
Unix
The 1969 Bell Labs ancestor; source of the file model, the shell, and the design philosophy Linux inherits.
The kernel
The Linux project proper — one codebase, versioned (6.x today), coordinated upstream by Linus and maintainers.
Userland
The GNU tools and libraries (shell, coreutils, glibc) that surround the kernel to make a usable system.
Distribution
Kernel + userland + a package manager, bundled and maintained together — Ubuntu, Fedora, Debian, and Android are all distributions in this sense.

Why the split matters: When someone says "a Linux bug", ask which layer: a kernel panic is a very different animal from a broken package or a misconfigured shell. Most day-to-day "Linux" problems live in the userland, not the kernel.

Tap to enlarge
03

Processes & threads — the unified task model

Back in Part B we drew a sharp line between processes and threads. Linux draws that line in a way that surprises many engineers: internally, it barely distinguishes them at all. The kernel schedules a single kind of entity — a task — and every task is represented by one big structure in kernel memory called task_struct. A single-threaded process is one task; a process with ten threads is ten tasks. There is no separate "thread object" and "process object"; there are just tasks that share more or less with each other.

task_struct is the process control block we described abstractly, made concrete. It holds the task’s PID, its state (running, sleeping, stopped, zombie), a pointer to its memory (an mm_struct describing the address space), a pointer to its open-file table, its signal handlers, its scheduling priority, and a pointer to its parent. When we said the OS keeps "bookkeeping" for every process, task_struct is that bookkeeping — thousands of them chained together in the kernel at any moment.

The trick that unifies threads and processes is a single system call: `clone`. Both `fork` and `pthread_create` are built on top of it; they differ only in what they ask to share. Call `clone` telling it to copy the address space, the file table, and the signal handlers, and you get a new independent process — that is `fork`. Call `clone` telling it to share the same address space and file table with the caller, and you get a new thread inside the same process. Same mechanism, different sharing flags.

This is where PID and TGID come in. Every task has its own unique PID (process ID). Threads of one process each have a distinct PID but share a common TGID (thread group ID), which equals the PID of the first thread — and that TGID is what user space calls "the process ID". So `getpid()` returns the TGID (stable across all threads), while the kernel’s per-task ID is the TID. It sounds like a naming quirk; it is actually the whole reason `top` and `ps` can show you either processes or threads from the same underlying table.

The unified task model, made visible: same process, many tasks (threads).bash
$ ps -eLf | grep -c chrome        # -L shows one line per THREAD
142                                # 142 tasks, far fewer processes

$ ls /proc/$$/task                 # every thread of this shell...
4021                               # ...appears as a task dir here

$ cat /proc/self/status | egrep "^(Pid|Tgid|Threads):"
Pid:    4055                       # this task’s id (TID)
Tgid:   4055                       # the "process id" user space sees
Threads: 1

Callback to Part B: The process/thread distinction you learned is real and useful for reasoning — but remember that under Linux it is one abstraction (the task) with a sharing dial. "A thread is a process that shares memory" is not a metaphor here; it is literally how `clone` implements it.

Tap to enlarge
04

Scheduling — CFS, EEVDF, and the classes above them

In Chapter 12 we studied scheduling as a set of goals in tension: fairness, responsiveness, throughput. Linux resolves them with a layered design. At the top sit scheduling classes, consulted in strict priority order. If any real-time task is runnable, it runs before any normal task, full stop; only when the real-time and deadline classes have nothing to do does the ordinary class — the one that runs virtually every process you have ever launched — get the CPU.

That ordinary class was for over a decade the Completely Fair Scheduler (CFS), and in kernel 6.6 (2023) it was replaced by EEVDF (Earliest Eligible Virtual Deadline First). Both chase the same ideal we described: give every runnable task a fair share of the CPU. CFS tracked each task’s virtual runtime — roughly, how much CPU time it has consumed, weighted by priority — and always ran the task with the least, keeping the runnable tasks in a red-black tree so "pick the most-deserving task" is a fast leftmost-node lookup. EEVDF refines this by also honouring a per-task deadline, so latency-sensitive tasks that wake up briefly get served promptly rather than merely fairly.

Priority in the normal class is expressed as a nice value from -20 (greediest) to +19 (most generous), and it is not a hard priority — it is a weight. A lower nice value multiplies a task’s share so its virtual runtime accrues more slowly, letting it run more often, without ever starving the polite tasks entirely. This is the practical knob you reach for when a background job (a backup, a compile) is stealing responsiveness from foreground work.

Inspect and change scheduling priority on a live system.bash
$ nice -n 10 tar czf backup.tgz /data   # start a job at low priority

$ ps -o pid,ni,cls,cmd -p 4102          # NI = nice, CLS = class
  PID  NI CLS CMD
 4102  10  TS  tar czf backup.tgz /data  # TS = time-sharing (normal)

$ chrt -p 4102                          # query the scheduling class
pid 4102’s current scheduling policy: SCHED_OTHER
pid 4102’s current scheduling priority: 0
Scheduling class
A priority tier (deadline > real-time > normal > idle); higher tiers always preempt lower ones.
CFS / EEVDF
The normal-class scheduler (SCHED_OTHER) that fairly shares the CPU among ordinary tasks; EEVDF replaced CFS in kernel 6.6.
Virtual runtime
Weighted CPU time consumed; the scheduler favours the task that has received the least, which is how "fairness" is enforced.
Nice value
A -20…+19 weight on a normal task’s CPU share; lower is greedier. The real-time classes ignore it entirely.

Callback to Chapter 12: The abstract "fair-share scheduler" we designed is not a toy — it is CFS/EEVDF, running on the machine you are reading this on. When you `nice` a batch job or pin a real-time thread, you are steering exactly the mechanism from that chapter.

Tap to enlarge
05

Memory management — virtual memory, the page cache & the OOM killer

Everything from Part D shows up here at once. Each Linux process gets its own virtual address space, described by that mm_struct we met earlier and translated to physical frames by the page tables and the hardware MMU. Memory is handed out lazily through demand paging: when you allocate a gigabyte, Linux does not find a gigabyte of RAM — it just records the mapping, and only when you actually touch a page does a page fault fire and a real physical frame get wired in. This is why a program can `malloc` far more than exists and only pay for what it uses.

The single most important thing to understand about Linux memory in practice is the page cache. Linux refuses to let RAM sit idle: any memory not needed by processes is used to cache file data read from or written to disk. This is why a freshly booted server shows lots of "free" memory and a busy one shows almost none — the "missing" memory is not lost, it is caching your files to make the next read instant. The famous line "Linux ate my RAM" is a misunderstanding of exactly this: cache is reclaimable the moment a process genuinely needs the memory.

Two more features earn their place in an engineer’s mental model. Huge pages let the kernel map memory in 2 MB (or 1 GB) chunks instead of 4 KB, which slashes the number of page-table entries and TLB misses for memory-hungry workloads like databases and JVMs. And the OOM killer is Linux’s grim last resort: when RAM and swap are both exhausted and no memory can be reclaimed, rather than freezing the whole system the kernel scores every process by a "badness" heuristic (mostly how much memory it uses) and kills the worst offender to survive. If a container or a service on your box vanishes with no crash in its own logs, `dmesg` showing an OOM kill is the first thing to check.

See virtual memory, the page cache, and the OOM killer in the wild.bash
$ free -h
               total        used        free      buff/cache
Mem:            31Gi        8.2Gi       1.1Gi        22Gi
#                                      ^small    ^page cache, reclaimable

$ grep -i huge /proc/meminfo | head -2
AnonHugePages:   1048576 kB     # transparent huge pages in use
Hugepagesize:       2048 kB     # 2 MB pages, not 4 KB

$ dmesg | grep -i "killed process"
Out of memory: Killed process 5123 (java) total-vm:9GB ...

Callback to Part D: Page tables, the MMU, demand paging, and page replacement were the theory. The page cache is where they pay off daily: most of your server’s "used" memory is the kernel doing exactly the caching-and-reclaim dance we studied, quietly making your I/O fast.

Tap to enlarge
06

The file system stack — VFS and “everything is a file”

In Part E we described the file system as an abstraction over raw blocks. Linux makes that abstraction explicit and reusable through the Virtual File System, the VFS. Your program calls `open`, `read`, `write`, `close` — one uniform API — and the VFS is the layer that routes each call to whichever concrete file system actually backs that path. The application never knows or cares whether the bytes live on an ext4 partition, an XFS volume, a Btrfs snapshot, a remote NFS share, or a RAM-backed tmpfs. One interface, many implementations: it is polymorphism, done in C, at the heart of the kernel.

The VFS works by defining a set of operations — how to look up a name in a directory, how to read an inode, how to read and write a file’s data — as a table of function pointers. Each file system fills in that table with its own implementations at mount time. So "adding a new file system to Linux" means writing those functions; every existing program then works on it unchanged, because it only ever spoke to the VFS. This is the exact abstraction-layer idea from the introduction, realised as the kernel’s most elegant piece of engineering.

The VFS is also what makes the old Unix slogan "everything is a file" literally true in Linux. Because a file is just an object that exposes read/write through the VFS, the kernel exposes far more than disk data this way. Your keyboard, disk, and null device appear as files under /dev. Network sockets and pipes are file descriptors. And two special virtual file systems expose the kernel’s own mind: /proc presents per-process and kernel state as readable files, and /sys presents devices and kernel objects. Every /proc path we have used in this chapter is the VFS at work — there is no "proc.txt" on any disk; the kernel synthesises those files on read.

“Everything is a file”: reading kernel and process state through the VFS.bash
$ mount | awk '{print $5}' | sort -u    # the many FS types behind one API
ext4  proc  sysfs  tmpfs  xfs

$ cat /proc/cpuinfo | grep -m1 "model name"   # CPU info, as a "file"
model name : Intel(R) Xeon(R) Platinum 8375C

$ cat /proc/loadavg                    # live load average, synthesised
0.42 0.55 0.61 2/834 5210

$ echo 1 > /proc/sys/net/ipv4/ip_forward   # writing a file tunes the kernel

Callback to Part E: Inodes, directories, and journaling live below the VFS, inside ext4 and friends. The VFS is the ceiling over all of them — the reason a single `read()` in your code works identically on a laptop SSD and a cloud network volume.

Tap to enlarge
07

Security — from Unix permissions to namespaces & seccomp

Linux security is built in layers, and it starts with the classic Unix model from Chapter 32. Every process runs as a user (UID) and belongs to groups (GIDs); every file carries read/write/execute permission bits for its owner, its group, and everyone else. This is simple and has held up for fifty years, but it has one blunt edge: the root user (UID 0) can do everything, which makes "I just need to bind to port 80" escalate to "run the whole thing as root".

Capabilities fix that bluntness by chopping root’s omnipotence into dozens of independent privileges. Binding to a low port is CAP_NET_BIND_SERVICE; loading a kernel module is CAP_SYS_MODULE. You can grant a process exactly the one it needs and nothing more, so a web server can bind port 80 without the power to reformat a disk. This is the principle of least privilege made operational.

The two features that made containers possible are namespaces and cgroups, and they are pure Linux kernel mechanisms — Docker and Kubernetes are just clever userland orchestration on top. A namespace virtualises what a process can see: a PID namespace gives it its own process-number space (so its "PID 1" is not the host’s), a mount namespace its own file-system view, a network namespace its own interfaces. Cgroups (control groups) virtualise what a process can use: how much CPU, memory, and I/O it may consume. A container is nothing more mystical than a normal process running inside a fresh set of namespaces with cgroup limits attached.

Two more layers harden the picture. Mandatory Access Control systems — SELinux (Red Hat) and AppArmor (Ubuntu) — sit above the discretionary Unix bits and enforce policy the file owner cannot override, confining even root to a labelled sandbox. And seccomp (secure computing) lets a process filter its own system calls, presenting the kernel with an allow-list so that even if the process is compromised, it simply cannot invoke the syscalls its filter forbids. This is how container runtimes and browsers shrink their attack surface at the exact boundary — the syscall — we studied in Chapter 6.

The security layers, made visible.bash
$ ls -l /etc/shadow          # classic permissions: only root may read
-rw-r----- 1 root shadow 1234 Jul  9 09:00 /etc/shadow

$ getcap /usr/bin/ping       # a capability instead of setuid-root
/usr/bin/ping cap_net_raw=ep

$ lsns -t pid | head -3      # namespaces = the basis of containers
        NS TYPE  NPROCS   PID USER   COMMAND
4026531836 pid      312     1 root   /sbin/init
4026532210 pid        8  5321 nobody /app/server   # a container

Callback to Chapter 32: Users, groups, and permission bits were the foundation; capabilities, namespaces, cgroups, MAC, and seccomp are the modern kernel features stacked on top. Every container you have ever run is these primitives, composed. There is no "container" object in the kernel — just processes wearing namespaces and cgroups.

Tap to enlarge
08

Android — a whole platform on a Linux kernel

Android is the most widely deployed operating system in the world, and at its base is a Linux kernel — the same task_struct, the same scheduler, the same VFS and page cache we have spent this chapter on. Google did not write a new OS from scratch; they took Linux and built a mobile platform on top of it, adding what a phone needs and reusing everything they could. Understanding Android is therefore mostly a matter of seeing where it reuses Linux and where it extends it.

The kernel is Linux with a few phone-specific additions, some of which have since flowed back upstream: a low-memory killer more aggressive than the desktop OOM killer (a phone must free memory before it stalls), wakelocks for aggressive power management, and — most importantly — Binder, a high-performance inter-process communication driver. Android is intensely multi-process: every app and every system service runs in its own process, and they talk constantly. Binder is the IPC mechanism that makes those cross-process calls fast and secure, and it is the backbone of the entire framework. It is a concrete answer to the IPC chapters: this is what production IPC looks like at scale.

Apps do not run as native binaries; they run as bytecode on the Android Runtime (ART), which ahead-of-time and just-in-time compiles app code to native instructions. To launch apps quickly, Android uses a beautiful trick called the zygote: a single process starts at boot, pre-loads the runtime and all the common framework classes, and then simply `fork`s itself every time a new app launches. Thanks to copy-on-write (straight from Part D), each new app instantly shares all those pre-loaded pages with zygote and only copies what it changes — so app startup skips re-loading megabytes of shared runtime. It is `fork` and copy-on-write from this course, used to solve a real product problem.

Finally, the Android security model is the Linux security model, applied with unusual discipline. Each installed app is given its own unique UID, so the kernel’s ordinary user-based permissions isolate apps from each other for free — one app literally cannot read another’s files because it is a different "user". SELinux enforces mandatory policy on top, and seccomp filters restrict the syscalls apps may make. The per-app sandbox that keeps a flashlight app from reading your banking app’s data is not exotic mobile magic; it is UIDs, permissions, SELinux, and seccomp — the Chapter 32 and Chapter 6 primitives, arranged for a billion phones.

Modified Linux kernel
Standard Linux plus phone-oriented additions: Binder, wakelocks, and an aggressive low-memory killer.
Binder
Android’s fast, secure IPC driver — the bus every app uses to call system services across process boundaries.
ART
The Android Runtime that compiles and runs app bytecode; it replaced the older Dalvik VM.
Zygote
A warm process that pre-loads the runtime once and forks to launch every app, sharing pages via copy-on-write.
App sandbox
Isolation by giving each app its own UID, reinforced by SELinux and seccomp — Linux security primitives, applied per app.

The synthesis: Android is this entire course in your pocket: tasks and clone, a fair scheduler, virtual memory and copy-on-write, the VFS, IPC, and the Unix security model — recombined into a platform. Nothing in it is new physics; it is the same OS ideas, extended with taste.

Tap to enlarge
09

Why engineers care — the substrate under everything you ship

Step back and the reason this chapter matters becomes obvious: Linux is the substrate under nearly everything you build. Your CI runner is a Linux container; your staging box is a Linux VM; your production fleet is Linux; the base image in your Dockerfile is a Linux userland. When you understand the kernel underneath, the whole stack stops being a series of magic boxes and becomes one system you can reason about end to end.

Concretely, that understanding turns into debugging power. When a service is slow, you know to count syscalls with strace and check the scheduler and load average. When it is killed with no crash log, you know to look for the OOM killer in dmesg. When memory looks "full", you know the page cache is reclaimable and not the problem. When a container behaves strangely, you know it is a process in namespaces with cgroup limits, and you can inspect exactly those. And the single most useful habit of all: read /proc. Every fact about every running process — its memory, its open files, its threads, its limits — is a file away.

The everyday toolkit: interrogate what you are actually running on.bash
$ top -o %MEM               # live processes sorted by memory
$ ps -eLf                   # every process AND thread (task)
$ cat /proc/<pid>/status    # one process: state, memory, threads
$ ls /proc/<pid>/fd         # exactly which files/sockets it holds open
$ cat /proc/<pid>/limits    # its resource limits (fd count, memory...)
$ dmesg -T | tail            # recent kernel events (OOM kills, drivers)

We have now seen the whole course assembled inside one real system. Processes became task_struct and clone; the scheduler became CFS and EEVDF; virtual memory became demand paging and the page cache; the file system became the VFS and /proc; security became permissions, capabilities, namespaces, and seccomp; and all of it reappeared, recombined, as Android. The abstractions are no longer floating in the air — they have names, they have files, and you can run commands against them.

What is next: Linux is one design point, not the only one. In the next chapter we hold it up against Windows — a system with the same jobs to do but different answers: a hybrid kernel, a different process and threading model, a different security architecture. Seeing the contrast is what turns "how Linux does it" into "how operating systems do it".

Tap to enlarge