Kernel and userland
The kernel runs in ring 0 and owns every CPU, memory page, and device. Your programs run in ring 3 and can only touch hardware by asking the kernel via a syscall — a deliberate, auditable crossing of the privilege boundary.
You type ls in your terminal. The shell calls openat() to read the directory, then write() to send the names to your screen. Both operations touch hardware — the disk and the display — but your shell code never talks to hardware directly. Something in the middle handles that: the kernel.
The split between “code that owns hardware” (the kernel) and “code that doesn’t” (everything else — your shell, Python interpreter, nginx) is the most important structural boundary in an operating system. Every crash, every permission error, every segfault, every container escape you will ever debug touches this boundary. Understanding it is the prerequisite for understanding everything else in this track.
After this lesson you can explain what the kernel owns, what userland is and why it exists, what a syscall is and why it is the only crossing point, and what CPU privilege rings mean in practice.
The kernel owns the hardware — exclusively. When Linux boots, the kernel loads first and immediately takes control of every CPU core, every memory page, every network card, every disk controller. No other software can directly read a byte from disk or send a packet without the kernel’s cooperation. This is not a convention or a suggestion — the CPU hardware enforces it.
CPUs implement privilege levels called rings. Ring 0 (kernel mode) is the most privileged: code here can execute any instruction, map any memory, talk to any device. Ring 3 (user mode) is the least privileged: code here cannot address hardware directly. Modern x86-64 and ARM CPUs only use rings 0 and 3 in practice.
The kernel runs in ring 0. Every process you launch — your shell, your database, your editor — runs in ring 3.
Userland is everything above the kernel. The moment the kernel finishes booting, it launches the first userland process (PID 1, which becomes systemd or another init). From there, every program you ever run is a userland process. The C standard library (glibc), bash, nginx, Python, the entire package ecosystem — all of it is userland. They share one thing: they cannot talk to hardware directly.
This separation protects the system. A bug in your application code cannot corrupt kernel memory — the CPU will raise a fault first. A malicious program cannot open a raw socket and sniff network traffic without asking the kernel, which will check permissions.
The practical consequence: if your program crashes, it crashes. The kernel keeps running. If the kernel crashes, the whole box goes down — that’s a kernel panic.
A syscall is how userland asks the kernel for privileged work. When your program needs to read a file, write to stdout, open a socket, or fork a child process, it issues a syscall. The CPU transitions from ring 3 to ring 0 in a controlled way — the program cannot choose what happens in ring 0, only which syscall it invokes and with what arguments.
# strace shows every syscall a process makes
strace ls /tmp 2>&1 | head -20You will see calls like openat(AT_FDCWD, "/tmp", O_RDONLY|O_DIRECTORY) and getdents64(...). Those are ls asking the kernel to open the directory and read its entries. The kernel checks that ls has permission, then does the hardware work, then returns the result to ring 3.
Linux has around 400 syscalls. You use dozens of them every time you run a command, but they are hidden behind library wrappers. fopen() in C calls openat() under the hood. Python’s open() eventually calls openat() too.
The syscall boundary is auditable and enforceable. Because every interaction with hardware must cross this boundary, the kernel can log, filter, and block syscalls. seccomp (secure computing mode) lets you restrict which syscalls a process may invoke. Docker uses this to prevent containers from calling dangerous syscalls like reboot() or kexec_load(). strace captures every crossing for debugging.
# See which syscalls a process uses (summarised)
strace -c ls /tmpThe output shows counts and time per syscall. A process that has never called socket() almost certainly has no network activity — that is a meaningful security assertion.
The cost of a syscall is real but modest. Crossing the ring boundary requires the CPU to save the process state, switch to the kernel stack, execute the syscall handler, then restore state and return. On modern hardware this costs roughly 100–300 ns per crossing. For most programs this is negligible. For high-throughput I/O code (a database writing millions of rows, a proxy handling millions of packets), the aggregate cost matters — which is why kernel developers invest in batching APIs like io_uring that let userland queue many operations and cross the boundary once.
Understanding this cost is why you will later learn about sendfile(), zero-copy networking, and why memory-mapped files can outperform read() loops.
Trace what happens when you run cat /etc/hostname.
strace cat /etc/hostname 2>&1 | grep -E 'open|read|write'You will see:
openat(AT_FDCWD, "/etc/hostname", O_RDONLY)— kernel opens the file, returns a file descriptor (integer).read(3, "myserver\n", 131072)— kernel reads bytes from disk into a kernel buffer, copies them to userland. The3is the file descriptor.write(1, "myserver\n", 9)— kernel writes bytes from userland memory to stdout (fd 1), which the terminal driver renders.
Three syscalls, three ring crossings. The file never “moves” from disk to your terminal — the kernel mediates every step. cat itself does nothing except call the right syscalls in the right order.
▸Why this works
macOS uses a BSD-derived kernel (XNU) with the same ring model, but different syscall numbers and conventions. strace does not exist on macOS — use dtruss (requires root) or dtrace instead. The conceptual split (kernel/userland/syscall boundary) is identical; the implementation details differ. Everything in this track uses Linux syscall names and numbers.
▸Common mistake
A common misconception: “the kernel is just a program.” It is a program in the sense that it is compiled C code — but it runs at a privilege level that makes it categorically different. The kernel cannot be killed with kill. It cannot be debugged with gdb from userland. It has no parent process. When you hear “the OS crashed,” that is the kernel panicking — the only way to recover is a reboot.
A process running in ring 3 needs to write a byte to disk. What must happen first?
The kernel runs in ring 0 and owns every hardware resource on the machine. Userland processes run in ring 3 and are physically prevented by the CPU from touching hardware. The only crossing point is the syscall boundary: a controlled trap from ring 3 to ring 0, which the kernel uses to check permissions and do the privileged work. Every file read, network packet, and process fork crosses this boundary. Tools like strace make it visible. The cost is ~100–300 ns per crossing — negligible for most code, material for high-throughput I/O.
Practice
Start at the top. Tasks go easiest → hardest: recall a fact, apply it to a case, then a senior-level stretch. Open one, attempt it, then reveal.
Something unclear?
Ask a question about this lesson. Questions are anonymous and go straight to the author to make the lesson better.