Skip to content
FreeBSDDeep Dive Published Updated 8 min readViews unavailable

FreeBSD POSIX Shared Memory and Named Semaphores: Lifecycle Without Races

Create, map, synchronize, inspect, and retire FreeBSD POSIX shared-memory objects with explicit sizing, permissions, error handling, and recovery.

POSIX shared memory lets cooperating processes access the same memory object without copying every payload through a pipe or socket. A name opened with shm_open identifies the object; mmap maps its bytes into each process. A named semaphore can coordinate access or signal that a producer has published a record. These are separate mechanisms: shared memory does not provide a lock, message queue, transaction, or crash-recovery protocol.

This distinction determines whether an implementation is safe. The object name controls discovery and lifetime, the file descriptor controls one process’s access to the object, each mapping has its own virtual address, and the application defines the byte layout and synchronization contract. A correct deployment must specify all four.

Confirm the API and operating-system contract

FreeBSD provides shm_open and shm_unlink as system calls. The manual documents that names beginning with a slash identify a shared object consistently between processes; a string that looks like a filesystem path is not necessarily a real directory hierarchy. POSIX portable code should use the standard open flags supported by the target API and avoid relying on FreeBSD-only extensions such as shm_rename or SHM_ANON unless the application intentionally targets FreeBSD.

Before diagnosing an application, record its operating-system release and process identity:

freebsd-version -kru
ps -axo pid,ppid,user,group,command | grep -E '[s]ervice|[w]orker'
sysctl -a | grep -E '^kern[.]ipc.*shm'

The exact shared-memory resource-control MIBs and accepted sysctl names vary with kernel configuration and release. Inspect local sysctl output and the installed shm_open(2) manual rather than copying a Linux /dev/shm runbook. FreeBSD implements named shared-memory objects in the kernel; treating a mount point or a directory listing as the authoritative inventory can therefore mislead an operator.

Use a dedicated, namespaced object name such as /com.example.cache.v1. Names should identify the protocol version, not a transient PID that another process cannot reliably discover. Define a single owner for object creation, schema version, initialization, and unlinking. Multiple independent services that choose the same name may otherwise attach to memory with incompatible layouts.

Create, size, and map an object in a safe order

A newly created object starts with a size of zero. The creator must set its expected length before another process maps it. This abbreviated C example illustrates the ordering; production code must check every return value, validate integer conversions, and implement cleanup for each failure path.

int fd = shm_open("/com.example.cache.v1",
    O_CREAT | O_EXCL | O_RDWR, 0600);
if (fd == -1) {
    /* Inspect errno; do not blindly unlink an existing object. */
}

size_t length = 1024 * 1024;
if (ftruncate(fd, (off_t)length) == -1) {
    /* Close fd and unlink only if this process created the name. */
}

void *region = mmap(NULL, length, PROT_READ | PROT_WRITE,
    MAP_SHARED, fd, 0);
if (region == MAP_FAILED) {
    /* Close and conditionally unlink according to the owner policy. */
}

O_CREAT with O_EXCL makes accidental reuse visible. If the name already exists, the caller must decide whether it is an expected live object, stale state from a crashed service, or a protocol mismatch. Opening a pre-existing object with O_RDWR and assuming it has the requested size is unsafe. Use fstat to compare the actual size to the protocol’s minimum and maximum before mapping it, and reject incompatible versions rather than reading beyond the object.

ftruncate establishes object length; it does not initialize application records. The creator should publish a header containing a magic value, schema version, capacity, and initialization state. Write the header and initialize synchronization before setting the state to ready. Consumers should wait for readiness using an explicit protocol, not assume that a successful shm_open means initialization has completed.

MAP_SHARED is necessary for updates to be visible through mappings of the same object. The returned virtual address can differ in each process, so shared structures must contain offsets or indices rather than raw pointers. Store lengths in fixed-width types, define alignment, and include a version field. Validate every offset against the mapped length before dereferencing it.

After mmap succeeds, closing the original descriptor does not unmap the region. Conversely, munmap only releases the caller’s mapping; it does not remove the shared object name. Keep these operations distinct in the service’s cleanup routine and metrics.

Synchronize with named semaphores deliberately

sem_open creates or opens a named semaphore. Its name follows a similar namespace rule: it begins with a slash and contains no additional slash. Use O_CREAT | O_EXCL for the designated creator and set an intentional permission mode. A second process should open the existing semaphore without O_CREAT only after the shared-memory header confirms which protocol instance it is joining.

sem_t *ready = sem_open("/com.example.cache.ready.v1",
    O_CREAT | O_EXCL, 0600, 0);
if (ready == SEM_FAILED) {
    /* Check errno and follow the documented owner/recovery policy. */
}

/* Creator initializes the shared header and records READY. */
if (sem_post(ready) == -1) {
    /* Record the failure; peers must not wait forever without a timeout. */
}

The initial value of zero makes a wait block until a successful post. A semaphore counts permits; it does not identify which record became available, guarantee fairness, or make a consumer’s side effect atomic with its decrement. For a single-slot handoff, define whether each post represents one item or only a state-change notification. For a ring buffer, maintain bounded producer and consumer indices and define how a process recovers if it exits after reserving a slot.

Use sem_timedwait or another bounded-wait policy where an indefinitely blocked worker would prevent service recovery. Log timeout separately from an invalid semaphore handle, permission error, or process termination. A process that has died while holding a separate mutex can leave peers blocked permanently unless the chosen lock design supports owner-death recovery.

Do not assume an unnamed process-shared semaphore is portable or supported merely because another operating system accepts sem_init with a nonzero pshared argument. Check the FreeBSD sem_init(3) contract for the target release. A named semaphore opened with sem_open has an explicit shared name and lifecycle, which is often easier to audit across independently launched processes.

Define ownership, permissions, and cleanup

shm_unlink removes the shared-memory name from future lookup; existing descriptors and mappings can continue to refer to the object until they are released. sem_unlink has analogous name-removal semantics for a named semaphore. A service restart that unlinks too early can prevent late-starting peers from joining even while old processes still hold the previous object.

Choose one owner to create and unlink both names. Make creation idempotent at the service layer, not by silently accepting O_EXCL failure. During recovery, first identify all processes using the object, capture their command lines and service generation, inspect the object size and application header if safe, and determine whether queued work can be discarded. Then stop or coordinate peers before removing stale names.

Permissions are part of the protocol. A mode such as 0600 restricts access to the creator’s effective identity subject to the system’s ownership and access rules. If distinct service accounts must attach, provision their group and mode intentionally, and document who may unlink the object. Never broaden permissions merely to make EACCES disappear; check the creator’s uid, group membership, umask, and object owner first.

Shared memory is volatile IPC, not a durable database. It does not replicate to another host, survive a reboot as application state, or make an external transaction atomic. If work must survive a machine failure, persist the authoritative record elsewhere and use shared memory only as a cache or coordination aid. A successful msync or memory barrier is not a substitute for an application-level commit protocol.

Diagnose failures with evidence

For ENOENT, confirm the exact name and whether the owner unlinked it. For EEXIST, inspect the service generation and object owner rather than deleting it reflexively. For EACCES, verify credentials and mode. For EINVAL or ENOMEM from mmap, compare length, offset, alignment, address-space limits, and available resources. For a peer that never wakes, verify that it opened the same semaphore name and protocol version and that the producer reached the post path.

Capture logs with timestamps around creator startup, object sizing, mapping, readiness publication, and consumer attach. Add metrics for object generation, map failures, semaphore wait duration, timeout count, queue or ring occupancy, and rejected schema versions. A process listing alone cannot tell whether a process still holds a stale mapping. Instrument the application lifecycle so the operator can correlate the shared object generation with the active worker generation.

Test crash points in a disposable environment: terminate the creator after object creation but before sizing; terminate it during header initialization; kill a producer after reserving a slot; restart a consumer while the object remains; and remove a name while peers still map it. Each case should have an expected recovery action and a bounded outcome. Do not perform these tests against production state.

Acceptance criteria

An IPC rollout is ready when names and owners are documented, object size and schema are validated before mapping, synchronization and timeout behavior are explicit, cleanup does not race live peers, and reboot or process-crash behavior is understood. Verify a harmless producer-consumer exchange under the intended service identities, then demonstrate that incompatible versions fail closed rather than corrupting memory.

The kernel supplies a shared byte region and named counting primitive. Availability, ordering, durability, and recovery remain application responsibilities.

Related:

Sources:

Comments