Hi Mihir, On Thu, Apr 04, 2019 at 06:25:00PM +0530, Mihir Luthra wrote: > I have made the memory expansion idea precise and filtered out bugs > from the one I sent previously. > Please check the pdf in the attachment.
I see you're getting the hang of the difficulties of the project now :) You have the right ideas to solve the problem. Do keep in mind though that CAS will only work up to a word size or a double word size at most, i.e. swapping more than 64 bit with CAS atomically is probably not going to work. I'm not sure whether we will actually need a separate process to increase the allocation size. Consider this proposal: struct shared_block_mgmt { size_t last_known_size; size_t refcnt; int blockfd; struct shared_block* block; }; struct shared_block_mgmt block_mgmt; struct shared_block { size_t size; size_t used; char memory[]; }; This struct would mean that the first `used` bytes of `memory` are in-use by our data structure and everything after that up to `size` bytes is free[1]. Any allocation request where used + request_size <= size would succeed and we could change `used` using CAS to ensure that this allocation operation is atomic. Any allocation request where used + request_size > size would trigger growing the memory area. Growing would calculate the size we want to grow to, let's call this `target_size`. Then, it would attempt to grow atomically using: size_t local_size; size_t local_used; size_t target_used; size_t target_size; bool needs_resize; do { local_used = block_mgmt.block->used; local_size = block_mgmt.block->size; target_used = local_used + request_size; needs_resize = target_used < local_size; if (needs_resize) { // growing required target_size = local_size + BLOCK_GROWTH; ftruncate(block_mgmt.blockfd, target_size); } } while ( (needs_resize && !CAS(&block_mgmt.block->size, local_size, target_size) && !CAS(&block_mgmt.block->used, local_used, target_used)) || (!needs_resize && !CAS(&block_mgmt.block->used, local_used, target_used))); // At this point, either resizing was not needed and block->used is // what we expect it to be (i.e. allocation succeeded), or resizing // was needed, and we did successfully resize, and after resizing we // did update block->used (i.e. allocation also succeeded). This will oportunistically call ftruncate with the bigger size on the mmap'd file descriptor, but calling ftruncate in multiple processes at the same time should not be a problem unless the truncation would ever shrink the file (which we can not completely avoid, but make very unlikely by making BLOCK_GROWTH sufficiently large so that any pending extension requests would be processed before another extension is required). You correctly identified that we'd have to re-mmap(2) the file after this operation, which is why I've included last_known_size in struct shared_block_mgmt. If last_known_size differs from the size stored in struct shared_memory, that would trigger creation of a new struct shared_block_mgmt with a fresh mmap with the larger size. We cannot unmap the old mapping until all threads are no longer using it, so we'd have to keep a reference count (field refcnt). And finally, we'd probably have to check whether any offsets in the data structure are larger than last_known_size, and if so, retry the current lookup operation with a fresh mmap(2) of the shared memory area, since it must have grown during the pending lookup. I believe this would allow us to avoid the separate process to grow the shared memory and also not require the use of a spinlock to block all processes for the resize operation. What do you think? [1] This memory management approach may actually not work for us later on and we may have to use a free-list or buddy system with a bitmap, but for the point of discussing the extension, let's simplify the memory allocation for now. -- Clemens