Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
shm_pool_offset_ptr_data.cpp
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
28#include "ipc/util/util_fwd.hpp"
31#include <flow/log/simple_ostream_logger.hpp>
32#include <boost/interprocess/permissions.hpp>
33#include <boost/interprocess/shared_memory_object.hpp>
34#include <boost/interprocess/mapped_region.hpp>
35#include <boost/interprocess/sync/scoped_lock.hpp>
36
38{
39
40// Static initializers.
41
45std::optional<boost::interprocess::named_mutex> Shm_pool_offset_ptr_data_base::s_pool_id_mutex_or_none;
46
47// Implementations.
48
50{
51 using std::call_once;
52 using Shm_obj = decltype(s_pool_id_shm_obj_or_none);
53 using Shm_region = decltype(s_pool_id_shm_region_or_none);
54 using Sh_lock = bipc::scoped_lock<bipc::named_mutex>;
55
56 /* Please read our doc header for key background. Then come back here.
57 *
58 * We need unique (across all processes) 31-bit IDs. For this we use a tiny SHM region, which is not managed
59 * (no memory allocation) but rather stores only the next ID. */
60
61 pool_id_t id = 0;
62
63 /* For performance open the SHM region once and leave it open. However this needs to happen no more than once,
64 * for which we use call_once() which will likely use a mutex; we don't anticipate pool creation to be so frequent
65 * as to introduce serious lock contention, especially since the critical section here is usually tiny. */
66 call_once(s_pool_id_shm_region_init_flag, [&]()
67 {
68 /* We don't really need to log, normally, but if something goes wrong, it'd be good to at least log to standard
69 * error (even if it gets interleaves with something) -- we're gonna abort() thereafter. So set up a
70 * make-shift console logger. */
74 using flow::log::Simple_ostream_logger;
75 using flow::log::Config;
76 using flow::log::Sev;
77 using flow::Flow_log_component;
78 using bipc::permissions;
79 using bipc::interprocess_exception;
80
81 Config std_log_config{Sev::S_WARNING}; // Errors only. (Only errors would appear anyway as of now but still.)
82 std_log_config.init_component_to_union_idx_mapping<Flow_log_component>
83 (1000, Config::standard_component_payload_enum_sparse_length<Flow_log_component>(), true);
84 std_log_config.init_component_names<Flow_log_component>(flow::S_FLOW_LOG_COMPONENT_NAME_MAP, false, "flow-");
85 std_log_config.init_component_to_union_idx_mapping<Log_component>
86 (2000, Config::standard_component_payload_enum_sparse_length<Log_component>(), true);
87 std_log_config.init_component_names<Log_component>(S_IPC_LOG_COMPONENT_NAME_MAP, false, "ipc-");
88
89 Simple_ostream_logger std_logger{&std_log_config}; // Go to default (cout, cerr). Only cerr in practice.
90 FLOW_LOG_SET_CONTEXT(&std_logger, Log_component::S_SHM);
91
92 /* The post-condition of this call_once():
93 * - s_pool_id_shm_region_or_none.get_address() = mapping of opened handle to the SHM object (pool).
94 * - That pool is properly sized and contains the last-used ID (so we can ++ that and return copy thereof).
95 *
96 * If call_once() is called not-the-1st-time, then we've already guaranteed it before in this process.
97 * If called the 1st time, then we are here (where you're reading) and must ensure the above post-condition.
98 * Then read on:
99 *
100 * There are two possibilities: The SHM object (pool) does not yet exist system-wide; then we must get it to
101 * the above state: create it; size it; and set ID inside to initial value zero. Or: it does exist; then we simply
102 * open it. However in the former case:
103 * - Those 3 steps must be done atomically versus other processes executing identical code (the call_once()
104 * ensures it won't be other threads in *this* process). Hence why named-mutex *s_pool_id_mutex_or_none
105 * protects it, so we must lock it around this code.
106 *
107 * In addition we add an optimization: In the have-to-create-pool case we'd otherwise fill-out the ID to 0 (invalid)
108 * and then ourselves in the next section (after the call_once()) re-lock mutex, ++ it, and return copy.
109 * However since we ourselves are initializing the ID to 0, we might as well do the ++ in one swoop: hence
110 * simply set it to 1 immediately and return 1; thus short-circuiting and returning before
111 * the post-call_once() section.
112 *
113 * So now let's see whether pool exists yet or not. The only way to do it is try opening it and react
114 * if already exists. Note that any attempt to use OPEN_OR_CREATE does not suit us: then we cannot tell if
115 * we opened or created it, hence we don't know whether we must initialize its size and contents (the latter
116 * being the dangerous part; setting the size would just be redundant but not harmful). Note that the
117 * "physical" memory after SHM object creation is not zeroed consistently across processes (even though
118 * it might appear that way); so it is not sufficient to just create the object and then ++ the memory, thinking
119 * it is zero: It'll "be" zero in process A... but possibly zero in process B around the same time; chaos results.
120 * (This comes from experience. Nasty sporadic bugs have resulted from assuming the memory is zeroed from
121 * the get-do.) So: we do need to OPEN_ONLY and then CREATE_ONLY if necessary -- separately. */
122
123 const auto SUFFIX = Shared_name::ct("shm") / "arenaLend" / "nextPoolId";
126 little_pool_name /= SUFFIX;
127 mutex_name /= SUFFIX;
128
129 permissions perms;
130 perms.set_unrestricted(); // We have no choice given need to do cross-app writing. @todo Provide configure knobs?
131
132 // The bipc SHM manip procedures can throw. Spoiler alert: if that happens sans remedy => kaput/abort program.
133 Error_code err_code;
135 "generate_pool_id().init",
136 [&]()
137 {
138 /* The mutex (prob. semaphore) itself needs to be created-or-opened atomically. Luckily there are no
139 * initialization problems with these guys (or we'd really be screwed). */
140 s_pool_id_mutex_or_none.emplace(util::OPEN_OR_CREATE, mutex_name.native_str(), perms);
141 // It threw if failed.
142
143 // Okay, now we can lock it as noted above; and we will reuse this handle to it from now on in this process.
144
145 {
146 Sh_lock sh_lock{*s_pool_id_mutex_or_none};
147
148 // Let's see if indeed pool already exists.
149 try
150 {
151 s_pool_id_shm_obj_or_none // Currently default-cted.
152 = Shm_obj{util::OPEN_ONLY, little_pool_name.native_str(), bipc::read_write};
153
154 // Didn't throw? Then pool already existed and was therefore initialized by someone else; we have a handle.
155 }
156 catch (const boost::interprocess::interprocess_exception& exc) // Threw? Then:
157 {
158 /* Probably it failed due to not-existing; that would be normal: we won the race to here among processes.
159 * Otherwise rethrow and let the error handling below deal with it (spoiler alert: catastrophic). */
160 if (exc.get_error_code() != boost::interprocess::not_found_error)
161 {
162 throw;
163 }
164 // else: Indeed, not found. Must create underlying pool.
165
166 s_pool_id_shm_obj_or_none // Currently default-cted (still).
167 = Shm_obj{util::CREATE_ONLY, little_pool_name.native_str(), bipc::read_write, perms};
168 // It threw if failed.
169
170 /* We were the ones to create it; so we must initialize it for everyone before unlocking. Mutex
171 * is still locked; it protects both the existence-while-initialized-or-non-existence of the SHM-pool
172 * across the system; and the value stored inside it. Or simply put it protects its entire state. */
173
174 s_pool_id_shm_obj_or_none.truncate(sizeof(pool_id_t));
175 // It threw if failed.
176
177 // To actually initialize it we must map a local vaddr area to it.
178 s_pool_id_shm_region_or_none = Shm_region{s_pool_id_shm_obj_or_none, bipc::read_write, sizeof(pool_id_t)};
179 // It threw if failed.
180
181 // Lastly start the ID in this deterministic initial state. (Only ++ after this, in all processes.)
182 id = *(static_cast<pool_id_t*>(s_pool_id_shm_region_or_none.get_address())) = 1;
183
184 return; // id != 0 => we return from method next.
185
186 /* Alternatively we could have set it to 0 and not returned yet, letting the general case below
187 * ++ it (maybe to 1, or maybe higher if another guy is racing us and manages to ++ it before we
188 * re-lock the mutex below). (Except s_pool_id_shm_region_or_none would not need to be assigned,
189 * as we had to do that above.) This way is just more performant and arguably simpler
190 * to think about. */
191 } // catch () // From Shm_obj() ctor.
192 // Got here: catch(){} body did not execute; pool handle opened fine.
193 } // Sh_lock sh_lock(*s_pool_id_mutex_or_none)
194
195 // Got here: opened pool handle, and pool in solid shape, but we do still need to map it for subsequent access.
196 s_pool_id_shm_region_or_none = Shm_region{s_pool_id_shm_obj_or_none, bipc::read_write, sizeof(pool_id_t)};
197 // It threw if failed.
198 }); // op_with_possible_bipc_exception()
199 if (id != 0)
200 {
201 return; // This is the case where we initialized `id = 1` and the pool contents accordingly.
202 }
203 // else
204
205 if (err_code) // It logged details already.
206 {
207 FLOW_LOG_FATAL("Due to above shocking error at making a named mutex+tiny SHM pool+mapping when trying to handle "
208 "SHM pool ID creation, we abort.");
209 std::abort();
210 // @todo We could be nicer about errors -- some error reporting to higher level. May or may not be worth it.
211 }
212 // else
213 }); // call_once(init)
214 if (id != 0)
215 {
216 return id; // This is the case where we initialized `id = 1` and the pool contents accordingly.
217 }
218 // else
219
220 /* Post-condition of call_once() is now guaranteed: s_pool_id_shm_region_or_none is mapped to an active pool
221 * pool that stores some valid ID. Whether that's because this process had already made it so before, or
222 * because just did so above by locking mutex/opening pool/unlocking mutex, it doesn't matter. We shall
223 * now access the pool's contents and ++ that ID, at the same time. Hence lock mutex.
224 *
225 * (We could also lock mutex around all of the above and below together. Doesn't matter too much; it only
226 * makes a difference the first time per process in which case we lock/unlock/lock/unlock; but this way in
227 * the not-first-time case the critical section is a bit smaller. Though maybe the code would be simpler
228 * the other way. Not quite a to-do in my opinion (ygoldfel).) */
229
230 {
231 Sh_lock sh_lock{*s_pool_id_mutex_or_none};
232
233 // Get the next ID; and zero the MSB (reserved for the selector), leaving the proper-width next pool ID.
234 auto& id_ref = *(static_cast<pool_id_t*>(s_pool_id_shm_region_or_none.get_address()));
235 id = (++id_ref) << 1 >> 1;
236
237 if (id == 0)
238 {
239 ++id_ref;
240 id = 1;
241
242 assert((((id_ref << 1 >> 1) == 1)) && "(X00...0 + 1) must be (X00...1). Is the above logic broken?");
243 }
244 /* Overwhelmingly likely the `if` will not execute {body}. Certainly the first time we'll yield >1, avoiding 0
245 * as required (it is a reserved value for important reasons explained elsewhere). However, as also explained
246 * in this method's doc header, we do technically allow for wrapping around back to 0 (note that this would occur
247 * before the full 32-bit # fully wraps around, meaning when it goes from 00...0 to 01...1 to 10...0; and
248 * then a 2nd time when it goes from 11...1 to 0...0). If that occurs, we need to skip the 0.
249 * (@todo Maybe WARNING if this shockingly unlikely thing does get observed?) */
250 } // Sh_lock sh_lock{*s_pool_id_mutex_or_none}
251
252 return id;
253} // Shm_pool_offset_ptr_data_base::generate_pool_id()
254
255} // namespace ipc::shm::arena_lend::detail
static std::once_flag s_pool_id_shm_region_init_flag
Used with std::call_once() to ensure s_pool_id_shm_region_or_none is initialized no more than once.
detail::pool_id_t pool_id_t
Pool ID type alias. Please see detail::pool_id_t doc header for discussion as to the chosen bit width...
static std::optional< boost::interprocess::named_mutex > s_pool_id_mutex_or_none
Initialized no more than once (in this process) by generate_pool_id(), a handle to a named-mutex prot...
static bipc::mapped_region s_pool_id_shm_region_or_none
Initialized no more than once (in this process) by generate_pool_id(), a handle to tiny SHM region st...
static pool_id_t generate_pool_id()
Returns a heretofore-unused pool ID so as to be used to identify (in a Shm_pool_offset_ptr_data::Repo...
static bipc::shared_memory_object s_pool_id_shm_obj_or_none
The SHM-object (pool) mapped by s_pool_id_shm_region_or_none.
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
static Shared_name ct(const Source &src)
Copy-constructs from a char-sequence container such as string or util::String_view.
static const Shared_name S_RESOURCE_TYPE_ID_SHM
Relative-folder fragment (no separators) identifying the resource type for: SHM pools.
static const Shared_name S_RESOURCE_TYPE_ID_MUTEX
Relative-folder fragment (no separators) identifying the resource type for: boost....
Segregated private stuff for ipc::shm::arena_lend.
uint32_t pool_id_t
Pool ID type.
@ S_SHM_BIPC_MISC_LIBRARY_ERROR
Low-level boost.ipc.shm: boost.interprocess emitted miscellaneous library exception sans a system cod...
const Open_or_create OPEN_OR_CREATE
Tag value indicating an open-if-exists-else-create operation.
Definition: util.cpp:36
Shared_name build_conventional_non_session_based_shared_name(const Shared_name &resource_type)
Builds an absolute name according to the path convention explained in Shared_name class doc header; t...
void op_with_possible_bipc_exception(flow::log::Logger *logger_ptr, Error_code *err_code, const Error_code &misc_bipc_lib_error, String_view context, const Func &func)
Internal (to ipc) utility that invokes the given function that invokes a boost.interprocess operation...
Definition: util.hpp:35
Log_component
The flow::log::Component payload enumeration containing various log components used by Flow-IPC inter...
Definition: common.hpp:338
const boost::unordered_multimap< Log_component, std::string > S_IPC_LOG_COMPONENT_NAME_MAP
The map generated by flow::log macro magic that maps each enumerated value in ipc::Log_component to i...
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313