Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
owner_shm_pool_repository.hpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
26#pragma once
27
30#include "ipc/shm/arena_lend/shm_pool.hpp"
31#include <flow/util/thread_lcl_obj.hpp>
32#include <flow/util/util.hpp>
33#include <boost/container/flat_map.hpp>
34#include <boost/unordered/unordered_flat_map.hpp>
35#include <map>
36#include <memory>
37#include <vector>
38#include <type_traits>
39
41{
42
43// Types.
44
45/**
46 * Helper singleton for each of at least the 2 essential central flat SHM-pool repositories
47 * (Owner_shm_pool_repository, `Borrower_shm_pool_collection_repository`), `Shm_pool_repo_lookup_core_rev<R>`
48 * (where `R` is a repo type) encapsulates `R`'s central flat set of SHM-pool IDs and base-vaddrs
49 * and, therein, a performant multi-threaded reverse-lookup from vaddr to the containing SHM-pool's ID and
50 * offset in bytes from the pool-base vaddr.
51 *
52 * @note This guy is for reverse-lookup (basically vaddr -> containing pool). How come there isn't
53 * a `Shm_pool_repo_lookup_core_fwd`, a similar thing but for forward lookup, as of this writing?
54 * Answer: perhaps there should be; but the plain truth is at least 2 places need access to
55 * the Thread_local_pool_lookup_rev engine; while only 1 place (`Borrower_shm_pool_collection_repository`
56 * as of this writing) needs access to Thread_local_pool_lookup_fwd. Hence there is no code duplication
57 * by doing the legwork required by Thread_local_pool_lookup_fwd directly in that one place; and it
58 * does cut down on boiler-plate. Though the alternative would be more symmetrical, and the consumer
59 * of Thread_local_pool_lookup_fwd would be shorter and cleaner. YMMV, they say.
60 *
61 * This is arguably "just" glue code, so we need not pontificate further, and the method
62 * doc headers should make clear-enough how to use a `*this`. To gain relevant background, one could start
63 * with doc headers for either or both of our 2 known consumers noted at the top of our doc header. That would
64 * be the approach from a high level. Alternatively or additionally one could approach from the low level:
65 * Thread_local_pool_lookup_rev.
66 *
67 * ### Impl ###
68 * The great majority of our insides is dictated by the requirements of Thread_local_pool_lookup_rev; we are
69 * a glorified wrapper around that singleton (w/r/t #Repository) and the canonical map #m_pools_by_base.
70 *
71 * @tparam Repository_t
72 * See #Repository.
73 * @tparam LOOKUP_CAN_FAIL
74 * See #S_LOOKUP_CAN_FAIL.
75 * @tparam Shm_arena_t
76 * See #Shm_arena.
77 */
78template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
80{
81public:
82 // Types.
83
84 /// The consuming repository type (e.g., Owner_shm_pool_repository, `Borrower_shm_pool_collection_repository`).
85 using Repository = Repository_t;
86 /// See Pool_lookup_global_stats.
87 using Shm_arena = Shm_arena_t;
88 /// `true` <=> lookup() can legitimately fail to find the containing pool.
89 static constexpr bool S_LOOKUP_CAN_FAIL = LOOKUP_CAN_FAIL;
90 /// Short-hand for pool-offset type.
91 using pool_offset_t = Shm_pool::size_t;
92 /// Short-hand for the thread-local reverse-lookup cache type parameterized on #Repository.
94
95 // Methods.
96
97 /**
98 * Returns the process-wide singleton instance of this class.
99 * @return Reference to the singleton.
100 */
102
103 /**
104 * Adds a SHM-pool to the canonical map and pushes the updated state into all per-thread
105 * reverse-lookup caches. Locks `Tl_copy::s_registry` for the duration.
106 *
107 * @param pool
108 * Pool to register (ID and base-vaddr are extracted; no ownership taken).
109 */
110 void insert(const Shm_pool& pool);
111
112 /**
113 * Undoes insert().
114 *
115 * @param base
116 * Base vaddr of the pool to remove.
117 */
118 void erase(const void* base);
119
120 /**
121 * Internal callback invoked by #Tl_copy ctor (via `Thread_local_state_registry`) during per-thread
122 * cache creation.
123 *
124 * @note Not for direct use by callers of this class. Public only because #Tl_copy
125 * (via `Thread_local_state_registry`) requires it.
126 *
127 * @param pools_sorted_by_base
128 * Out-arg: map to populate with base-vaddr -> Pool_by_base entries.
129 */
130 void tl_copy_rev_pull_pools(typename Tl_copy::Map* pools_sorted_by_base) const;
131
132 /**
133 * Performs a high-performance reverse-lookup, as for (e.g.) Owner_shm_pool_repository::from_address(). The contract
134 * is identical (within reason).
135 *
136 * @param address
137 * See above.
138 * @param pool_id
139 * See above.
140 * @param pool_offset
141 * See above.
142 */
143 void lookup(const void* address, pool_id_t& pool_id, pool_offset_t& pool_offset) const;
144
145private:
146 // Methods.
147
148 /**
149 * Helper for insert() and erase() that pushes the current canonical #m_pools_by_base state into all
150 * per-thread #Tl_copy caches. `Tl_copy::s_registry.while_locked()` must be in effect at entry.
151 *
152 * @param caches_per_thread
153 * Arg passed to `Tl_copy::s_registry.while_locked()` in the caller.
154 */
155 void tl_copy_push_pools(const typename decltype(Tl_copy::s_registry)::State_per_thread_map& caches_per_thread) const;
156
157 // Data.
158
159 /**
160 * Canonical sorted map: pool-base vaddr -> Pool_by_base. Guarded by `Tl_copy::s_registry`'s
161 * central mutex (all accesses occur within `while_locked()`).
162 */
163 std::map<void*, Pool_by_base> m_pools_by_base;
164}; // class Shm_pool_repo_lookup_core_rev
165
166/**
167 * Singleton repository for SHM-pool data pertaining to all SHM-arenas owned by this process.
168 * All arenas (each being an Owner_shm_pool_collection; for SHM-jemalloc: sub-class jemalloc::Ipc_arena)
169 * in this process funnel their pool info here, enabling offset-pointer resolution
170 * (to_address(), from_address()).
171 *
172 * @see Borrower_shm_pool_collection_repository which is a *somewhat* comparable singleton on the opposing side
173 * (the *borrower* as opposed to the *owner*).
174 * @todo It might be nice to name similar ops between Borrower_shm_pool_collection_repository and
175 * Owner_shm_pool_repository similarly: `insert()` vs `register_shm_pool()`. Though, the status quo is
176 * defensible; `Borrower_...` registers 2+ types of things; `Owner_...` only SHM-pools.
177 *
178 * Understanding the similarities and differences between what the two do can be very helpful in grokking
179 * the SHM-arena-lend (SHM-jemalloc more specifically at least) design. On the owner side, a `*this` is
180 * straightforward, as a given SHM-pool's lifecycle is controlled by the memory-manager (e.g., jemalloc as
181 * wrapped by jemalloc::Memory_manager and jemalloc::Ipc_arena). So we just do enough to make
182 * global pool-base/pool-ID lookup work.
183 *
184 * We shall now explain. We'll use SHM-jemalloc (the first and as of this writing only arena-lend-type SHM-provider
185 * in Flow-IPC) in the explanation, as being specific makes it easier.
186 *
187 * During a low-level SHM-allocation (jemalloc::Memory_manager::allocate(), arena-aware), jemalloc shall decide
188 * it needs a new *extent* (contiguous vaddr area) of a certain size but instead of memory-mapping (`mmap()` in Linux
189 * et al) it in process heap as normal it'll invoke an *extent hook* we (in jemalloc::Ipc_arena) register
190 * from the start; now we are in charge of the memory-mapping -- but also importantly SHM-pool creation. In short
191 * that means creating a uniquely named SHM-object and a SHM-pool-handle to it (`shm_open()` in Linux et al) and
192 * then memory-mapping it in a SHM-oriented way. This is when an *owner* SHM-pool -- `Shm_pool` object more or less --
193 * must be created and, relevantly to the present singleton, registered. That's insert(). Note that this occurs
194 * much more rarely than a precipitating jemalloc `allocate()`, but it is always done synchronously within it in
195 * the end user's allocating thread alone.
196 *
197 * In subsequent operations (perhaps jemalloc::Memory_manager::deallocate() and
198 * jemalloc::Memory_manager::destroy_arena(), the latter triggered by jemalloc::Ipc_arena destruction) jemalloc
199 * would decide to unmap an extent and again trigger an extent hook as above. This time it might lead to
200 * the removal of a SHM-pool. (Incidentally this implies there are zero allocated buffers within it, though
201 * `*this` does not care about it.) The SHM-pool must be deregistered. That's erase().
202 *
203 * @note A key point is that an *owner* SHM-pool like this, as identified by its `pool_id_t` (which is globally unique
204 * forever within the machine boundary, up to reboot), can never come back after this moment. This is in contrast
205 * to the borrower process, wherein (e.g.) session 1 can borrow an arena and pool within it, use it, then
206 * reach session-end, and then session 2 might start and borrow them again -- that time at a different base-vaddr!
207 * This fact drives the different implementation of the forward- and reverse-lookups in this
208 * Owner_shm_pool_repository versus the borrower-side counterpart Borrower_shm_pool_collection_repository.
209 * It is easy to miss/forget this subtlety and thus design/code bugs by making the two things too similar.
210 * @note It is possible for process X to borrow from process X via an X-X session. Things still work without
211 * corner-case code (in our context anyway), at least in Linux, because `mmap()` of already-mapped SHM-pool
212 * X shall auto-return the same base vaddr; hence there will not be 2+ vaddrs referring to the same RAM byte.
213 *
214 * All that said, on the owner side the principal SHM-pool lifecycle managers are *not*
215 * Owner_shm_pool_repository but rather the individual arenas' per-arena `Shm_pool_collection`s
216 * (jemalloc::Ipc_arena for SHM-jemalloc). They already track all the pools. So why do we exist?
217 * Answer: exclusively to enable (performant) forward- and reverse-lookups, to_address() and from_address().
218 * This enables high-performance computations for `Shm_pool_offset_ptr`s... which will absolutely litter
219 * any STL-compliant-container-stored-in-SHM.
220 *
221 * insert() and erase() merely feed the information needed for those to work.
222 *
223 * @note Borrower_shm_pool_collection_repository is different, in that it *also* tracks the (borrowed) SHM-pool
224 * (handle) lifecycles. It has no need, at this same relatively high level, to break them down by arenas.
225 * Owner is the administrator of pools; borrower is only a consumer of pools.
226 *
227 * ### Impl: forward/reverse lookup and the 2 mutexes ###
228 * See Borrower_shm_pool_collection_repository eponymous doc header section. Same thing here.
229 *
230 * ### Impl: Forward-lookup ###
231 * Happily this part is extremely simple to make performant. The reason that is the case here, but not the case
232 * in Borrower_shm_pool_collection_repository, is what we wrote above re. how once a pool with its ID and base-vaddr
233 * goes away, it can never come back (at all... but more saliently at a different base-vaddr).
234 *
235 * It is a per-thread hash-map (`Pool_bases_cache_map`, the per-thread cache). In to_address(),
236 * in the fast-path it'll find the desired `pool_id_t`, and that's it. No lock. The first time (slow-path) it
237 * won't (a/k/a cache miss), in which case look it up in the single canonical map controlled by insert() and
238 * erase(); this requires a mutex lock/unlock.
239 *
240 * We don't worry about removing entries from the thread-local maps on erase(), as that would be difficult; does not
241 * really affect perf either way; and leaks only a bit of memory over time, in the big picture.
242 *
243 * @note Corollary: It is not correct to try to use to_address() to check whether a particular SHM-pool still
244 * exists (is mapped).
245 *
246 * ### Impl: Forward-lookup: Challenge at thread exit ###
247 * There is a stealthy potential problem around thread exit. The basic scenario is simply to_address() executing
248 * during thread exit (probably in some destructor). (Formally or informally we aren't really worrying about the
249 * user doing this, per se; but with or without that possibility there is for sure internal Flow-IPC code
250 * that does it: `~Thread_lcl_obj_db_admin()` dtor is an important piece of code that runs at thread exit.
251 * It'll run through any objects `construct()`ed by user in that thread that are alive but have use-count=0,
252 * meaning no user references left and delete them. (These are called zombies, but despite the name their existence
253 * is totally normal; Thread_lcl_obj_db_admin et al operate in a thread-local, opportunistic/piggy-backed
254 * fashion.) When deleting a user T, one runs `~T()`. If `T` is allocator-enabled, then STL-y code is likely
255 * to perform from_address() and to_address() conversions, via `pointer_traits::pointer_to()` and the like.
256 * to_address() is us, on the owner side. `~Thread_lcl_obj_db_admin()` dtor is driven by
257 * `Thread_local_state_registry` which is, in this context, basically `boost::thread_specific_ptr`.) So when
258 * the to_address() occurs during a `boost::thread_specific_ptr` cleanup execution (in the aforementioned particular
259 * case that cleanup being the `TLODBA` dtor), this problem can manifest.
260 *
261 * That is because `thread_specific_ptr` cleanup runs at an interesting time. The source code for
262 * `flow::util::Thread_local_ptr` explains much of this stuff. In short, though, at least in Linux:
263 * `tsp` cleanup runs *before* `thread_local` deinit phase (when thread is a `boost::thread`) and *after*
264 * `thread_local` deinit phase (when thread is an `std::thread`).
265 *
266 * So what's the issue with to_address() at this time? If it executes before `thread_local` deinit (`boost::thread`):
267 * no issue. If it executes after it then, if accessing the actual `thread_local` map, it's undefined behavior:
268 * the map is destroyed.
269 *
270 * The solution is conceptually straightforward: is the per-thread map still alive? If yes: great, use it.
271 * If no: OK, fall back to the canonical central map, with the locking. Perf at that stage is of low importance.
272 * That is exactly the service `flow::util::Thread_local_obj_deinit_safe` provides (see its doc header for the
273 * full mechanism, including the first-access-near-thread-death corner cases); so we access the per-thread map
274 * exclusively through it (see #Pool_bases_cache). Death of the map is tracked per-map, not per-phase -- so
275 * this resolves even `thread_local` deinit of (something else) trying to use to_address() for whatever reason.
276 *
277 * @note We mentioned from_address(). There is also borrower-side
278 * Borrower_shm_pool_collection_repository::to_address() (and `from_address()`). Do these 3 have some similar
279 * problem? Answer: Yes and no. No, in that they do not use `thread_local` deinit but, ultimately,
280 * `thread_specific_ptr` cleanup as of this writing. Yes, in that it means, at least, that same
281 * `~Thread_lcl_obj_db_admin()` dtor -- also off a `thread_specific_ptr` cleanup -- may try to use
282 * various `from_address()` and borrower-`to_address()` impls which use `thread_specific_ptr` cleanup too.
283 * It is a different complication, out of our purview here. Spoiler alert: It works fine, as
284 * `tsp` cleanup executes a stubborn loop which allows cleanup functions to re-insert TL-values (which are
285 * added to the stubborn-cleanup list). So the modules responsible for the address-lookup participate in this
286 * mechanism (as of this writing through a `Thread_local_state_registry` through a `Thread_local_ptr` which
287 * is on top of a `tsp`), and it works fine.
288 *
289 * ### Impl: Reverse-lookup ###
290 * See Borrower_shm_pool_collection_repository eponymous doc header section. Same thing here, with the notable
291 * difference that in our case Shm_pool_repo_lookup_core_rev::S_LOOKUP_CAN_FAIL is `true` (addresses not-in-SHM --
292 * such as to stack or regular heap -- are supported).
293 * In short: Just use `detail::Shm_pool_repo_lookup_core_rev<..., true>` (#Core_rev).
294 *
295 * ### Logging ###
296 * There isn't any. The reason is that the would-be logging ops, insert() and erase(), are simple, and we
297 * TRACE-log (at least) all the same info from other parts of the code such as jemalloc::Ipc_arena.
298 * The per-thread-fanning-out in #Core_rev is somewhat interesting, but `Core_rev` does not log due to an extreme
299 * priority on perf. So we just don't need a `Logger`.
300 *
301 * @tparam Shm_arena_t
302 * The SHM-arena type (e.g. `jemalloc::Ipc_arena`). Each distinct `Shm_arena_t` yields a fully
303 * independent singleton and thus fully independent registries. `Shm_arena_t`'s API is not accessed;
304 * it serves as a compile-time discriminator only.
305 */
306template<typename Shm_arena_t>
308 private boost::noncopyable
309{
310public:
311 // Types.
312
313 /// The SHM-arena type; see class template parameter doc header.
314 using Shm_arena = Shm_arena_t;
315 /// Short-hand for pool-offset type.
318 /// Short-hand for `shared_ptr<Shm_pool>`.
319 using Shm_pool_ptr = std::shared_ptr<Shm_pool>;
320
321 // Methods.
322
323 /**
324 * Returns the process-wide singleton instance of this class.
325 *
326 * @return Reference to the singleton.
327 */
329
330 /**
331 * Key high-performance API: Given an in-SHM location as the pool and in-pool offset (values that remain constant
332 * between the borrowing process(es) and us, the lending process) returns the process-local (local process being us,
333 * the owner) vaddr pointing to that in-SHM location.
334 *
335 * Context: In particular, when processing fancy-pointers `Shm_pool_offset_ptr` (in STL-compliant structures
336 * stored in SHM and owned by this process) and top-level owned objects, this shall be called.
337 * E.g., when traversing some in-SHM STL-container, such lookups may be quite frequent.
338 *
339 * Perf: as of this writing this is a thread-local hash-map lookup of vaddr by integer; plus an offset addition.
340 * A slow-path exists (once per pool per thread) wherein one locks a central mutex, inserts into thread-local
341 * hash-map, and unlocks mutex.
342 *
343 * ### Corner cases ###
344 * Formally: If `pool_id` is invalid (never owned, no longer owned): undefined behavior. Informally:
345 * - If never owned: undefined behavior (in the classic crashy sense).
346 * - If no longer owned (removed, whether due to memory manager deciding to eliminate pool or because
347 * entire owner arena has shut down): might be classic crashy undefined-behavior; might return
348 * formerly correct result but now invalid memory.
349 *
350 * So do not try to use to_address() to somehow suss out whether a given pool still exists.
351 *
352 * Assuming `pool_id` is owned: Returned vaddr shall simply equal the pool's base-vaddr plus
353 * `pool_offset`. If it is negative or beyond the pool's size, then the returned address shall be outside
354 * the specified pool; we shall not bounds-check it.
355 *
356 * @param pool_id
357 * The identifier of the SHM-pool.
358 * @param pool_offset
359 * The offset in bytes off the pool `pool_id` base vaddr.
360 * @return Non-null vaddr.
361 */
362 static void* to_address(pool_id_t pool_id, pool_offset_t pool_offset);
363
364 /**
365 * Key high-performance API: Essentially the reverse of to_address().
366 *
367 * Context: A reverse-lookup should be much less frequent than forward-lookup to_address(); but typical STL-container
368 * code at times must figure out a `pointer` (in our case `Shm_pool_offset_ptr`) from a vaddr. (In heap-land
369 * the two are the same, but in SHM-land they are not.)
370 *
371 * Perf: identical to `Borrower_shm_pool_collection_repository::from_address()`.
372 *
373 * If `address` is not in any owned pool, `pool_id` is set to 0 and `pool_offset` is left untouched.
374 *
375 * @param address
376 * Vaddr to within a SHM-pool.
377 * @param pool_id
378 * Out-arg where the identifier of the pool containing `address` shall be placed; or zero if not found.
379 * @param pool_offset
380 * Out-arg where the offset of a byte within pool `pool_id` at which `address` points shall
381 * be placed, unless output `pool_id == 0`.
382 */
383 static void from_address(const void* address, pool_id_t& pool_id, pool_offset_t& pool_offset);
384
385 /**
386 * Registers an owned SHM-pool. The pool is unconditionally added (no ref-counting; owner-side pool IDs are
387 * globally unique and never recur).
388 *
389 * @param shm_pool
390 * Pool to register; moved-from on return.
391 */
392 void insert(Shm_pool_ptr&& shm_pool);
393
394 /**
395 * Deregisters an owned SHM-pool. Undoes insert().
396 *
397 * @param pool_id
398 * Globally unique pool identifier.
399 */
400 void erase(pool_id_t pool_id);
401
402private:
403 // Types.
404
405 /// Short-hand for the reverse-lookup (vaddr -> pool-ID + offset) module. It's a singleton like us.
407 /// Short-hand for the mutex type guarding #m_pool_bases_by_id.
408 using Pool_bases_by_id_mutex = flow::util::Mutex_non_recursive;
409 /// Short-hand for the lock type for #Pool_bases_by_id_mutex.
410 using Pool_bases_by_id_lock = flow::util::Lock_guard<Pool_bases_by_id_mutex>;
411
412 /// Per-thread cache map: pool-ID -> base-vaddr. Populated lazily in to_address(). Entries are never removed.
413 using Pool_bases_cache_map = boost::unordered_flat_map<pool_id_t, void*>;
414
415 /**
416 * Death-aware access to this thread's #Pool_bases_cache_map: to_address() can run during thread exit
417 * (see class doc header, section "Impl: Forward-lookup: Challenge at thread exit"), when a plain
418 * `thread_local` map may be destroyed already; `this_thread_obj_or_null()` returns null at such a time, and
419 * to_address() falls back to the canonical (locking) lookup. See `Thread_local_obj_deinit_safe` doc header.
420 *
421 * (The tag: #Pool_bases_cache_map is the same type across our per-`Shm_arena` instantiations -- and
422 * conceivably beyond -- so we disambiguate with the most natural tag available, ourselves.)
423 *
424 * Bottom line: It is as if we have a `static thread_local Pool_bases_cache_map s_cache` here, and to access
425 * `s_cache` -- so as to do something with it -- one instead uses `*(Pool_bases_cache::this_thread_obj_or_null())`;
426 * except that that accessor returns null when `s_cache` has been destroyed already. (Then we can fall-back to
427 * cache-avoiding behavior that does not need would-be `s_cache`.)
428 */
429 using Pool_bases_cache = flow::util::Thread_local_obj_deinit_safe<Pool_bases_cache_map, Owner_shm_pool_repository>;
430
431 // Constructors/destructor.
432
433 /// Constructor. Zero-arg due to singleton pattern.
435
436 // Methods.
437
438 /**
439 * Forward-lookup slow-path helper: returns the base vaddr for the given pool ID from
440 * the canonical #m_pool_bases_by_id (locking #m_pool_bases_by_id_mutex).
441 *
442 * @param pool_id
443 * Pool to look up.
444 * @return Base vaddr of the pool.
445 */
446 void* pull_pool_base_by_id(pool_id_t pool_id) const;
447
448 // Data.
449
450 /// Mutex guarding #m_pool_bases_by_id.
452
453 /**
454 * Canonical map: pool-ID -> `Shm_pool` handle (and thus base-vaddr via `->get_address()`).
455 * Guarded by #m_pool_bases_by_id_mutex.
456 */
457 boost::unordered_flat_map<pool_id_t, Shm_pool_ptr> m_pool_bases_by_id;
458}; // class Owner_shm_pool_repository
459
460// Template implementations.
461
462// Shm_pool_repo_lookup_core_rev template implementations.
463
464template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
467{
468 static Shm_pool_repo_lookup_core_rev s_core; // Thread-safe in C++17 at least.
469 return s_core;
470}
471
472template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
474{
475 /* We are not in thread Ti for all Ti (possibly one of them). Overview:
476 * -# Lock Tl_copy::s_registry central mutex; its State_per_thread_map cannot change during that time, and
477 * we also synchronize this set with central pool collection m_pools_by_base *and* its copies in each
478 * thread Ti's Tl_copy.
479 * -# So update both of those things.
480 * -# To protect against simultaneous read of thread-local essentially-copy of m_pools_by_base
481 * in lookup() in each thread Ti within each tl_copy_push_pools(), a per-thread mutex is locked. */
482
483 Tl_copy::s_registry.while_locked([&](const auto& state_per_thread)
484 {
485 m_pools_by_base[pool.get_address()] = Pool_by_base{ pool.get_id(),
486 S_LOOKUP_CAN_FAIL ? pool.get_size() : 0 };
487 tl_copy_push_pools(state_per_thread);
488 });
489}
490
491template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
493{
494 /* Overview: identical to insert(); just the nature of the update to m_pools_by_base is erasure instead
495 * of insertion. */
496
497 Tl_copy::s_registry.while_locked([&](const auto& state_per_thread)
498 {
499 m_pools_by_base.erase(const_cast<void*>(base));
500 tl_copy_push_pools(state_per_thread);
501 });
502}
503
504template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
506 (const typename decltype(Tl_copy::s_registry)::State_per_thread_map& caches_per_thread) const
507{
508 using Map = typename Tl_copy::Map;
509
510 // This continues insert() or erase(). Tl_copy::s_registry.while_locked() is in effect (`caches_per_thread` arg).
511
512 /* Prep an (essentially) copy of central m_pools_by_base; then move a copy of that into
513 * each thread-local Tl_copy. */
514
515 // Attn! We are using tl_copy_rev_pull_pools() -- called during *pull* from elsewhere -- as a helper here.
516 Map pools_sorted_by_base;
517 tl_copy_rev_pull_pools(&pools_sorted_by_base);
518
519 // As a small optimization, for the last one move the temp pools_sorted_by_base itself.
520 size_t n = caches_per_thread.size();
521 for (const auto& [cache, nil] : caches_per_thread)
522 {
523 // The per-thread mutex is locked inside here (reminder: to protect against simultaneous read in thread Ti).
524 cache->push_pools(((--n) == 0) ? std::move(pools_sorted_by_base)
525 : Map{pools_sorted_by_base});
526 }
527} // Shm_pool_repo_lookup_core_rev::tl_copy_push_pools()
528
529template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
531 (typename Tl_copy::Map* pools_sorted_by_base) const
532{
533 using std::is_same_v;
534 using Map = typename Tl_copy::Map;
535 using Src_map = std::decay_t<decltype(m_pools_by_base)>;
536
537 /* If called during actual pull directly from Thread_local_pool_lookup_rev: FYI:
538 * By contract this is called from Tl_copy() (the ctor), meaning in this thread Ti a new
539 * thread-local map-copy is being created on-demand. Tl_copy::s_registry.while_locked() is in effect (by contract
540 * of how Thread_local_state_registry works).
541 *
542 * If called from tl_copy_push_pools() as a helper: FYI:
543 * We ourselves ensured Tl_copy::s_registry.while_locked() is in effect. */
544
545 assert(pools_sorted_by_base->empty() && "By contract we are to load an empty map-copy.");
546
547 // Optimize by making some assumptions about the types involved.
548
549 static_assert(is_same_v<Map, boost::container::flat_map<void*, Pool_by_base>>,
550 "Optimization below assumes target Map = sorted flat map (sorted-by-key in array). "
551 "Try maintaining this in spirit if possible. Do note this is not the fast-path though.");
552 static_assert(is_same_v<Src_map, std::map<void*, Pool_by_base>>,
553 "Optimization below assumes Src_map = sorted map of some kind (std::map is one such thing, "
554 "and there are probably others, but we are just sanity-checking here. "
555 "Try maintaining this in spirit if possible. Do note this is not the fast-path though.");
556
557 pools_sorted_by_base->reserve(m_pools_by_base.size()); // Internal array pre-sized for fuss-free insertion.
558 for (const auto& base_and_pool : m_pools_by_base)
559 {
560 // Give hint to insert at the end, since we are iterating in sorted order. Hence in total the loop is linear-time.
561 pools_sorted_by_base->emplace_hint(pools_sorted_by_base->end(),
562 base_and_pool.first, base_and_pool.second);
563 }
564} // Shm_pool_repo_lookup_core_rev::tl_copy_rev_pull_pools()
565
566template<typename Repository_t, bool LOOKUP_CAN_FAIL, typename Shm_arena_t>
568 pool_id_t& pool_id,
569 pool_offset_t& pool_offset) const
570{
571 /* Fast-path. Internally this locks a per-thread mutex to protect against simultaneous
572 * tl_copy_push_pools() (unlikely hence low chance of lock contention and only against 1 thread at worst). */
573 Tl_copy::s_registry.this_thread_state()->lookup(address, pool_id, pool_offset);
574}
575
576// Owner_shm_pool_repository template implementations.
577
578template<typename Shm_arena_t>
580
581template<typename Shm_arena_t>
584{
585 static Owner_shm_pool_repository s_repository; // Thread-safe in C++17 at least.
586 return s_repository;
587}
588
589template<typename Shm_arena_t>
591{
592 void* pool_base;
593 const auto pool_bases_by_id = Pool_bases_cache::this_thread_obj_or_null();
594 if (pool_bases_by_id)
595 {
596 auto& pool_base_ref = (*pool_bases_by_id)[pool_id];
597 if (!pool_base_ref)
598 {
599 // Slow-path. (E.g., it will lock m_pool_bases_by_id_mutex to access central m_pool_bases_by_id.)
600 pool_base_ref = get_instance().pull_pool_base_by_id(pool_id);
601 }
602 // else { Fast-path }
603 pool_base = pool_base_ref;
604 }
605 else
606 {
607 /* Thread teardown, past this thread's `thread_local` deinit: the per-thread cache map is gone.
608 * Skip it; use the canonical lookup -- the very same one the cache-miss slow-path uses below. Perf does not
609 * matter much here. (Why would this ever happen? See class doc header Impl section(s) for details.) */
610 pool_base = get_instance().pull_pool_base_by_id(pool_id);
611 }
612 return reinterpret_cast<void*>(reinterpret_cast<uintptr_t>(pool_base) + pool_offset);
613}
614
615template<typename Shm_arena_t>
617 pool_id_t& pool_id,
618 pool_offset_t& pool_offset) // Static.
619{
620 // Fast-path. It will lock a mutex but with ~zero lock contention (see .lookup() body).
621 Core_rev::get_instance().lookup(address, pool_id, pool_offset);
622}
623
624template<typename Shm_arena_t>
626{
627 const auto& pool = *shm_pool; // Save ref before potential move.
628
629 { // Locks and unlocks central mutex 1.
630 Pool_bases_by_id_lock pool_bases_by_id_lock{m_pool_bases_by_id_mutex};
631 m_pool_bases_by_id.emplace(pool.get_id(), std::move(shm_pool));
632 }
633
634 Core_rev::get_instance().insert(pool); // Locks and unlocks central mutex 2.
635
636 /* Hence insert()/erase() can interleave with another insert()/erase().
637 * Class doc header section "Impl: forward/reverse lookup and the 2 mutexes" talks about this. */
638}
639
640template<typename Shm_arena_t>
642{
643 /* As in insert(), essentially do two independent ops, with their own locking each.
644 * Though in this case we get the pool-base before op 1 so as to use it in op 2. */
645
646 void* base;
647 { // Locks and unlocks central mutex 1.
648 Pool_bases_by_id_lock pool_bases_by_id_lock{m_pool_bases_by_id_mutex};
649
650 const auto it = m_pool_bases_by_id.find(pool_id);
651 base = it->second->get_address();
652
653 m_pool_bases_by_id.erase(it);
654 }
655
656 Core_rev::get_instance().erase(base); // Locks and unlocks central mutex 2.
657}
658
659template<typename Shm_arena_t>
661{
662 // Protect against concurrent slow-path lookup().
663 Pool_bases_by_id_lock pool_bases_by_id_lock{m_pool_bases_by_id_mutex};
664
665 return m_pool_bases_by_id.find(pool_id)->second->get_address();
666 // Unlock central mutex here.
667}
668
669} // namespace ipc::shm::arena_lend::detail
Singleton repository for SHM-pool data pertaining to all SHM-arenas owned by this process.
static void from_address(const void *address, pool_id_t &pool_id, pool_offset_t &pool_offset)
Key high-performance API: Essentially the reverse of to_address().
static void * to_address(pool_id_t pool_id, pool_offset_t pool_offset)
Key high-performance API: Given an in-SHM location as the pool and in-pool offset (values that remain...
void insert(Shm_pool_ptr &&shm_pool)
Registers an owned SHM-pool.
Shm_arena_t Shm_arena
The SHM-arena type; see class template parameter doc header.
boost::unordered_flat_map< pool_id_t, Shm_pool_ptr > m_pool_bases_by_id
Canonical map: pool-ID -> Shm_pool handle (and thus base-vaddr via ->get_address()).
flow::util::Thread_local_obj_deinit_safe< Pool_bases_cache_map, Owner_shm_pool_repository > Pool_bases_cache
Death-aware access to this thread's Pool_bases_cache_map: to_address() can run during thread exit (se...
flow::util::Mutex_non_recursive Pool_bases_by_id_mutex
Short-hand for the mutex type guarding m_pool_bases_by_id.
std::shared_ptr< Shm_pool > Shm_pool_ptr
Short-hand for shared_ptr<Shm_pool>.
Owner_shm_pool_repository()
Constructor. Zero-arg due to singleton pattern.
void * pull_pool_base_by_id(pool_id_t pool_id) const
Forward-lookup slow-path helper: returns the base vaddr for the given pool ID from the canonical m_po...
static Owner_shm_pool_repository & get_instance()
Returns the process-wide singleton instance of this class.
void erase(pool_id_t pool_id)
Deregisters an owned SHM-pool.
boost::unordered_flat_map< pool_id_t, void * > Pool_bases_cache_map
Per-thread cache map: pool-ID -> base-vaddr. Populated lazily in to_address(). Entries are never remo...
flow::util::Lock_guard< Pool_bases_by_id_mutex > Pool_bases_by_id_lock
Short-hand for the lock type for Pool_bases_by_id_mutex.
typename Shm_pool_repo_lookup_core_rev< Owner_shm_pool_repository, true, Shm_arena >::pool_offset_t pool_offset_t
Short-hand for pool-offset type.
Pool_bases_by_id_mutex m_pool_bases_by_id_mutex
Mutex guarding m_pool_bases_by_id.
Helper singleton for each of at least the 2 essential central flat SHM-pool repositories (Owner_shm_p...
void tl_copy_push_pools(const typename decltype(Tl_copy::s_registry)::State_per_thread_map &caches_per_thread) const
Helper for insert() and erase() that pushes the current canonical m_pools_by_base state into all per-...
static Shm_pool_repo_lookup_core_rev & get_instance()
Returns the process-wide singleton instance of this class.
void tl_copy_rev_pull_pools(typename Tl_copy::Map *pools_sorted_by_base) const
Internal callback invoked by Tl_copy ctor (via Thread_local_state_registry) during per-thread cache c...
void lookup(const void *address, pool_id_t &pool_id, pool_offset_t &pool_offset) const
Performs a high-performance reverse-lookup, as for (e.g.) Owner_shm_pool_repository::from_address().
void insert(const Shm_pool &pool)
Adds a SHM-pool to the canonical map and pushes the updated state into all per-thread reverse-lookup ...
Shm_pool::size_t pool_offset_t
Short-hand for pool-offset type.
std::map< void *, Pool_by_base > m_pools_by_base
Canonical sorted map: pool-base vaddr -> Pool_by_base.
static constexpr bool S_LOOKUP_CAN_FAIL
true <=> lookup() can legitimately fail to find the containing pool.
Repository_t Repository
The consuming repository type (e.g., Owner_shm_pool_repository, Borrower_shm_pool_collection_reposito...
Thread_local_pool_lookup_rev<R>.s_registry, where R is a distinct pool-repository type,...
boost::container::flat_map< void *, Pool_by_base > Map
Reverse-lookup map type (base vaddr -> Pool_by_base).
static flow::util::Thread_local_state_registry< Thread_local_pool_lookup_rev > s_registry
Global registry of per-thread *this instances.
Segregated private stuff for ipc::shm::arena_lend.
uint32_t pool_id_t
Pool ID type.
Value type in the reverse-lookup map: identifies the pool and, when LOOKUP_CAN_FAIL is true for the c...