diff options
author | Maria Matejka <mq@ucw.cz> | 2021-12-22 15:35:29 +0100 |
---|---|---|
committer | Maria Matejka <mq@ucw.cz> | 2021-12-22 15:35:49 +0100 |
commit | ffa6a51c58e7880a330e2bc3da22e5e5174e2100 (patch) | |
tree | 80c79463dbd904cf357a6286f2e9e35f4ba594ba /doc | |
parent | a840170e84b004b398552687f8e3fd54816ae2a5 (diff) |
Thread documentation: Chapter 4 on memory management
Diffstat (limited to 'doc')
-rw-r--r-- | doc/threads/03_coroutines.md | 4 | ||||
-rw-r--r-- | doc/threads/04_memory_management.md | 223 |
2 files changed, 225 insertions, 2 deletions
diff --git a/doc/threads/03_coroutines.md b/doc/threads/03_coroutines.md index 94ea3eda..fccc3bbf 100644 --- a/doc/threads/03_coroutines.md +++ b/doc/threads/03_coroutines.md @@ -235,5 +235,5 @@ Anyway, for now it is good enough as it is. *It's still a long road to the version 2.1. This series of texts should document what is needed to be changed, why we do it and how. The [previous chapter](https://en.blog.nic.cz/2021/06/14/bird-journey-to-threads-chapter-2-asynchronous-route-export/) -showed how the route export had to change to allow parallel execution. In the next chapter, we're most likely going -to show performance difference between BIRD v2.0.8 and the parallelized implementation. Stay tuned!* +showed how the route export had to change to allow parallel execution. In the next chapter, some memory management +details are to be explained together with the reasons why memory management matters. Stay tuned!* diff --git a/doc/threads/04_memory_management.md b/doc/threads/04_memory_management.md new file mode 100644 index 00000000..24ef89d3 --- /dev/null +++ b/doc/threads/04_memory_management.md @@ -0,0 +1,223 @@ +# BIRD Journey to Threads. Chapter 4: Memory and other resource management. + +BIRD is mostly a large specialized database engine, storing mega/gigabytes of +Internet routing data in memory. To keep accounts of every byte of allocated data, +BIRD has its own resource management system which must be adapted to the +multithreaded environment. The resource system has not changed much, yet it +deserves a short chapter. + +BIRD is a fast, robust and memory-efficient routing daemon designed and +implemented at the end of 20th century. We're doing a significant amount of +BIRD's internal structure changes to make it run in multiple threads in parallel. + +## Resources + +Inside BIRD, (almost) every piece of allocated memory is a resource. To achieve this, +every such memory block includes a generic `struct resource` header. The node +is enlisted inside a linked list of a *resource pool* (see below), the class +pointer defines basic operations done on resources. + +``` +typedef struct resource { + node n; /* Inside resource pool */ + struct resclass *class; /* Resource class */ +} resource; + +struct resclass { + char *name; /* Resource class name */ + unsigned size; /* Standard size of single resource */ + void (*free)(resource *); /* Freeing function */ + void (*dump)(resource *); /* Dump to debug output */ + resource *(*lookup)(resource *, unsigned long); /* Look up address (only for debugging) */ + struct resmem (*memsize)(resource *); /* Return size of memory used by the resource, may be NULL */ +}; + +void *ralloc(pool *, struct resclass *); +``` + +Resource cycle begins with an allocation of a resource. To do that, you should call `ralloc()`, +passing the parent pool and the appropriate resource class as arguments. BIRD +allocates a memory block of size given by the given class member `size`. +Beginning of the block is reserved for `struct resource` itself and initialized +by the given arguments. Therefore, you may sometimes see an idiom where a structure +has a first member `struct resource r;`, indicating that this item should be +allocated as a resource. + +The counterpart is resource freeing. This may be implicit (by resource pool +freeing) or explicit (by `rfree()`). In both cases, the `free()` function of +the appropriate class is called to cleanup the resource before final freeing. + +To account for `dump` and `memsize` calls, there are CLI commands `dump +resources` and `show memory`, using these to dump resources or show memory +usage as perceived by BIRD. + +The last, `lookup`, is quite an obsolete way to identify a specific pointer +from a debug interface. You may call `rlookup(pointer)` and BIRD should dump +that resource to the debug output. This mechanism is probably incomplete as no +developer uses it actively for debugging. + +Resources can be also moved between pools by `rmove` when needed. + +## Resource pools + +The first internal resource class is a recursive resource – a resource pool. In +the singlethreaded version, this is just a simple structure: + +``` +struct pool { + resource r; + list inside; + struct birdloop *loop; /* In multithreaded version only */ + const char *name; +}; +``` + +Resource pools are used for grouping resources together. There are pools everywhere +and it is a common idiom inside BIRD to just `rfree` the appropriate pool when +e.g. a protocol or table is going down. Everything left there is cleaned up. + +There are anyway several classes which must be freed with care. In the +singlethreaded version, the *slab* allocator (see below) must be empty before +it may be freed and this is kept to the multithreaded version while other +restrictions have been added. + +There is also a global pool, `root_pool`, containing every single resource BIRD +knows about, either directly or via another resource pool. + +### Thread safety in resource pools + +In the multithreaded version, every resource pool is bound to a specific IO +loop and therefore includes an IO loop pointer. This is important for allocations +as the resource list inside the pool is thread-unsafe. All pool operations +therefore require the IO loop to be entered to do anything with them, if possible. +(In case of `rfree`, the pool data structure is not accessed at all so no +assert is possible. We're currently relying on the caller to ensure proper locking. +In future, this may change.) + +Each IO loop also has its base resource pool for its allocations. All pools +inside the IO loop pool must belong to the same loop or to a loop with a +subordinate lock (see the previous chapter for lock ordering). If there is a +need for multiple IO loops to access one shared data structure, it must be +locked by another lock and allocated in such a way that is independent on these +accessor loops. + +The pool structure should follow the locking order. Any pool should belong to +either the same loop as its parent or its loop lock should be after its parent +loop lock in the locking order. This is not enforced explicitly, yet it is +virtually impossible to write some working code violating this recommendation. + +### Resource pools in the wilderness + +Root pool contains (among others): + +* route attributes and sources +* routing tables +* protocols +* interfaces +* configuration data + +Each table has its IO loop and uses the loop base pool for allocations. +The same holds for protocols. Each protocol has its pool; it is either its IO +loop base pool or an ordinary pool bound to main loop. + +## Memory allocators + +BIRD stores data in memory blocks allocated by several allocators. There are 3 +of them: simple memory blocks, linear pools and slabs. + +### Simple memory block + +When just a chunk of memory is needed, `mb_alloc()` or `mb_allocz()` is used +to get it. The first with `malloc()` semantics, the other is also zeroed. +There is also `mb_realloc()` available, `mb_free()` to explicitly free such a +memory and `mb_move()` to move that memory to another pool. + +Simple memory blocks consume a fixed amount of overhead memory (32 bytes on +systems with 64-bit pointers) so they are suitable mostly for big chunks, +taking advantage of the default *stdlib* allocator which is used by this +allocation strategy. There are anyway some parts of BIRD (in all versions) +where this allocator is used for little blocks. This will be fixed some day. + +### Linear pools + +Sometimes, memory is allocated temporarily. When the data may just sit on +stack, we put it there. Anyway, many tasks need more structured execution where +stack allocation is incovenient or even impossible (e.g. when callbacks from +parsers are involved). For such a case, a *linpool* is the best choice. + +This data structure allocates memory blocks of requested size with negligible +overhead in functions `lp_alloc()` (uninitialized) or `lp_allocz()` (zeroed). +There is anyway no `realloc` and no `free` call; to have a larger chunk, you +need to allocate another block. All this memory is freed at once by `lp_flush()` +when it is no longer needed. + +You may see linpools in parsers (BGP, Linux netlink, config) or in filters. + +In the multithreaded version, linpools have received an update, allocating +memory pages directly by `mmap()` instead of calling `malloc()`. More on memory +pages below. + +### Slabs + +To allocate lots of same-sized objects, a [slab allocator](https://en.wikipedia.org/wiki/Slab_allocation) +is an ideal choice. In versions until 2.0.8, our slab allocator used blocks +allocated by `malloc()`, every object included a *slab head* pointer and free objects +were linked into a single-linked list. This led to memory inefficiency and to +contra-intuitive behavior where a use-after-free bug could do lots of damage +before finally crashing. + +Versions from 2.0.9, and also all the multithreaded versions, are coming with +slabs using directly allocated memory pages and usage bitmaps instead of +single-linking the free objects. This approach however relies on the fact that +pointers returned by `mmap()` are always divisible by page size. Freeing of a +slab object involves zeroing (mostly) 13 least significant bits of its pointer +to get the page pointer where the slab head resides. + +This update helps with memory consumption by about 5% compared to previous +versions; exact numbers depend on the usage pattern. + +## Raw memory pages + +Until 2.0.8 (incl.), BIRD allocated all memory by `malloc()`. This method is +suitable for lots of use cases, yet when gigabytes of memory should be +allocated by little pieces, BIRD uses its internal allocators to keep track +about everything. This brings some ineffectivity as stdlib allocator has its +own overhead and doesn't allocate aligned memory unless asked for. + +Slabs and linear pools are backed by blocks of memory of kilobyte sizes. As a +typical memory page size is 4 kB, it is a logical step to drop stdlib +allocation from these allocators and to use `mmap()` directly. This however has +some drawbacks, most notably the need of a syscall for every memory mapping and +unmapping. For allocations, this is not much a case and the syscall time is typically +negligible compared to computation time. When freeing memory, this is much +worse as BIRD sometimes frees gigabytes of data in a blink of eye. + +To minimize the needed number of syscalls, there is a per-thread page cache, +keeping pages for future use: + +* When a new page is requested, first the page cache is tried. +* When a page is freed, the per-thread page cache keeps it without telling the kernel. +* When the number of pages in any per-thread page cache leaves a pre-defined range, + a cleanup routine is scheduled to free excessive pages or request more in advance. + +This method gives the multithreaded BIRD not only faster memory management than +ever before but also almost immediate shutdown times as the cleanup routine is +not scheduled on shutdown at all. + +## Other resources + +Some objects are not only a piece of memory; notable items are sockets, owning +the underlying mechanism of I/O, and *object locks*, owning *the right to use a +specific I/O*. This ensures that collisions on e.g. TCP port numbers and +addresses are resolved in a predictable way. + +All these resources should be used with the same locking principles as the +memory blocks. There aren't many checks inside BIRD code to ensure that yet, +nevertheless violating this recommendation may lead to multiple-access issues. + +*It's still a long road to the version 2.1. This series of texts should document +what is needed to be changed, why we do it and how. The +[previous chapter](TODO) +showed the locking system and how the parallel execution is done. +The next chapter will cover a bit more detailed explanation about route sources +and route attributes and how lockless data structures are employed there. Stay tuned!* |