lockedpool.cpp
1 // Copyright (c) 2016-2022 The Bitcoin Core developers 2 // Distributed under the MIT software license, see the accompanying 3 // file COPYING or http://www.opensource.org/licenses/mit-license.php. 4 5 #include <support/lockedpool.h> 6 #include <support/cleanse.h> 7 8 #ifdef WIN32 9 #include <windows.h> 10 #else 11 #include <sys/mman.h> // for mmap 12 #include <sys/resource.h> // for getrlimit 13 #include <limits.h> // for PAGESIZE 14 #include <unistd.h> // for sysconf 15 #endif 16 17 #include <algorithm> 18 #include <limits> 19 #include <stdexcept> 20 #include <utility> 21 #ifdef ARENA_DEBUG 22 #include <iomanip> 23 #include <iostream> 24 #endif 25 26 LockedPoolManager* LockedPoolManager::_instance = nullptr; 27 28 /*******************************************************************************/ 29 // Utilities 30 // 31 /** Align up to power of 2 */ 32 static inline size_t align_up(size_t x, size_t align) 33 { 34 return (x + align - 1) & ~(align - 1); 35 } 36 37 /*******************************************************************************/ 38 // Implementation: Arena 39 40 Arena::Arena(void *base_in, size_t size_in, size_t alignment_in): 41 base(base_in), end(static_cast<char*>(base_in) + size_in), alignment(alignment_in) 42 { 43 // Start with one free chunk that covers the entire arena 44 auto it = size_to_free_chunk.emplace(size_in, base); 45 chunks_free.emplace(base, it); 46 chunks_free_end.emplace(static_cast<char*>(base) + size_in, it); 47 } 48 49 Arena::~Arena() 50 { 51 } 52 53 void* Arena::alloc(size_t size) 54 { 55 // Round to next multiple of alignment 56 size = align_up(size, alignment); 57 58 // Don't handle zero-sized chunks 59 if (size == 0) 60 return nullptr; 61 62 // Pick a large enough free-chunk. Returns an iterator pointing to the first element that is not less than key. 63 // This allocation strategy is best-fit. According to "Dynamic Storage Allocation: A Survey and Critical Review", 64 // Wilson et. al. 1995, https://www.scs.stanford.edu/14wi-cs140/sched/readings/wilson.pdf, best-fit and first-fit 65 // policies seem to work well in practice. 66 auto size_ptr_it = size_to_free_chunk.lower_bound(size); 67 if (size_ptr_it == size_to_free_chunk.end()) 68 return nullptr; 69 70 // Create the used-chunk, taking its space from the end of the free-chunk 71 const size_t size_remaining = size_ptr_it->first - size; 72 char* const free_chunk = static_cast<char*>(size_ptr_it->second); 73 auto allocated = chunks_used.emplace(free_chunk + size_remaining, size).first; 74 chunks_free_end.erase(free_chunk + size_ptr_it->first); 75 if (size_ptr_it->first == size) { 76 // whole chunk is used up 77 chunks_free.erase(size_ptr_it->second); 78 } else { 79 // still some memory left in the chunk 80 auto it_remaining = size_to_free_chunk.emplace(size_remaining, size_ptr_it->second); 81 chunks_free[size_ptr_it->second] = it_remaining; 82 chunks_free_end.emplace(free_chunk + size_remaining, it_remaining); 83 } 84 size_to_free_chunk.erase(size_ptr_it); 85 86 return allocated->first; 87 } 88 89 void Arena::free(void *ptr) 90 { 91 // Freeing the nullptr pointer is OK. 92 if (ptr == nullptr) { 93 return; 94 } 95 96 // Remove chunk from used map 97 auto i = chunks_used.find(ptr); 98 if (i == chunks_used.end()) { 99 throw std::runtime_error("Arena: invalid or double free"); 100 } 101 auto freed = std::make_pair(static_cast<char*>(i->first), i->second); 102 chunks_used.erase(i); 103 104 // coalesce freed with previous chunk 105 auto prev = chunks_free_end.find(freed.first); 106 if (prev != chunks_free_end.end()) { 107 freed.first -= prev->second->first; 108 freed.second += prev->second->first; 109 size_to_free_chunk.erase(prev->second); 110 chunks_free_end.erase(prev); 111 } 112 113 // coalesce freed with chunk after freed 114 auto next = chunks_free.find(freed.first + freed.second); 115 if (next != chunks_free.end()) { 116 freed.second += next->second->first; 117 size_to_free_chunk.erase(next->second); 118 chunks_free.erase(next); 119 } 120 121 // Add/set space with coalesced free chunk 122 auto it = size_to_free_chunk.emplace(freed.second, freed.first); 123 chunks_free[freed.first] = it; 124 chunks_free_end[freed.first + freed.second] = it; 125 } 126 127 Arena::Stats Arena::stats() const 128 { 129 Arena::Stats r{ 0, 0, 0, chunks_used.size(), chunks_free.size() }; 130 for (const auto& chunk: chunks_used) 131 r.used += chunk.second; 132 for (const auto& chunk: chunks_free) 133 r.free += chunk.second->first; 134 r.total = r.used + r.free; 135 return r; 136 } 137 138 #ifdef ARENA_DEBUG 139 static void printchunk(void* base, size_t sz, bool used) { 140 std::cout << 141 "0x" << std::hex << std::setw(16) << std::setfill('0') << base << 142 " 0x" << std::hex << std::setw(16) << std::setfill('0') << sz << 143 " 0x" << used << std::endl; 144 } 145 void Arena::walk() const 146 { 147 for (const auto& chunk: chunks_used) 148 printchunk(chunk.first, chunk.second, true); 149 std::cout << std::endl; 150 for (const auto& chunk: chunks_free) 151 printchunk(chunk.first, chunk.second->first, false); 152 std::cout << std::endl; 153 } 154 #endif 155 156 /*******************************************************************************/ 157 // Implementation: Win32LockedPageAllocator 158 159 #ifdef WIN32 160 /** LockedPageAllocator specialized for Windows. 161 */ 162 class Win32LockedPageAllocator: public LockedPageAllocator 163 { 164 public: 165 Win32LockedPageAllocator(); 166 void* AllocateLocked(size_t len, bool *lockingSuccess) override; 167 void FreeLocked(void* addr, size_t len) override; 168 size_t GetLimit() override; 169 private: 170 size_t page_size; 171 }; 172 173 Win32LockedPageAllocator::Win32LockedPageAllocator() 174 { 175 // Determine system page size in bytes 176 SYSTEM_INFO sSysInfo; 177 GetSystemInfo(&sSysInfo); 178 page_size = sSysInfo.dwPageSize; 179 } 180 void *Win32LockedPageAllocator::AllocateLocked(size_t len, bool *lockingSuccess) 181 { 182 len = align_up(len, page_size); 183 void *addr = VirtualAlloc(nullptr, len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); 184 if (addr) { 185 // VirtualLock is used to attempt to keep keying material out of swap. Note 186 // that it does not provide this as a guarantee, but, in practice, memory 187 // that has been VirtualLock'd almost never gets written to the pagefile 188 // except in rare circumstances where memory is extremely low. 189 *lockingSuccess = VirtualLock(const_cast<void*>(addr), len) != 0; 190 } 191 return addr; 192 } 193 void Win32LockedPageAllocator::FreeLocked(void* addr, size_t len) 194 { 195 len = align_up(len, page_size); 196 memory_cleanse(addr, len); 197 VirtualUnlock(const_cast<void*>(addr), len); 198 } 199 200 size_t Win32LockedPageAllocator::GetLimit() 201 { 202 size_t min, max; 203 if(GetProcessWorkingSetSize(GetCurrentProcess(), &min, &max) != 0) { 204 return min; 205 } 206 return std::numeric_limits<size_t>::max(); 207 } 208 #endif 209 210 /*******************************************************************************/ 211 // Implementation: PosixLockedPageAllocator 212 213 #ifndef WIN32 214 /** LockedPageAllocator specialized for OSes that don't try to be 215 * special snowflakes. 216 */ 217 class PosixLockedPageAllocator: public LockedPageAllocator 218 { 219 public: 220 PosixLockedPageAllocator(); 221 void* AllocateLocked(size_t len, bool *lockingSuccess) override; 222 void FreeLocked(void* addr, size_t len) override; 223 size_t GetLimit() override; 224 private: 225 size_t page_size; 226 }; 227 228 PosixLockedPageAllocator::PosixLockedPageAllocator() 229 { 230 // Determine system page size in bytes 231 #if defined(PAGESIZE) // defined in limits.h 232 page_size = PAGESIZE; 233 #else // assume some POSIX OS 234 page_size = sysconf(_SC_PAGESIZE); 235 #endif 236 } 237 238 void *PosixLockedPageAllocator::AllocateLocked(size_t len, bool *lockingSuccess) 239 { 240 void *addr; 241 len = align_up(len, page_size); 242 addr = mmap(nullptr, len, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0); 243 if (addr == MAP_FAILED) { 244 return nullptr; 245 } 246 if (addr) { 247 *lockingSuccess = mlock(addr, len) == 0; 248 #if defined(MADV_DONTDUMP) // Linux 249 madvise(addr, len, MADV_DONTDUMP); 250 #elif defined(MADV_NOCORE) // FreeBSD 251 madvise(addr, len, MADV_NOCORE); 252 #endif 253 } 254 return addr; 255 } 256 void PosixLockedPageAllocator::FreeLocked(void* addr, size_t len) 257 { 258 len = align_up(len, page_size); 259 memory_cleanse(addr, len); 260 munlock(addr, len); 261 munmap(addr, len); 262 } 263 size_t PosixLockedPageAllocator::GetLimit() 264 { 265 #ifdef RLIMIT_MEMLOCK 266 struct rlimit rlim; 267 if (getrlimit(RLIMIT_MEMLOCK, &rlim) == 0) { 268 if (rlim.rlim_cur != RLIM_INFINITY) { 269 return rlim.rlim_cur; 270 } 271 } 272 #endif 273 return std::numeric_limits<size_t>::max(); 274 } 275 #endif 276 277 /*******************************************************************************/ 278 // Implementation: LockedPool 279 280 LockedPool::LockedPool(std::unique_ptr<LockedPageAllocator> allocator_in, LockingFailed_Callback lf_cb_in) 281 : allocator(std::move(allocator_in)), lf_cb(lf_cb_in) 282 { 283 } 284 285 LockedPool::~LockedPool() = default; 286 287 void* LockedPool::alloc(size_t size) 288 { 289 std::lock_guard<std::mutex> lock(mutex); 290 291 // Don't handle impossible sizes 292 if (size == 0 || size > ARENA_SIZE) 293 return nullptr; 294 295 // Try allocating from each current arena 296 for (auto &arena: arenas) { 297 void *addr = arena.alloc(size); 298 if (addr) { 299 return addr; 300 } 301 } 302 // If that fails, create a new one 303 if (new_arena(ARENA_SIZE, ARENA_ALIGN)) { 304 return arenas.back().alloc(size); 305 } 306 return nullptr; 307 } 308 309 void LockedPool::free(void *ptr) 310 { 311 std::lock_guard<std::mutex> lock(mutex); 312 // TODO we can do better than this linear search by keeping a map of arena 313 // extents to arena, and looking up the address. 314 for (auto &arena: arenas) { 315 if (arena.addressInArena(ptr)) { 316 arena.free(ptr); 317 return; 318 } 319 } 320 throw std::runtime_error("LockedPool: invalid address not pointing to any arena"); 321 } 322 323 LockedPool::Stats LockedPool::stats() const 324 { 325 std::lock_guard<std::mutex> lock(mutex); 326 LockedPool::Stats r{0, 0, 0, cumulative_bytes_locked, 0, 0}; 327 for (const auto &arena: arenas) { 328 Arena::Stats i = arena.stats(); 329 r.used += i.used; 330 r.free += i.free; 331 r.total += i.total; 332 r.chunks_used += i.chunks_used; 333 r.chunks_free += i.chunks_free; 334 } 335 return r; 336 } 337 338 bool LockedPool::new_arena(size_t size, size_t align) 339 { 340 bool locked; 341 // If this is the first arena, handle this specially: Cap the upper size 342 // by the process limit. This makes sure that the first arena will at least 343 // be locked. An exception to this is if the process limit is 0: 344 // in this case no memory can be locked at all so we'll skip past this logic. 345 if (arenas.empty()) { 346 size_t limit = allocator->GetLimit(); 347 if (limit > 0) { 348 size = std::min(size, limit); 349 } 350 } 351 void *addr = allocator->AllocateLocked(size, &locked); 352 if (!addr) { 353 return false; 354 } 355 if (locked) { 356 cumulative_bytes_locked += size; 357 } else if (lf_cb) { // Call the locking-failed callback if locking failed 358 if (!lf_cb()) { // If the callback returns false, free the memory and fail, otherwise consider the user warned and proceed. 359 allocator->FreeLocked(addr, size); 360 return false; 361 } 362 } 363 arenas.emplace_back(allocator.get(), addr, size, align); 364 return true; 365 } 366 367 LockedPool::LockedPageArena::LockedPageArena(LockedPageAllocator *allocator_in, void *base_in, size_t size_in, size_t align_in): 368 Arena(base_in, size_in, align_in), base(base_in), size(size_in), allocator(allocator_in) 369 { 370 } 371 LockedPool::LockedPageArena::~LockedPageArena() 372 { 373 allocator->FreeLocked(base, size); 374 } 375 376 /*******************************************************************************/ 377 // Implementation: LockedPoolManager 378 // 379 LockedPoolManager::LockedPoolManager(std::unique_ptr<LockedPageAllocator> allocator_in): 380 LockedPool(std::move(allocator_in), &LockedPoolManager::LockingFailed) 381 { 382 } 383 384 bool LockedPoolManager::LockingFailed() 385 { 386 // TODO: log something but how? without including util.h 387 return true; 388 } 389 390 void LockedPoolManager::CreateInstance() 391 { 392 // Using a local static instance guarantees that the object is initialized 393 // when it's first needed and also deinitialized after all objects that use 394 // it are done with it. I can think of one unlikely scenario where we may 395 // have a static deinitialization order/problem, but the check in 396 // LockedPoolManagerBase's destructor helps us detect if that ever happens. 397 #ifdef WIN32 398 std::unique_ptr<LockedPageAllocator> allocator(new Win32LockedPageAllocator()); 399 #else 400 std::unique_ptr<LockedPageAllocator> allocator(new PosixLockedPageAllocator()); 401 #endif 402 static LockedPoolManager instance(std::move(allocator)); 403 LockedPoolManager::_instance = &instance; 404 }