How bpftime Implements Interprocess eBPF Maps Using Shared Memory
bpftime enables multiple processes to share eBPF maps without kernel involvement by storing map descriptors and data structures in a Boost.Interprocess managed shared memory segment, enabling zero-copy access across processes via offset pointers that remain valid across different address spaces.
bpftime is a userspace eBPF runtime that allows agents running eBPF programs and daemons managing them to communicate through shared data structures. By implementing interprocess eBPF maps using shared memory, the runtime eliminates syscall overhead and kernel-userspace copying while maintaining compatibility with standard eBPF map APIs.
Global Shared-Memory Segment Initialization
The foundation for interprocess communication begins with bpftime_initialize_global_shm() defined in runtime/src/bpftime_shm_internal.cpp. This function creates or opens a Boost.Interprocess managed_shared_memory segment using placement-new to construct a bpftime_shm object directly within the shared region.
The bpftime_shm instance maintains a handler_manager that stores a registry of all eBPF objects—including programs, maps, and links—within the segment. Because this registry lives in shared memory, any process that attaches to the same named segment (identified by bpftime_shm::get_global_shm_name()) can access the identical object handles.
Map Registration and Lifetime Management
When a user calls bpftime_maps_create(), the C shim forwards the request to bpftime_shm::bpftime_maps_create(), which ultimately invokes bpf_map_handler::map_init in runtime/src/handler/map_handler.cpp. This method receives a reference to the managed_shared_memory object and constructs the concrete map implementation directly within the shared segment.
The initialization uses memory.construct<impl_type>(container_name.c_str())(...) to allocate data structures such as hash_map_impl, array_map_impl, or ringbuf_map_impl inside the shared memory. Because Boost.Interprocess allocators manage these objects, they persist independently of the creating process and remain accessible to any process mapping the same segment.
Reference Counting for Safe Destruction
To manage map lifetime across process boundaries, bpftime allocates a map_refcount_t (an std::atomic_uint32_t) within the shared segment using memory.construct<map_refcount_t>(anonymous_instance)(1). This atomic counter tracks how many processes currently hold the map open. When a process exits or closes its handle, the counter decrements; only when it reaches zero does the implementation destroy the map data structures within the shared segment.
Zero-Copy Map Handles with Offset Pointers
The critical mechanism enabling cross-process map access is the use of Boost.Interprocess offset pointers. In runtime/src/handler/map_handler.hpp, the bpf_map_handler class stores the map implementation as boost::interprocess::offset_ptr<void> map_impl_ptr.
Unlike raw pointers, offset pointers encode the byte offset from the base of the shared memory segment rather than absolute virtual addresses. When Process A creates a hash map and Process B maps the same segment at a different virtual address, the offset pointer still resolves correctly by adding the stored offset to the current process's segment base. This allows processes to share handles regardless of where the shared memory maps in their respective address spaces.
Map Operations Across Processes
Public C APIs such as bpftime_map_lookup_elem(), bpftime_map_update_elem(), and bpftime_map_delete_elem() delegate to the handler manager, which retrieves the appropriate bpf_map_handler and forwards the call to the implementation object stored in shared memory.
Because the underlying containers—whether Boost.Unordered for hash maps or Boost.CircularBuffer for ring buffers—reside in the shared segment, all processes operate directly on the same physical memory. This architecture eliminates data copying between kernel and userspace or between separate processes, providing latency characteristics comparable to in-process data structures while maintaining strict isolation between agents and daemons.
GPU-Backed Maps (Optional Extension)
For GPU-compatible map implementations, bpftime extends the same shared-memory pattern. In runtime/src/bpf_map/gpu/nv_gpu_shared_hash_map.cpp, the implementation allocates CUDA memory and registers it with cudaHostRegister while keeping the map descriptor and metadata within the Boost shared segment.
The bpf_map_attr::gpu_thread_count field stores GPU-specific configuration, allowing the agent and daemon to coordinate GPU-accelerated map access through the same shared memory handles used for CPU-backed maps. This enables heterogeneous computing scenarios where eBPF programs trigger GPU kernels while map data remains accessible to CPU-side processes.
Summary
- bpftime creates a global Boost.Interprocess
managed_shared_memorysegment viabpftime_initialize_global_shm()inruntime/src/bpftime_shm_internal.cpp, establishing the foundation for interprocess eBPF maps using shared memory. - Map implementations are constructed directly in shared memory using
memory.construct<impl_type>()withinbpf_map_handler::map_init(runtime/src/handler/map_handler.cpp), ensuring data structures are visible to all processes. - Offset pointers (
boost::interprocess::offset_ptr) enable cross-process addressing by storing relative offsets rather than absolute virtual addresses, allowing processes to share handles regardless of where the segment maps in their address space. - Reference counting via
map_refcount_t(atomic uint32_t in shared memory) ensures safe map destruction when the last process closes its handle. - All map operations (lookup, update, delete) execute directly on shared memory containers (Boost.Unordered, Boost.CircularBuffer, etc.), providing zero-copy access between agents and daemons.
Frequently Asked Questions
How does bpftime ensure that shared memory pointers remain valid when processes map the segment at different virtual addresses?
bpftime uses Boost.Interprocess offset pointers (boost::interprocess::offset_ptr<void>) stored in the bpf_map_handler class. Unlike raw pointers, offset pointers encode the distance from the base of the shared memory segment rather than absolute virtual addresses. When a process maps the shared segment at a different base address, the offset pointer still resolves to the correct location by adding the stored offset to the current process's segment base. This mechanism is defined in runtime/src/handler/map_handler.hpp and used throughout runtime/src/handler/map_handler.cpp when constructing and accessing map implementations.
What happens to eBPF maps when the process that created them crashes or exits?
The shared memory segment persists independently of any single process because it is backed by operating system shared memory primitives (POSIX shm_open or Windows shared memory objects). When a process exits, the map_refcount_t atomic counter—allocated in shared memory via memory.construct<map_refcount_t> in runtime/src/handler/map_handler.cpp—is decremented. The map data structures remain accessible to other processes as long as the reference count is non-zero. Only when the last process closes the map (or exits, triggering automatic cleanup) does the reference count reach zero, causing destruction of the map implementation within the shared segment. This design ensures fault tolerance and allows daemons to outlive agents.
Can bpftime share eBPF maps between processes running on different physical machines?
No, bpftime's shared memory implementation relies on Boost.Interprocess managed shared memory segments, which are backed by operating system primitives (POSIX shm_open or Windows shared memory) that are inherently local to a single machine. The shared memory segment is identified by a name (returned by bpftime_shm::get_global_shm_name()) and mapped into each process's virtual address space on the same host. For distributed scenarios across physical machines, bpftime would require a different transport mechanism (such as network-based replication or remote procedure calls) layered on top of the local shared memory architecture. The current implementation in runtime/src/bpftime_shm_internal.cpp and runtime/src/handler/map_handler.cpp is explicitly designed for single-host interprocess communication.
How does the performance of bpftime's shared memory maps compare to kernel eBPF maps?
bpftime eliminates the syscall overhead and kernel-user space copying inherent in kernel eBPF maps. In runtime/src/handler/map_handler.cpp, map operations delegate directly to Boost containers (such as boost::unordered_map for hash maps or boost::circular_buffer for ring buffers) that reside in the shared memory segment. Because both the agent and daemon map the same physical memory into their address spaces, zero-copy access is achieved—reads and writes operate directly on the shared data structures without marshalling through system calls or socket buffers. Benchmarks referenced in the bpftime documentation indicate this architecture can deliver up to 10× higher throughput for observability workloads compared to traditional kernel-based map access, particularly when frequent updates or low-latency lookups are required. The trade-off is that bpftime maps are limited to single-host interprocess communication and do not provide the kernel's security isolation guarantees.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →