Chapter 2 · Part I: Low-Level Systems and CS Engineering
Operating Systems Engineering and Kernel Mechanics
The kernel is the only program that can talk to the hardware. Every read, every write, every thread wake-up crosses a boundary that costs real time. This chapter measures that boundary and shows the three ways that modern systems avoid paying it.
In this chapter
- The boundary and what it costs
- Virtual memory as the kernel implements it
- Scheduling: who runs next
- The file I/O path and the two ways through it
- From select to epoll to io_uring
- eBPF: running your code inside the kernel
- Interrupts and the network receive path
- Time, timers, and what a timestamp means
- Sources and further reading
The boundary and what it costs
A processor runs in one of several privilege levels. On x86-64 the kernel runs in ring 0 and user code runs in ring 3. Ring 3 cannot touch a device register, cannot change a page table, and cannot mask an interrupt. Any program that wants those things must ask the kernel, and the request crosses the boundary through a system call.
A system call is not a function call. The syscall instruction switches the
privilege level, switches the stack, saves the user register state, and enters
a kernel entry path that sanitizes the arguments. On the way back it restores
everything and switches back. The bare hardware cost is about 50 nanoseconds
on a 2026 core, or roughly 180 cycles. A function call costs about 1
nanosecond.
Spectre and Meltdown made the boundary more expensive. Kernel page table
isolation, or KPTI, gives the kernel and user space separate page tables, so
every crossing flushes part of the TLB. Indirect branch restricted speculation
and retpolines add more. With all mitigations active, a minimal system call
such as getpid costs between 250 and 700 nanoseconds. That is a factor of
five to fourteen over the unmitigated path.
Table 2.1: cost of crossing the kernel boundary, 2026 hardware
| Operation | Cost | Note |
|---|---|---|
| Function call, same binary | ~1 ns | no privilege change |
clock_gettime through the vDSO |
15 to 25 ns | no boundary crossing |
getpid, mitigations off |
50 to 70 ns | pure hardware cost |
getpid, KPTI and IBRS on |
250 to 700 ns | TLB flush dominates |
read of 4 KiB from page cache |
1,000 to 2,000 ns | copy plus boundary |
| Thread wake-up through futex | 2,000 to 5,000 ns | scheduler runs |
| Context switch, same core | 1,000 to 3,000 ns | plus cache pollution |
| Context switch, different core | 5,000 to 20,000 ns | working set must migrate |
| Page fault, minor | 500 to 2,000 ns | page already in memory |
| Page fault, major, NVMe backed | 80,000 to 200,000 ns | drive read |
The table explains the whole design of modern high-performance I/O. If one crossing costs 400 nanoseconds and a service handles 200,000 requests per second with four syscalls per request, the service spends 320 milliseconds of every second inside the boundary. That is 32 percent of one core, spent on nothing but transitions.
Three techniques reduce the bill. Batching amortizes one crossing over many
operations, which is what io_uring does. Mapping moves the data into a
shared region so that no copy and no crossing happen at all, which is what the
vDSO and memory mapped files do. Moving the code into the kernel removes the
crossing entirely, which is what eBPF does. The rest of this chapter covers
all three.
The vDSO is the cheapest kernel call
The virtual dynamic shared object, or vDSO, is a small shared library
that the kernel maps into every process. It holds the code and the
data for a few calls that need no privilege, above all
clock_gettime. The library reads a timestamp page that the kernel
updates, applies the clock multiplier, and returns. There is no
privilege change and no TLB flush. Any program that timestamps every
request already depends on this. Without the vDSO, a request tracer
that takes four timestamps per request costs more than the request
handler.
Virtual memory as the kernel implements it
Chapter 1 described the hardware side of address translation. The kernel owns the other half. It builds the page tables, it decides which physical page backs which virtual page, and it takes the fault when no page exists.
A process address space is a set of virtual memory areas, or VMAs. Each VMA holds a range, a set of permissions, and a backing object. An anonymous VMA has no file behind it and holds the heap and the stacks. A file backed VMA maps a file, and the page cache holds its pages. Linux stores VMAs in a maple tree, which replaced the older red-black tree and cuts lookup contention.
Physical pages arrive lazily. malloc of one gigabyte returns immediately and
allocates nothing. The kernel records the VMA and moves on. The first write to
each page takes a page fault, and the fault handler allocates a physical page,
zeroes it, and installs the page table entry. A minor fault, where the page
already sits in memory, costs 500 to 2,000 nanoseconds. A major fault, where
the page must come from a drive, costs 80,000 nanoseconds or more.
user: store to 0x7f3c00001000
|
v
MMU: page table entry not present --> page fault exception
|
v
kernel fault handler
|
+-- VMA not found ------------------> SIGSEGV
|
+-- anonymous, first touch ---------> allocate page, zero it,
| install PTE (minor)
|
+-- file backed, page in page cache -> install PTE (minor)
|
+-- file backed, page not cached ----> submit read to device,
| block the thread (major)
|
+-- present but read-only, and the
VMA allows write -----------------> copy-on-write:
copy page, install RW (minor)
Figure 2.1 What happens on a write to an address that has no page.
Copy on write makes fork cheap. The child gets the same page tables with
every writable page marked read-only. A write in either process takes a fault
and copies that one page. A shell that forks and immediately runs execve
copies almost nothing. A language runtime that forks a 40 gigabyte heap copies
pages at the rate that the two processes write them, and a garbage collector
that touches every object defeats the optimization completely.
The page cache holds file data in memory. Every buffered read and write
goes through it. A read that hits the cache costs a copy of about 1 to 2
microseconds for 4 KiB. A read that misses goes to the drive. Writes land in
the cache, mark the page dirty, and return before any device sees the data.
Writeback threads flush dirty pages later, under limits that
vm.dirty_background_ratio and vm.dirty_ratio set.
A successful write is not a durable write
A write that returns zero errors has put bytes in the page cache and
nothing more. A power loss at that moment loses the data. Durability
needs fsync or fdatasync, and both wait for the device.
fdatasync skips the metadata update when the file size did not
change, which makes it cheaper. A further trap follows: on many file
systems an fsync error marks the error as reported and clears it, so
a second fsync returns success even though the data never landed.
Treat an fsync failure as unrecoverable for that file and restart
the write from a known good state. Chapter 7 builds the whole
durability model of a database on this one call.
When memory runs low, the kernel reclaims. It keeps pages on active and inactive lists and moves pages between them based on reference bits. Since kernel 6.1 the multi-generational LRU, or MGLRU, replaces the two list scheme with several generations and ages pages by scanning page tables rather than the pages themselves. The result is lower reclaim CPU and better choices under pressure.
Reclaim comes in two forms and only one of them is safe. The kswapd thread
reclaims in the background and does not block anybody. Direct reclaim happens
inside an allocating thread when kswapd cannot keep up, and it blocks that
thread until pages come free. Direct reclaim is the usual cause of a service
that suddenly shows 200 millisecond pauses with no change in request rate.
Read pressure, not free memory
Free memory is a useless signal on Linux, because the kernel uses all
of it for the page cache. Read Pressure Stall Information instead. The
files /proc/pressure/memory, /proc/pressure/io, and
/proc/pressure/cpu give the percentage of time that at least one
task waited, and the percentage of time that every task waited. Alert
on some avg10 above 10 for memory, and treat any non-zero full
value as a stall that a user felt. Chapter 14 wires these into a
service level objective.
Scheduling: who runs next
The Linux scheduler divides tasks into scheduling classes and serves them in strict priority order. Deadline tasks run first, then real-time tasks, then normal tasks, then idle tasks. Almost all application code lands in the normal class.
The normal class used the Completely Fair Scheduler, or CFS, for fifteen years. CFS tracked virtual runtime per task and always ran the task with the smallest value. Kernel 6.6 replaced it with the Earliest Eligible Virtual Deadline First scheduler, or EEVDF. EEVDF gives every task a deadline based on its requested time slice and runs the eligible task with the earliest deadline. The change improves latency for tasks that ask for short slices and removes several heuristics that CFS needed.
Kernel 6.12 added sched_ext, which lets an eBPF program implement the
scheduling policy. The kernel keeps the mechanism and hands the policy to user
supplied code that loads and unloads at runtime with no reboot. A watchdog
returns the system to the built-in scheduler when the eBPF scheduler stalls.
Several distributions now ship sched_ext enabled, and the scx_rusty,
scx_lavd, and scx_bpfland schedulers cover server, gaming, and interactive
workloads.
+--------------------------------------------------------------+ | SCHED_DEADLINE runtime / period / deadline, admission control| +--------------------------------------------------------------+ | SCHED_FIFO, SCHED_RR priority 1..99, no time slice for FIFO | +--------------------------------------------------------------+ | SCHED_EXT policy supplied by an eBPF program (6.12+) | +--------------------------------------------------------------+ | SCHED_NORMAL EEVDF since 6.6, CFS before that | | SCHED_BATCH same class, longer slices, no wake-up boost | +--------------------------------------------------------------+ | SCHED_IDLE runs only when nothing else can | +--------------------------------------------------------------+ A single SCHED_FIFO task that never sleeps starves every SCHED_NORMAL task on its CPU. Use SCHED_FIFO only with a watchdog and a CPU budget.
Figure 2.2 Scheduling classes, in the order that the kernel serves them.
Two scheduler behaviors cost real money in production. The first is wake-up migration. When a task wakes, the scheduler picks a CPU, and it prefers an idle one. An idle CPU on a different NUMA node costs the task its entire warm cache and its local memory. The second is time slice length. A task that runs for 1 millisecond and then blocks pays a context switch every millisecond, and each switch costs between 1 and 20 microseconds of direct cost plus the cache damage.
# Which CPU does a thread actually run on, and how often does it move?
perf sched record -- sleep 10 && perf sched latency --sort max
# Pin a latency-sensitive service to one NUMA node and disable
# wake-up migration across nodes for it.
taskset -c 0-15 ./service
echo 0 > /proc/sys/kernel/numa_balancing
# Involuntary context switches per second. A high number means that the
# task is preempted, not that it blocks.
pidstat -w -p $(pgrep -f service) 1
Control groups version 2, or cgroup v2, apply resource limits to a tree of
processes. The unified hierarchy replaced the many separate hierarchies of
version 1. Four controllers carry most of the weight. The cpu controller
sets cpu.max as a quota and period pair, and cpu.weight as a share. The
memory controller sets memory.max as a hard limit and memory.high as a
throttling point. The io controller sets io.max per device. The pids
controller caps the process count.
The difference between memory.high and memory.max matters more than any
other cgroup detail. A cgroup that crosses memory.high gets throttled and
reclaimed, and it keeps running. A cgroup that crosses memory.max triggers
the out of memory killer inside that cgroup. Set memory.high about 10
percent below memory.max and the workload slows down instead of dying.
CPU quota causes tail latency, not average latency
The cpu.max quota works over a 100 millisecond period by default. A
container with a quota of 2 CPUs and 16 runnable threads burns its
whole quota in 12 milliseconds and then stops for 88 milliseconds.
Average CPU use looks correct. The p99 latency shows an 88 millisecond
cliff. Find it in cpu.stat under nr_throttled and
throttled_usec. The fix is to match the thread count to the quota,
not to raise the quota. For the JVM, set -XX:ActiveProcessorCount.
For Go, set GOMAXPROCS from the cgroup quota rather than from the
host core count.
The file I/O path and the two ways through it
Buffered I/O goes through the page cache. A read copies from the cache into
the user buffer. A write copies the other way and marks the page dirty. The
kernel handles readahead, caching, and writeback, and the application gets
good behavior with no work.
Direct I/O bypasses the page cache. open with O_DIRECT makes the kernel
transfer between the device and the user buffer with no intermediate copy. The
application then owns the caching decision. Databases use direct I/O because
they know their access pattern better than the kernel does, and because double
caching wastes memory.
Direct I/O has hard requirements. The buffer address, the file offset, and the
transfer length must all align to the logical block size, which is 512 or 4096
bytes. An unaligned direct read returns EINVAL. Use posix_memalign or
aligned_alloc for the buffer, and keep every offset on a block boundary.
/* Direct I/O requires alignment on all three of address, offset, length. */
#define _GNU_SOURCE
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
int read_block_direct(const char *path, off_t offset, void **out) {
int fd = open(path, O_RDONLY | O_DIRECT);
if (fd < 0) return -1;
void *buf = NULL;
if (posix_memalign(&buf, 4096, 4096) != 0) { close(fd); return -1; }
ssize_t n = pread(fd, buf, 4096, offset); /* offset % 4096 == 0 */
close(fd);
if (n != 4096) { free(buf); return -1; }
*out = buf;
return 0;
}
The choice between the two paths follows one question. Does the application
know which blocks it will need again? A database with its own buffer pool
knows, and direct I/O wins. A web server that serves static files does not
know, and the page cache wins because it tracks the real access pattern for
free. A log writer that never reads its own output wants buffered writes with
periodic fsync, because the page cache absorbs bursts.
Readahead is a guess that the kernel makes for you
The kernel detects sequential reads and fetches ahead, up to
/sys/block/<dev>/queue/read_ahead_kb, which defaults to 128. A
random access workload gains nothing from readahead and pays for the
wasted bandwidth. Call posix_fadvise with POSIX_FADV_RANDOM to
turn it off for one file descriptor, or POSIX_FADV_SEQUENTIAL to
double it. A database that runs buffered I/O sets POSIX_FADV_RANDOM
on its data files and leaves readahead on for its write-ahead log.
From select to epoll to io_uring
Network servers need to wait on many file descriptors at once. Three generations of interface solve that problem, and each one removes a cost that the previous one paid.
select and poll take the full descriptor set on every call. The kernel
walks the whole set, and the cost grows with the number of descriptors rather
than with the number of ready ones. At 10,000 connections the walk dominates.
epoll moves the set into the kernel. epoll_ctl registers a descriptor
once. epoll_wait returns only the ready descriptors, so the cost tracks the
number of events. This is the design that carried Linux network servers from
2002 to now, and it still carries most of them.
epoll leaves two costs. First, readiness is not completion. epoll_wait
says that a socket is readable, and the program must then call read, which
is a second syscall. A request that takes one read and one write costs three
crossings. Second, epoll works for sockets and pipes and never worked for
regular files, because a regular file is always ready.
io_uring replaces readiness with completion and replaces one syscall per
operation with one syscall per batch. The kernel and the process share two
ring buffers in mapped memory. The submission queue carries requests from user
space to the kernel. The completion queue carries results back. Neither ring
needs a system call to read or write, because both sides map the same pages.
user space kernel
+-------------------------+ +----------------------------+
| submission queue (SQ) | | |
| [ sqe ][ sqe ][ sqe ] | -------> | io_uring_enter() picks up |
| tail ^ | | entries and starts the I/O |
+-------------------------+ | |
| worker pool or inline |
+-------------------------+ | |
| completion queue (CQ) | <------- | posts one cqe per finished |
| [ cqe ][ cqe ] | | operation |
| head ^ | +----------------------------+
+-------------------------+
Plain mode: one io_uring_enter() submits N operations and reaps M.
SQPOLL mode: a kernel thread polls the SQ. Submission costs no syscall.
Registered buffers and files skip per-operation reference counting.
Multishot accept/recv post many completions from one submission.
Figure 2.3 The io_uring shared rings. Both sides map the same memory.
The gain is real and it is not universal. Benchmarks that submit large batches
of storage I/O show io_uring reaching millions of operations per second with
far fewer crossings than epoll plus read. Benchmarks that stream bulk data
over a few sockets sometimes show epoll ahead, because the per-operation
work dominates and epoll has a shorter path. The honest summary for 2026 has
two halves. io_uring wins for storage I/O and for services with tens of
thousands of concurrent connections. epoll remains a correct default for an
ordinary network service.
/* Submit one read and wait for it. Real code submits many at once. */
#include <liburing.h>
int read_one(struct io_uring *ring, int fd, void *buf, unsigned len,
off_t off) {
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_read(sqe, fd, buf, len, off);
sqe->user_data = 1;
io_uring_submit(ring); /* one crossing for the whole batch */
struct io_uring_cqe *cqe;
int rc = io_uring_wait_cqe(ring, &cqe);
if (rc < 0) return rc;
int result = cqe->res; /* bytes read, or -errno */
io_uring_cqe_seen(ring, cqe);
return result;
}
Four options change the economics further. Registered buffers pin user pages
once instead of per operation. Registered files skip the descriptor table
lookup. Submission queue polling puts a kernel thread on the ring so that
submission costs zero syscalls, at the price of one busy core. Multishot
operations post many completions from one submission, which suits accept and
recv on a busy listener.
io_uring is a security surface
The interface gives user space a wide, asynchronous path into the
kernel, and it produced a steady stream of vulnerabilities. Google
disabled it on Android and on ChromeOS and restricted it inside Google
production. Container runtimes often block it in the default seccomp
profile. Before you adopt it, find out whether your platform allows
it. The control is /proc/sys/kernel/io_uring_disabled, where 0
allows it for everybody, 1 restricts it to a group, and 2 disables it.
eBPF: running your code inside the kernel
The extended Berkeley Packet Filter, or eBPF, is a virtual machine inside the Linux kernel. A program in eBPF bytecode attaches to a hook, runs when the hook fires, and reports results through a map. The design solves the oldest problem in kernel extension: a kernel module can crash the machine, and an eBPF program cannot.
Safety comes from a verifier. Before it loads a program, the kernel walks every possible path through it and proves several properties. Every memory access stays inside a known object. Every loop terminates, either because it is unrolled or because it carries a bounded counter. The program finishes within an instruction budget, which is one million instructions in current kernels. A program that fails any of these tests never loads.
The verifier is strict, and the strictness is the price of running in kernel
context with no memory protection. Writing eBPF means writing code that the
verifier accepts, which is a smaller language than C. Compilers and libraries
such as libbpf hide most of the pain. The Compile Once Run Everywhere
mechanism, or CO-RE, also lets one binary run across kernel versions, because
it relocates structure offsets at load time.
Table 2.2: eBPF attach points and what each one sees
| Hook | Fires on | Typical use |
|---|---|---|
kprobe / kretprobe |
any kernel function entry or return | ad hoc tracing |
fentry / fexit |
same, with a faster trampoline | production tracing |
| tracepoint | a stable, named kernel event | portable tracing |
uprobe / USDT |
a user space function or marker | language runtime tracing |
| perf event | a hardware or software counter overflow | CPU profiling |
| XDP | a packet, in the driver, before sk_buff |
DDoS drop, load balance |
TC (clsact) |
a packet, after sk_buff allocation |
policy, shaping, NAT |
socket filter, sockops |
socket level events | connection steering |
| cgroup hooks | connect, bind, sendmsg in a cgroup |
per-container policy |
| LSM | a security hook | mandatory access control |
struct_ops |
a kernel operations table | TCP congestion, sched_ext |
Maps carry state. A map is a typed key and value store that lives in the kernel and that both the eBPF program and user space can read. Hash maps, arrays, per-CPU variants, LRU variants, ring buffers, and stack trace maps cover almost every need. A per-CPU map removes the contention that a shared counter creates. User space sums the per-CPU values when it reads.
XDP deserves a separate mention because of where it runs. An XDP program runs
inside the network driver, before the kernel allocates a socket buffer. It can
drop, pass, redirect, or transmit a packet. Dropping in XDP costs a few tens
of nanoseconds, against several microseconds for an iptables drop. A single
core drops tens of millions of packets per second in XDP, which turns volume
based denial of service from an outage into a line on a graph. Chapter 10
builds a container network on the same mechanism.
/* Count packets per destination port, and drop one port entirely. */
SEC("xdp")
int xdp_count_and_drop(struct xdp_md *ctx) {
void *data = (void *)(long)ctx->data;
void *data_end = (void *)(long)ctx->data_end;
struct ethhdr *eth = data;
if ((void *)(eth + 1) > data_end) return XDP_PASS; /* bounds check */
if (eth->h_proto != bpf_htons(ETH_P_IP)) return XDP_PASS;
struct iphdr *ip = (void *)(eth + 1);
if ((void *)(ip + 1) > data_end) return XDP_PASS;
if (ip->protocol != IPPROTO_TCP) return XDP_PASS;
struct tcphdr *tcp = (void *)ip + ip->ihl * 4;
if ((void *)(tcp + 1) > data_end) return XDP_PASS;
__u16 dport = bpf_ntohs(tcp->dest);
__u64 *count = bpf_map_lookup_elem(&port_counts, &dport);
if (count) __sync_fetch_and_add(count, 1);
return dport == 9999 ? XDP_DROP : XDP_PASS;
}
Every bounds test in that program exists because the verifier demands it. Omit one and the program does not load. The discipline looks heavy on a twenty line example and it pays for itself when the same code runs on every packet that reaches a production host.
Interrupts and the network receive path
A device signals the CPU with an interrupt. The CPU stops what it does, saves state, and runs an interrupt handler. Handlers must be short, because interrupts are disabled while one runs. Linux therefore splits the work. The top half acknowledges the device and schedules a softirq. The bottom half, the softirq, does the real work with interrupts enabled.
A 100 gigabit network card that raises one interrupt per packet raises about 8 million interrupts per second at small packet sizes. The machine then does nothing else. NAPI solves this. On the first packet the driver disables further receive interrupts and the kernel polls the ring. It keeps polling while packets keep arriving, and it re-enables interrupts when the ring runs dry. Under load the machine takes almost no interrupts, and under a light load it takes one per packet with the lowest latency.
Scaling across cores needs the packets spread out. Receive side scaling, or RSS, computes a hash over the packet headers in the network card and picks one of several hardware queues from it. Each queue binds to one CPU through its own interrupt. Receive packet steering, or RPS, does the same in software when the card cannot. Receive flow steering, or RFS, goes further and delivers a flow to the CPU where the application thread that owns the socket runs, which keeps the socket data in one cache.
Three configuration changes that fix most network tail latency
Give the network card one receive queue per application core, and pin
each queue interrupt to its own CPU with
/proc/irq/<n>/smp_affinity_list. Turn off irqbalance, because it
moves interrupts and destroys cache locality. Turn on RFS with
rps_sock_flow_entries and per-queue rps_flow_cnt so that packets
land on the CPU that runs the reading thread. These three changes
routinely cut p99 latency by 20 to 40 percent on a busy service and
cost nothing.
Time, timers, and what a timestamp means
A server has several clocks and they answer different questions.
CLOCK_REALTIME gives wall clock time, and it jumps when NTP steps it or when
an operator changes it. CLOCK_MONOTONIC counts from an arbitrary point and
never goes backward, but it does not advance during suspend. CLOCK_BOOTTIME
behaves like CLOCK_MONOTONIC and includes suspend time.
CLOCK_MONOTONIC_RAW skips NTP frequency adjustment.
The rule is short. Measure durations with CLOCK_MONOTONIC. Record events
with CLOCK_REALTIME. A latency histogram built on CLOCK_REALTIME will show
negative durations the first time NTP steps the clock backward, and the bug
will appear once a month and never reproduce.
The underlying hardware source is usually the time stamp counter, or TSC. A
modern TSC is invariant, which means that it ticks at a constant rate across
frequency changes and across cores. The kernel picks a clock source and
exposes the choice in
/sys/devices/system/clocksource/clocksource0/current_clocksource. If that
file says hpet or acpi_pm instead of tsc, every timestamp costs hundreds
of nanoseconds instead of twenty, and the cause is usually a virtualization or
firmware problem worth fixing.
Timers have a resolution and a slack. High resolution timers give nanosecond
granularity in the interface and deliver with the accuracy that the hardware
and the scheduler allow, which is tens of microseconds in practice. The kernel
also applies a default timer slack of 50 microseconds per task so that it can
group wake-ups and save power. A program that needs tighter timing sets the
slack with prctl(PR_SET_TIMERSLACK, ...).
Sources and further reading
Robert Love, "Linux Kernel Development", for the structure. Brendan Gregg,
"Systems Performance", second edition, and "BPF Performance Tools", for the
measurement discipline and for most of the tooling in this chapter. The kernel
documentation tree under Documentation/admin-guide/cgroup-v2.rst for cgroup
semantics and Documentation/scheduler/sched-ext.rst for sched_ext. Jens
Axboe's liburing repository and its examples for io_uring. The Cilium eBPF
reference guide for the verifier and the map types.
What this chapter established
- A system call costs 50 nanoseconds of hardware and 250 to 700 nanoseconds with speculation mitigations active. Four syscalls per request at 200,000 requests per second consume a third of a core.
- Three techniques avoid the cost: batch the crossings with
io_uring, remove them with a shared mapping such as the vDSO, or move the code into the kernel with eBPF. - Memory is lazy.
mallocallocates nothing, the first write takes a fault, andforkcopies a page only when one side writes it. - A
writethat returns success is not durable. Onlyfsyncorfdatasyncis, and anfsyncerror is not retryable. - Free memory is not a health signal. Pressure Stall Information is. Direct reclaim inside an allocating thread is the usual cause of an unexplained 200 millisecond pause.
- EEVDF replaced CFS in kernel 6.6, and
sched_exthas let an eBPF program own the scheduling policy since 6.12. - A cgroup CPU quota produces a tail latency cliff, not a slowdown. Match the runtime thread count to the quota.
epollreports readiness and needs a second syscall per operation.io_uringreports completion and amortizes submission across a batch.epollremains a correct default for an ordinary network service.- eBPF trades expressiveness for a verifier that proves termination and memory safety before load. An XDP drop costs tens of nanoseconds against microseconds for
iptables. - Measure durations with
CLOCK_MONOTONICand record events withCLOCK_REALTIME. Mixing them produces negative latencies once a month.