1 /// Constructs for allocating and managing memory without the GC 2 module mecca.lib.memory; 3 4 // Licensed under the Boost license. Full copyright information in the AUTHORS file 5 6 import std.exception; 7 import std..string; 8 import std.conv; 9 import std.uuid; 10 11 import core.atomic; 12 import core.stdc.errno; 13 import core.sys.posix.fcntl; 14 import core.sys.posix.sys.mman; 15 import core.sys.posix.unistd; 16 import core.sys.posix.sys.mman: MAP_ANON; 17 18 import mecca.lib.exception; 19 import mecca.lib.reflection: setToInit, abiSignatureOf, as; 20 import mecca.platform.os : MAP_POPULATE, MREMAP_MAYMOVE, mremap, MmapArguments; 21 22 import mecca.log; 23 24 enum SYS_PAGE_SIZE = 4096; 25 26 shared static this() { 27 auto actual = sysconf(_SC_PAGESIZE); 28 enforce(SYS_PAGE_SIZE == actual, "PAGE_SIZE = %s".format(actual)); 29 } 30 31 public import mecca.platform.x86: prefetch; 32 33 /** 34 * A variable size array backed by mmap-allocated memory. 35 * 36 * Behaves just like a native dynamic array, but all methods are `@nogc`. 37 * 38 * Params: 39 * shrink = Determines whether the capacity of the array can be reduced by setting .length. 40 * Allocated memory can always be freed by calling free(). 41 */ 42 public struct MmapArray(T, bool shrink = false) { 43 private: 44 void *_ptr = MAP_FAILED; 45 size_t _capacity = 0; 46 T[] _arr; 47 bool registerWithGC = false; 48 49 public: 50 51 /// Returns `true` if the array currently has no allocated memory. 52 @property bool closed() const pure nothrow @nogc { return (_ptr == MAP_FAILED); } 53 54 /// Returns the array as a standard D slice. 55 @property inout(T[]) arr() inout pure nothrow @nogc { return _arr; } 56 57 /// Returns the number of elements the array can grow to with no further allocations 58 @property size_t capacity() const pure nothrow @nogc { return _capacity / T.sizeof; } 59 60 /// Get/set the number of elements in the array. 61 @property size_t length() const pure nothrow @nogc { return _arr.length; } 62 /// ditto 63 @property size_t length(size_t numElements) @trusted @nogc { 64 return this.lengthImpl!true = numElements; 65 } 66 67 @disable this(this); 68 ~this() nothrow @safe @nogc { 69 free(); 70 } 71 72 alias arr this; 73 74 /// Pre-allocate enough memory for at least numElements to be appended to the array. 75 @notrace void reserve(size_t numElements) @trusted @nogc { 76 if (this.capacity >= numElements) { 77 return; 78 } 79 reserveImpl(numElements); 80 } 81 82 /// Initial allocation of the array memory. 83 /// 84 /// Params: 85 /// numElements = The initial number of elements of the array. 86 /// registerWithGC = Whether the array's memory should be scanned by the GC. This is required for arrays holding 87 /// pointers to GC-allocated memory. 88 /// 89 /// Notes: 90 /// Should be called on a closed array. 91 /// 92 /// This method is added for symmetry with `free()`. Its use is optional. If `registerWithGC` is `false`, this 93 /// call has the same effect as setting `length` on a closed array. 94 @notrace void allocate(size_t numElements, bool registerWithGC = false) @trusted @nogc { 95 assert (closed, "Already opened"); 96 assert (numElements > 0); 97 this.registerWithGC = registerWithGC; 98 this.lengthImpl!false = numElements; 99 } 100 101 /// Free all allocated memory and set length to 0. 102 @notrace void free() nothrow @trusted @nogc { 103 if (closed) { 104 return; 105 } 106 107 this.gcUnregister(); 108 munmap(_ptr, _capacity); 109 this._ptr = MAP_FAILED; 110 this._capacity = 0; 111 this._arr = null; 112 } 113 114 private: 115 116 @notrace void reserveImpl(size_t numElements) @trusted @nogc { 117 immutable size_t newCapacity = ((((numElements * T.sizeof) + SYS_PAGE_SIZE - 1) / SYS_PAGE_SIZE) * SYS_PAGE_SIZE); 118 static if (shrink) { 119 if (newCapacity == _capacity) { 120 return; 121 } 122 } else { 123 if (newCapacity <= _capacity) { 124 return; 125 } 126 } 127 128 if (numElements == 0) { 129 free(); 130 return; 131 } 132 133 this.gcUnregister(); 134 void* ptr = MAP_FAILED; 135 enum MmapArguments mmapArguments = { 136 prot: PROT_READ | PROT_WRITE, 137 flags: MAP_PRIVATE | MAP_ANON | MAP_POPULATE, 138 fd: -1, 139 offset: 0 140 }; 141 142 // initial allocation - mmap 143 if (closed) { 144 ptr = mmap(null, newCapacity, mmapArguments.tupleof); 145 enforceFmt!ErrnoException(ptr != MAP_FAILED, "mmap(%s bytes) failed", newCapacity); 146 } 147 148 // resize existing allocation - mremap 149 else { 150 ptr = as!"@nogc"(() => mremap(mmapArguments, _ptr, _capacity, newCapacity, MREMAP_MAYMOVE)); 151 enforceFmt!ErrnoException(ptr != MAP_FAILED, "mremap(%s bytes -> %s bytes) failed", _capacity, newCapacity); 152 } 153 154 immutable size_t currLength = this.length; 155 this._ptr = ptr; 156 this._capacity = newCapacity; 157 this._arr = (cast(T*)_ptr)[0 .. currLength]; 158 this.gcRegister(); 159 } 160 161 @property size_t lengthImpl(bool forceSetToInit)(size_t numElements) @trusted @nogc { 162 import std.algorithm : min; 163 import std.traits : hasElaborateDestructor; 164 immutable size_t currLength = this.length; 165 166 static if (hasElaborateDestructor!T) { 167 foreach (ref a; this._arr[min($, numElements) .. $]) { 168 a.__xdtor(); 169 } 170 } 171 172 this.reserveImpl(numElements); 173 this._arr = this._arr.ptr[0 .. numElements]; 174 175 // XXX DBUG change this runtime if() to static if with __traits(isZeroInit, T) once it's implemented 176 if (forceSetToInit || typeid(T).initializer.ptr !is null) { 177 foreach (ref a; this._arr[min($, currLength) .. $]) { 178 setToInit!true(a); 179 } 180 } 181 182 return numElements; 183 } 184 185 @notrace gcRegister() const nothrow @trusted @nogc { 186 import core.memory; 187 if (registerWithGC) { 188 GC.addRange(_ptr, _capacity); 189 } 190 } 191 @notrace gcUnregister() const nothrow @trusted @nogc { 192 import core.memory; 193 if (registerWithGC && !closed) { 194 GC.removeRange(_ptr); 195 } 196 } 197 } 198 199 alias MmapBuffer = MmapArray!ubyte; 200 201 @nogc unittest { 202 MmapArray!ubyte arr; 203 assert (arr is null); 204 assert (arr.length == 0); 205 arr.allocate(1024); 206 assert(arr.length == 1024); 207 arr[4] = 199; 208 arr[$-1] = 200; 209 arr.free(); 210 assert (arr is null); 211 212 arr.length = SYS_PAGE_SIZE / 4; 213 assert (arr.capacity == SYS_PAGE_SIZE); 214 arr[$-1] = 0x13; 215 arr.length = SYS_PAGE_SIZE / 2; 216 arr[$-1] = 0x37; 217 arr.length = SYS_PAGE_SIZE / 4; 218 assert (arr[$-1] == 0x13); 219 arr.length = SYS_PAGE_SIZE / 2; 220 assert (arr[$-1] == 0); 221 assert (arr.capacity == SYS_PAGE_SIZE); 222 223 arr.length = 4*SYS_PAGE_SIZE - 100; 224 assert (arr.capacity == 4*SYS_PAGE_SIZE); 225 assert (arr[SYS_PAGE_SIZE/4 - 1] == 0x13); 226 arr.length = SYS_PAGE_SIZE / 4; 227 assert (arr.capacity == 4*SYS_PAGE_SIZE); 228 assert (arr[$ - 1] == 0x13); 229 arr.length = 0; 230 assert (arr.empty); 231 assert (arr.capacity == 4*SYS_PAGE_SIZE); 232 233 arr.free(); 234 assert (arr is null); 235 assert (arr.empty); 236 assert (arr.capacity == 0); 237 } 238 239 @nogc unittest { 240 static ulong count; 241 count = 0; 242 243 struct S { 244 int x; 245 ~this() @nogc { 246 // count is global (static) to avoid allocating a closure. 247 count++; 248 } 249 } 250 MmapArray!S arr; 251 252 arr.reserve(100); 253 assert (arr.empty); 254 assert (arr.capacity >= 100); 255 256 arr.length = 10; 257 assert (!arr.empty); 258 assert (count == 0); 259 260 arr[9].x = 9; 261 arr.length = 5; 262 assert (count == 5); 263 arr.length = 20; 264 assert (count == 5); 265 assert (arr[9].x == 0); 266 arr.length = 5; 267 assert (count == 20); 268 } 269 270 struct SharedFile { 271 string filename; 272 int fd = -1; 273 ubyte[] data; 274 275 void open(string filename, size_t size, bool readWrite = true) { 276 assert (data is null, "Already open"); 277 assert (fd < 0, "Already open (fd)"); 278 279 //fd = .open(filename.toStringz, O_EXCL | O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644); 280 //bool created = (fd >= 0); 281 //if (fd < 0 && errno == EEXIST) { 282 // // this time without O_EXCL 283 // fd = .open(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644); 284 //} 285 //errnoEnforce(fd >= 0, "open(%s) failed".format(filename)); 286 287 //fd = errnoCall!(.open)(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644); 288 fd = .open(filename.toStringz, O_CREAT | (readWrite ? O_RDWR : O_RDONLY), octal!644); 289 scope(failure) { 290 .close(fd); 291 fd = -1; 292 } 293 294 auto roundedSize = ((size + SYS_PAGE_SIZE - 1) / SYS_PAGE_SIZE) * SYS_PAGE_SIZE; 295 errnoCall!ftruncate(fd, roundedSize); 296 297 auto ptr = mmap(null, size, PROT_READ | (readWrite ? PROT_WRITE : 0), MAP_SHARED, fd, 0); 298 errnoEnforce(ptr != MAP_FAILED, "mmap(%s) failed".format(size)); 299 300 data = (cast(ubyte*)ptr)[0 .. size]; 301 this.filename = filename; 302 } 303 void close() @nogc { 304 if (fd >= 0) { 305 .close(fd); 306 } 307 if (data) { 308 munmap(data.ptr, data.length); 309 data = null; 310 } 311 } 312 @property bool closed() @nogc @safe pure nothrow{ 313 return data is null; 314 } 315 316 void unlink() { 317 if (filename) { 318 errnoCall!(.unlink)(filename.toStringz); 319 filename = null; 320 } 321 } 322 void lock() { 323 errnoCall!lockf(fd, F_LOCK, 0); 324 } 325 void unlock() { 326 errnoCall!lockf(fd, F_ULOCK, 0); 327 } 328 329 alias data this; 330 } 331 332 unittest { 333 enum fn = "/tmp/mapped_file_ut"; 334 SharedFile sf; 335 scope(exit) { 336 sf.unlink(); 337 sf.close(); 338 } 339 sf.open(fn, 7891); 340 sf[7890] = 8; 341 assert(sf[$-1] == 8); 342 } 343 344 struct SharedFileStruct(T) { 345 import std.traits; 346 static assert (is(T == struct)); 347 static assert (!hasIndirections!T, "Shared struct cannot hold pointers"); 348 349 struct Wrapper { 350 shared size_t inited; 351 ulong abiSignature; // make sure it's the same `T` in all users 352 UUID uuid; // make sure it belongs to the same object in all users 353 align(64) T data; 354 } 355 356 SharedFile sharedFile; 357 358 void open(string filename, UUID uuid = UUID.init) { 359 sharedFile.open(filename, Wrapper.sizeof); 360 sharedFile.lock(); 361 scope(exit) sharedFile.unlock(); 362 363 if (atomicLoad(wrapper.inited) == 0) { 364 // we have the guarantee that the first time the file is opened, it's all zeros 365 // so let's init it here 366 setToInit(wrapper.data); 367 static if (__traits(hasMember, T, "sharedInit")) { 368 wrapper.data.sharedInit(); 369 } 370 wrapper.abiSignature = abiSignatureOf!T; 371 wrapper.uuid = uuid; 372 atomicStore(wrapper.inited, 1UL); 373 } 374 else { 375 // already inited 376 assert (wrapper.uuid == uuid); 377 assert (wrapper.abiSignature == abiSignatureOf!T); 378 } 379 } 380 void close() @nogc { 381 sharedFile.close(); 382 } 383 @property bool closed() @nogc { 384 return sharedFile.closed(); 385 } 386 void unlink() { 387 sharedFile.unlink(); 388 } 389 390 private @property ref Wrapper wrapper() @nogc {pragma(inline, true); 391 return *cast(Wrapper*)sharedFile.data.ptr; 392 } 393 @property ref T data() @nogc {pragma(inline, true); 394 return (cast(Wrapper*)sharedFile.data.ptr).data; 395 } 396 397 void lock() { 398 //import core.sys.posix.sched: sched_yield; 399 //while (!cas(&wrapper.locked, 0UL, 1UL)) { 400 // sched_yield(); 401 //} 402 sharedFile.lock(); 403 } 404 void unlock() { 405 //assert (wrapper.locked); 406 //atomicStore(wrapper.locked, 0UL); 407 sharedFile.unlock(); 408 } 409 } 410 411 unittest { 412 struct S { 413 ulong x = 17; 414 ulong y = 18; 415 } 416 417 SharedFileStruct!S sfs; 418 sfs.open("/tmp/mapped_file_ut"); 419 scope(exit) { 420 sfs.unlink(); 421 sfs.close(); 422 } 423 424 sfs.data.x++; 425 sfs.data.y++; 426 assert (sfs.data.x == 18); 427 assert (sfs.data.y == 19); 428 } 429 430 431 // Adapted from https://github.com/D-Programming-Language/druntime/blob/master/src/gc/stats.d 432 struct GCStats { 433 size_t poolSizeBytes; // total size of pool (in bytes) 434 size_t usedSizeBytes; // bytes allocated 435 size_t freeBlocks; // number of blocks marked FREE 436 size_t freeListSize; // total of memory on free lists 437 size_t pageBlocks; // number of blocks marked PAGE 438 } 439 440 struct DRuntimeStackDescriptor { 441 private import core.sync.mutex: Mutex; 442 private import core.thread: Thread; 443 444 import std.conv: to; 445 446 void* bstack; /// Stack bottom 447 void* tstack; /// Stack top 448 void* ehContext; 449 DRuntimeStackDescriptor* within; 450 DRuntimeStackDescriptor* next; 451 DRuntimeStackDescriptor* prev; 452 453 private enum mutextInstanceSize = __traits(classInstanceSize, Mutex); 454 private enum mangleSuffix = mutextInstanceSize.to!string ~ "v"; 455 456 static if (__traits(hasMember, Thread, "_locks")) { 457 pragma(mangle, "_D4core6thread6Thread6_locksG2G" ~ mangleSuffix) extern __gshared static 458 void[__traits(classInstanceSize, Mutex)][2] _locks; 459 @notrace private Mutex _slock() nothrow @nogc { 460 return cast(Mutex)_locks[0].ptr; 461 } 462 } else { 463 pragma(mangle,"_D4core6thread6Thread6_slockG72" ~ mangleSuffix) extern __gshared static 464 void[__traits(classInstanceSize, Mutex)] _slock; 465 @notrace private Mutex _slock() nothrow @nogc { 466 return cast(Mutex)_slock.ptr; 467 } 468 } 469 470 static if (__VERSION__ < 2077) { 471 pragma(mangle, "_D4core6thread6Thread7sm_cbegPS4core6thread6Thread7Context") extern __gshared static 472 DRuntimeStackDescriptor* sm_cbeg; 473 } else { 474 pragma(mangle, "_D4core6thread6Thread7sm_cbegPSQBdQBbQx7Context") extern __gshared static 475 DRuntimeStackDescriptor* sm_cbeg; 476 } 477 478 @notrace void add() nothrow @nogc { 479 auto slock = _slock(); 480 slock.lock_nothrow(); 481 scope(exit) slock.unlock_nothrow(); 482 483 if (sm_cbeg) { 484 this.next = sm_cbeg; 485 sm_cbeg.prev = &this; 486 } 487 sm_cbeg = &this; 488 } 489 490 @notrace void remove() nothrow @nogc { 491 auto slock = _slock(); 492 slock.lock_nothrow(); 493 scope(exit) slock.unlock_nothrow(); 494 495 if (this.prev) { 496 this.prev.next = this.next; 497 } 498 if (this.next) { 499 this.next.prev = this.prev; 500 } 501 if (sm_cbeg == &this) { 502 sm_cbeg = this.next; 503 } 504 } 505 }