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