A high-performance multi-process trading system built in C++17 as an OS capstone project. The system demonstrates production-inspired HFT infrastructure built entirely from scratch: a custom virtual memory manager, slab allocator, lock-free ring buffer, seqlock, shared memory IPC, and a supervisor with crash recovery.
Platform: Linux (Docker container), C++17, CMake
Sanitizers: AddressSanitizer, ThreadSanitizer
market_data_receiver ──ring buffer──▶ strategy_engine
│ │
└──────── seqlock (TopOfBook) ─────────┘
│ │
└──────── heartbeat region ────────────┘
│ │
└──────── latency region ──────────────┘
│
supervisor
(monitors both,
restarts on crash)
Three processes communicate exclusively through shared memory — no sockets, no pipes, no kernel round-trips in the hot path.
| Process | Role |
|---|---|
supervisor |
Creates the heartbeat region, spawns the other two processes, monitors liveness via pid_alive + heartbeat staleness, restarts crashed processes |
market_data_receiver |
Simulates a market feed: generates Message structs, pushes them onto the ring buffer, updates the order book (and seqlock), stamps latency measurements |
strategy_engine |
Consumes messages from the ring buffer, reads the top-of-book seqlock, makes trade-signal decisions, detects peer restarts via generation counter |
| Name | Type | Creator |
|---|---|---|
/trading_ringbuf |
RingBuffer<1024> |
market_data_receiver |
/trading_tob |
Seqlock<TopOfBook> |
market_data_receiver |
/trading_heartbeat |
HeartbeatRegion |
supervisor |
/trading_latency |
LatencyRegion |
market_data_receiver |
Two-layer system built on mmap / mprotect:
VirtualAddressSpace— reserves a 4 GB virtual address space withPROT_NONEup front.commit(offset, size)promotes pages toPROT_READ|PROT_WRITEviamprotect;uncommitrevokes them and callsmadvise(MADV_FREE)to return physical pages to the OS.RegionManager— carves the VAS into named typed regions using a watermark bump allocator. Each region gets guard pages (permanentlyPROT_NONE) on both sides to catch overflows and underflows. Supports up to 16 regions with zero heap allocation (std::array<Region, 16>).
Installed on an alternate stack (sigaltstack) so it runs even when the main stack overflows. On SIGSEGV, checks whether the faulting address is a guard page:
- Guard page hit — prints a diagnostic (region name, fault direction) to stderr using
write()(async-signal-safe), dumps a backtrace viabacktrace_symbols_fd, then_exit(1). - Non-guard fault — resets to
SIG_DFLand re-raises to produce a core dump.
Fixed-size object allocator backed by the RegionManager. Uses an intrusive free list (next pointer stored in the first 8 bytes of each free slot) for O(1) alloc and free with no heap touches. Doubles committed memory on expansion. Validates pointer alignment and region membership on deallocate — aborts on bad pointer rather than silently corrupting the list.
Lock-free SPSC ring buffer. Key design points:
- Template parameter
Nmust be a power of 2; indices are never-wrappinguint64_twith& (N-1)masking. - Head (write index) and tail (read index) sit in separate
alignas(64)structs with 56 bytes of padding each to prevent false sharing. - Producer: writes message, then
store(head+1, release)— ensures data visible before head advances. - Consumer:
load(head, acquire)— ensures all writes before the release are visible.
Reader-writer synchronization without a read-side lock:
- Writer:
seq.store(s+1, release)→ write data →seq.store(s+2, release). Odd sequence indicates a write in progress. - Reader: load seq → spin while odd →
atomic_thread_fence(acquire)→ copy data →atomic_thread_fence(acquire)→ reload seq → retry if changed. - Sequence counter and data on separate cache lines to avoid writer traffic invalidating reader cache lines.
Seqlock completely eliminates writer starvation that plagues pthread_rwlock under heavy read pressure (rwlock write p50: 32 ms vs seqlock write p50: 32–64 ns).
Named constructor pattern: SharedMemory::create(name, size) and SharedMemory::attach(name, size).
createcallsshm_unlinkfirst to clear stale objects from previous crashes, thenshm_open+ftruncate+mmap.attachretries up to 100 × 10 ms to handle the race where the consumer starts before the creator.- Destructor calls
shm_unlinkonly if this instance is the owner (is_ownerflag).cleanup()is idempotent so it's safe to call from both the destructor and a signal handler.
Tracks best bid and best ask using Order objects allocated from the slab. On each update(Message&), the displaced order is immediately returned to the slab — at most 2 live Order objects exist at any time. Writes a TopOfBook snapshot to the seqlock whenever best bid or ask changes.
Each process calls beat() every 1000 messages, which increments a counter and stores now_ns(). The supervisor calls is_stale(500ms) in its 100 ms poll loop. A generation counter is incremented by the supervisor on each restart; the strategy engine watches the receiver's generation to detect peer restarts and reset expected_seq = 0.
Power-of-2 histogram with nanosecond resolution (63 - __builtin_clzll(ns) bucket). Measures:
- Ring buffer push latency (receiver side)
- Order book update latency (receiver side)
- Ring IPC latency:
pop_time - msg.timestamp(end-to-end across process boundary) - Top-of-book seqlock read latency
- Full pipeline latency:
tob_read_time - msg.timestamp
Forks and execs both child processes (receiver first, then engine after a 200 ms delay to let the receiver create shared memory). Main loop runs every 100 ms: reaps zombies, checks pid_alive and heartbeat freshness for each process. On failure, increments the generation counter, sends SIGTERM → waits 50 ms → SIGKILL if still alive, then re-spawns. Prints a status table every 5 seconds.
# Enter the Docker container
docker-compose up -d
docker-compose exec trading-dev bash
# Inside the container
cd /workspace
mkdir -p build && cd build
# Release build (default for running the system)
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
# Debug build
cmake -DCMAKE_BUILD_TYPE=Debug ..
make -j$(nproc)
# With AddressSanitizer
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON ..
make -j$(nproc)
# With ThreadSanitizer
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_TSAN=ON ..
make -j$(nproc)
# Build a single target
make market_data_receiver
make supervisor
make seqlock_testIf CMake output goes to the wrong directory, clean and rebuild:
cd /workspace/build && rm -rf *
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)Important: all binaries must be run from /workspace/build so that the supervisor can resolve ./market_data_receiver and ./strategy_engine.
cd /workspace/buildThe supervisor manages both child processes and restarts them on crash:
./supervisorExpected output:
supervisor: starting
supervisor: heartbeat region created at 0x...
supervisor: spawned market_data_receiver pid=... restart_count=1
supervisor: spawned strategy_engine pid=... restart_count=1
supervisor: monitoring processes (heartbeat threshold: 500 ms)
market_data_receiver: creating shared memory regions
market_data_receiver: starting as generation 0
strategy_engine: attaching to shared memory
strategy_engine: running
TRADE SIGNAL #1: ...
Stop with Ctrl-C. The supervisor forwards SIGTERM to both children and waits for them to exit cleanly. The strategy engine prints the full latency report on shutdown.
# Terminal 1
./market_data_receiver &
# Terminal 2
./strategy_engineNote: market_data_receiver must start first so it can create the shared memory regions. strategy_engine will retry attachment for up to ~1 second.
With the supervisor running, send SIGKILL to a child process:
kill -9 $(pgrep market_data_receiver)The supervisor detects the death within 100 ms (via pid_alive), increments the generation counter, and re-spawns the process. The strategy engine detects the generation change and resets its sequence counter.
If a previous run crashed without cleaning up:
rm -f /dev/shm/trading_*Each test is a standalone binary. Run from the build directory:
cd /workspace/build
./virtual_memory_manager_test
./slab_allocator_test
./ring_buffer_test
./seqlock_testRun with sanitizers:
# Rebuild with TSan first
cd /workspace/build && rm -rf *
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_TSAN=ON .. && make -j$(nproc)
./seqlock_test
./ring_buffer_test
# Rebuild with ASan
cd /workspace/build && rm -rf *
cmake -DCMAKE_BUILD_TYPE=Debug -DENABLE_ASAN=ON .. && make -j$(nproc)
./slab_allocator_test
./virtual_memory_manager_testcd /workspace/build
# Slab vs malloc allocation benchmark
./allocator_benchmark
# Seqlock vs pthread_rwlock benchmark
./seqlock_benchmarkinclude/
virtual_memory_manager.h # VirtualAddressSpace, RegionManager
signal_handler.h # install_signal_handler()
slab_allocator.h # SlabAllocator
ring_buffer.h # RingBuffer<N>, Message (header-only template)
shared_memory.h # SharedMemory
order_book.h # Order, TopOfBook, OrderBook
seqlock.h # Seqlock<T> (header-only template)
heartbeat.h # Heartbeat, HeartbeatRegion (header-only)
latency.h # Histogram, LatencyRegion (header-only)
src/
virtual_memory_manager.cpp
signal_handler.cpp
slab_allocator.cpp
shared_memory.cpp
order_book.cpp
market_data_receiver.cpp # producer process
strategy_engine.cpp # consumer process
supervisor.cpp # process monitor / crash recovery
tests/
virtual_memory_manager_test.cpp
slab_allocator_test.cpp
ring_buffer_test.cpp
seqlock_test.cpp
allocator_benchmark.cpp
seqlock_benchmark.cpp
CMakeLists.txt
build/ # out-of-source build directory