JOVANA
Explore Library Glossary Getting Started Three Levels Fields How it works Mission
Join the mission
All guides

User Mode, Kernel Mode, and the Line Between

Your program is not allowed to touch the disk, the network, or another process's memory — and that is on purpose. This guide draws the single most important line in the whole system: the one between your code and the kernel, why the hardware enforces it, and the narrow doorway you must knock on to cross it.

Your program runs with the safety on

By now you can read what the CPU actually does: it walks through a stream of instructions, moving bytes between registers and memory, one tiny step at a time. But here is a thing that earlier picture quietly hid. When your program wants to read a file, print to the screen, or open a network connection, not a single instruction in your binary can do that. The CPU your code runs on is deliberately running in a restricted mode where the dangerous instructions — the ones that talk to hardware — simply do not work for you. Your program runs with the safety on, and it cannot take the safety off by itself.

This restricted mode has a name: user mode. There is exactly one other mode, kernel mode, in which every instruction is permitted and the code can touch any hardware, any memory, anything at all. The distinction is not a software convention you could argue your way around — it is a single bit of state inside the CPU, set by the hardware itself, that gates which instructions are legal. Your shell, your editor, your game, this very program: all of them run in user mode. The only software that runs in kernel mode is the kernel, the core of the operating system. That is the whole game, and the rest of this rung is about the doorway between these two worlds.

Why hand power to a referee at all

It is worth being honest about why this wall exists, because at first it feels like the operating system is just getting in your way. Picture a machine where every program ran in kernel mode. One program with a wild pointer could overwrite another program's memory; a buggy loop could seize the CPU and never give it back; any process could read your passwords straight out of another process's address space; a single crash would take down the whole machine. There would be no isolation and no fairness, because there would be nobody with the authority to enforce them. The wall exists so that a buggy or hostile program can only hurt itself.

So the kernel sets itself up as the single referee. Because it alone runs in kernel mode, it alone decides who gets which slice of the CPU, which pages of physical memory a process may see, and whether this particular program is even allowed to open that particular file. This is the kernel acting as a resource manager: every scarce, shareable, or dangerous thing in the machine — the processor, memory, the disk, the network card — is fenced off behind it, and your only way to reach any of it is to ask. The cost is that asking is not free, and we will face that cost squarely in a later guide; the benefit is everything above.

On most CPUs the user/kernel split is the visible part of a slightly finer scheme called privilege rings — x86 numbers them ring 0 (most privileged) through ring 3 (least), and in practice the kernel lives in ring 0 while your programs live in ring 3. The in-between rings exist but are rarely used by mainstream systems, so for everything in this rung you can keep the simple two-level picture: a privileged inside and an unprivileged outside, with a guarded crossing in between.

The doorway: a system call

So how does a user-mode program ever get anything done? Through one narrow, controlled doorway called a system call (often shortened to syscall). A system call is the mechanism by which your program asks the kernel to perform a privileged action on its behalf — read these bytes from that file, give me more memory, start a new process. Crucially, a system call is not an ordinary function call. When you call one of your own functions, the CPU just jumps to another address and keeps running in user mode. A system call instead executes a special instruction that flips the CPU into kernel mode and hands control to a fixed entry point inside the kernel — it crosses the line.

That distinction is the single most important idea in this rung, so let it land. A plain function call and a system call can look identical in your C source — both are `name(args)` — but underneath they are wildly different beasts. The function call stays on your side of the wall and costs a few nanoseconds. The system call is a guarded border crossing: it traps into the kernel, switches privilege, lets the kernel inspect your request and do the work, then drops you back into user mode with a result. We will take that crossing apart instruction by instruction in the very next guides; for now, just hold the shape of it.

  USER MODE (ring 3)              |   KERNEL MODE (ring 0)
  -----------------------------   |   ---------------------------
  your code:                      |
      n = read(fd, buf, 100);     |
          |                       |
          | (special trap instr)  |
          +-------- crosses ----------->  kernel entry point
                                  |          checks fd, copies bytes,
                                  |          does the privileged work
          +-------- returns ------<----  flips back to user mode
          v                       |
      n == bytes read, or -1      |
One read() call: control leaves user mode through the trap, the kernel does the privileged work, and control returns with a result. The dashed line is the privilege boundary; only the kernel side may touch the hardware.

libc: the friendly wrapper you have been using all along

If a system call needs a special trap instruction, how have you been calling read() and open() and write() as if they were normal functions? Because they nearly are. For almost every system call, the C standard librarylibc — ships a tiny ordinary C function with the same name that does the fiddly part for you: it puts your arguments in the exact registers the kernel expects, executes the trap instruction, and hands you back the result. This thin glue function is libc acting as a syscall wrapper. When you write `write(1, "hi\n", 3)`, you are calling a normal libc function, and that function is the thing that actually crosses into the kernel.

This is why it is easy to blur the two together, and why it pays to keep them apart in your head. A library call like printf() or malloc() runs entirely in user mode and may never bother the kernel at all. A system call is the kernel crossing itself. The confusing part is that one library call often hides several system calls (or none): printf() formats your text in user-mode memory and then, only sometimes, calls write() underneath to flush it. So "did this line touch the kernel?" is a real question with a real answer — and in the debugging rung you met a tool, strace, whose entire job is to print the answer by listing every system call a program makes.

What you ask for, and what you get back

Two more pieces complete the doorway. The first is the question of which calls exist at all. The kernel exposes a fixed menu of system calls — open, read, write, close, fork, and a few hundred more — and the agreed-upon shape of that menu, the names and what each one promises to do, is the POSIX API. POSIX is a standard (first published in 1988) that nails down this interface so the same C source can be compiled and run on Linux, macOS, the BSDs, and other Unix-like systems. It is the reason the file-and-process vocabulary you are learning is genuinely portable knowledge and not just one operating system's quirk; write to the POSIX API and you have written to a whole family of systems.

The second piece is what comes back. The kernel cannot raise a C++ exception across the boundary; it can only hand you a return value. So almost every POSIX call follows one stern convention: on success it returns something useful (a count of bytes, a file descriptor, 0), and on failure it returns a sentinel — usually -1 — and stashes a numeric reason code in a global called errno. This means you must check the return value of every system call, every time. A read() that returns -1 has not read anything; trusting it would corrupt your program. We devote a whole later guide to errno and doing this correctly, but the rule starts here: check the call, then read errno only when the call told you it failed.

  1. Make the call and capture its return value: `ssize_t n = read(fd, buf, 100);` — never call read() and throw away what it returns.
  2. Test for the sentinel first: if `n == -1`, the call failed and read nothing — handle that before you trust n for anything.
  3. Only after a failure, read errno for the reason (e.g. EBADF, a bad descriptor); reading errno after a successful call is meaningless and may show a stale value.
  4. On success, treat n as a real count in the range 0 to 100 — and notice it may be smaller than the 100 you asked for. That is a short read, a separate idea you will meet soon.

Finally, three doorways are always open the moment your program starts, before you open anything yourself. These are the standard streams: standard input, standard output, and standard error, which the kernel hands you as file descriptors 0, 1, and 2. When you call printf() it writes to descriptor 1; perror() writes to descriptor 2. Because these are just file descriptors, the shell can quietly point them at a file, a pipe, or another program — that is exactly what a pipeline does. The standard streams are why a program can read its input and write its output without knowing or caring whether the other end is a keyboard, a file, or another process; you write to a number, and the kernel routes it.

Standing at the line

Step back and look at the shape you now hold. The whole rest of this rung lives at this one boundary. "What is a system call?" zooms into the doorway itself. "How a syscall traps into the kernel" follows a single crossing down to the registers and the trap instruction. "errno and reporting errors" is about reading what the kernel hands back when a crossing fails. And "libc, POSIX, and the standard streams" widens out to the whole interface — the wrapper, the standard, and those three always-open doors. Every one of them is just a closer look at the line you have now drawn.

And the practical takeaway is a habit, not just a fact. From here on, when you read C code, ask of each call: does this stay in user mode, or does it cross? A pure computation, a strlen(), a malloc() that had spare memory — those stay. An open(), a read(), a fork() — those cross, they can fail, and they cost far more than an ordinary call. Carry that question with you, and you will read systems code the way the machine actually executes it: as your code on one side, the kernel on the other, and a small, guarded, checkable set of doorways in between.