Flow-IPC 2.0.0
Flow-IPC project: Full implementation reference.
pool_arena.hpp
Go to the documentation of this file.
1/* Flow-IPC: Shared Memory
2 * Copyright 2023 Akamai Technologies, Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the
5 * "License"); you may not use this file except in
6 * compliance with the License. You may obtain a copy
7 * of the License at
8 *
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in
12 * writing, software distributed under the License is
13 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
14 * CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing
16 * permissions and limitations under the License. */
17
18/// @file
19#pragma once
20
25#include <flow/util/basic_blob.hpp>
26#include <boost/interprocess/managed_shared_memory.hpp>
27#include <boost/interprocess/indexes/null_index.hpp>
28#include <optional>
29
30namespace ipc::shm::classic
31{
32
33// Types.
34
35/**
36 * A SHM-classic interface around a single SHM pool with allocation-algorithm services by boost.interprocess,
37 * as in `bipc::managed_shared_memory`, with symmetric read/write semantics, compatible with ipc::shm::stl
38 * STL-compliance and SHM-handle borrowing manually or via ipc::session.
39 *
40 * ### When to use ###
41 * Generally, this is a simple way to work with SHM. It is very easy to set up and has very little infrastructure
42 * on top of what is provided by a typically-used subset of bipc's SHM API -- which, itself, is essentially a thin
43 * wrapper around a classic OS-supplied SHM pool (segment) API, plus a Boost-supplied heap-like allocation algorithm.
44 * Nevertheless this wrapper, when combined with ipc::shm::stl, is eminently usable and flexible. Its main
45 * limitations may or may not be serious in production use, depending on the context. These limitations include
46 * the following.
47 * - When buffers are allocated and deallocated, bipc's default memory allocation algorithm -- `rbtree_best_fit` --
48 * is what is used. (We could also allow for the other supplied algo, `simple_seq_fit`, or a custom
49 * user-supplied one.) While surely care was taken in writing this, in production one might demand
50 * something with thread caching and just general, like, industry relevance/experience; a jemalloc or
51 * tcmalloc maybe.
52 * - Possible contingency: Not really. In many cases this does not matter too much; but if one wants general
53 * performance equal to that of the general heap in a loaded production environment, then classic::Pool_arena
54 * is probably not for you. Consider jemalloc-based SHM provided elsewhere in ipc::shm.
55 * - It works within exactly one *pool* a/k/a `mmap()`ped segment, and that pool's max size must be specified
56 * at creation. Once it is exhausted via un-deallocated allocations, it'll throw exceptions likely to wreak
57 * havoc in your application.
58 * - Possible contingency: You may set the max pool size to a giant value. This will *not* take it from
59 * the OS like Linux: only when a page is actually touched, such as by allocating in it, does that actual
60 * RAM get assigned to your application(s). There is unfortunately, at least in Linux, some configurable
61 * kernel parameters as to the sum of max pool sizes active at a given time -- `ENOSPC` (No space left on device)
62 * may be emitted when trying to open a pool beyond this. All in all it is a viable approach but may need
63 * a measure of finesse.
64 * - The ability to allocate in a given backing pool via any process's Pool_arena handle to that
65 * backing pool -- not to mention deallocate in process B what was allocated in process A -- requires
66 * guaranteed read-write capability in all `Pool_arena`s accessing a given pool. That read-write capability
67 * in and of itself (orthogonally to allocations) provides algorithmic possibilities which are not
68 * easily available in an asymmetric setup, where only one process can write or deallocate, while others
69 * can only borrow handles and read. How is this a limitation, you ask? While providing power and
70 * simplicity, it also hugely increases the number and difficulty of dealing with unexpected conditions.
71 * That is -- any process can write and corrupt the pool's contents, thus "poisoning" other processes;
72 * any process crashing means the others cannot trust the integrity of the pool's contents; things of that
73 * nature.
74 * - Possible contingency: It would not be difficult to enable read-only access from all but one process;
75 * we provide such a constructor argument. However, one cannot use the SHM-handle borrowing method
76 * borrow_object() which severely stunts the usefulness of the class in that process and therefore across
77 * the system. That said one could imagine somewhat extending Pool_arena to enable lend-borrow and
78 * delegated deallocation (and maybe delegated allocation while at it) by adding some internal
79 * message-passing (maybe through a supplied transport::Channel or something). Doable but frankly
80 * goes against the spirit of simplicty and closeness-to-the-"source-material" cultivated by
81 * classic::Pool_arena. The jemalloc-based API available elsewhere in ipc::shm is the one that
82 * happily performs message-passing IPC internally for such reasons. It's a thought though.
83 *
84 * ### Properties ###
85 * Backing pool structure: One (1) SHM pool, explicitly named at construction. Can open handle with create-only,
86 * create-or-open (atomic), or open-only semantics. Pool size specified at construction/cannot be changed.
87 * Vaddr structure is not synchronized (so `void* p` pointing into SHM in process 1 cannot be used in process 2
88 * without first adjusting it based on the different base vaddr of the mapped pool in process 2 versus 1).
89 *
90 * Handle-to-arena structure: Pool_arena is the only type, used by any process involved, that accesses the underlying
91 * arena, and once open all capabilities are symmetrically available to all Pool_arena objects in all processes.
92 * - Any `Pool_arena` can allocate (and deallocate what it has allocated).
93 * - Any `Pool_arena` can deallocate what any other `Pool_arena` has allocated (as long as the correct
94 * locally-dereferenceable `void*` has been obtained in the deallocating process).
95 *
96 * Allocation algorithm: As of this writing the bipc default, `rbtree_best_fit`. See its description in bipc docs.
97 * Note it does not perform any thread-caching like modern `malloc()`s.
98 *
99 * Allocation/deallocation API: See section below for proper use techniques.
100 *
101 * Cleanup: The underlying SHM pool is deleted if and only if one calls remove_persistent(), supplying it the
102 * pool name. This is not invoked internally at all, so it is the user's responsibility. ipc::session-managed
103 * `Pool_arena`s will be automatically cleaned up, as ipc::session strives to clean all persistent shared
104 * resources via a general algorithm. See remove_persistent() and for_each_persistent() doc headers.
105 *
106 * Satisfies `Arena` requirements for shm::stl::Stateless_allocator: Yes. I.e., it is easy to store
107 * STL-compliant containers directly in SHM by using `Stateless_allocator<Pool_arena>` as the allocator at all
108 * levels.
109 * - SHM-stored pointer type provided: Yes, #Pointer. This is, in reality, `bipc::offset_ptr`.
110 * - Non-STL-compliant data structures with pointers, such as linked lists, can be written in terms
111 * of allocate() and deallocate(), but only if one uses #Pointer as opposed to raw `T*`
112 * pointers. We recommend against this, but if it cannot be avoided due to legacy code or what-not....
113 *
114 * Handle borrowing support: Yes. Pool_arena directly provides an API for this. Internally it uses minimalistic
115 * atomic ref-counting directly in SHM without any IPC messaging used. Due to this internal simplicity this support
116 * is symmetric and supports unlimited proxying out of the box. That is, any process of N, each with a Pool_arena
117 * open to the same pool named P, can construct a borrowable object, then lend it to any other process which can also
118 * lend it to any other of the N processes. Internally the last Pool_arena's borrowed (or originally lent)
119 * handle to reach 0 intra-process ref-count shall invoke the object's dtor and deallocate the underlying buffer.
120 * (Algorithms like this = why symmetric read/write capability is fairly central to Pool_arena as written.)
121 * - However, as of this writing, this support is deliberately basic. In particular if a borrower process dies
122 * ungracefully (crash that does not execute all destructors, and so on), then the memory will leak until
123 * cleanup via remove_persistent().
124 *
125 * ### Allocation API and how to properly use it ###
126 * The most basic and lowest-level API consists of allocate() and deallocate(). We recommend against
127 * user code using these, as it is easy to leak and double-free (same as with `new` and `delete` in regular heap,
128 * except as usual with SHM anything that was not allocated will persist in RAM until remove_persistent()).
129 * - shm::stl allocator(s) will use these APIs safely.
130 * - As noted earlier, if one writes a non-STL-compliant data structure (such as a manually written linked list),
131 * it is appropriate to use allocate(), deallocate(), and #Pointer. It is best to avoid
132 * such data structures in favor of shm::stl-aided STL-compliant structures.
133 *
134 * The next level of API is construct(). `construct<T>()` returns a regular-looking `shared_ptr`. If it is
135 * never lent to another process, then the constructed `T` will be destroyed automatically as one would expect.
136 * If one *does* properly lend such a `shared_ptr` (which we call a *handle*) to another process (and which it
137 * can proxy-lend to another process still), then the `T` will be destroyed by the last process whose
138 * handle reaches ref-count 0. No explicit locking on the user's part is required to make this work.
139 * - If `T` is a POD (plain old data-type), then that's that.
140 * - If not, but `T` is a shm::stl-allocator-aided STL-compliant-structure, then you're good.
141 * - If not (e.g., the aforementioned manually implemented linked list) but the `T` destructor
142 * performs the necessary inner deallocation via deallocate(), then you're good. Again, we recommend
143 * against this, but sometimes it cannot be helped.
144 * - If not, then one must manually do the inner deallocation first, then let the handle (`shared_ptr`) group
145 * reach ref-count 0. The key is to do it in that order (which is why doing it via `T::~T()` is easiest).
146 *
147 * `T` cannot be a native array; and there is no facility for constructing such. Use `std::array` or `boost::array`
148 * as `T` if desired.
149 */
151 public flow::log::Log_context,
152 private boost::noncopyable
153{
154public:
155 // Types.
156
157 /**
158 * SHM-storable fancy-pointer. See class doc header for discussion. Suitable for shm::stl allocator(s).
159 *
160 * @tparam T
161 * The pointed-to type. `Pointer<T>` acts like `T*`.
162 */
163 template<typename T>
164 using Pointer = ::ipc::bipc::offset_ptr<T>;
165
166 /**
167 * Outer handle to a SHM-stored object; really a regular-looking `shared_ptr` but with custom deleter
168 * that ensures deallocation via Pool_arena as well as cross-process ref-count semantics.
169 * See class doc header and construct(). A handle can also be lent/borrowed between processes;
170 * see lend_object() and borrow_object().
171 *
172 * ### Rationale ###
173 * Why have an alias, where it's a mere `shared_ptr`? Two-fold reason:
174 * - (Mostly) While it *is* just `shared_ptr<T>`, and acts just as one would expect if borrow_object() and
175 * lend_object() are not involved, (1) it is an *outer* handle to a SHM-stored object, unlike the inner
176 * (subordinate) #Pointer values as maintained by STL-compliant logic or other data structure-internals code; and
177 * (2) it has special (if hopefully quite intuitive) capabilities of invoking ref-counting "across" processes
178 * via lend_object() and borrow_object().
179 * - (Minor) It's nice not to visibly impose a particular `shared_ptr` impl but kinda hide it behind an
180 * alias. Ahem....
181 *
182 * @tparam T
183 * The pointed-to type. Its dtor, informally, should ensure any inner deallocations subordinate
184 * to the managed `T` are performed before the `shared_ptr` reaches ref-count 0 in all processes
185 * to get the handle. See class doc header.
186 */
187 template<typename T>
188 using Handle = boost::shared_ptr<T>;
189
190 /**
191 * Alias for a light-weight blob. They're little; TRACE-logging of deallocs and copies is of low value;
192 * otherwise this can be switched to `flow::util::Blob`.
193 */
194 using Blob = flow::util::Blob_sans_log_context;
195
196 // Constructors/destructor.
197
198 /**
199 * Construct Pool_arena accessor object to non-existing named SHM pool, creating it first.
200 * If it already exists, it is an error. If an error is emitted via `*err_code`, methods shall return
201 * sentinel/`false` values.
202 *
203 * @param logger_ptr
204 * Logger to use for subsequently logging.
205 * @param pool_name
206 * Absolute name at which the persistent SHM pool lives.
207 * @param mode_tag
208 * API-choosing tag util::CREATE_ONLY.
209 * @param perms
210 * Permissions to use for creation. Suggest the use of util::shared_resource_permissions() to translate
211 * from one of a small handful of levels of access; these apply almost always in practice.
212 * The applied permissions shall *ignore* the process umask and shall thus exactly match `perms`,
213 * unless an error occurs.
214 * @param pool_sz
215 * Pool size. Note: OS, namely Linux, shall not in fact take (necessarily) this full amount from general
216 * availability but rather a small amount. Chunks of RAM (pages) shall be later reserved as they begin to
217 * be used, namely via the allocation API. It may be viable to set this to a quite large value to
218 * avoid running out of pool space. However watch out for (typically configurable) kernel parameters
219 * as to the sum of sizes of active pools.
220 * @param err_code
221 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
222 * various. Most likely creation failed due to permissions, or it already existed.
223 * An `ENOSPC` (No space left on device) error means the aforementioned kernel parameter has been
224 * hit (Linux at least); pool size rebalancing in your overall system may be required (or else one
225 * might tweak the relevant kernel parameter(s)).
226 */
227 explicit Pool_arena(flow::log::Logger* logger_ptr, const Shared_name& pool_name,
228 util::Create_only mode_tag, size_t pool_sz,
229 const util::Permissions& perms = util::Permissions{}, Error_code* err_code = nullptr);
230
231 /**
232 * Construct Pool_arena accessor object to non-existing named SHM pool, or else if it does not exist creates it
233 * first and opens it (atomically). If an error is emitted via `*err_code`, methods shall return
234 * sentinel/`false` values.
235 *
236 * @param logger_ptr
237 * Logger to use for subsequently logging.
238 * @param pool_name
239 * Absolute name at which the persistent SHM pool lives.
240 * @param mode_tag
241 * API-choosing tag util::OPEN_OR_CREATE.
242 * @param perms_on_create
243 * Permissions to use for creation. Suggest the use of util::shared_resource_permissions() to translate
244 * from one of a small handful of levels of access; these apply almost always in practice.
245 * The applied permissions shall *ignore* the process umask and shall thus exactly match `perms_on_create`,
246 * unless an error occurs.
247 * @param pool_sz
248 * Pool size. See note in first ctor.
249 * @param err_code
250 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
251 * various. Most likely creation failed due to permissions, or it already existed.
252 */
253 explicit Pool_arena(flow::log::Logger* logger_ptr, const Shared_name& pool_name,
254 util::Open_or_create mode_tag, size_t pool_sz,
255 const util::Permissions& perms_on_create = util::Permissions{}, Error_code* err_code = nullptr);
256
257 /**
258 * Construct Pool_arena accessor object to existing named SHM pool. If it does not exist, it is an error.
259 * If an error is emitted via `*err_code`, methods shall return sentinel/`false` values.
260 *
261 * @param logger_ptr
262 * Logger to use for subsequently logging.
263 * @param pool_name
264 * Absolute name at which the persistent SHM pool lives.
265 * @param mode_tag
266 * API-choosing tag util::OPEN_ONLY.
267 * @param read_only
268 * If and only if `true` the calling process will be prevented by the OS from writing into the pages
269 * mapped by `*this` subsequently. Such attempts will lead to undefined behavior.
270 * Note that this includes any attempt at allocating as well as writing into allocated (or otherwise)
271 * address space. Further note that, internally, deallocation -- directly or otherwise -- involves
272 * (in this implementation) writing and is thus also disallowed. Lastly, and quite significantly,
273 * borrow_object() can be called, but undefined behavior shall result when the resulting `shared_ptr`
274 * (#Handle) group reaches ref-count 0, as internally that requires a decrement of a counter (which is
275 * a write). Therefore borrow_object() cannot be used either. Therefore it is up to you, in that
276 * case, to (1) never call deallocate() directly or otherwise (i.e., through an allocator);
277 * and (2) to design your algorithms in such a way as to never require lending to this Pool_arena.
278 * In practice this would be quite a low-level, stunted use of `Pool_arena` across 2+ processes;
279 * but it is not necessarily useless. (There might be, say, test/debug/reporting use cases.)
280 * @param err_code
281 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
282 * various. Most likely creation failed due to permissions, or it already existed.
283 */
284 explicit Pool_arena(flow::log::Logger* logger_ptr, const Shared_name& pool_name,
285 util::Open_only mode_tag, bool read_only = false, Error_code* err_code = nullptr);
286
287 /**
288 * Destroys Pool_arena accessor object. In and of itself this does not destroy the underlying pool named
289 * #m_pool_name; it continues to exist as long as (1) any other similar accessor objects (or other OS-created
290 * handles) do; and/or (2) its entry in the file system lives (hence until remove_persistent() is called
291 * for #m_pool_name). This is analogous to closing a descriptor to a file.
292 */
293 ~Pool_arena();
294
295 // Methods.
296
297 /**
298 * Removes the named SHM pool object. The name `name` is removed from the system immediately; and
299 * the function is non-blocking. However the underlying pool if any continues to exist until all handles
300 * (accessor objects Pool_arena or other OS-created handles) to it are closed; their presence in this or other
301 * process is *not* an error. See also dtor doc header for related notes.
302 *
303 * @note The specified pool need not have been created via a Pool_arena object; it can be any pool
304 * created by name ultimately via OS `shm_open()` or equivalent call. Therefore this is a utility
305 * that is not limited for use in the ipc::shm::classic context.
306 * @see `util::remove_each_persistent_*`() for a convenient way to remove more than one item. E.g.,
307 * `util::remove_each_persistent_with_name_prefix<Pool_arena>()` combines remove_persistent() and
308 * for_each_persistent() in a common-sense way to remove only those `name`s starting with a given prefix;
309 * or simply all of them.
310 *
311 * Trying to remove a non-existent name *is* an error.
312 *
313 * Logs INFO message.
314 *
315 * @param logger_ptr
316 * Logger to use for subsequently logging.
317 * @param name
318 * Absolute name at which the persistent SHM pool lives.
319 * @param err_code
320 * See `flow::Error_code` docs for error reporting semantics. #Error_code generated:
321 * various. Most likely it'll be a not-found error or permissions error.
322 */
323 static void remove_persistent(flow::log::Logger* logger_ptr, const Shared_name& name,
324 Error_code* err_code = nullptr);
325
326 /**
327 * Lists all named SHM pool objects currently persisting, invoking the given handler synchronously on each one.
328 *
329 * Note that, in a sanely set-up OS install, all existing pools will be listed by this function;
330 * but permissions/ownership may forbid certain operations the user may typically want to invoke on
331 * a given listed name -- for example remove_persistent(). This function does *not* filter-out any
332 * potentially inaccessible items.
333 *
334 * @note The listed pools need not have been created via Pool_arena objects; they will be all pools
335 * created by name ultimately via OS `shm_open()` or equivalent call. Therefore this is a utility
336 * that is not limited for use in the ipc::shm::classic context.
337 *
338 * @tparam Handle_name_func
339 * Function object matching signature `void F(const Shared_name&)`.
340 * @param handle_name_func
341 * `handle_name_func()` shall be invoked for each item. See `Handle_name_func`.
342 */
343 template<typename Handle_name_func>
344 static void for_each_persistent(const Handle_name_func& handle_name_func);
345
346 /**
347 * Allocates buffer of specified size, in bytes, in the accessed pool; returns locally-derefernceable address
348 * to the first byte. Returns null if no pool attached to `*this`. Throws exception if ran out of space.
349 *
350 * Take care to only use this when and as appropriate; see class doc header notes on this.
351 *
352 * ### Rationale for throwing exception instead of returning null ###
353 * This does go against the precedent in most of ::ipc, which either returns sentinel values or uses
354 * Flow-style #Error_code based emission (out-arg or exception). The original reason may appear somewhat arbitrary
355 * and is 2-fold:
356 * - It's what bipc does (throws `bipc::bad_alloc_exception`), and indeed we propagate what it throws.
357 * - It's what STL-compliant allocators (such as our own in shm::stl) must do; and they will invoke this
358 * (certainly not exclusively).
359 *
360 * I (ygoldfel) claim it's a matter of... synergy, maybe, or tradition. It really is an exceptional situation to
361 * run out of pool space. Supposing some system is built on-top of N pools, of which `*this` is one, it can certainly
362 * catch it (and in that case it shouldn't be frequent enough to seriously affect perf by virtue of slowness
363 * of exception-throwing/catching) and use another pool. Granted, it could use Flow semantics, which would throw
364 * only if an `Error_code*` supplied were null, but that misses the point that allocate() failing to
365 * allocate due to lack of space is the only thing that can really go wrong and is exceptional. Adding
366 * an `Error_code* err_code` out-arg would hardly add much value.
367 *
368 * @param n
369 * Desired buffer size in bytes. Must not be 0 (behavior undefined/assertion may trip).
370 * @return Non-null on success (see above); null if ctor failed to attach pool.
371 */
372 void* allocate(size_t n);
373
374 /**
375 * Undoes effects of local allocate() that returned `buf_not_null`; or another-process's
376 * allocate() that returned pointer whose locally-dereferenceable equivalent is `but_not_null`.
377 * Returns `false` if and only if no pool attached to `*this`. Does not throw exception. Behavior is
378 * undefined if `buf_not_null` is not as described above; in particular if it is null.
379 *
380 * @param buf_not_null
381 * See above.
382 * @return `true` on success; `false` if ctor failed to attach a pool.
383 */
384 bool deallocate(void* buf_not_null) noexcept;
385
386 /**
387 * Constructs an object of given type with given ctor args, having allocated space directly in attached
388 * SHM pool, and returns a ref-counted handle that (1) guarantees destruction and deallocation shall occur
389 * once no owners hold a reference; and (2) can be lent to other processes (and other processes still
390 * indefinitely), thus adding owners beyond this process, via lend_object()/borrow_object().
391 * Returns null if no pool attached to `*this`. Throws exception if ran out of space.
392 *
393 * Is better to use this than allocate() whenever possible; see class doc header notes on this.
394 *
395 * Note that that there is no way to `construct()` a native array. If that is your aim please use
396 * `T = std::array<>` or similar.
397 *
398 * ### Integration with shm::stl::Stateless_allocator ###
399 * This method, bracketing the invocation of the `T` ctor, sets the thread-local
400 * `shm::stl::Arena_activator<Pool_arena>` context to `this`. Therefore the caller need not do so.
401 * If `T` does not store an STL-compliant structure that uses `Stateless_allocator`, then this is harmless
402 * albeit a small perf hit. If `T` does do so, then it is a convenience.
403 *
404 * Arguably more importantly: The returned `shared_ptr` is such that when garbage-collection of the created
405 * data structure does occur -- which may occur in this process, but via lend_object() and borrow_object()
406 * may well occur in another process -- the `T::~T()` *dtor* call shall also be bracketed by the aforementioned
407 * context. Again: If `T` does not rely on `Stateless_allocator`, then it's harmless; but if it *does* then
408 * doing this is quite essential. That is because the user cannot, typically (or at least sufficiently easily),
409 * control the per-thread allocator context at the time of dtor call -- simply because who knows who or what
410 * will be running when the cross-process ref-count reaches 0.
411 *
412 * @tparam T
413 * Object type. See class doc header for discussion on appropriate properties of `T`.
414 * Short version: PODs work; STL nested container+POD combos work, as long as
415 * a shm::stl allocator is used at all levels; manually-implemented non-STL-compliant data
416 * structures work if care is taken to use allocate() and #Pointer.
417 * @tparam Ctor_args
418 * `T` ctor arg types.
419 * @param ctor_args
420 * 0 or more args to `T` constructor.
421 * @return Non-null on success; `null` if ctor failed to attach a pool.
422 */
423 template<typename T, typename... Ctor_args>
424 Handle<T> construct(Ctor_args&&... ctor_args);
425
426 /**
427 * Adds an owner process to the owner count of the given construct()-created handle, and returns
428 * an opaque blob, such that if one passes it to borrow_object() in the receiving process, that borrow_object()
429 * shall return an equivalent #Handle in that process. The returned `Blob` is guaranteed to have non-zero
430 * size that is small enough to be considered very cheap to copy; in particular internally as of this writing
431 * it is a `ptrdiff_t`. Returns `.empty()` object if no pool attached to `*this`.
432 *
433 * It is the user's responsibility to transmit the returned blob, such as via a transport::Channel or any other
434 * copying IPC mechanism, to the borrowing process and there pass it to `x->borrow_object()` (where `x`
435 * is a Pool_arena accessing the same-named SHM pool as `*this`). Failing to do so will leak the object until
436 * remove_persistent(). That borrowing process dying without running #Handle dtor(s) on #Handle returned
437 * by `x` borrow_object() will similarly leak it until remove_persistent().
438 *
439 * @tparam T
440 * See construct().
441 * @param handle
442 * Value returned by construct() (lending from original allocating process) or borrow_object() (proxying); or
443 * copied/moved therefrom. Note this is a mere `shared_ptr<T>` albeit with unspecified custom deleter
444 * logic attached. See #Handle doc header.
445 * @return See above. `.empty()` if and only if ctor failed to attach a pool.
446 */
447 template<typename T>
448 Blob lend_object(const Handle<T>& handle);
449
450 /**
451 * Completes the cross-process operation begun by lend_object() that returned `serialization`; to be invoked in the
452 * intended new owner process. Returns null if no pool attached to `*this`.
453 *
454 * Consider the only 2 ways a user may obtain a new #Handle to a `T` from `*this`:
455 * - construct(): This is allocation by the original/first owner of the `T`.
456 * - borrow_object(), after lend_object() was called on a previous #Handle in another process, acquired *there*
457 * however it was acquired:
458 * - Acquired via construct(): I.e., the original/first owner lent to us. I.e., it's the original loan.
459 * - Acquired via another borrow_object(): I.e., it was itself first borrowed from another. I.e., it's a loan
460 * by a lender a/k/a *proxying*.
461 *
462 * ### Integration with shm::stl::Stateless_allocator ###
463 * Crucially, the 2nd paragraph of similarly named section of construct() doc header -- where it speaks of
464 * applying `Stateless_allocator` context around dtor call possibly invoked by returned handle's deleter --
465 * applies exactly equally here. Please read it.
466 *
467 * @tparam T
468 * See lend_object().
469 * @param serialization
470 * Value, not `.empty()`, returned by lend_object() and transmitted bit-for-bit to this process.
471 * @return Non-null on success; `null` if ctor failed to attach a pool.
472 */
473 template<typename T>
474 Handle<T> borrow_object(const Blob& serialization);
475
476 /**
477 * Returns `true` if and only if `handle` came from either `this->construct<T>()` or `this->borrow_object<T>()`.
478 * Another way of saying that is: if and only if `handle` may be passed to `this->lend_object<T>()`.
479 * (The words "came from" mean "was returned by or is a copy/move of one that was," or
480 * equivalently "belongs to the `shared_ptr` group of one that was returned by.")
481 *
482 * @param handle
483 * An object, or copy/move of an object, returned by `construct<T>()` or `borrow_object<T>()`
484 * of *a* Pool_arena (not necessarily `*this`), while `*this` was already constructed.
485 * @return See above.
486 */
487 template<typename T>
488 bool is_handle_in_arena(const Handle<T>& handle) const;
489
490 // Data.
491
492 /// SHM pool name as set immutably at construction.
494
495private:
496 // Types.
497
498 /**
499 * The SHM pool type one instance of which is managed by `*this`.
500 * It would be possible to parameterize this somewhat further, such as specifying different allocation algorithms
501 * or speed up perf in single-thread situations. See class doc header for discussion. It is not a formal
502 * to-do yet.
503 *
504 * We *do* specify it as `basic_managed_shared_memory` with explicit template-parameters, as opposed to the more
505 * common `managed_shared_memory` alias, as of this writing because we don't need an index (by-name construction
506 * and lookup of objects), so we specify `null_index` to avoid wasting space on it. (So then we must specify the
507 * other params explicitly.)
508 */
509 using Pool = ::ipc::bipc::basic_managed_shared_memory<char,
510 ::ipc::bipc::rbtree_best_fit<::ipc::bipc::mutex_family>,
511 ::ipc::bipc::null_index>;
512
513 /**
514 * The data structure stored in SHM corresponding to an original construct()-returned #Handle;
515 * exactly one of which exists per construct() call invoked from any Pool_arena connected to the underlying pool.
516 * It is created in construct() and placed in the SHM pool. It is destroyed once its #m_atomic_owner_ct
517 * reaches 0, which occurs once the last process for which a #Handle (`shared_ptr`) group ref-count reaches 0
518 * detects this fact in its custom deleter and internally invokes deallocate() for the buffer,
519 * wherein the Handle_in_shm resides.
520 *
521 * @tparam T
522 * The `T` from the associated #Handle.
523 */
524 template<typename T>
526 {
527 // Types.
528
529 /**
530 * Atomically accessed count of each time the following events occurs for a given Handle_in_shm in
531 * the backing pool:
532 * - initial construction via construct(), which returns the original #Handle and creates
533 * the Handle_in_shm;
534 * - each call to lend_object() on that #Handle (internally, on the associated Handle_in_shm);
535 * - *minus* any corresponding invocations of the #Handle custom deleter.
536 */
537 using Atomic_owner_ct = std::atomic<unsigned int>;
538
539 // Data.
540
541 /**
542 * The constructed object; `Handle::get()` returns `&m_obj`. This must be the first member in
543 * Handle_in_shm, because the custom deleter `reinterpret_cast`s `Handle::get()` to mean `Handle_in_shm*`.
544 * If this arrangement is modified, one would need to use `offsetof` (or something) as well.
545 */
547
548 /// See #Atomic_owner_ct doc header. This value is 1+; once it reaches 0 `*this` is destroyed in SHM.
550 }; // struct Handle_in_shm
551
552 // Constructors.
553
554 /**
555 * Helper ctor delegated by the 2 `public` ctors that take `Open_or_create` or `Create_only` mode.
556 *
557 * @tparam Mode_tag
558 * Either util::Open_or_create or util::Create_only.
559 * @param logger_ptr
560 * See `public` ctors.
561 * @param pool_name
562 * See `public` ctors.
563 * @param mode_tag
564 * See `public` ctors.
565 * @param pool_sz
566 * See `public` ctors.
567 * @param perms
568 * See `public` ctors.
569 * @param err_code
570 * See `public` ctors.
571 */
572 template<typename Mode_tag>
573 explicit Pool_arena(Mode_tag mode_tag, flow::log::Logger* logger_ptr, const Shared_name& pool_name, size_t pool_sz,
574 const util::Permissions& perms, Error_code* err_code);
575
576 // Methods.
577
578 /**
579 * `std::construct_at()` equivalent; unavailable until C++20, so here it is.
580 *
581 * @tparam T
582 * Object type.
583 * @tparam Ctor_args
584 * `T` ctor arg types.
585 * @param obj
586 * Pointer to uninitialized `T`.
587 * @param ctor_args
588 * Ctor args for `T::T()`.
589 */
590 template<typename T, typename... Ctor_args>
591 static void construct_at(T* obj, Ctor_args&&... ctor_args);
592
593 /**
594 * Identical deleter for #Handle returned by both construct() and borrow_object(); invoked when a given process's
595 * (borrow_object() caller's) `shared_ptr` group reaches ref-count 0. It decrements Handle_in_shm::m_atomic_owner_ct;
596 * and if and only if that made it reach zero performs the opposite of what construct() did including:
597 * - runs `T::~T()` dtor;
598 * - deallocate().
599 *
600 * Otherwise (if `m_atomic_owner_ct` is not yet 0) does nothing further; other owners still remain.
601 *
602 * @param handle_state
603 * `Handle<T>::~Handle()` shall run, and if that made shared-ref-count reach 0, call
604 * `handle_deleter_impl(...that_ptr...)`. Since the #Handle returned by construct() and borrow_object()
605 * is really an alias-cted `shared_ptr<T>` to `shared_ptr<Handle_in_shm<T>>`, and Handle_in_shm::m_obj
606 * (of type `T`) is the first member in its type, those addresses are numerically equal.
607 */
608 template<typename T>
609 void handle_deleter_impl(Handle_in_shm<T>* handle_state);
610
611 // Data.
612
613 /// Attached SHM pool. If ctor fails in non-throwing fashion then this remains null. Immutable after ctor.
614 std::optional<Pool> m_pool;
615}; // class Pool_arena
616
617// Free functions: in *_fwd.hpp.
618
619// Template implementations.
620
621template<typename T>
623{
624 const auto p = reinterpret_cast<const uint8_t*>(handle.get());
625 const auto pool_base = static_cast<const uint8_t*>(m_pool->get_address());
626 return (p >= pool_base) && (p < (pool_base + m_pool->get_size()));
627}
628
629template<typename T, typename... Ctor_args>
631{
632 using Value = T;
633 using Shm_handle = Handle_in_shm<Value>;
634 using boost::shared_ptr;
635
636 if (!m_pool)
637 {
638 return Handle<Value>{};
639 }
640 // else
641
642 const auto handle_state = static_cast<Shm_handle*>(allocate(sizeof(Shm_handle)));
643 // Buffer acquired but uninitialized. Construct the owner count to 1 (just us: no lend_object() yet).
644 construct_at(&handle_state->m_atomic_owner_ct, 1);
645 // Construct the T itself. As advertised try to help out by setting selves as the current arena.
646 {
647 Pool_arena_activator ctx{this};
648 construct_at(&handle_state->m_obj, std::forward<Ctor_args>(ctor_args)...);
649 }
650
651 shared_ptr<Shm_handle> real_shm_handle{handle_state, [this](Shm_handle* handle_state)
652 {
653 handle_deleter_impl<Value>(handle_state);
654 }}; // Custom deleter.
655
656 // Return alias shared_ptr whose .get() gives &m_obj but in reality aliases to real_shm_handle.
657 return Handle<Value>{std::move(real_shm_handle), &handle_state->m_obj};
658} // Pool_arena::construct()
659
660template<typename T>
662{
663 using Value = T;
664 using Shm_handle = Handle_in_shm<Value>;
665 using util::Blob_const;
666 using flow::util::buffers_dump_string;
667
668 if (!m_pool)
669 {
670 return Blob{};
671 }
672 // else
673
674 const auto handle_state = reinterpret_cast<Shm_handle*>(handle.get());
675 const auto new_owner_ct = ++handle_state->m_atomic_owner_ct;
676
677 const ptrdiff_t offset_from_pool_base = reinterpret_cast<const uint8_t*>(handle_state)
678 - static_cast<const uint8_t*>(m_pool->get_address());
679
680 Blob serialization{sizeof(offset_from_pool_base)};
681 *(reinterpret_cast<ptrdiff_t*>(serialization.data())) = offset_from_pool_base;
682
683 FLOW_LOG_TRACE("SHM-classic pool [" << *this << "]: Serializing SHM outer handle [" << handle << "] before "
684 "IPC-transmission: Owning process-count incremented to [" << new_owner_ct << "] "
685 "(may change concurrently). "
686 "Handle points to SHM-offset [" << offset_from_pool_base << "] (serialized). Serialized contents are "
687 "[" << buffers_dump_string(serialization.const_buffer(), " ") << "].");
688
689 return serialization;
690} // Pool_arena::lend_object()
691
692template<typename T>
694{
695 using Value = T;
696 using Shm_handle = Handle_in_shm<Value>;
697 using flow::util::buffers_dump_string;
698 using boost::shared_ptr;
699
700 if (!m_pool)
701 {
702 return Handle<Value>{};
703 }
704 // else
705
706 ptrdiff_t offset_from_pool_base;
707 assert((serialization.size() == sizeof(offset_from_pool_base))
708 && "lend_object() and borrow_object() incompatible? Bug?");
709
710 offset_from_pool_base = *(reinterpret_cast<decltype(offset_from_pool_base) const *>
711 (serialization.const_data()));
712 const auto handle_state
713 = reinterpret_cast<Shm_handle*>
714 (static_cast<uint8_t*>(m_pool->get_address()) + offset_from_pool_base);
715
716 // Now simply do just as in construct():
717
718 shared_ptr<Shm_handle> real_shm_handle{handle_state, [this](Shm_handle* handle_state)
719 {
720 handle_deleter_impl<Value>(handle_state);
721 }};
722
723 FLOW_LOG_TRACE("SHM-classic pool [" << *this << "]: Deserialized SHM outer handle [" << real_shm_handle << "] "
724 "(type [" << typeid(Value).name() << "]) "
725 "after IPC-receipt: Owner-count is at [" << handle_state->m_atomic_owner_ct << "] "
726 "(may change concurrently; but includes us at least hence must be 1+). "
727 "Handle points to SHM-offset [" << offset_from_pool_base << "] (deserialized). Serialized "
728 "contents are [" << buffers_dump_string(serialization.const_buffer(), " ") << "].");
729
730 return Handle<Value>{std::move(real_shm_handle), &handle_state->m_obj};
731} // Pool_arena::borrow_object()
732
733template<typename T>
735{
736 using Value = T;
737 using Atomic_owner_ct = typename Handle_in_shm<Value>::Atomic_owner_ct;
738
739 const auto prev_owner_ct = handle_state->m_atomic_owner_ct--;
740 // Post-op form to avoid underflow for sure, for this assert():
741 assert((prev_owner_ct != 0) && "How was owner_ct=0, yet handle was still alive? Bug?");
742
743 FLOW_LOG_TRACE("SHM-classic pool [" << *this << "]: Return SHM outer handle [" << handle_state << "] "
744 "(type [" << typeid(Value).name() << "]) "
745 "because, for a given owner, a Handle is being destroyed due to shared_ptr ref-count reaching 0: "
746 "Owner-count decremented to [" << (prev_owner_ct - 1) << "] (may change concurrently "
747 "unless 0). If it is 0 now, shall invoke dtor and SHM-dealloc now.");
748 if (prev_owner_ct == 1)
749 {
750 // Now it is zero. Time to destroy the whole thing; yay! Execute the reverse (order) of construct<>() logic.
751 {
752 /* As promised, and rather crucically, help out by setting this context (same as we had around ctor --
753 * but this time it's more essential, since they can pretty easily do it themselves when constructing; but
754 * doing it at the time of reaching shared_ptr ref-count=0... that's a tall order). */
755 Pool_arena_activator ctx{this};
756
757 // But regardless:
758 (handle_state->m_obj).~Value();
759 }
760 (handle_state->m_atomic_owner_ct).~Atomic_owner_ct();
761
762 deallocate(static_cast<void*>(handle_state));
763 }
764 // else if (prev_owner_ct > 1) { It is now 1+; stays alive. Done for now. }
765} // Pool_arena::handle_deleter_impl()
766
767template<typename T, typename... Ctor_args>
768void Pool_arena::construct_at(T* obj, Ctor_args&&... ctor_args)
769{
770 using Value = T;
771
772 // Use placement-new expression used by C++20's construct_at() per cppreference.com.
773 ::new (const_cast<void*>
774 (static_cast<void const volatile *>
775 (obj)))
776 Value(std::forward<Ctor_args>(ctor_args)...);
777}
778
779template<typename Handle_name_func>
780void Pool_arena::for_each_persistent(const Handle_name_func& handle_name_func) // Static.
781{
782 util::for_each_persistent_shm_pool(handle_name_func);
783 // (See that guy's doc header for why we didn't just do what's necessary right in here.)
784}
785
786} // namespace ipc::shm::classic
A SHM-classic interface around a single SHM pool with allocation-algorithm services by boost....
Definition: pool_arena.hpp:153
::ipc::bipc::offset_ptr< T > Pointer
SHM-storable fancy-pointer.
Definition: pool_arena.hpp:164
static void construct_at(T *obj, Ctor_args &&... ctor_args)
std::construct_at() equivalent; unavailable until C++20, so here it is.
Definition: pool_arena.hpp:768
std::optional< Pool > m_pool
Attached SHM pool. If ctor fails in non-throwing fashion then this remains null. Immutable after ctor...
Definition: pool_arena.hpp:614
bool deallocate(void *buf_not_null) noexcept
Undoes effects of local allocate() that returned buf_not_null; or another-process's allocate() that r...
Definition: pool_arena.cpp:133
::ipc::bipc::basic_managed_shared_memory< char, ::ipc::bipc::rbtree_best_fit<::ipc::bipc::mutex_family >, ::ipc::bipc::null_index > Pool
The SHM pool type one instance of which is managed by *this.
Definition: pool_arena.hpp:511
boost::shared_ptr< T > Handle
Outer handle to a SHM-stored object; really a regular-looking shared_ptr but with custom deleter that...
Definition: pool_arena.hpp:188
const Shared_name m_pool_name
SHM pool name as set immutably at construction.
Definition: pool_arena.hpp:493
flow::util::Blob_sans_log_context Blob
Alias for a light-weight blob.
Definition: pool_arena.hpp:194
Pool_arena(flow::log::Logger *logger_ptr, const Shared_name &pool_name, util::Create_only mode_tag, size_t pool_sz, const util::Permissions &perms=util::Permissions{}, Error_code *err_code=nullptr)
Construct Pool_arena accessor object to non-existing named SHM pool, creating it first.
Definition: pool_arena.cpp:60
static void remove_persistent(flow::log::Logger *logger_ptr, const Shared_name &name, Error_code *err_code=nullptr)
Removes the named SHM pool object.
Definition: pool_arena.cpp:165
void * allocate(size_t n)
Allocates buffer of specified size, in bytes, in the accessed pool; returns locally-derefernceable ad...
Definition: pool_arena.cpp:103
static void for_each_persistent(const Handle_name_func &handle_name_func)
Lists all named SHM pool objects currently persisting, invoking the given handler synchronously on ea...
Definition: pool_arena.hpp:780
Handle< T > construct(Ctor_args &&... ctor_args)
Constructs an object of given type with given ctor args, having allocated space directly in attached ...
Definition: pool_arena.hpp:630
~Pool_arena()
Destroys Pool_arena accessor object.
Definition: pool_arena.cpp:98
Blob lend_object(const Handle< T > &handle)
Adds an owner process to the owner count of the given construct()-created handle, and returns an opaq...
Definition: pool_arena.hpp:661
bool is_handle_in_arena(const Handle< T > &handle) const
Returns true if and only if handle came from either this->construct<T>() or this->borrow_object<T>().
Definition: pool_arena.hpp:622
Handle< T > borrow_object(const Blob &serialization)
Completes the cross-process operation begun by lend_object() that returned serialization; to be invok...
Definition: pool_arena.hpp:693
void handle_deleter_impl(Handle_in_shm< T > *handle_state)
Identical deleter for Handle returned by both construct() and borrow_object(); invoked when a given p...
Definition: pool_arena.hpp:734
RAII-style class operating a stack-like notion of a the given thread's currently active SHM-aware Are...
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
ipc::shm sub-module with the SHM-classic SHM-provider. See ipc::shm doc header for introduction.
Definition: classic_fwd.hpp:26
util::Shared_name Shared_name
Short-hand for util::Shared_name; used in particular for SHM pool names at least.
Definition: classic_fwd.hpp:48
bipc::permissions Permissions
Short-hand for Unix (POSIX) permissions class.
Definition: util_fwd.hpp:161
bipc::open_only_t Open_only
Tag type indicating an ideally-atomic open-if-exists-else-fail operation.
Definition: util_fwd.hpp:155
void for_each_persistent_shm_pool(const Handle_name_func &handle_name_func)
Equivalent to shm::classic::Pool_arena::for_each_persistent().
Definition: util.hpp:131
bipc::open_or_create_t Open_or_create
Tag type indicating an atomic open-if-exists-else-create operation.
Definition: util_fwd.hpp:152
bipc::create_only_t Create_only
Tag type indicating a create-unless-exists-else-fail operation.
Definition: util_fwd.hpp:158
boost::asio::const_buffer Blob_const
Short-hand for an immutable blob somewhere in memory, stored as exactly a void const * and a size_t.
Definition: util_fwd.hpp:134
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:298
The data structure stored in SHM corresponding to an original construct()-returned Handle; exactly on...
Definition: pool_arena.hpp:526
Atomic_owner_ct m_atomic_owner_ct
See Atomic_owner_ct doc header. This value is 1+; once it reaches 0 *this is destroyed in SHM.
Definition: pool_arena.hpp:549
T m_obj
The constructed object; Handle::get() returns &m_obj.
Definition: pool_arena.hpp:546
std::atomic< unsigned int > Atomic_owner_ct
Atomically accessed count of each time the following events occurs for a given Handle_in_shm in the b...
Definition: pool_arena.hpp:537