The System Call Interface
The system call is the OS’s API. User-space programs cannot touch hardware, another process’s memory, or the page table directly — the CPU enforces this in user mode. Any privileged operation must go through a syscall: a controlled trap into the kernel that runs privileged code on the program’s behalf and returns.
This boundary is the entire reason the OS is trustworthy: everything a process does that matters goes through code the kernel verifies. When the OS Architecture topic traced a read() call, it showed the flow — here we go deeper into the interface itself.
How a Syscall Works
- The program places arguments in registers (and a syscall number in a dedicated register — e.g.,
raxon x86-64). - It executes the special instruction
syscall(x86-64) orsvc(ARM). - The CPU traps to kernel mode, switching the stack to the kernel stack and jumping to the syscall handler.
- The kernel validates arguments (pointers, permissions), performs the operation, and sets a result.
- The handler returns to user mode with the syscall number’s return value in a register.
On x86-64 the syscall numbers are a fixed ABI table: 0 = read, 1 = write, 2 = open, 57 = fork, 59 = execve, and so on. This ABI is a contract — it must never change, which is why adding a syscall is additive (new numbers) rather than a renumbering.
Syscall vs Library Call
A library call (read() in glibc, printf, malloc) may do all its work in user space or wrap one or more syscalls:
| Call | What actually happens |
|---|---|
strlen() | Pure user-space — no syscall |
printf() | Writes into a user-space buffer; syscalls write() only when the buffer flushes |
malloc() | Mostly user-space heap management; syscalls brk/mmap occasionally to grow the heap |
read()/write() | Direct syscall wrapper |
This layering is why tracing library calls (ltrace) and tracing syscalls (strace) show different pictures, and why buffered I/O is so much faster than unbuffered: each syscall costs a mode switch, and buffering amortizes that cost across many logical operations.
The Cost of a System Call
A syscall is expensive relative to a function call because the CPU must:
- switch privilege levels (kernel/user) and re-validate state,
- switch stacks,
- flush or save parts of the pipelined CPU state,
- and (historically) flush the TLB.
A single syscall costs on the order of ~100 ns to ~1 µs — thousands of times a plain function call. At scale this dominates: a high-throughput server issuing one syscall per small I/O operation can be syscall-bound. This is the deep reason behind buffering, mmap, sendfile, io_uring (async, batched syscalls), and the general engineering rule: batch your system calls.
POSIX: The Portable Interface
POSIX is the standard that normalizes the syscall API across Unix-like systems (Linux, BSD, macOS). Code written against POSIX — open, read, fork, pthread_create — is portable to any POSIX system because those names map to each OS’s syscall ABI. The main POSIX categories:
| Category | Representative calls |
|---|---|
| Process control | fork, execve, wait, exit, kill |
| File I/O | open, read, write, close, lseek, fsync, mmap |
| Inter-process communication | pipe, socket, connect, send, recv |
| Threads | pthread_create, pthread_mutex_lock |
| Time & signals | clock_gettime, signal, nanosleep |
| Memory | brk, mmap, munmap |
The abstraction layer above POSIX (the VFS for files, sockets for networking) is why the same read() works on a file, a socket, and a device — the kernel funnels them through one interface.
Error Handling Convention
Syscalls report failure by returning -1 and setting the global errno. There is no exception mechanism — every call must be checked. -EINTR (interrupted by a signal) and -EAGAIN (would block) are not fatal errors but conditions to retry, and mishandling them is a classic source of subtle bugs. This “every call returns an error” style is the ancestor of Go’s explicit error returns and Rust’s Result.
Tracing with strace
strace intercepts every syscall a process makes, printing arguments and results:
strace ls
strace -e trace=open,read ls # filter
strace -p 1234 # attach to a running process
strace -c ls # syscall summary (count + time)
strace -c is the fastest way to answer “why is this slow?”: it shows which syscalls dominate, exposing pathological patterns like a syscall per byte, repeated stat calls, or constant connect retries. It is the first tool for the Debugging & Profiling topic, and the mental model of “every program is a stream of syscalls” underpins everything from performance engineering to container security.
Practice Trajectory
- Run
strace -c lsand name the top three syscalls; explain whyread/write/statdominate. - Compare
strace -cfor a buffered vs unbuffered C program writing 10 MB — watch the syscall count collapse with buffering. stracea server you run and identify a single hot syscall to optimize (e.g., replacingread+writewithsendfileorio_uring).- Use
grep -E "syscall|nr=" /usr/include/asm/unistd_64.hto find how the ABI table is declared, and confirmreadis entry 0. - Trace a program that handles
EINTR: interrupt a blocking read withkill -INTand observe the retry loop.
When It’s the Right Tool
| Situation | Takeaway |
|---|---|
| Performance investigation | Profile syscalls first — hot syscalls reveal the real bottleneck |
| Debugging I/O or networking bugs | Trace the boundary; app code often lies, syscalls don’t |
| Container/security auditing | seccomp filters are syscall allow-lists |
| Understanding any abstraction | Follow it down to its syscalls to see what it really does |