Quant developer HFT course · Module 6: The operating system
Processes, threads and shared memory
The question
The feed handler and the strategy are separate processes so that a strategy crash cannot take down the feed. How should they talk to each other, and what does it cost?
Processes or threads
Threads share one address space: passing data is a pointer and a queue, and a crash or memory corruption in one takes down all of them. Processes have separate address spaces: a crash in one leaves the others running, they can be restarted and deployed independently, and one can be written in a different language, but passing data needs an inter-process mechanism.
Trading systems use both. A common shape is one process per function (market-data handling, each strategy, order gateway, risk), with several pinned threads inside each, communicating through shared memory. The question's design reflects that.
The mechanisms and their costs
| Mechanism | Path per message | Typical use |
|---|---|---|
| Pipe, Unix domain socket, TCP over loopback | write and read system calls, a copy into and out of the kernel, possibly a wake-up | control messages, anything not latency-critical |
| Message queues (POSIX, System V) | system calls and kernel copies | rare in trading |
| Signals | asynchronous interrupt of the receiver | never for data |
| Shared memory | ordinary loads and stores to a region mapped into both processes | the hot path |
Every mechanism that goes through the kernel costs at least two system calls and two copies per message, plus a wake-up if the reader sleeps. That is microseconds. Shared memory costs what a cache line transfer between cores costs.
Shared memory with a ring buffer
Both processes map the same physical pages, so a data structure placed in that region is visible to both. The SPSC ring from Module 4 works unchanged, as long as it uses only atomics and plain data and contains no pointers.
// both processes
int fd = shm_open("/md_to_strat", O_CREAT | O_RDWR, 0600);
ftruncate(fd, sizeof(Ring));
void* p = mmap(nullptr, sizeof(Ring), PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, fd, 0);
// creator only: placement-new the ring so its atomics are constructed once
Ring* ring = static_cast<Ring*>(p);
Rules for structures in shared memory:
- No pointers. The region may be mapped at different addresses in each process. Use offsets or indices.
- Only lock-free atomics, and check
std::atomic<T>::is_always_lock_free; an atomic implemented with a hidden lock would put the lock in one process's memory. - Trivially copyable, fixed-layout data, checked with
static_asserton sizes, since the two sides may be built separately. - A version and layout check in a header at the start of the region, so a reader built against an old layout refuses to attach.
- Huge pages and pre-faulting for large regions, as in the previous lessons.
What happens when one side dies
Shared memory makes crash isolation partial. If the strategy dies, the ring keeps its state, the feed keeps writing, and the ring fills; the feed must treat a full ring as "consumer gone or slow" and drop rather than block. If the feed dies, the strategy sees no new data and needs a heartbeat to tell silence from a dead producer. A restarted process must be able to attach to an existing region and resume, or start from a clean one. A mutex in shared memory is particularly dangerous: if a process dies holding it, the others wait for ever unless it is a robust mutex (PTHREAD_MUTEX_ROBUST), which is one more reason the hot path uses lock-free structures.
Many readers
Market data is usually consumed by several strategies. Rather than one ring per reader written by the feed, a common design is a single broadcast ring: the feed writes each message once, each reader keeps its own read position, and the writer never waits for readers. A reader that falls a full lap behind detects that its position has been overwritten (from a sequence number in each slot) and resynchronises from a snapshot. This is the same idea as the seqlock, applied to a log of messages.
Check yourself
Question 1 of 3
Why is a Unix domain socket slower than shared memory for passing each message?
Kernel-mediated IPC pays for the system calls and the copies on every message. With shared memory, the data moves through the cache coherence protocol like any other shared write.
Question 2 of 3
Why must a structure placed in shared memory avoid raw pointers?
An address valid in one process may be meaningless in the other. Offsets from the start of the region work in both.
Question 3 of 3
The consumer process crashes. What should the producer writing a shared ring do?
A producer that blocks on a dead consumer turns one failure into two. Detect it, keep running, and let monitoring and restart logic deal with the consumer.
In the interview
Say your own answer out loud first, then open the one we would give.
When would you use separate processes rather than threads?
When I want fault isolation, independent deployment and restart, or different languages: a strategy crash should not take down market data or the gateway. Threads are simpler when components must share a lot of state and fail together anyway. Trading systems often use both: processes per function, pinned threads inside.
What is the fastest way for two processes on one machine to exchange messages?
Shared memory with a lock-free ring buffer: both processes map the same pages and exchange data with ordinary loads and stores, so there are no system calls or kernel copies per message. The structure must use offsets instead of pointers, lock-free atomics and a fixed, versioned layout.
What are the risks of shared memory IPC?
Layout mismatches between separately built processes, which a version header and static asserts guard against; pointers that are meaningless in the other process; locks held by a process that dies; and needing heartbeats to detect a dead peer. Lock-free structures and a producer that never waits on consumers keep failures contained.