Networks, Hardware and Trading Infrastructure · Technology
3Network Cards and Kernel Bypass
Between the wire and a UDP socket’s recv a datagram is copied by the card into the host’s memory, announced by an interrupt, picked up by the kernel’s networking code in a software interrupt, matched to a socket, queued, and copied again into the buffer the program handed to recv; the program, meanwhile, was asleep and must be woken. On the laptop this book is written on, the median datagram spends under a microsecond getting into the kernel over loopback and about fifteen more microseconds getting out of it to a sleeping reader. None of that work decides anything a trading program cares about.
This chapter follows a packet through the network card and the kernel, then removes the kernel from the path. One Quant Book 13 (chapter 13) tuned the host so that busy polling and kernel bypass can do their job; this chapter looks at the same machinery from the card’s side: descriptor rings, interrupts and their coalescing, the steering of flows to queues, the stacks that run in user space, and the timestamps the card itself can take.
3.1 The path of a packet through the kernel
A network card and its driver share memory. The card writes arriving packets into buffers that the driver has given it, and says which ones it has filled; the driver takes them, hands the buffers back, and passes the packets up.
Definition 3.1 (Descriptor ring)
A descriptor ring is a circular array of descriptors, each pointing at a packet buffer in host memory and carrying an ownership flag. On the receive side the card fills the next descriptor it owns with an arriving packet (by direct memory access), marks it as the host’s, and advances its head; the host processes the descriptors it owns in order and hands them back to the card. When the next descriptor still belongs to the host, the card has nowhere to put the packet and drops it.
On Linux the kernel side of this is NAPI, the networking stack’s event mechanism: the card raises an interrupt, the kernel schedules the ring’s processing in a software interrupt, and the processing runs until the ring is empty or a budget is spent, with further interrupts from that ring disabled meanwhile. Under load the kernel is thus already polling; what it does not do by default is spare the program the socket queue, the copy and the wake-up.
Proposition 3.2 (When a ring overflows)
Let a ring of descriptors receive packets at rate while the host processes none for a time , starting from a ring in which the host holds descriptors. The ring drops packets. A host that hands descriptors back in batches of holds up to processed descriptors at any time, so it must size the ring for the longest pause plus .
Proof. The card has descriptors to fill; each arrival consumes one and none come back during the pause. Descriptors processed but not yet handed back remain the host’s until the batch completes. ∎
3.2 Interrupts, coalescing and receive-side scaling
An interrupt costs the host a switch into the kernel, the handler, and, for a sleeping reader, the scheduler’s wake-up: microseconds, paid per interrupt. Cards therefore wait before interrupting.
Definition 3.3 (Interrupt coalescing)
Interrupt coalescing is a card’s practice of delaying the receive interrupt after a packet arrives, until a given time has passed or a given number of packets are waiting, so that one interrupt serves several packets. On Linux the two limits are set with ethtool -C as rx-usecs and rx-frames; zero microseconds and one frame disable coalescing.
Proposition 3.4 (What coalescing buys and costs)
Let packets arrive as a Poisson process of rate , let the card interrupt after the first waiting packet (the frame limit never binding), and let each interrupt cost of CPU and each packet . Each interrupt serves on average packets, the host spends a fraction
of one core, and the first packet of each batch waits for the interrupt. Without coalescing () the host saturates one core at .
Proof. A batch opens with a packet and lasts ; by the memorylessness of the Poisson process, more packets arrive on average. Interrupts therefore come at rate , each costing , and every packet costs . ∎
Definition 3.5 (Receive-side scaling, flow steering)
Receive-side scaling (RSS) is a multi-queue card’s distribution of arriving packets over several receive rings, by a hash of their addresses and ports, so that different cores can process different flows. Flow steering is the card’s placing of chosen flows (by exact match on addresses and ports) on a chosen ring, overriding the hash.
A trading host uses both against RSS’s intent. RSS spreads load evenly, which is what a web server wants; a feed handler wants every packet of line A on the one ring its pinned core polls, and nothing else there. Flow steering puts the venue’s groups on the rings of the cores that handle them and the rest of the machine’s traffic elsewhere, and the core that polls a ring should sit on the socket the card is attached to (One Quant Book 13, chapter 4).
3.3 Polling drivers and user-space stacks
Definition 3.6 (Poll-mode driver, user-space network stack)
A poll-mode driver runs in the application’s process: it maps a card’s rings into user space and reads and refills them in a loop on a dedicated core, with no interrupts and no system calls. A user-space network stack implements IP, UDP and TCP in the application’s process on top of such access to the card, usually behind the standard socket calls, so that an unmodified program runs over it.
Remark 3.7 (The families of bypass)
Three kinds are in use, and a firm often runs two. Raw ring access through a vendor’s interface (AMD’s ef_vi for its Solarflare cards) or a portable poll-mode framework (DPDK) gives the program frames and nothing else: it must parse its own UDP and, for orders, bring its own TCP. A user-space stack behind the socket calls (OpenOnload, open source, which intercepts the socket calls of an unmodified binary) keeps the program’s code and removes the kernel from its path. And Linux’s own AF_XDP moves frames from a card’s ring to a region of the program’s memory through an in-kernel filter, in copy or in zero-copy mode, with the kernel still in charge of the device. Method 3.8 says which to use where.
Method 3.8 (Choosing a receive path)
- Market data on the hot path: raw ring access, polled by a pinned core on the card’s socket, the handler parsing UDP itself; the feed is multicast UDP, which needs no protocol state.
- Order entry: a user-space TCP stack, or the card vendor’s TCP in hardware (chapter 7), because order entry needs TCP’s state and retransmission and writing one’s own TCP is a project of its own.
- Everything else (drop copy, reference data, monitoring): the kernel, on cores and rings of its own, so that it never shares a queue with the hot path.
- In every case measure: the gain depends on the card, the host’s tuning and the traffic, and a poller that is ever descheduled loses more than it gained.
bool nic_rx(std::uint64_t seq, std::uint32_t length) {
const std::uint32_t i = head_;
if (owner_[i] == 1) { ++drops; return false; } // still the host's: the ring is full
seq_[i] = seq, len_[i] = length, owner_[i] = 1;
head_ = (i + 1) & (size_ - 1);
++rx;
return true;
}
std::uint32_t poll(std::uint32_t budget) {
std::uint32_t n = 0;
while (n < budget && rx > processed) {
const std::uint32_t i = tail_;
mix(seq_[i]), mix(len_[i]);
tail_ = (i + 1) & (size_ - 1);
++processed, ++pending_, ++n;
if (pending_ >= refill_) give_back();
}
return n;
}
firm.nicring: the card fills descriptors it owns; the poll loop processes those it owns and hands them back in batches. code/firm/nicring/cpp/firm_nicring.hpp3.4 Zero copy and the transmit side
Definition 3.9 (Zero-copy networking)
Zero-copy networking is the delivery of a packet to the program, or from it, in the buffer the card reads or writes by direct memory access, without the processor copying its bytes between a kernel buffer and the program’s.
A copy of a 100-byte datagram is a few nanoseconds of memory traffic; what zero copy saves on the receive side is less the copy than the machinery around it (the socket buffer, its accounting, the cache lines of a second location). On the transmit side the same ring structure runs in reverse: the program writes an order into a transmit buffer, fills a descriptor and writes the card’s doorbell register, and the card fetches the frame by direct memory access. Each of those steps is a crossing of the processor’s interconnect to the card (One Quant Book 13, chapter 4). Some cards let the program push the frame’s bytes into the card’s own memory with processor writes instead: AMD’s ef_vi documents such a programmed-I/O region, which “avoids the latency associated with a DMA read”, can be filled before the critical path and updated on it, and, in a cut-through mode, starts transmitting while the frame is still crossing the bus. The cheapest transmit is the one prepared before it is needed: the order’s frame built in advance, with only price, size and identifier to fill, the same idea chapter 7 takes into hardware.
3.5 Timestamps on the card
The kernel can timestamp a packet when it enters the kernel (SO_TIMESTAMPING, software receive timestamps), and a card with its own clock can timestamp it when it reaches the card’s port, a hardware timestamp in the sense of One Quant Book 13, chapter 5. The difference is the time the packet spent in the card and its ring, which is exactly what a firm tuning its receive path wants to see and what software timestamps cannot show. The card’s clock is only as good as its synchronisation to the firm’s time reference, the subject of chapter 4, and the timestamps of the whole cage are compared in chapter 5.
3.6 Tutorial: the kernel’s receive path, measured and modelled
Goal. Measure what the kernel’s receive path costs on the laptop, model what interrupts and polling cost on a card, and run the descriptor ring in three languages. End state: Table 3.1, Figures 3.3 and 3.4, and the ring’s fixture green in Python, C++ and Rust.
- Three ways to read.
cpp/nw_rx.hppsends 64-byte datagrams over loopback, each carrying its send time, and reads them by a blockingrecvmsg, by non-blocking reads in a loop, and byrecvmmsgin batches of 16, with the kernel’s software receive timestamp in each (Listing 3.2).python bench_rx.pyruns 20 000 datagrams per mode, 20 microseconds apart, and writes the quantiles. - Split the path. The kernel’s timestamp splits each datagram’s latency into getting into the kernel and getting out of it to the program.
- Model the card.
nw_nic.coalesceandpollsimulate a card’s interrupts with coalescing and a polling core, with the model’s costs;python fig_nic.pywrites Figure 3.4’s data. - The ring.
firm.nicringruns the same stream of arrivals and polls through four ring configurations in Python, C++ and Rust and checks counters and hashes against the shared fixture.
What to change next. Pin the busy-polling reader to a core and the sender to another (taskset) and read its tail again; in the ring’s fixture, find the smallest ring that drops nothing when the host hands back in batches of 32.
while (static_cast<int>(out.size()) < n) {
int got = 0;
if (mode == 2) {
for (int k = 0; k < kBatch; ++k) mm[k].msg_hdr.msg_controllen = sizeof ctrl[k];
got = recvmmsg(rx, mm, kBatch, 0, nullptr);
} else {
mm[0].msg_hdr.msg_controllen = sizeof ctrl[0];
const ssize_t r = recvmsg(rx, &mm[0].msg_hdr, mode == 1 ? MSG_DONTWAIT : 0);
got = r > 0 ? 1 : 0;
}
const std::int64_t app = now_ns();
for (int k = 0; k < got; ++k) {
std::int64_t sent = 0;
std::memcpy(&sent, data[k], sizeof sent);
out.push_back({sent, kernel_stamp(mm[k].msg_hdr), app});
}
}
| Read method | Into the kernel, p50 | Kernel to program, p50 | Total, p50 | Total, p99 |
|---|---|---|---|---|
Blocking recvmsg | ||||
| Busy polling | ||||
Batched recvmmsg |
-O2. Data: bench_rx.py, measured_rx.csv.sendto to the program holding the datagram, on the laptop’s loopback, by read method. Busy polling cuts the median tenfold; without an isolated core the polling thread is sometimes descheduled for milliseconds, and every datagram behind it waits: from the 95th quantile on, it is the worst of the three. Measured on a laptop (Intel Core Ultra 7 155H) under WSL2, no isolated cores. Data: bench_rx.py.The measurement shows the two halves of this chapter’s argument (Table 3.1). Getting a datagram into the kernel costs under a microsecond; getting it out to a sleeping program costs fifteen at the median and hundreds of microseconds at the 99th percentile, because the program must be woken and scheduled. A program that never sleeps removes that: busy polling brings the median down to . And a program that never sleeps must never be interrupted: on a laptop whose cores are shared with everything else the poller is sometimes descheduled, and the datagrams that arrive meanwhile wait for milliseconds. Batching amortises the system call but not the wake-up, and waits for its batch. Loopback involves no card at all, so these figures are the kernel’s share alone; a real card adds its ring, its interrupt and its coalescing.
def coalesce(lam, u, m, c_irq=C_IRQ, w=WAKE, c_pkt=C_PKT, n=100_000, seed=1):
rng = np.random.default_rng(seed)
t = np.cumsum(rng.exponential(1.0 / lam, n))
lat = np.empty(n)
i, busy_until, irqs, cpu = 0, 0.0, 0, 0.0
while i < n:
first = t[i]
deadline = first + u
j = i
while j < n and j - i < m and t[j] <= deadline:
j += 1
fire = t[j - 1] if j - i == m else deadline # m-th packet or the timer
start = max(fire + w, busy_until) + c_irq
done = start + c_pkt * np.arange(1, j - i + 1)
lat[i:j] = done - t[i:j]
busy_until = done[-1]
irqs += 1
cpu += c_irq + c_pkt * (j - i)
i = j
span = t[-1] - t[0]
return {"mean_us": float(lat.mean()), "p99_us": float(np.quantile(lat, 0.99)), "cpu": float(cpu / span),
"irq_rate": float(irqs / span)}
fig_nic.py.The model (Listing 3.3) takes of CPU per interrupt, of wake-up and per packet: model values of the order of the laptop’s wake-up, not a particular card’s. At 100 000 packets a second an interrupt per packet gives of mean latency for 23% of a core; an timer gives for 14%; polling gives for the whole core. At 500 000 an interrupt per packet is beyond one core (the saturation rate of Proposition 3.4 is 435 000 a second), and coalescing is what keeps the host alive. A trading host resolves the trade-off by not choosing it: the hot path polls on cores bought for the purpose, and everything else coalesces.
3.7 Build: the descriptor ring
Purpose. A model of a card’s receive ring precise enough to reason about drops, batching and polling budgets, in the three languages of the firm’s systems.
Interface. Python firm_nicring.Ring(size, refill) with nic_rx(seq, length), poll(budget), run(events) and the counters rx, drops, processed, doorbells, max_owned, hash; make_events. C++20 firm::nicring::Ring (cpp/firm_nicring.hpp) and Rust firm_nicring::Ring with the same methods.
Rules. The ring’s size is a power of two; the card never overwrites a descriptor the host owns; the host processes in order, never more than its budget per poll, and hands back in batches of refill, one doorbell per batch; the hash is FNV-1a over the processed packets’ sequence numbers and lengths in processing order.
Acceptance tests. code/firm/nicring/: the shared fixture (20 000 arrivals, four configurations) reproduced exactly in Python, C++ and Rust; a full ring drops and a processed but unreturned descriptor still counts as full; under random interleavings every accepted packet is processed once and in order.
Stretch. A transmit ring with a completion queue; several rings with a steering function, to reproduce the imbalance of a hash that puts two busy groups on one ring.
Sources and further reading
- Linux kernel documentation: “NAPI”, “Scaling in the Linux Networking Stack”, “Timestamping”, “AF_XDP”;
include/uapi/linux/ethtool.h(struct ethtool_coalesce); man pagerecvmmsg(2). - DPDK Programmer’s Guide, “Poll Mode Driver”; AMD, ef_vi User Guide; OpenOnload README.
3.8 Exercises
Exercise 3.1 ★
A ring of 512 descriptors, of which the host holds 40, receives 1.5 million packets a second while its core is interrupted for . How many packets are dropped?
Solution
Solution of Exercise 3.1.
arrivals for free descriptors: 128 packets dropped.
Exercise 3.2 ★
With coalescing at 20 microseconds, and packets at 200 000 a second, how many packets does an interrupt serve on average, and at what rate do interrupts arrive?
Solution
Solution of Exercise 3.2.
packets an interrupt; interrupts at a second.
Exercise 3.3 ★
From Table 3.1, what share of the blocking read’s median is spent after the datagram entered the kernel?
Solution
Solution of Exercise 3.3.
: 95%. Almost all of it is the wake-up and scheduling of the sleeping reader, not the network stack.
Exercise 3.4 ★★
Why does receive-side scaling’s even spreading of flows work against a feed handler, and what does flow steering change?
Solution
Solution of Exercise 3.4.
RSS hashes flows over rings to balance cores; a feed handler wants all of a line’s groups on the one ring its pinned core polls, and no other traffic there. Flow steering pins the venue’s flows to the chosen ring and keeps the rest of the host’s traffic on others.
Exercise 3.5 ★★
With the model’s costs, at what packet rate does an interrupt per packet use a whole core? And if every interrupt serves 16 packets?
Solution
Solution of Exercise 3.5.
packets a microsecond: 435 000 a second. With 16 packets an interrupt, million a second.
Exercise 3.6 ★★
Busy polling improved the median tenfold and made the 99th percentile sixteen times worse (Table 3.1). Explain both, and say what must change on a production host.
Solution
Solution of Exercise 3.6.
The poller never sleeps, so the wake-up (most of the blocking median) disappears. But on shared cores the scheduler sometimes runs something else on the poller’s core for milliseconds, and every datagram arriving meanwhile waits. On a production host the polling thread gets an isolated core with its interrupts and timer tick moved away (One Quant Book 13, chapter 13), on the card’s socket.
Exercise 3.7 ★★★
Coding. With firm.nicring and the fixture’s events, find the smallest ring size that drops nothing when descriptors are handed back in batches of 32.
Solution
Solution of Exercise 3.7.
128 descriptors: with batches of 32, a ring of 64 still drops 36 packets of the fixture, a ring of 128 drops none.
Exercise 3.8 ★★★
Find the flaw. “We moved order entry to a raw poll-mode interface and wrote our own minimal TCP: no retransmission timer, since colocation links do not lose packets. Latency fell by two microseconds.”
Solution
Solution of Exercise 3.8.
Loss is rare, not impossible (a flapping optic, a switch buffer during a burst, a failover): without retransmission one lost segment leaves the session stuck or silently missing an order, and the venue’s session layer will disconnect or cancel. The two microseconds came from removing the kernel, not from removing reliability; use a user-space or hardware TCP that implements it.
3.9 Problem: Interrupts or Polling
Problem 3.1
Weekend problem — the price of a sleeping reader
A feed arrives at 200 000 packets a second on one receive ring. Use the chapter’s model costs: of CPU per interrupt, of wake-up, per packet, and a polling loop that checks the ring every .
Part I — One interrupt per packet.
- What share of a core do interrupts and packets take?
- What is the smallest latency a packet can have?
- The model’s mean latency is . Why is it above the minimum?
- At what rate would the core saturate?
Part II — A 10-microsecond timer.
- How many packets does an interrupt serve on average?
- At what rate do interrupts come?
- What share of the core is used?
- The model’s mean latency is . Where does the increase come from?
Part III — Polling.
- What is the mean wait for the next check of the ring?
- What is the mean latency, and the model’s ()?
- What share of a core is used?
- What must be true of that core?
Part IV — The verdict.
- State the named result: the mean latency and CPU of the timer and of polling at this rate, and the rate at which interrupts alone would take a whole core.
- How many microseconds per packet does polling save over the timer?
- At what packet rate does the timer’s CPU reach a quarter of a core?
- Why do trading hosts poll the hot path and coalesce the rest?
- What would you measure on the real card before believing the model?
- What does a descriptor ring of 1 024 entries give the polling core, in time, at this rate?
- In one sentence: what does a sleeping reader cost?
- Which of the chapter’s three families of bypass would you use for this feed, and why?
Solution
Solution of Problem 3.1.
- : 46% of a core.
- .
- Queueing: an interrupt that arrives while the previous one is still being handled waits for it.
- per microsecond: 435 000 packets a second.
- .
- a second.
- : 19.3%.
- From the timer: the first packet of a batch waits for it, later ones less, before the of handling.
- Half a loop, .
- ; the model’s 0.41 adds the rare packet that arrives while the previous one is processed.
- The whole core.
- Isolated, never descheduled, on the card’s socket, with nothing else to do.
- Named result. At 200 000 packets a second: a timer gives of mean latency for 19.3% of a core; polling gives for all of it; interrupts alone would take a whole core at 435 000 packets a second.
- About (12.4 against 0.41).
- Where : about 324 000 packets a second.
- The hot path is worth a core; the rest of the host’s traffic is not, and coalescing keeps it from interrupting the machine all day.
- Interrupt cost and wake-up with a profiler and hardware timestamps, the card’s coalescing behaviour under the real feed, and the distribution of pauses of the polling core.
- seconds: of pause before the ring overflows.
- Microseconds of wake-up per batch, and a tail as long as the scheduler’s longest decision.
- Raw ring access for the multicast feed, polled on an isolated core: the feed needs no TCP, and raw access removes the most.
3.10 Interview questions
Interview question 3.1 ★ developer
Walk me through what happens between a packet arriving at the card and recv returning.
Solution
Solution of Interview question 3.1.
The card writes the frame by DMA into a buffer named by the next receive descriptor and marks it done; it raises an interrupt (perhaps after coalescing); the kernel schedules NAPI processing in a software interrupt, which reads the ring, builds the socket buffer, passes it up IP and UDP, queues it on the socket and wakes the reader; recv copies it into the program’s buffer.
What the interviewer is looking for: DMA, the ring, the interrupt and softirq, the socket queue, the copy and the wake-up.
Interview question 3.2 ★★ developer
What is interrupt coalescing? How would you set it on a market-data host?
Solution
Solution of Interview question 3.2.
The card delays the receive interrupt by a time or a packet count so that one interrupt serves several packets: less CPU, more latency. On a market-data host the hot rings are polled, so their coalescing does not matter; for the others choose moderate values and turn adaptive coalescing off where predictability matters.
What the interviewer is looking for: the latency-CPU trade-off and that the hot path avoids it by polling.
Interview question 3.3 ★★ developer
What is kernel bypass, what forms does it take, and what do you give up?
Solution
Solution of Interview question 3.3.
Running the data path in user space: raw ring access (vendor interfaces, DPDK), user-space stacks behind the sockets API, or AF_XDP. It removes interrupts, system calls, copies and wake-ups. You give up the kernel’s tools, isolation and TCP (unless the stack brings its own), and you spend a core per polling thread.
What the interviewer is looking for: the families and the costs, not only the speed.
Interview question 3.4 ★★ developer
Your busy-polling feed handler has a great median and a terrible 99.9th percentile. What do you check?
Solution
Solution of Interview question 3.4.
Whether the polling core is really isolated (other tasks, interrupts, timer ticks, kernel threads on it), frequency and idle states, page faults and allocation on the path, the ring size against the longest pause, and whether the tail coincides with bursts. Measure with hardware timestamps against the software ones.
What the interviewer is looking for: scheduling interference first, then the ring.
Interview question 3.5 ★★★ developer
A descriptor ring drops packets although the handler’s average load is 30%. Explain how, and how you would size the ring.
Solution
Solution of Interview question 3.5.
Average load says nothing about pauses: while the host is not processing (a burst, a descheduling, a batched refill), the card consumes free descriptors at the arrival rate, and a pause longer than free descriptors divided by that rate drops packets. Size the ring for the burst rate times the longest pause plus the refill batch.
What the interviewer is looking for: pause times rate, and the descriptors held by batching.
Interview question 3.6 ★★ developer, researcher
Why would you want hardware receive timestamps when you already timestamp in the kernel?
Solution
Solution of Interview question 3.6.
The kernel’s timestamp is taken after the card, its ring, the interrupt and the driver; the card’s is taken at the port. Their difference is the receive path the firm is trying to tune, and only the card’s timestamp compares with the timestamps of switches and taps (chapter 5).
What the interviewer is looking for: where on the path each timestamp is taken.