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

What Is a System Call?

Guide 1 drew the line between user mode and kernel mode. A system call is the one official doorway across that line — the only way your program can ask the kernel to touch a file, the network, or another process. Here we meet the idea itself, before guide 3 opens up the trap mechanism underneath.

The one door across the line

In guide 1 we drew a hard line through the machine: your code runs in user mode, where the CPU forbids the dangerous instructions, while the kernel runs in the privileged mode that is allowed to touch the disk, the network card, and the memory of every program. That line is not a suggestion — the hardware enforces it. So a real question follows immediately: if your program literally cannot run the instruction that reads a disk block, how does `read()` ever get a byte off the disk? The answer is the subject of this whole rung. There is exactly one official way across the line, and it is called a system call.

A system call is a request your program makes to the kernel: "please do this privileged thing on my behalf." Open a file, send bytes over the network, create a new process, ask for more memory — every one of these is something only the kernel may actually do, because each one touches hardware or another program's world. Your code cannot do it directly, so instead it fills in what it wants, hands control to the kernel, and waits. The kernel checks that you are allowed, does the work in its privileged mode, and hands a result back. Think of it as the kernel being the only one with keys to the building, and a system call being the intercom by which you ask the doorman to fetch something for you.

What actually crosses, and what comes back

Because the kernel cannot trust anything in user mode, a system call is a narrow, formal interview, not a casual chat. Every kind of request the kernel offers has a fixed number — read might be syscall number 0 on one system, write number 1, open number 2 — and a fixed list of arguments it expects. To make a call, your program puts that number and those arguments into agreed-upon places (CPU registers, on the architectures you'll meet) and then triggers the crossing. The kernel reads the number, looks up the matching routine, validates every argument as if it came from an untrusted stranger — because it did — and only then does the work.

When the kernel finishes, control returns to the instruction right after your call, and a single value comes back — typically left in one register. By a near-universal convention, a non-negative return means success (for `read()` it's the number of bytes actually read; for `open()` it's a new file descriptor, a small integer handle), and a negative return signals an error. That sign is the whole protocol: one number that is either a useful result or a complaint. Guide 4 is devoted to how that complaint gets turned into a readable reason through errno; for now, just hold the shape in mind — you pass a request in, you get one number back, and its sign tells you which story it's telling.

your program (user mode)                 kernel (privileged)
--------------------------                ---------------------
number  = 0 (read)
args    = fd, &buf, count
trigger the crossing  --------------->   look up syscall 0
                                         check fd, buf, count are valid
                                         copy bytes from disk into buf
         <---------------------------    return value: bytes read (>=0)
                                                       or error  (<0)
resume here, inspect the return value
The shape of one system call: a number plus arguments go in, a single signed value comes back.

You don't call the kernel — libc does

Here is the part that surprises people. When you write `read(fd, buf, 100)` in a C program, you are not, with your own hands, putting a number in a register and triggering the crossing. You are calling an ordinary C function named `read` that lives in the C standard library — libc, the library linked into nearly every C program. That little function is a thin wrapper: it takes your friendly arguments, arranges them the exact way the kernel demands, triggers the crossing for you, and then translates the raw signed result back into the tidy return value and error reporting you expect. This is the role captured by libc as a syscall wrapper.

This is exactly the library call versus system call distinction, and it's worth nailing down because the words blur together in everyday speech. A library call is a plain function call that runs in user mode: `strlen()`, `malloc()`, `printf()` doing its formatting work. A system call is a request that crosses into the kernel. The confusing twist is that the same name — `read`, `write`, `open` — is often both: a user-mode libc function whose entire job is to perform the underlying system call. So when someone says "call read," they usually mean "call the libc wrapper named read, which performs the read system call." The wrapper is the visible handle; the system call is the thing it reaches through to.

POSIX: the shared menu of syscalls

If every operating system invented its own list of system calls with its own names, a C program that opened a file on Linux would have to be rewritten from scratch for macOS or the BSDs. The thing that mostly saves us is POSIX — a written standard that agrees on a common menu of operations and what they're called from C: `open`, `read`, `write`, `close`, `fork`, `wait`, and a few hundred more, with agreed arguments and behavior. POSIX doesn't dictate the syscall numbers or the internal trap mechanism — those differ wildly between systems and even change between kernel versions — it standardizes the user-facing C functions, the libc wrappers, so your source code stays portable.

Be honest about how far this gets you, though. POSIX standardizes the interface — the names, the arguments, the broad behavior — but not every system implements every corner of it, and behavior at the edges can still differ. "POSIX-compliant" gets your file and process code remarkably portable; it does not make every program move untouched between systems, and Windows is famously its own world here. Treat POSIX as a strong shared contract that covers the common cases beautifully, with the honest footnote that the contract has gaps and dialects. The earlier rungs taught you to value the kernel/user split; POSIX is what makes that split's user-facing edge look similar across the Unix family.

The three streams every program is born with

Now a concrete, everyday payoff of all this. When the kernel starts your program, it doesn't hand it a blank slate — it opens three file descriptors for it automatically, the standard streams. Descriptor 0 is standard input (where bytes come in, by default your keyboard), descriptor 1 is standard output (where normal results go out, by default your terminal), and descriptor 2 is standard error (where complaints go, also the terminal by default, but kept separate on purpose). These are just ordinary file descriptors with reserved numbers — the very same handles `open()` would hand back — so `read()` and `write()` work on them with no setup at all.

This is also the layer the friendly `printf()` sits on top of: it formats your text in user mode and ultimately calls `write()` on descriptor 1 to push the bytes out. Keeping standard error separate from standard output is not fussiness — it's what lets you redirect a program's real results into a file while still seeing its error messages on screen, or pipe the output of one program into the input of another without the error chatter getting mixed in. Guide 5 returns to these streams and the buffering libc wraps around them with the stdio layer; here, just notice that they are file descriptors, born from the same syscall world as everything else.