Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/mandober/alligator
Free list allocator
https://github.com/mandober/alligator
allocator c-language
Last synced: 14 days ago
JSON representation
Free list allocator
- Host: GitHub
- URL: https://github.com/mandober/alligator
- Owner: mandober
- License: mit
- Created: 2024-12-21T13:15:57.000Z (18 days ago)
- Default Branch: main
- Last Pushed: 2024-12-21T14:51:37.000Z (18 days ago)
- Last Synced: 2024-12-21T15:26:43.758Z (18 days ago)
- Topics: allocator, c-language
- Language: C
- Homepage:
- Size: 3.91 KB
- Stars: 0
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# alligator
Attempt at implementing malloc-like free list allocator.
## The buffer
The memory we are to manage may be supplied by the user, if they already have a buffer at hand, e.g. in form of a stack-allocated array, or we can get the user-specified amount of memory from mmap (well, at the moment from malloc) to manage on their behalf.
Once we have a buffer, we need to "format" it, so it contains a single, free, contiguous, the first and only, block.
## The block
Block
- Header: size, status
- Node (only if block is free): no data, just a marker
- Chunk
- Footer: size, statusA block is our unit of memory management. A *block* consists of a *header* (8 bytes) that tracks the size of the associated chunk and its status (free or occupied). The header is followed by a *chunk* that is the available, usuable space in the block. The block is closed by a *footer* (8 bytes), which is just the repeated header (so it also contains the chunk's size and status).
The diagram of the formatted buffer:
```
header footer
↓ ↓
┌──┬──────────────────── B₀ ───────────────────┬──┐
│H₀│∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙chunk∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙│F₀│
└──┴───────────────────────────────────────────┴──┘
```A block consists of a chunk surrounded by a header on the left and a footer on the right.
## Allocation
When the user makes an allocation request, we could traverse the buffer from the beginning, looking for a suitable block, and, initially, there is only one block, so we'd find it immediately. However, later, when the buffer is fragmented, finding a suitable block by traversing the buffer may not be very efficient.
To make it efficient, we introduce a linked list, known as a *free list*, whose job is to track free blocks. Instead of traversing the buffer from the beginning every time the user makes an allocation request, encountering both occupied and free blocks, we traverse the free list which only traverses free blocks in the buffer.
The free list is a doubly-linked list (the reason for every choice is explained in due time), a sequence of nodes, each of which is embedded at the beginning of the chunk, right after the header. The best thing is that we are using the chunk's free space to allocate the nodes - only free blocks have an embedded node.
A list's node holds no data, except for the two pointers, to the previous, `prev`, and to the next, `next` node. The size of a node is thus 16 bytes (the size of pointers is 8 bytes @ x86_64), and this determines the minimum chunk's size. Summing the size of a header, footer and the minimal chunk's size, we get that the *minimum block's size* is 32 bytes.
The diagram of the formatted buffer with a node:
```
header
│ node footer
↓ ↓ ↓
┌──┬──┬───────────────── B₀ ───────────────────┬──┐
│H₀│N₀│∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙chunk∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙│F₀│
└──┴──┴────────────────────────────────────────┴──┘
```When the user makes an allocation request, we traverse the free list until we find a node that is sufficiently big to accomodate their request. Initially, the very first (and only) node is associated with such a block. However, since the chunk is too large, we need to split it in two. When the chunk in a block is larger then the requested size, we add 32 bytes to the request and check again. If it's still larger, we split; otherwise we give the entire chunk to the user (so at most 32 bytes will be wasted since the user can only expect to have gotten what they have requested - there is no way to let them know we gave them more, which is in line with how malloc behaves).
The threshold of 32 bytes is the same as the minimal block size - there is no point in splitting a block if the remainder is less then 32 bytes.
## Splitting
The diagram below shows the splitting of a block. Having found a suitable block, `B₀` - that is too large compared to what the user has requested - we split it into two blocks, `B₁` and `B₂`, with the plan to give the second block to the user.
We might as well give to the user the chunk of the first block, but then we'd have to "move" the node in there to the second block. By giving the user a pointer to the second chunk, we can leave the node in the first block alone.
We need to format the two blocks by inserting a new footer for the first block, and a new header for the second block. This is done through pointer arithmetic.
```
header
│ node footer
↓ ↓ ↓
┌──┬──┬───────────────── B₀ ───────────────────┬──┐
│H₀│N₀│∙∙∙∙∙∙∙∙∙∙∙∙∙chunk size∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙∙│F₀│
└──┴──┴────────────────────────────────────────┴──┘┌──┬──┬───── B₁ ─────────┬──┰──┬────── B₂ ─────┬──┐
│H₀│N₀│∙∙∙∙∙∙∙rem∙∙∙∙∙∙∙∙│F₁┃H₁│∙∙∙∙∙∙req∙∙∙∙∙∙│F₀│
└──┴──┴──────────────────┴──┸──┴───────────────┴──┘
↑ ↑ ↑ ↑ ↑ ↑
│ node │ │ ptr footer
header footerNew headerNew
```Finally, we return the `void* ptr` to the user, just like malloc.
## Deallocation
Deallocation is done from the perspective of the returned `ptr`. The user returns a `ptr`, which we know nothing about except that it points to an address that does fall within the buffer. To find out the size of that allocation, we walk back 8 bytes (i.e. the size of a header) to locate the block's header. The header tells us the size of the allocation (of the chunk). We update the header, setting the block's status to 0 (free). Knowing the chunk's size, we can then locate the block's footer and update the status there as well. The last thing to do is to embed a new node right at the address pointed by `ptr`, and then prepend it to the free list.
A block needs to have a header to hold the size (and status) of the chunk. However, a footer is not needed for allocation and deallocation, but it is essential for coalesence.
## Coalesence
The returned pointer let's us free the chunk of memory and make it again available for future allocation. However, if the surrounding blocks are also free, it would be good to merge them into a signle block.
There should probably be some analysis whether to merge the blocks or not; if the user allocation patterns show that the blocks' sizes are suitable for reuse, then we shouldn't coalesce. In this implementation, however, we always coalesce the free neighbouring blocks.
Coaleascing the current block with the next one is like splitting a block in reverse.