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