Last Minute Notes

I/O and Linux Internals

How servers really do I/O and how containers really work: blocking vs non-blocking vs async I/O, select, poll, epoll and kqueue, the C10K problem and event loops, io_uring, zero-copy (sendfile, splice), namespaces, cgroups v2, capabilities and seccomp, how a container is built from these, eBPF, and the Linux commands and /proc files every engineer should know.

📘 I/O and Linux Internals

This page connects OS theory to the systems you run every day. It explains how one Node.js or nginx process handles tens of thousands of connections, and how a Docker container is just a normal Linux process with a few kernel features switched on.

Table of Contents

  1. I/O Models
  2. I/O Multiplexing: select, poll, epoll
  3. The C10K Problem and Event Loops
  4. io_uring
  5. Zero-Copy
  6. Namespaces
  7. cgroups
  8. Capabilities, seccomp, and LSMs
  9. What a Container Really Is
  10. eBPF
  11. Linux Toolbox
  12. Questions

1. I/O Models

The five classic Unix I/O models (from Stevens' Unix Network Programming):

ModelWaiting for dataCopying data to the appExample
BlockingThread blocksThread blocksDefault read() on a socket
Non-blockingReturns EAGAIN immediately; app retriesThread blocks brieflyO_NONBLOCK sockets
I/O multiplexingBlock on select / epoll for many FDs at onceThen read the ready onesnginx, Redis, Node.js
Signal-drivenKernel sends SIGIO when readyApp readsRare
AsynchronousKernel does both and notifies on completionKernel copiesio_uring, Windows IOCP, POSIX AIO

Synchronous vs asynchronous is about whether the app does the copy step. Blocking vs non-blocking is about whether a call waits. Readiness-based APIs (epoll) say "you can read now without blocking"; completion-based APIs (io_uring, IOCP) say "your read is done, here is the data".


2. I/O Multiplexing: select, poll, epoll

APIHow it worksCost per callLimits
selectPass bitmaps of FDs; kernel scans allO(n)1,024 FDs (FD_SETSIZE), bitmaps rebuilt each call
pollPass an array of FDsO(n)No hard limit, still scans all
epoll (Linux)Register FDs once; kernel keeps a ready listO(ready FDs)Scales to millions
kqueue (BSD, macOS)Similar to epoll, also handles files, signals, timersO(ready)
IOCP (Windows)Completion ports, completion basedO(completed)
int ep = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);

struct epoll_event events[64];
for (;;) {
    int n = epoll_wait(ep, events, 64, -1);        // sleep until something is ready
    for (int i = 0; i < n; i++) {
        if (events[i].data.fd == listen_fd) {
            int c = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);
            struct epoll_event cev = { .events = EPOLLIN, .data.fd = c };
            epoll_ctl(ep, EPOLL_CTL_ADD, c, &cev);
        } else {
            handle_readable(events[i].data.fd);    // read until EAGAIN
        }
    }
}
Trigger modeBehaviorRule
Level-triggered (default)Reports an FD as long as it is readyForgiving; can read partially
Edge-triggered (EPOLLET)Reports only on the change to readyMust read until EAGAIN, or you will never be notified again

EPOLLEXCLUSIVE and SO_REUSEPORT spread accepts across worker processes without the thundering herd (all workers waking for one connection).


3. The C10K Problem and Event Loops

Around 1999 the question was how to serve 10,000 concurrent connections on one machine. Thread-per-connection needed 10,000 threads, each with a stack (often 1 to 8 MB reserved) and context-switch costs.

ModelHowExamplesWeakness
Thread per connectionBlocking I/O, one thread eachClassic Apache prefork, Tomcat BIOMemory and switching at high concurrency
Thread pool + blocking I/OBounded threadsMost Java serversPool exhaustion under slow requests
Event loopOne thread, non-blocking I/O, epollNode.js, nginx, Redis, NettyOne CPU-heavy task blocks everyone
Event loop per coreN loops, SO_REUSEPORTnginx workers, Envoy, SeastarLoad imbalance between loops
M:N green threadsBlocking-style code, runtime uses epoll underneathGo, Java virtual threads, ErlangRuntime complexity hidden from you

Node.js: libuv runs the event loop on epoll / kqueue / IOCP. Network I/O is truly non-blocking; file system operations, DNS lookup, and crypto run on a thread pool (4 threads by default, UV_THREADPOOL_SIZE) because regular file I/O cannot be made non-blocking with epoll. See Advanced JavaScript for the event loop phases.

Today C10M (ten million) is the benchmark, reached with kernel bypass (DPDK), io_uring, or careful epoll tuning.


4. io_uring

io_uring (Linux 5.1, 2019) is a completion-based async I/O interface built on two ring buffers shared between the application and the kernel.

AdvantageDetail
Fewer system callsBatch many operations per io_uring_enter, or none at all with kernel-side polling (SQPOLL)
Truly async file I/OUnlike epoll, works for regular files
Wide coverageNetwork, files, openat, statx, timeouts, linked operations
Registered buffers and filesSkip per-operation setup

Used by high-performance databases and storage engines (ScyllaDB via Seastar, TigerBeetle, PostgreSQL 18's new io_method = io_uring option), Rust runtimes (tokio-uring, glommio), and QEMU. Because of a history of security bugs, some environments (Google production, ChromeOS, Android apps, many container runtimes' default seccomp profiles) restrict or disable it.


5. Zero-Copy

Sending a file over a socket the naive way copies data four times and makes two system calls per chunk:

read():  disk -> page cache (DMA) -> user buffer (CPU copy)
write(): user buffer -> socket buffer (CPU copy) -> NIC (DMA)
TechniqueEffect
sendfile(out_fd, in_fd, ...)Page cache to socket inside the kernel; no user-space copy. nginx sendfile on, Kafka consumers, Java FileChannel.transferTo
splice / teeMove data between FDs through a pipe without user copies
mmap + writeOne fewer copy, but page fault overhead
MSG_ZEROCOPYSend from user memory without copying, notification when the buffer is reusable
Kernel TLS (kTLS)Kernel or NIC encrypts, so sendfile works even for HTTPS (Netflix serves video this way)
Kernel bypass (DPDK, RDMA, AF_XDP)NIC to user space directly, for extreme packet rates

Kafka's throughput story is largely sequential disk I/O, the page cache, batching, and sendfile.


6. Namespaces

Namespaces give a process its own view of a global resource.

NamespaceIsolatesEffect inside a container
PIDProcess IDsApp sees itself as PID 1, cannot see host processes
MountMount pointsIts own root file system
NetworkInterfaces, IPs, routes, ports, iptablesOwn eth0, can bind port 80 independently
UTSHostnameOwn hostname
IPCSystem V IPC, POSIX message queuesSeparate shared memory
UserUID and GID mappingsRoot inside maps to an unprivileged user outside (rootless containers)
CgroupView of the cgroup treeSees its own cgroup as the root
TimeMonotonic and boot clocksUseful for checkpoint and restore
sudo unshare --pid --fork --mount-proc --uts --net bash   # a crude "container"
hostname sandbox; ps aux                                   # only a few processes visible
lsns                                                       # list namespaces
nsenter -t <pid> -n ip addr                                # run a command in another process's net namespace

7. cgroups

Control groups limit and account for the resources a group of processes may use. cgroups v2 (a single unified hierarchy) is the default on current distributions and Kubernetes.

ControllerKey filesControls
cpucpu.max, cpu.weight, cpu.statQuota and relative share, throttling stats
memorymemory.max, memory.high, memory.current, memory.eventsHard limit (OOM kill), soft throttling, OOM counts
ioio.max, io.weightDisk bandwidth and IOPS
pidspids.maxMax processes (stops fork bombs)
cpusetcpuset.cpus, cpuset.memsPin to cores and NUMA nodes
cat /sys/fs/cgroup/<group>/memory.max
cat /sys/fs/cgroup/<group>/cpu.stat     # nr_throttled, throttled_usec
systemd-cgls                            # tree of cgroups
systemd-cgtop                           # live usage per cgroup

Namespaces decide what a process can see; cgroups decide how much it can use.


8. Capabilities, seccomp, and LSMs

MechanismPurposeExample
CapabilitiesSplit root's power into about 40 piecesCAP_NET_BIND_SERVICE to bind port 80 without full root; Docker drops most by default
seccomp-bpfFilter which system calls a process may makeDocker's default profile blocks dozens of rarely needed syscalls
LSMsMandatory access control policiesSELinux (RHEL), AppArmor (Ubuntu), Landlock (unprivileged sandboxing)
no_new_privsChildren cannot gain privileges via setuidSet by container runtimes

9. What a Container Really Is

A container is an ordinary process that:

  1. Runs in new namespaces (its own view of PIDs, mounts, network, hostname).
  2. Is placed in a cgroup with resource limits.
  3. Has a root file system built from image layers with overlayfs (the old chroot idea, done with pivot_root).
  4. Has reduced privileges via capabilities, seccomp, and an LSM profile.

The stack: Docker or Kubernetes (via CRI) call containerd or CRI-O, which call an OCI runtime like runc (or crun, or gVisor / Kata Containers for stronger isolation) to make the kernel calls.

Consequences worth knowing:

  • ps on the host shows container processes; they are not hidden inside a VM.
  • A kernel exploit in one container can compromise the host, which is why multi-tenant platforms use gVisor, Kata, or Firecracker microVMs.
  • The container sees the host's total CPU and memory in /proc/cpuinfo and /proc/meminfo unless tools like LXCFS are used, so runtimes must read cgroup limits to size themselves.
  • PID 1 inside the container must handle signals and reap zombies (see Processes and Threads).

10. eBPF

eBPF lets you load small, verified programs into the kernel and attach them to events: system calls, function entries, network packets, tracepoints. The verifier guarantees they terminate and cannot crash the kernel.

UseTools
Observabilitybpftrace, BCC tools (execsnoop, opensnoop, biolatency, tcplife), Pixie, Parca
NetworkingCilium (Kubernetes networking and kube-proxy replacement), Katran load balancer, XDP packet filtering
SecurityFalco, Tetragon runtime detection
Schedulingsched_ext custom CPU schedulers
ProfilingContinuous CPU profiling with low overhead
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_openat { printf("%s %s\n", comm, str(args->filename)); }'

11. Linux Toolbox

Processes and CPU

CommandShows
top, htopLive processes, CPU, memory, load
ps aux, ps -eLfProcesses, threads
pstree -pProcess tree
uptimeLoad averages (1, 5, 15 min)
vmstat 1Run queue (r), blocked (b), swapping, context switches, CPU split
mpstat -P ALL 1Per-core usage
pidstat 1Per-process CPU, I/O, context switches
perf top, perf record -gCPU profiling, flame graphs
strace -f -p <pid>System calls of a running process
ltraceLibrary calls
kill -TERM <pid>, pkill, killallSend signals
nice, renice, taskset, chrtPriority, affinity, real-time policy

Memory and disk

CommandShows
free -hMemory including page cache and available
smem, pmap -x <pid>Per-process memory (PSS, mappings)
df -h, df -iSpace and inodes per file system
du -sh *Space per directory
iostat -xz 1Per-device utilization, latency (await), queue depth
iotopPer-process disk I/O
lsblk, blkid, mountBlock devices and mounts
lsof, lsof +L1Open files, deleted-but-open files
dmesg -TKernel log: OOM kills, disk errors, hardware issues

Useful /proc and /sys files

PathContents
/proc/<pid>/statusState, memory, threads, capabilities
/proc/<pid>/maps, smapsMemory mappings
/proc/<pid>/fd/Open file descriptors
/proc/<pid>/limitsResource limits
/proc/loadavg, /proc/meminfo, /proc/cpuinfoSystem-wide stats
/proc/pressure/{cpu,memory,io}Pressure stall information
/proc/sys/Tunables, set with sysctl
/sys/fs/cgroup/cgroup hierarchy

The 60-second performance checklist

Adapted from Brendan Gregg's well-known checklist, run in order on a slow Linux box:

uptime                 # load trend
dmesg -T | tail        # OOM kills, errors
vmstat 1               # run queue, swapping, CPU split
mpstat -P ALL 1        # one hot core?
pidstat 1              # which process
iostat -xz 1           # disk latency and saturation
free -m                # memory and page cache
sar -n DEV 1           # network throughput
sar -n TCP,ETCP 1      # connections and retransmits
top                    # overview

Think in USE: for every resource, check Utilization, Saturation (queueing), and Errors.


12. Questions

QuestionShort answer
Blocking vs non-blocking vs async I/O?Wait vs return EAGAIN vs kernel completes and notifies
select vs poll vs epoll?Scan bitmap (1,024 limit) vs scan array vs kernel-maintained ready list, O(ready)
Level vs edge triggered?Report while ready vs only on change; edge needs read until EAGAIN
How does Node.js handle concurrency with one thread?Event loop on epoll for sockets, thread pool for files, DNS, crypto
What is io_uring?Shared submission and completion rings for batched, truly async I/O
What is zero-copy?Avoid user-space copies, e.g. sendfile from page cache to socket
Namespaces vs cgroups?What a process can see vs how much it can use
What is a container?A process with namespaces, cgroups, an overlay root FS, and reduced privileges on a shared kernel
Container vs VM security?Shared kernel vs separate kernel; microVMs and gVisor bridge the gap
What is eBPF?Verified programs running in the kernel on events, for tracing, networking, security
High load average but idle CPU?Tasks in uninterruptible I/O wait (D state)
How do you find what a process is doing?strace, perf, /proc/<pid>, lsof

Next: OS Interview Playbook.

On this page