Phase 4 — Systems Programming (M6-M7)

Two months. This is where C stops being a language and starts being a contract with the kernel. Every non-trivial C program on Linux is really a syscall driver with some computation glued between the traps. Until you feel that in your bones, you’re writing code that happens to compile — not code that survives production.

You’ve spent five months on the language itself: memory, pointers, tooling, data structures, generic patterns. Now you learn how a process is born, how it reads a file without lying to itself, how it dies gracefully, and how it talks to other processes without corrupting anyone’s state. This is the phase where study questions like “what happens between write(fd, buf, 4096) and the bytes hitting the disk?” become questions you actually enjoy.

Why this phase matters for you as an applied ML engineer

Inference servers are systems programs. TensorRT, ONNX Runtime, llama.cpp, vLLM — under the ML veneer, they are file descriptors, mmap, epoll, shared memory, and careful signal handling. When your model server hangs at 2 AM, the bug will almost never be in your Python glue. It will be a zombie process, a leaked fd, a SIGPIPE you didn’t catch, or a mmap region that wasn’t MAP_POPULATE’d and page-faulted through your p99. This phase is the one that lets you read those bug reports without freezing.

M6-M7 target

By the end of M7, you should:

  • Read man 2 syscall fluently and predict errno values from a code snippet without running it.

  • Reach for sigaction (not signal) reflexively, and know why.

  • Explain to a colleague, in one whiteboard, the difference between fork, vfork, clone, and posix_spawn.

  • Choose between pipes, POSIX shm, and Unix domain sockets for IPC based on the actual workload — not habit.

  • Read an strace output and locate the misbehaving syscall in under 60 seconds.

What you will NOT do in this phase

  • No kernel module writing. That’s a distraction — the industry doesn’t hire junior kernel devs off C roadmaps.

  • No deep dive into obscure System V IPC. It’s mostly dead in 2026; we’ll mention it and move on.

  • No premature epoll. That’s Phase 5. Here you master blocking I/O first, because you can’t reason about async until you can reason about sync.

The stretch curriculum

#

File

What you get out of it

1

01_the_unix_syscall_model.md

User vs kernel mode, strace, errno discipline, EINTR retry pattern

2

02_file_io_and_fds.md

open/read/write vs stdio, dup2, O_CLOEXEC, mmap, sendfile

3

03_process_control.md

fork, exec, wait, zombies, sigaction, signalfd, self-pipe trick

4

04_ipc_pipes_shm_sockets.md

Pipes, FIFOs, POSIX shm, Unix domain sockets — the real 2026 winner

5

05_the_linux_specifics.md

procfs, sysfs, cgroups, namespaces, io_uring overview

6

06_binary_and_linker.md

ELF, static vs dynamic, PIC/PIE, LD_PRELOAD, mold linker

7

07_performance_intro.md

perf, flamegraphs, cachegrind, when to reach for eBPF

projects.md

Mini-shell + tail -f clone

Time budget (M6-M7, 10-15h/week)

  • M6 weeks 1-2: Files 01-02. Get strace and open/read/write into muscle memory.

  • M6 weeks 3-4: Files 03-04. Start the mini-shell project on weekends.

  • M7 weeks 1-2: Files 05-06. Ship v1 of the shell (fork/exec/pipes/redirects working).

  • M7 weeks 3-4: File 07 + tail -f clone + polish shell (signals, background jobs).

Exit criteria — you don’t leave Phase 4 until you can honestly say yes to all of these

  • I can write a program that forks a child, sets up a pipe, execs two commands with the pipe wired between them, and reaps both — from memory, no reference.

  • I know why signal() is a footgun and can name the three specific behaviors sigaction() gives you that signal() doesn’t.

  • I have shipped a working mini-shell of at least 500 LOC that handles |, <, >, &, and Ctrl-C correctly.

  • I can produce a flamegraph of my own program and identify the top self-time function.

  • I have run strace -c on at least one real program and interpreted the output.

  • I can explain what happens to a file descriptor across fork and exec — and what O_CLOEXEC changes.

What most people get wrong about this phase

They rush to epoll and io_uring. Don’t. You’ll spend a decade fighting concurrency bugs you don’t understand because you skipped the boring part where read() might return fewer bytes than you asked for and you didn’t write a wrapper for it. Blocking I/O first. Signals first. Process lifecycle first. The async stuff in Phase 5 will feel obvious after this — and terrifying if you skip ahead.

Anchor references for the whole phase

  • W. Richard Stevens, Advanced Programming in the UNIX Environment (APUE), 3rd ed. — still the reference. Chapters 3-10, 15 map directly onto this phase.

  • Michael Kerrisk, The Linux Programming Interface (TLPI) — the Linux-specific companion to APUE. More current on Linux-only features (signalfd, epoll, namespaces).

  • man 2 <syscall> — read it every time. Not man 3 (that’s libc wrappers, different beast).

  • Julia Evans’ zines (wizardzines.com) — strace and Linux debugging zines are excellent quick refs.


Return to roadmap root · Next: 01_the_unix_syscall_model.md