The Unix Syscall Model¶
Every C program you’ve ever written eventually calls the kernel. printf calls write. malloc calls mmap or brk. Even exiting a program is a syscall (exit_group). Understanding this handoff — the moment your code stops running and the kernel takes over — is the single most useful mental model you’ll build in this phase. Once you see it, you’ll never read a C program the same way again.
User mode vs kernel mode — the one-picture version¶
Your CPU has (at least) two privilege rings. Ring 3 (user mode) is where your process runs: it can execute arithmetic, jump around its own memory, call functions in its address space. Ring 0 (kernel mode) is where the kernel runs: it can talk to hardware, remap memory, kill processes, mount filesystems. A syscall is the doorway between them.
On x86-64 Linux, this doorway is the syscall instruction. It:
Saves user-mode registers.
Switches CPU to ring 0.
Loads the kernel’s stack.
Jumps to the kernel’s syscall entry point.
Kernel dispatches based on
rax(syscall number) to the right handler.On return, does all of the above in reverse.
That transition is not free. Anchor these numbers in your head:
Operation |
Approximate cost (modern x86-64 Linux, 2020s) |
|---|---|
Function call within your process |
~1 ns |
|
~230 ns |
|
~700 ns |
Full |
~1500 ns |
Context switch to another process |
1-5 μs (dominated by TLB/cache pollution, not the switch itself) |
Source anchors: LWN and kernel-internals.org syscall cost analyses; verify on your box with perf bench syscall basic.
Takeaway for the ML engineer in you: if your inference loop makes 100 syscalls per request and you’re at 1000 rps, that’s 70ms/sec of pure kernel-entry overhead — before doing any actual work. This is why io_uring and batching exist. But we get there in Phase 5.
strace — the syscall X-ray¶
strace intercepts every syscall your process makes and prints it. It is the fastest debugging tool in existence for a class of bugs you’ll increasingly hit: “the program does the wrong thing but I can’t tell what it’s actually asking the OS to do.”
strace -f -e trace=openat,read,write,close ./myprog
strace -c ./myprog # summary: which syscalls, how often, how expensive
strace -p <pid> # attach to a running process
strace -f -e trace=network ./myprog # only network syscalls
ltrace is the same idea but for library calls (calls into libc, not the kernel). Less used, but useful when you want to see malloc and free, not brk and mmap.
One habit that will save you hours: when a program “hangs,” always strace -p it first. Nine times out of ten it’s blocked in read, futex, or poll, and now you know exactly where.
errno discipline — the rule you must never break¶
Every syscall that can fail returns -1 (or NULL for pointer-returning ones like mmap) and sets the global-per-thread errno. The discipline is:
Check the return value immediately.
If it indicates failure, read
errnobefore doing anything else that could clobber it.Use
strerror(errno)orperror("context")to translate it, or match againstEAGAIN/EINTR/etc.
ssize_t n = read(fd, buf, BUFSZ);
if (n < 0) {
int saved = errno; // save immediately
fprintf(stderr, "read: %s\n", strerror(saved));
/* ... cleanup that may itself set errno ... */
return -saved;
}
Almost every “flaky” C program I’ve reviewed at Zoho had one of these errors:
Checked
n == 0as failure (it’s not — it’s EOF forread, or a legitimate empty message for a socket).Called
printfbetween the syscall and theerrnoread (printf can set errno).Called a function that might do its own syscalls before reading
errno.
Retry-on-EINTR — the pattern you owe yourself¶
Any slow syscall (read, write, accept, wait, poll, select, sleep, …) can be interrupted by a signal delivered to your process. When that happens, the syscall returns -1 with errno == EINTR and has done no work (for most cases; read/write may have done partial work).
You have two options:
Option A: retry loop (correct for most cases).
ssize_t xread(int fd, void *buf, size_t n) {
ssize_t r;
do { r = read(fd, buf, n); } while (r < 0 && errno == EINTR);
return r;
}
Option B: install the signal handler with SA_RESTART so the kernel restarts the syscall for you automatically. This is what sigaction gives you and signal() sometimes silently doesn’t (see file 03).
Do not ignore EINTR. It doesn’t happen often — until you install a SIGCHLD handler for a shell, or a SIGWINCH handler for a TUI, and then it happens all the time and your program mysteriously fails every time the terminal is resized.
Why read(2) may return less than you asked for — the short-read reality¶
This is the one every self-taught programmer gets wrong. read(fd, buf, 4096) is not a promise to give you 4096 bytes. It says: “give me up to 4096 bytes, whatever’s available right now, or block until at least one byte is.” Legitimate reasons for a short read:
Reading from a pipe/socket and only 200 bytes have arrived — you get 200 bytes.
Reading from a terminal — you get one line, ended by newline.
Reading from a file near the end — you get whatever’s left.
Reading and a signal arrived after some bytes were transferred — you get the partial.
For files this rarely surprises anyone. For sockets and pipes it is the default case. The fix is the standard read_full/write_full wrapper:
ssize_t read_full(int fd, void *buf, size_t n) {
size_t total = 0;
while (total < n) {
ssize_t r = read(fd, (char*)buf + total, n - total);
if (r < 0) {
if (errno == EINTR) continue;
return -1;
}
if (r == 0) break; // EOF
total += r;
}
return (ssize_t)total;
}
write has an analogous write_full. Put both in your personal C toolbox on day one and never write bare read/write on a socket again.
What most people get wrong about this¶
They treat syscalls as function calls. They aren’t. A function call is a jump within your address space; a syscall is a context switch into the kernel and back, with potential blocking, signals, interruption, partial completion, and errno side effects. Internalise the difference and half of the “weird bugs” in your career vanish.
Practice this week¶
Run
strace -c ls /and identify the three most-called syscalls. Explain why.Write
read_full/write_fullfrom scratch. Test them with a pipe where the writer sends 1 byte per second, and verify your reader assembles them correctly.Write a program that calls
sleep(100)and then, from another terminal, send itSIGUSR1. Observe withstrace -pthatsleepreturned early withEINTR.Look up any three of:
EAGAIN,EWOULDBLOCK,EINTR,EPIPE,ECONNRESET,EMFILE,ENFILE. Know cold what triggers each.
References¶
man 2 intro— the master intro page listing all sections and conventions.man 2 syscalls— full list of Linux syscalls with kernel version added.APUE ch. 1-3.
TLPI ch. 3 (“System Programming Concepts”).
Julia Evans, “How does strace work?” — one of the clearest short explanations online.
Return to README.md · Next: 02_file_io_and_fds.md