From 76517486919657ecadda9867125dc6730f29a5b7 Mon Sep 17 00:00:00 2001 From: Kimplul Date: Sat, 6 Jul 2024 18:14:04 +0300 Subject: pretty massive virtual memory rewrite + The system is now a bit simpler and hopefully easier to understand, while also extending the shared memory to be 1:N, where there is one owner who may become a zombie while waiting for the N to die. --- src/regions.c | 761 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 761 insertions(+) create mode 100644 src/regions.c (limited to 'src/regions.c') diff --git a/src/regions.c b/src/regions.c new file mode 100644 index 0000000..66ee546 --- /dev/null +++ b/src/regions.c @@ -0,0 +1,761 @@ +/* SPDX-License-Identifier: copyleft-next-0.3.1 */ +/* Copyright 2021 - 2022, Kim Kuparinen < kimi.h.kuparinen@gmail.com > */ + +/** + * @file regions.c + * Memory region handling, used by both device memory and user virtual memory + * subsystems. + */ + +#include +#include +#include +#include +#include + +/** Memory node "subsystem" instance. */ +static struct node_root root; + +void init_mem_nodes() +{ + init_nodes(&root, sizeof(struct mem_region)); +} + +void destroy_mem_nodes() +{ + destroy_nodes(&root); +} + +/** + * Allocate a new memory region node and return it. + * + * @return New memory region node. + */ +static struct mem_region *get_mem_node() +{ + return (struct mem_region *)get_node(&root); +} + +/** + * Free a memory region node. + * + * @param m Memory region node to free. + */ +static void free_mem_node(struct mem_region *m) +{ + free_node(&root, (void *)m); +} + +/** + * Readability wrapper for marking region used. + * + * @param r Region flags to set. + */ +#define mark_region_used(r) set_bit(r, MR_USED) + +/** + * Readability wrapper for marking region unused. + * + * @param r Region flags to clear. + */ +#define mark_region_unused(r) clear_bit(r, MR_USED) + +/* pretty major slowdown when we get to some really massive numbers, not + * entirely sure why. Will need to check up on this at some point, have I + * somehow managed to come up with a _very_ bad situation for my sp_trees? + * + * EDIT: apparently, yeah. Max depth of 106 with a million entries, interesting. + * I guess since in this scenario all sizes are 1, and I just shove everything + * to the right? Maybe? + * + * EDIT upon EDIT: yeah, when taking the start position of the region into + * account we get a much more sensible max depth of 39 for 5 million entries. + * Seems I have found a weakness in sp_trees :D + * + * Duplicate entries don't work well with any trees, I think. Good to know, + * maybe not even anything with sp_trees but more a weakness of binary trees in + * general? + */ + +/** + * Insert free memory region. + * + * @param r Memory region root to insert \c m into. + * @param m Free memory region to insert. + * @return \c m. + */ +static struct mem_region *__insert_free_region(struct mem_region_root *r, + struct mem_region *m) +{ + /* this could be simplified by using my gsptrees in kmx, but at least + * this ensures 'inlining' of the condition checking so I'll let it stay + * for now */ + struct sp_node *n = sp_root(&r->free_regions), *p = NULL; + vm_t start = m->start; + size_t size = m->end - m->start; + enum sp_dir d = SP_LEFT; + + m->sp_n = (struct sp_node){ 0 }; + + while (n) { + struct mem_region *t = mem_container(n); + size_t nsize = t->end - t->start; + p = n; + + if (size < nsize) { + n = sp_left(n); + d = SP_LEFT; + } + + else if (size > nsize) { + n = sp_right(n); + d = SP_RIGHT; + } + + else if (start < t->start) { + n = sp_left(n); + d = SP_LEFT; + } + + else { + n = sp_right(n); + d = SP_RIGHT; + } + } + + sp_insert(&sp_root(&r->free_regions), p, &m->sp_n, d); + return m; +} + +/** + * Insert used memory region. + * + * @param r Memory region root to insert \c m into. + * @param m Memory region to insert. + * @return \c m. + */ +static struct mem_region *__insert_used_region(struct mem_region_root *r, + struct mem_region *m) +{ + struct sp_node *n = sp_root(&r->used_regions), *p = NULL; + vm_t start = m->start; + enum sp_dir d = SP_LEFT; + + m->sp_n = (struct sp_node){ 0 }; + + while (n) { + struct mem_region *t = mem_container(n); + + p = n; + + if (start < t->start) { + n = sp_left(n); + d = SP_LEFT; + } + + else { + /* we should never encounter a situation where start = + * t->start */ + n = sp_right(n); + d = SP_RIGHT; + } + } + + sp_insert(&sp_root(&r->used_regions), p, &m->sp_n, d); + return m; +} + +stat_t init_region(struct mem_region_root *r, vm_t start, size_t arena_size) +{ + /* convert bytes to pages */ + start = __page(start); + arena_size = __page(arena_size); + struct mem_region *m = get_mem_node(); + m->start = start; + m->end = start + arena_size; + __insert_free_region(r, m); + + return OK; +} + +/** + * Destroy memory region and all its children. + * + * @param n \ref sp_node of memory region to destroy. + */ +static void __destroy_region(struct sp_node *n) +{ + if (!n) + return; + + if (sp_left(n)) + __destroy_region(sp_left(n)); + + if (sp_right(n)) + __destroy_region(sp_right(n)); + + struct mem_region *m = mem_container(n); + free_mem_node(m); +} + +stat_t destroy_region(struct mem_region_root *r) +{ + __destroy_region(sp_root(&r->free_regions)); + __destroy_region(sp_root(&r->used_regions)); + /** \todo error checking? */ + return OK; +} + +/* interestingly this is now the main bottleneck :D + * + * eh, it's not a massive thing I guess, maybe the code could be a bit quicker + * but I mean 10 000 000 memory allocations in 20 s is good enough for now + * */ +struct mem_region *find_used_region(struct mem_region_root *r, vm_t start) +{ + /** @todo check that start is aligned to page boundary? */ + vm_t ref = __page(start); + struct sp_node *n = sp_root(&r->used_regions); + while (n) { + struct mem_region *t = mem_container(n); + if (ref == t->start) + return t; + + if (ref < t->start) + n = sp_left(n); + else + n = sp_right(n); + } + + return 0; +} + +/** + * Create memory region. + * + * @param start Start of region. + * @param end End of region. + * @param prev Previous region. + * @param next Next region. + * @return Created region. + */ +static struct mem_region *__create_region(vm_t start, vm_t end, + struct mem_region *prev, + struct mem_region *next) +{ + struct mem_region *m = get_mem_node(); + m->start = start; + m->end = end; + m->prev = prev; + m->next = next; + return m; +} + +/** + * Get first order size smaller than \c s in bytes. + * + * @param s Size to look for. + * @return Size of first order smaller than \c s. + */ +static size_t po_align(size_t s) +{ + for (size_t o = __mm_max_order; o > 0; --o) { + if (s >= order_size(o)) + return order_size(o); + } + + return 0; +} + +struct mem_region *find_closest_used_region(struct mem_region_root *r, + vm_t start) +{ + struct mem_region *closest = 0; + size_t md = (size_t)(-1); + struct sp_node *n = sp_root(&r->used_regions); + if (!n) + return mem_container(sp_root(&r->free_regions)); + + while (n) { + struct mem_region *t = mem_container(n); + size_t d = ABS((ssize_t)start - (ssize_t)t->start); + + if (d == 0) /* exact match */ + return t; + + if (d < md) { /* closest so far */ + closest = t; + md = d; + } + + if (start < t->start) + n = sp_left(n); + else + n = sp_right(n); + } + + return closest; +} + +/* should probably document this a bit better but in short, look for the "best" + * free block, meaning one that is hopefully aligned so as to allow us to later + * map it to higher order pages. If no block is found such that that is + * possible, also keep track of the smallest block that we found that the region + * still fits in, unaligned. If none of these criteria are met, a NULL is + * returned. Note that this does not check *all* possible memory blocks, only + * going up in increasing size so as to save time. */ +struct mem_region *find_free_region(struct mem_region_root *r, size_t size, + size_t *align) +{ + *align = 0; + size_t offset = __page(po_align(__addr(size))); + struct mem_region *quick_best = 0; + struct sp_node *n = sp_root(&r->free_regions); + while (n) { + struct mem_region *t = mem_container(n); + vm_t start = align_up(t->start, offset); + + size_t qsize = t->end - t->start; + size_t bsize = t->end - start; + + if (!quick_best && size <= qsize) + quick_best = t; + + if (size <= bsize) { + *align = start - t->start; + return t; + } + + n = sp_right(n); + } + + return quick_best; +} + +struct mem_region *find_first_region(struct mem_region_root *r) +{ + /* get used region with smallest address, likely also close to the start + * of the linked list */ + struct mem_region *m = find_closest_used_region(r, 0); + while (m->prev) { + m = m->prev; + } + + return m; +} + +/** + * Carve out new used memory region from free memory region. + * + * @param r Memory region root to work in. + * @param m Free memory region to carve used memory region out of. + * @param pages Number of base order pages to give used region. + * @param align Alignment of used region. In this case, start of used region + * @param pid Process ID to associate with region if shared. 0 if private. + * from start of free region. + * @param flags Flags of used region. + * @return Start address of used region. + */ +static vm_t __partition_region(struct mem_region_root *r, struct mem_region *m, + size_t pages, size_t align, vmflags_t flags, + id_t pid) +{ + sp_remove(&sp_root(&r->free_regions), &m->sp_n); + + vm_t pre_start = m->start; + vm_t pre_end = pre_start + align; + + vm_t start = pre_end; + vm_t end = start + pages; + + vm_t post_start = end; + vm_t post_end = m->end; + + if (pre_start != pre_end) { + struct mem_region *n = + __create_region(pre_start, pre_end, m->prev, m); + m->prev = n; + if (n->prev) + n->prev->next = n; + + __insert_free_region(r, n); + } + + if (post_start != post_end) { + struct mem_region *n = + __create_region(post_start, post_end, m, m->next); + m->next = n; + if (n->next) + n->next->prev = n; + + __insert_free_region(r, n); + } + + m->end = end; + m->start = start; + m->flags = flags; + m->pid = pid; + mark_region_used(m->flags); + __insert_used_region(r, m); + return __addr(start); +} + +vm_t alloc_shared_region(struct mem_region_root *r, size_t size, + size_t *actual_size, + vmflags_t flags, id_t pid) +{ + size_t asize = align_up(size, BASE_PAGE_SIZE); + if (actual_size) + *actual_size = asize; + + size_t pages = __page(asize); + + /* find best fitting, alignment etc. */ + size_t align = 0; + struct mem_region *m = find_free_region(r, pages, &align); + if (!m) + return 0; + + return __partition_region(r, m, pages, align, flags, pid); +} + +vm_t alloc_region(struct mem_region_root *r, size_t size, size_t *actual_size, + vmflags_t flags) +{ + return alloc_shared_region(r, size, actual_size, flags, 0); +} + +vm_t alloc_fixed_region(struct mem_region_root *r, vm_t start, size_t size, + size_t *actual_size, vmflags_t flags) +{ + size_t asize = align_up(size, BASE_PAGE_SIZE); + if (actual_size) + *actual_size = asize; + + size_t pages = __page(asize); + start = __page(start); + + struct mem_region *m = find_closest_used_region(r, start); + if (!m) + return 0; + + /* locate actual region where start is between the region start and end */ + while (!((m->start <= start) && (start < m->end))) { + if (start > m->start) + m = m->next; + else + m = m->prev; + } + + /* if region is already in use, forget it */ + if (is_region_used(m)) + return 0; + + /* region is too small */ + if (start + pages > m->end) + return 0; + + /* actually start marking region used */ + return __partition_region(r, m, pages, start - m->start, flags, 0); +} + +/** + * Try to coalesce two adjacent memory regions, iterating left. + * + * @param r Memory region root to work in. + * @param m Memory region to start trying to coalesce. + */ +static void __try_coalesce_prev(struct mem_region_root *r, struct mem_region *m) +{ + while (m) { + if (!m || is_region_used(m)) + return; + + struct mem_region *p = m->prev; + if (!p || is_region_used(p)) + return; + + m->start = p->start; + m->prev = p->prev; + + if (m->prev) + m->prev->next = m; + + sp_remove(&sp_root(&r->free_regions), &p->sp_n); + free_mem_node(p); + + m = m->prev; + } +} + +/** + * Try to coalesce two adjacent memory region, iterating right. + * + * @param r Memory region root to work in. + * @param m Memory region to start trying to coalesce. + */ +static void __try_coalesce_next(struct mem_region_root *r, struct mem_region *m) +{ + while (m) { + if (!m || is_region_used(m)) + return; + + struct mem_region *n = m->next; + if (!n || is_region_used(n)) + return; + + m->end = n->end; + m->next = n->next; + + if (m->next) + m->next->prev = m; + + sp_remove(&sp_root(&r->free_regions), &n->sp_n); + free_mem_node(n); + + m = m->next; + } +} + +/** + * Try coalescing memory regions. + * + * @param r Memory region root to work in. + * @param m Memory region to start trying to coalesce. + */ +static void __try_coalesce_regions(struct mem_region_root *r, + struct mem_region *m) +{ + /** @todo might free mem and then reuse it, not good */ + __try_coalesce_prev(r, m); + __try_coalesce_next(r, m); +} + +stat_t free_region(struct mem_region_root *r, vm_t start) +{ + /* addr not aligned to page boundary, corrupted or incorrect pointer */ + if (!is_aligned(start, BASE_PAGE_SIZE)) + return ERR_ALIGN; + + struct mem_region *m = find_used_region(r, start); + if (!m) + return ERR_NF; + + free_known_region(r, m); + return OK; +} + +void free_known_region(struct mem_region_root *r, struct mem_region *m) +{ + sp_remove(&sp_root(&r->used_regions), &m->sp_n); + mark_region_unused(m->flags); + + __try_coalesce_regions(r, m); + __insert_free_region(r, m); +} + +/** + * Align region starting at \p start of size \p bytes to start and end on + * BASE_PAGE boundaries. Place new start and size into \p startp and \p bytesp. + * + * @param start Start of region. + * @param bytes Size of region. + * @param startp Where to place new start. + * @param bytesp Where to place new size. + */ +static void align_region(vm_t start, size_t bytes, vm_t *startp, size_t *bytesp) +{ + size_t shift = order_shift(BASE_PAGE); + vm_t top = start + bytes; + /* reasonably fast align down */ + vm_t new_start = (start >> shift) << shift; + + /* to align up, we must first align down */ + vm_t new_top = ((top >> shift) << shift); + + /* if alignment did something, add a base page size to align up */ + if (new_top != top) + new_top += BASE_PAGE_SIZE; + + /* difference between top and start */ + size_t new_bytes = new_top - new_start; + + *startp = new_start; + *bytesp = new_bytes; +} + +/* assuming start is chosen to start on an aligned border, this should choose + * the 'optimal' fit for the mapping. + * + * NOTE: not actually optimal, this doesn't bother to go through possible + * permutations etc. which would be slow and I don't want to implement it. + */ +stat_t map_region(struct vmem *b, vm_t start, size_t bytes, enum mm_order order, + vmflags_t flags) +{ + /* adjust to nearest page sizes */ + align_region(start, bytes, &start, &bytes); + + size_t size = order_size(order); + while (bytes) { + if (size > bytes) + goto next_order; + + /* NULL does pass this check, so technically all NULL pages are + * aligned, but they're caught in the while expr so this should + * work even if someone tries to map NULL */ + if (!is_aligned(start, size)) + goto next_order; + + pm_t page = alloc_page(order); + if (!page) + goto next_order; + + stat_t res = map_vpage(b, page, start, flags, order); + if (res) + goto next_order; + + + start += size; + bytes -= size; + continue; + +next_order: + /* ran out of orders, stop */ + if (order == 0) + return ERR_MISC; + + order--; + size = order_size(order); + } + + return OK; +} + +stat_t map_fixed_region(struct vmem *b, vm_t v, pm_t start, size_t bytes, + vmflags_t flags) +{ + /* adjust to nearest page sizes, generally the region should be on a + * BASE_PAGE boundary but just to be safe */ + v = align_down(v, BASE_PAGE_SIZE); + align_region(start, bytes, &start, &bytes); + + size_t size = BASE_PAGE_SIZE; + while (bytes) { + stat_t ret = map_vpage(b, start, v, flags, BASE_PAGE); + if (ret) + return ret; + + start += size; + bytes -= size; + v += size; + } + + return OK; +} + +stat_t clone_region(struct vmem *b, struct vmem *g, vm_t from, vm_t to, + size_t bytes, vmflags_t flags) +{ + size_t from_size = 0; size_t to_size = 0; + align_region(from, bytes, &from, &from_size); + align_region(to, bytes, &to, &to_size); + + catastrophic_assert(from_size == to_size); + bytes = from_size; + + while (bytes) { + pm_t addr = 0; + enum mm_order order = BASE_PAGE; + stat_t res = stat_vpage(g, from, &addr, &order, NULL); + if (res) + return res; + + res = map_vpage(b, addr, to, flags, order); + if (res) + return res; + + size_t size = order_size(order); + bytes -= size; + from += size; + to += size; + } + + return OK; +} + +stat_t copy_region(struct vmem *b, struct vmem *g, vm_t from, vm_t to, + size_t bytes) +{ + size_t from_size = 0; size_t to_size = 0; + align_region(from, bytes, &from, &from_size); + align_region(to, bytes, &to, &to_size); + + catastrophic_assert(from_size == to_size); + bytes = from_size; + + while (bytes) { + pm_t addr = 0; + vmflags_t flags = 0; + enum mm_order order = BASE_PAGE; + stat_t res = stat_vpage(g, from, &addr, &order, &flags); + if (res) + return res; + + pm_t page = alloc_page(order); + if (!page) + return ERR_OOMEM; + + /* temporarily give us write permissions */ + res = map_vpage(b, page, to, flags, order); + if (res) { + free_page(order, page); + return res; + } + + size_t size = order_size(order); + memcpy((void *)page, (void *)addr, size); + bytes -= size; + from += size; + to += size; + } + + return OK; +} + +void unmap_region(struct vmem *b, vm_t v, size_t bytes) +{ + v = align_down(v, BASE_PAGE_SIZE); + bytes = align_up(v + bytes, BASE_PAGE_SIZE) - v; + while (bytes) { + pm_t addr = 0; + enum mm_order order = BASE_PAGE; + stat_t res = stat_vpage(b, v, &addr, &order, NULL); + if (res) + return; + + unmap_vpage(b, v); + free_page(order, addr); + size_t size = order_size(order); + bytes -= size; + v += size; + } +} + +void unmap_fixed_region(struct vmem *b, vm_t v, size_t bytes) +{ + v = align_down(v, BASE_PAGE_SIZE); + bytes = align_up(v + bytes, BASE_PAGE_SIZE) - v; + while (bytes) { + pm_t addr = 0; + enum mm_order order = BASE_PAGE; + stat_t res = stat_vpage(b, v, &addr, &order, NULL); + if (res) + return; + + unmap_vpage(b, v); + size_t size = order_size(order); + bytes -= size; + v += size; + } +} -- cgit v1.3