NAME Data::ReqRep::Shared - High-performance shared-memory request/response IPC for Linux SYNOPSIS use Data::ReqRep::Shared; # Server: create channel my $srv = Data::ReqRep::Shared->new('/dev/shm/rr.shm', 1024, 64, 4096); # path, req_capacity, resp_slots, resp_data_max # Server loop while (my ($req, $id) = $srv->recv_wait) { $srv->reply($id, process($req)); } # Client: open existing channel my $cli = Data::ReqRep::Shared::Client->new('/dev/shm/rr.shm'); # Synchronous my $resp = $cli->req("hello"); # With timeout (single deadline covers send + wait) my $resp = $cli->req_wait("hello", 5.0); # Asynchronous (multiple in-flight) my $id1 = $cli->send("req1"); my $id2 = $cli->send("req2"); my $r1 = $cli->get_wait($id1); my $r2 = $cli->get_wait($id2); # Integer variant (lock-free) use Data::ReqRep::Shared::Int; my $srv = Data::ReqRep::Shared::Int->new($path, 1024, 64); my $cli = Data::ReqRep::Shared::Int::Client->new($path); my $resp = $cli->req(42); DESCRIPTION Shared-memory request/response channel for interprocess communication on Linux. Multiple clients send requests, multiple workers process them, responses are routed back to the correct requester. All through a single shared-memory file -- no broker process, no socket pairs per connection. Linux-only, with "/proc" mounted (see "CONTAINERS") and a C library with "memfd_create" (glibc 2.27 or later, or musl). Requires a Perl with 64-bit integers. Architecture * Request queue -- bounded MPMC ring buffer. Str variant uses a futex mutex with circular arena for variable-length data. Int variant uses a lock-free MPMC queue. * Response slots -- fixed pool with per-slot futex for targeted wakeup and a generation counter for ABA-safe cancel/recycle. Flow: client acquires a response slot, pushes a request (carrying the slot ID), server pops the request, writes the response to that slot, client reads it and releases the slot. Variants Str -- "Data::ReqRep::Shared" / "Data::ReqRep::Shared::Client" Variable-length byte string requests and responses. Mutex-protected request queue with circular arena. Supports UTF-8 flag preservation. my $srv = Data::ReqRep::Shared->new($path, $cap, $slots, $resp_size); my $srv = Data::ReqRep::Shared->new($path, $cap, $slots, $resp_size, $arena); Int -- "Data::ReqRep::Shared::Int" / "Data::ReqRep::Shared::Int::Client" Single int64 request and response values: numbers outside that range, fractions and non-numeric strings are converted the way Perl converts to an integer, so check them first. Lock-free MPMC request queue. No arena, no mutex on the request path. my $srv = Data::ReqRep::Shared::Int->new($path, $cap, $slots); Both variants share the same response slot infrastructure, the same generation-counter ABA protection, the same eventfd integration, and the same response-slot crash recovery. Their request queues differ: see "CRASH SAFETY". Constructors Server (creates or opens the channel): ->new($path, ...) # file-backed ->new(undef, ...) # anonymous (fork-inherited) ->new_memfd($name, ...) # memfd (fd-passing or fork) ->new_from_fd($fd) # open from memfd fd An anonymous channel is an unnamed memfd: a forked child attaches with "Client->new_from_fd($srv->memfd)". Client (opens existing channel): ->new($path) ->new_from_fd($fd) Constructor arguments for Str: "$path, $req_cap, $resp_slots, $resp_size [, $arena [, $mode]]". For Int: "$path, $req_cap, $resp_slots [, $mode]". $mode is the octal backing-file permission (default 0600); see "SECURITY". The descriptor you pass is duplicated ("F_DUPFD_CLOEXEC"), so it stays yours to close and closing it does not disturb the handle. The other way round, a descriptor a handle returns ("memfd", "eventfd", "fileno", "ready_fd" and the like) stays the handle's: duplicate it with "+<&" rather than alias it with "+<&=", whose close closes it. "new" on a file that already holds a channel attaches to it with the sizes it was created with. The sizes you pass are not compared with those, but they must still be valid. A handle cannot be copied. A copy made by Storable or Clone croaks when used, and a new thread gets no usable handles; open the channel again instead. Destroying a client handle while another thread of the process is sending can, in a window of a few instructions, cancel the request that thread is sending and hand its slot to another client; destroy per-thread clients once the process's other threads have stopped sending. A process tells its handles on one channel apart by a 31-bit tag, so the (2^31 - 1)-th handle it opens after one still in use shares that one's identity: the second of the two to call "ready_fd" croaks, and destroying either with a request in flight cancels the other's too. Reuse handles rather than open one per request. Server API my ($data, $id) = $srv->recv; # non-blocking my ($data, $id) = $srv->recv_wait; # blocking my ($data, $id) = $srv->recv_wait($secs); # with timeout Returns "($request_data, $id)" or empty list. For Int, $data is an integer. Call them in list context: in scalar context they return the id. my $ok = $srv->reply($id, $response); Writes response and wakes the client. Returns false if the slot was cancelled or recycled (generation mismatch), or if $id names no slot. Only the process that received the request can reply to it: a reply from any other, such as a child forked after "recv", returns false. Batch (Str only): my @pairs = $srv->recv_multi($n); # up to $n under one lock my @pairs = $srv->recv_wait_multi($n, $timeout); my @pairs = $srv->drain; my @pairs = $srv->drain($max); Returns flat list "($data1, $id1, $data2, $id2, ...)". "recv_wait_multi" waits, up to $timeout, for one request, then takes up to $n of those queued. Management: $srv->clear; $srv->sync; $srv->unlink; $srv->size; $srv->capacity; $srv->is_empty; $srv->resp_slots; $srv->resp_size; $srv->stats; $srv->path; $srv->memfd; "path" returns the path as given, but as bytes: a character string comes back UTF-8 encoded. A relative one is looked up again from the current directory: "unlink" after a "chdir" misses the file, and treats a missing file as already removed. "Data::ReqRep::Shared->unlink($path)" removes a channel file without opening it, while "$srv->unlink" leaves a file alone that a newer channel has put in its place. "path" is undef for an anonymous or memfd channel, where "unlink" croaks, and "memfd" is -1 for a file-backed one. "stats" returns a hash reference with "requests", "replies", "recoveries", "send_full" and "recv_empty"; the waiter counts "recv_waiters", "send_waiters" and "slot_waiters"; "size", "capacity", "resp_slots", "resp_data_max" and "mmap_size"; and for Str "arena_cap" and "arena_used". "recoveries" counts what calls come across and take back from dead processes (a queue mutex, response slots, Int queue positions); a client giving up the request it was waiting for because its server died does not count, and gets undef either way. "send_full" and "recv_empty" count the times a call finds the queue full (on Str, or its arena short of room) or empty, not the calls: a waiting call looks several times as it starts to wait and again every two seconds, so both grow while callers wait idle. A send refused for want of a free response slot counts in neither. "recv_empty" and "recoveries" are 32-bit and wrap; the other counters are 64-bit. "stats" reads its values one at a time while the channel runs, "requests" before "replies", and Int counts a request just after queueing it, so under load, on either variant, "replies" can briefly exceed "requests". eventfd (see "Event Loop Integration"): $srv->eventfd; $srv->eventfd_set($fd); $srv->eventfd_consume; $srv->notify; $srv->fileno; # current request eventfd (-1 if none) $srv->reply_eventfd; $srv->reply_eventfd_set($fd); $srv->reply_eventfd_consume; $srv->reply_notify; $srv->reply_fileno; # current reply eventfd (-1 if none) Client API Synchronous: my $resp = $cli->req($data); # infinite wait my $resp = $cli->req_wait($data, $secs); # single deadline Both return undef if no reply arrives: "req_wait" on its timeout, and either at once when "clear" discards the request, or within two seconds of the server that received the request dying before its reply is complete (for the exceptions see "CRASH SAFETY"). Perl signal handlers, "alarm" included, run while a call waits for a request, a reply, a free slot, queue room or the Str queue mutex. A handler that dies ends the call: "req" and "req_wait" cancel their request, while "get_wait" leaves it in flight as its timeout would. One that returns lets the call carry on waiting. A call can return just as a handler is due, and if that handler dies what the call took is lost: a server's request goes unreplied and its client waits until its timeout, or for ever without one while that server lives; a client's reply is gone and waiting for it again returns undef. A constructor waiting for the file lock runs handlers only once that wait ends, and if 120 signals arrive meanwhile Perl dies of them, leaving that signal blocked. All this holds for Perl's default, deferred handlers: one installed unsafely ("POSIX::sigaction" without "safe", "Sys::SigAction", "PERL_SIGNALS=unsafe") runs inside the module's C code, and if it dies there it can leave a slot, arena room or the queue mutex held until the process exits. While a constructor reserves the segment, every signal but "SIGKILL" and "SIGSTOP" waits, about 0.2 seconds per GiB on tmpfs and the whole write-out on a filesystem without "fallocate"; one that then ends the process leaves a file-backed channel's file as an interrupted create (see "CRASH SAFETY") holding the reserved space, while a memfd or anonymous channel's goes with the process. Stop a server with a flag and a timed "recv_wait" rather than a dying handler, then answer what is queued with one "drain", as eg/graceful_shutdown.pl does, or on Int, which has no "drain", with at most "size" "recv" calls: a "recv" loop until the queue is empty keeps taking new requests for as long as each arrives before the queue empties, which a few clients sending in a loop keep up even when each waits for its reply. A request sent after that last take waits for the next server to open the same file (one that removes it and creates it again never sees it), and without a timeout its client waits for ever: stop the senders first, and in a chain of channels let each stage exit before stopping the one it sends to. A timeout of 0, like the non-blocking calls, never waits for a request, a reply, a slot or room: "req_wait" with one still sends the request, then gives it up at once. Such a call on a Str channel still waits for the queue mutex, for two seconds at most when the process holding it is stopped, and gives up as soon as a signal arrives; its handler runs once the call returns. A timed call keeps its deadline behind a stopped holder, except that "recv_wait_multi" taking the rest of its batch, and a send giving back arena room, its own as it times out or a dead process's, may each run up to two seconds over. A negative timeout, or NaN, waits for ever like no timeout at all, so clamp a computed remaining time at 0. "req_wait" reads an undef timeout as 0; the other waits read it as no timeout. Asynchronous: my $id = $cli->send($data); # non-blocking my $id = $cli->send_wait($data, $secs); # blocking my $resp = $cli->get($id); # non-blocking my $resp = $cli->get_wait($id, $secs); # blocking $cli->cancel($id); # abandon request A send returns undef when its request was not queued: "send" and "send_notify" when the queue is full or no response slot is free, "send_wait" and "send_wait_notify" when the timeout passes first. "cancel" releases the slot, dropping the reply if it has already arrived. It does not take the request back: a server still receives a request whose client cancelled it, gave up on it in "req_wait" or was destroyed, does its work, and gets false from "reply". A client that retries on timeout therefore adds work to a server that is behind; carry a deadline in the request if servers should skip stale ones. A reply still being copied in does not delay "cancel": the responder frees the slot when its copy finishes. A "get_wait" that times out leaves the request in flight: wait again or "cancel" it, or its slot stays held until the client handle is destroyed. An undef sooner than the timeout, or from a "get_wait" without one, means no reply will come (its server died, "clear" ran, or another process took the reply), and waiting again returns undef at once. An id that names no slot of the channel makes "get" and "get_wait" croak and "cancel" do nothing. Destroying a client cancels its requests still in flight, freeing their slots; a forked child destroying its copy of the parent's client leaves them alone. Destroying a server handle gives back nothing it has received: the requests stay with the process, which may still reply to them through another handle. A reply is taken by whichever process reads it with the request's id: a forked child that calls "get" with its parent's id receives the reply and frees the slot, and the parent then gets undef. Convenience (Str only): my $id = $cli->send_notify($data); # send + eventfd signal my $id = $cli->send_wait_notify($data, $secs); Status: $cli->pending; $cli->size; $cli->capacity; $cli->is_empty; $cli->resp_slots; $cli->resp_size; $cli->stats; $cli->path; $cli->memfd; "pending" counts the requests this process has in flight, from all its handles on the channel. eventfd (see "Event Loop Integration"): $cli->eventfd; $cli->eventfd_set($fd); $cli->eventfd_consume; $cli->fileno; $cli->notify; # signal request eventfd $cli->req_eventfd_set($fd); $cli->req_fileno; $cli->ready_fd; $cli->ready; # this client's own replies Event Loop Integration Two eventfds for bidirectional notification. Both are opt-in -- "send"/"reply" do not signal automatically. A client signals the request eventfd only after "req_eventfd_set": without one, "notify" does nothing and "send_notify" only sends. "notify" returns nothing either way. "eventfd_consume" and "reply_eventfd_consume" return the count of notifications since the last consume, or undef when there were none or the handle has no eventfd. # Request notification (client -> server) my $req_fd = $srv->eventfd; # create $srv->eventfd_consume; # drain in callback $cli->notify; # signal (or send_notify) $cli->req_eventfd_set($fd); # set inherited fd # Reply notification (server -> client) my $rep_fd = $srv->reply_eventfd; $srv->reply_notify; # signal after reply $cli->eventfd; # create (maps to reply fd) $cli->eventfd_consume; # drain in callback $cli->eventfd_set($fd); # set inherited fd The *_eventfd_set methods duplicate the descriptor, as "new_from_fd" does: the one you pass stays yours to close. They croak on a descriptor that is not open or is not an eventfd, since "notify" writes into it. One you consume through must be non-blocking ("EFD_NONBLOCK", as the module's own are), or "eventfd_consume" blocks when another process has already drained it; one in "EFD_SEMAPHORE" mode gives its notifications one per consume. Each eventfd is one counter for the whole channel, not one per client: an "eventfd_consume" in one client takes the notifications meant for all of them. With more than one client, use "ready_fd" instead, check every outstanding id after each wakeup, or wait with "get_wait" and a timeout. Even a single client keeps a timeout of its own: a server that dies before its "reply_notify" never signals the eventfd. Per-client reply notification: my $fd = $cli->ready_fd; # this client's own descriptor my $id = $cli->send($data); # replies to requests sent from now on wake it my $w = EV::io $fd, EV::READ, sub { handle($_, $cli->get($_)) for $cli->ready; }; # and a timeout of your own per id "ready_fd" gives the client a descriptor of its own that becomes readable when a reply to one of its requests is ready. Clients sharing a channel then neither wake each other nor take each other's notifications, and no descriptor has to pass between processes: "reply" notifies the client without being asked. It covers requests the client sends after the call, from the same process; a forked child calls "ready_fd" again for its own, and each keeps 4 bytes per response slot to remember what it has listed. "ready" returns the ids whose replies are ready and still unread, each once, a few thousand at a time; while any are left over the descriptor stays readable. It lists replies only: a request that will get none (its server died, or "clear" ran) never shows up, so keep a timeout of your own. At it "get" the id, and "cancel" it only if that returns undef: a server killed as it replies can leave a reply ready that was never announced. The descriptor is an abstract Unix datagram socket, so clients and servers must share a network namespace. A server that cannot open one (seccomp, or systemd's "RestrictAddressFamilies=" without "AF_UNIX") counts each notification as lost, so the descriptor never wakes and the client's own timeout is what brings it to "ready". A notification that finds the client's queue full ("net.unix.max_dgram_qlen") is not lost: the next "ready" reads every response slot of the channel instead, about 1 ms per 64K small ones. The kernel's default limit is only 10: a fresh network namespace such as a container's has that, and so does a host unless something raised it (systemd sets 512 at boot). A descriptor keeps the limit in force when "ready_fd" made it, so raise it before clients call "ready_fd", or keep a client's replies in flight under it. For cross-process use, create both eventfds before fork() so child inherits the fds: my $srv = Data::ReqRep::Shared->new($path, 1024, 64, 4096); my $req_fd = $srv->eventfd; my $rep_fd = $srv->reply_eventfd; if (fork() == 0) { my $cli = Data::ReqRep::Shared::Client->new($path); $cli->req_eventfd_set($req_fd); $cli->eventfd_set($rep_fd); $cli->send_notify($data); # wakes server # EV::io $rep_fd for reply ... exit; } # parent = server my $w = EV::io $req_fd, EV::READ, sub { $srv->eventfd_consume; while (my ($req, $id) = $srv->recv) { $srv->reply($id, process($req)); } $srv->reply_notify; }; Crash Safety * Stale mutex -- if a process dies holding the request queue mutex, other processes detect it via PID tracking and recover within 2 seconds (4 when it died unreaped, or its PID was reused, and signals keep interrupting the waits), and a waiter already parked for a request, a slot or room within one two-second tick more. * Stale response slots -- a slot held by a client that died, or by a server that died before replying, is taken back when its client waits for the reply, or when a later sender finds no free slot. A "send_wait" or "req" waiting for a slot or for queue room looks again every two seconds, so it notices when the holders are gone or a receiver that made room died before saying so. * ABA protection -- response slot IDs carry a generation counter. A cancelled-and-reacquired slot has a different generation, so stale "reply"/"get"/"cancel" calls are safely rejected. Tuning "req_cap" -- request queue capacity, rounded up to a power of 2. A request holds a response slot until its reply is read or it is given up, so at most "resp_slots" requests wait at once, beside given-up ones (cancelled, "req_wait" timing out included, or their client destroyed or killed), which stay queued until a server receives them: set it above "resp_slots" by as many as clients may give up, and raise "resp_slots" for bursts, on Str with an "arena" that holds their bytes, each rounded up to 8 (an empty one takes 8), plus one of the largest, which the ring can leave unused at its end (the default gives at least 256 bytes a position). Memory: 24 bytes/slot + arena (Str) or 24 bytes/slot (Int), beside about 10 KB a channel carries whatever its size (header, process records, counters). "resp_slots" -- max concurrent in-flight requests across all clients. One slot per outstanding async request. For synchronous req(), one per client suffices. Memory: 64 bytes/slot (Int) or (40 + "resp_size", rounded up to a multiple of 64) bytes/slot (Str), in a segment that must stay under 4 GiB, and under about 2.5 GiB for a 32-bit perl on a 64-bit kernel to map it. "resp_size" -- max response payload bytes (Str only). Fixed per slot. A longer reply croaks in the responder and leaves the request unanswered: reply again with a shorter one, or its client waits until its timeout, or for ever without one while the responder lives. Size it for the largest reply you send, not a typical one. "arena" -- request data arena bytes (Str only; 0 or undef gives the default "req_cap * 256", a value under 4096 is raised to 4096, others are rounded up to a multiple of 8, and "arena_cap" in "stats" gives the result). Increase for large requests: one longer than the arena, or than 2 GiB - 1 bytes, can never be queued, and a send croaks on it (request too long) instead of waiting for room. One longer than the arena croaks only once a response slot and queue room are free: until then it returns undef, or waits, as for any request. Monitor "arena_used" in stats(). A waiting sender that does not fit holds the room it needs, if no other waiting sender needs more: smaller requests use only what is left, so a large request is not kept out by a stream of small ones. It keeps the room while its signal handlers run (a send from one of them is not held back by it), and gives it back when its call ends, a handler dying included. A sender stopped while it waits keeps that room held until it continues, gives up, or "clear" empties the arena; one killed while it waits gives it back at the next send it holds up, which fails that once unless it waits; from a handle that found it alive in the last 10 ms, sends fail until those pass, and a waiting one takes up to two seconds. Benchmarks Linux x86_64. The single-process rows come from "bench/bench_int.pl" and "bench/bench.pl 200000", the cross-process rows from "bench/vs.pl 50000", which caps Forks::Queue at 10K; run them with "perl -Mblib". SINGLE-PROCESS ECHO (200K iterations) ReqRep::Int (lock-free) 1.8M req/s ReqRep::Str (12B, mutex) 1.2M req/s ReqRep::Str batch (100x) 1.4M req/s CROSS-PROCESS ECHO (50K iterations, 12B payload) Pipe pair (1:1) 240K req/s Unix socketpair (1:1) 222K req/s ReqRep::Int 202K req/s * ReqRep::Str 177K req/s * IPC::Msg (SysV) 165K req/s TCP loopback 115K req/s MCE::Channel 96K req/s Socketpair via broker 82K req/s Forks::Queue (Shmem) 5K req/s "*" = MPMC with per-request reply routing. Pipes and sockets are faster for simple 1:1 echo but require dedicated fd pairs per client-worker connection and cannot do MPMC without a broker (which costs about two thirds of their throughput). CRASH SAFETY Response slots are recovered from dead owners, and the Str request queue recovers a mutex held by a dead process. The Int request queue survives its users being killed too. A receiver killed while taking a message is moved past by the next sender or receiver. A sender killed between claiming a queue position and publishing its message holds up the queue only until a receiver finds that no live process holds the claim and skips the position; that message is lost. A sender stopped there holds up the queue until it continues. A process killed while it waits can stay counted among the waiters (the *_waiters in "stats") indefinitely: a count forgets its dead only when one handle's wakes find nobody parked several times in a row, or, for "send_waiters" and "slot_waiters", when "clear" finds nobody parked. Read these counts as upper bounds. A slot's generation counter is 32-bit. It guards against a stale id being honoured after the slot is recycled, which it does for any realistic run; after 2^32 re-acquisitions of the same slot an ancient id would compare equal again. Recovery tells a dead process from a live one by its PID and its start time, which a channel records for up to 1024 processes that use it at once, so a PID reused by another process does not keep what the dead one held. A process beyond those 1024, or one whose /proc/PID/stat cannot be read, is known by its PID alone: if that PID is reused before recovery runs, it is taken for the process that died. So is any later holder of a PID whose next channel user was killed while giving back what the dead one held. Once 1024 processes have used a channel, a new one takes the record of one that has exited and whose PID is free, and reads every response slot as it opens the channel (a forked child using its parent's handle: at its first call) to give back what that one held. That takes about 10 ms per million slots with small replies, growing with "resp_size" to about 0.1 s per million 4 KB ones. If no record is free, the new process is known by its PID alone, and each handle it opens looks at all 1024 records again, about 6 ms. A process keeps its PID and start time across "exec", so recovery cannot see the old program go. The new program's first open of the channel gives up what the old one held there, so make that open before starting threads that use the channel; a program that never opens it keeps that held until it exits, so answer or cancel before an "exec" into one. A handle stays attached to the segment it opened. When a server removes the file on shutdown, as the examples do, and its successor creates it again, clients still attached keep using the old segment, which nobody serves: open them again after a restart, which "stat" on the path, taken before opening, shows as a new inode. For the same reason keep a long-lived channel out of directories cleaned by age, such as /tmp under systemd-tmpfiles. There a channel loses its file once nothing has refreshed its times for the aging period, and on tmpfs traffic through the mapping refreshes none of them: only opening the file does, so a busy channel whose processes all opened it long ago is removed too. Likewise systemd-logind's "RemoveIPC" (on by default) deletes a non-system user's files in /dev/shm when that user's last login session ends: run long-lived channels as a system user, or with "loginctl enable-linger", or set "RemoveIPC=no". Every change to a response slot is one compare-and-swap on a word holding its generation, its state and the pid of the process responsible for it: the owner, or the server once it has received the request. A stale id or a stale reading therefore never moves a slot, and recovery acts only on a named process that is dead, never on elapsed time, so a process stopped by SIGSTOP, a debugger or a paused container is not taken for dead however long it stays stopped. When the server dies before its reply is complete, the client's "get_wait" or "req" gives up within two seconds and frees the slot, or within four when that server died unreaped, or its PID was reused, and signals keep interrupting the wait. One exception: a server that took a request off the queue but has not yet marked it received, a matter of a few instructions (for "recv_multi", "recv_wait_multi" and "drain", the whole batch), cannot be named. It is taken for dead once a client waiting for the request has seen it unmarked for four seconds; the client first looks at its next two-second check, so that is up to six seconds after the take, or eight when signals keep interrupting the wait. Each such request is timed from when its client first waits for it, so waiting for several in turn takes about four seconds each. A client times up to 16 such requests at once; any more are left to the caller's timeout, and wait for ever without one. "clear" may run while clients and servers are working. It discards the requests still queued, those a server has received but not answered (a later "reply" to them returns false), and replies not yet read: each gets undef. A reply still being written is left to its responder, which frees the slot when done. A request still being sent is not discarded, and on an Int channel neither are the requests queued behind it. An interrupted create is recovered too. A creator killed after the backing file is sized but before its header is committed leaves a full-size, all-zero file. "new" re-initializes such a file automatically, but only when it is exactly the size the requested geometry needs, is owned by your effective uid, and is still entirely zero -- a file holding data is never re-initialized. If the creator got as far as writing part of the header, the file cannot be told apart from a corrupt one and "new" croaks with "incomplete reqrep file left by an interrupted create; remove it and retry". A file left behind by an interrupted create never held data, so removing it is safe -- but a file whose header was corrupted after the fact reaches the same croak, so confirm it is an abandoned create before deleting anything you care about. CONTAINERS Stale-slot and stale-mutex recovery identify peers by PID, and a PID only means something inside one PID namespace. A peer attaching from another namespace would read live processes as dead -- taking their slots and misdelivering replies -- and unrelated local processes as alive, never recovering a real casualty. None of that is detectable after the fact, so "new" refuses it: the header records the creating process's PID namespace and the current boot id, and attaching from anywhere else croaks. Reading them needs "/proc": without it, or with its top-level files hidden (systemd's "ProcSubset=pid", a "subset=pid" mount), every constructor croaks unless the variable below is set. The /proc a peer sees must be that of its own PID namespace: after "unshare -p" without "--mount-proc", or "nsenter -p" without "-m", /proc/PID names the host's processes and recovery judges the wrong ones. All peers must therefore share a PID namespace -- "docker run --pid=container:NAME", or a Kubernetes pod with "shareProcessNamespace: true". Sharing only the filesystem or the IPC namespace is not enough. The same check rejects a file left over from a previous boot, whose recorded PIDs now name unrelated processes. Peers must also see the same boot time: one in a time namespace that offsets it ("unshare --boottime", a CRIU restore) reads every other peer's start time shifted, and takes them all for dead within a tick. Set "DATA_REQREP_SHARED_UNSAFE_PIDNS=1" (any value but an empty one, a zero number, "false", "no" or "off") to attach anyway. Only do that if you do not depend on recovery -- for example a fixed set of peers that never die mid-request -- because the failure mode it re-enables is silent corruption. For sharing across containers without a shared filesystem, create the segment with "new_memfd" and pass the descriptor over a unix socket with "SCM_RIGHTS"; a memfd needs no shared filesystem, though its peers still need the one PID namespace. If you use a file, note that a container's default "/dev/shm" is often only 64 MB. "new" reserves the whole segment when it creates one, memfd and anonymous channels too, so its memory is taken at once, and croaks if a filesystem limit leaves no room (a memory cgroup limit gets the process killed instead); a sparse segment would instead kill a process with SIGBUS at the first write that could not be backed. Set "DATA_REQREP_SHARED_SPARSE=1" to skip the reservation: creation then takes only the pages it writes (the header, the page holding each response slot's header, the creator's process record and, on Int, the request queue: nearly all of an Int segment), the rest as it is first used. The Str request queue and arena are rings: the queue is used all the way round after "req_cap" requests, and the arena too once requests stay queued long enough (it starts over only when the queue empties). What stays untaken is arena that a queue which often empties never reaches, process records (8 KB in all) no process has used and, when "resp_size" is over about 4 KB, response data past each slot's first page that no reply has reached. On a filesystem without "fallocate" (some FUSE mounts), glibc reserves the segment by writing it out block by block, and musl leaves it sparse. Under a user namespace, "new"'s ownership checks compare uids *as mapped in the caller's namespace*, so peers need a common id mapping. SEE ALSO Data::Buffer::Shared - typed shared array Data::HashMap::Shared - concurrent hash table Data::Queue::Shared - FIFO queue Data::PubSub::Shared - publish-subscribe ring Data::Sync::Shared - synchronization primitives Data::Pool::Shared - fixed-size object pool Data::Stack::Shared - LIFO stack Data::Deque::Shared - double-ended queue Data::Log::Shared - append-only log (WAL) Data::Heap::Shared - priority queue Data::Graph::Shared - directed weighted graph Data::BitSet::Shared - shared bitset (lock-free per-bit ops) Data::RingBuffer::Shared - fixed-size overwriting ring buffer SECURITY Backing files are created with mode 0600 (owner-only) by default, so only the creating user can open and attach them. To share a backing file across users, pass an explicit octal file mode such as 0660 as the final $mode argument to "new" -- for Str after the optional $arena ("new($path, $req_cap, $resp_slots, $resp_size, $arena, 0660)"), for Int as the fourth argument ("new($path, $req_cap, $resp_slots, 0660)"); the mode is applied when the file is created, and when a file left behind by an interrupted create is re-initialized (see "CRASH SAFETY"); a file already in use keeps its own permissions. An empty file at the path that you own, as a create refused for want of space leaves, is initialized like a new one, so it gets $mode too; one another user owns is refused. The file is opened with "O_NOFOLLOW", so a symlink planted at the path is refused, and created with "O_EXCL"; the on-disk header is validated when the file is attached. Attaching refuses a world-writable file owned by another user; share with a group mode such as 0660 instead. Any process that can open the file can hold its lock, so "new" waits for the lock for at most 10 seconds and then croaks. Any process you grant write access to a shared mapping is trusted not to corrupt its contents while other processes are using it. AUTHOR vividsnow LICENSE This is free software; you can redistribute it and/or modify it under the same terms as Perl itself.