How the Linux Kernel Works · Day 1 of 14
Kernel Mode Is a Bit and a Mapping
Run ps aux on any Linux machine and look for the kernel. It isn’t there. You will find systemd at PID 1, some daemons, your shell, the ps you just ran. You will find a handful of names in square brackets, like [kworker/0:1] and [ksoftirqd/0], and we will come back to those. But there is no process called linux or kernel sitting at the top of the list, waiting for requests. The thing that manages every page of memory and every byte on your disk does not appear in the process list at all.
That is not an oversight in ps. The kernel is not a program that runs. It is code that your program runs, after the CPU has been told to trust it.
The mental model most people arrive with is a client and a server. Your process wants to read a file, so it sends a message to the kernel, the kernel wakes up, does the work, and sends an answer back. It is a reasonable guess and it is wrong in a way that will trip you up for the rest of this course. There is no message. There is no handoff to another thread. When you call read(), your thread keeps running. It just starts running different code, at a higher privilege, with a different stack.
Here is what actually happens on an x86-64 machine. Your process executes a single instruction called syscall. The CPU does a few things at once. It saves the address of the next instruction into the register RCX and the flags into R11. It loads a new instruction pointer from a control register called LSTAR, which the kernel filled in at boot with the address of its entry function. And it clears two bits in the code segment register, taking the current privilege level — the hardware’s idea of who is allowed to do what — from 3 down to 0. Ring 3 is user mode. Ring 0 is kernel mode. On ARM64 the same distinction is called an exception level, EL0 for user and EL1 for kernel, but the idea is identical.
That’s it. That is the transition. The CPU does not even switch stacks for you on x86-64 — Linux has to do that itself in the first few instructions of the entry path, swapping in a pointer to the kernel stack that belongs to this particular thread. Every task on the system has one, 16 kilobytes on x86-64, allocated when the task was created and sitting there unused while the task is in user mode. Your thread now runs kernel code on its own private kernel stack. The kernel’s idea of “who am I working for right now” is a macro called current, and it resolves to a pointer to the structure describing your task. Not a lookup. Not a queue. Just the task that happens to be executing.
You can see this in the accounting. Time a program that does a lot of I/O and the shell will split the result into user time and system time. The system time is your process’s time. It was charged to your process because your process was on the CPU, burning cycles, the whole way through. The kernel had no separate thread to charge it to.
So if the kernel is just code you jump into, what stops you jumping into the middle of it? Two mechanisms, and they are the whole story.
The first is the privilege bit itself. You cannot set it. The syscall instruction raises privilege, but it does not let you choose where you land — the destination came from LSTAR, which only ring 0 code can write. Same for interrupts and faults, which land at addresses in a table the kernel controls. There are exactly a few doorways and the kernel picked all of them.
The second is the page table, and it is the more interesting half. Every page of memory has a handful of permission bits in the structure that maps it, including one that says whether ring 3 is allowed to touch this page at all. Kernel pages don’t have it set. If your process dereferences a kernel address, the hardware faults before anything is read. You get a segmentation fault, which is the kernel handling a fault your own thread just caused, in your own thread.
Now the part that makes the whole design work. On x86-64 with the usual four-level page tables, an address space is split in half. The bottom 128 terabytes, from zero up to 0x00007fffffffffff, is yours. The top half, from 0xffff800000000000 upward, is the kernel’s. And the kernel’s half is the same in every process. Your shell, your browser, and a database server all have different mappings in the bottom half and byte-for-byte identical mappings in the top half.
This is why a system call is cheap. The privilege level changes, but the address space does not. No page table swap, no flushing of the cached address translations the CPU keeps, no scheduling decision, no other process involved. On the order of a hundred nanoseconds for a trivial call like getpid(). Compare that to switching to a different process, which costs an order of magnitude more, for exactly the reasons a syscall avoids.
Those bracketed names in ps are the real exceptions, and they prove the point. Kernel threads like [kworker/0:1] are tasks with no user-space half at all — no user pages, nothing mapped in the bottom 128 terabytes. They exist because some kernel work genuinely has no process to borrow. But they are a small minority. The overwhelming majority of kernel code runs inside a thread that started life as somebody’s ls.
The shared mapping was always a trade. It buys speed and costs isolation, because the kernel’s memory is sitting in the address space of every process that might want to attack it. For decades the permission bits were considered enough. Then in January 2018, Meltdown showed that on many Intel processors, speculative execution would read those kernel pages and leave traces in the cache before the permission check caught up. The fix, called page table isolation, gives user mode a stripped-down page table with almost nothing of the kernel in it, and switches tables on every entry and exit — putting back exactly the cost the shared mapping existed to avoid. On syscall-heavy workloads the slowdown was severe enough that the kernel lets you turn the mitigation off at boot.
The kernel was never a separate program. It was a permission bit and a shared map, and it took a hardware flaw to make anyone pay for the second one.