the FYSOS registry system

The FYSOS memory allocation system specification 1.0.0-rc2

This document describes version 1.0.0-rc2 of the FYSOS memory allocation system: a free, simple, portable, personal, fully featured allocation system for embedded tasks and hobbyists alike. Minor changes made to this document (e.g. wording) that do not affect the registry system format are tracked by the third number in the document version number.

This memory allocation system is in the release development stage: this document supersedes any previous version of the memory allocation system specification with no care for backward compatibility.

Since this is a new memory allocation system, and some aspects are still to be defined, suggestions or corrections are welcome, for either the memory allocation system or this document. Please contact the author at: fys at fysnet.net. The author wishes to thank those who have submitted comments and criticism in order to improve this system.

This document contains the formal specification of the FYSOS memory allocation in-memory format. For an overview of this system and for downloads, please see the overview page.

Table of contents

Differences from the previous versions

Definitions

The following terms and conventions will be used throughout this specification:

Machine Size

This specification outlines a 32-bit memory module and a 64-bit memory module. When you compile for a 32-bit platform, pointers and size_t declarations are 32-bit wide. When you compile for a 64-bit platform, pointers and size_t declarations are 64-bit wide.

The struct Bucket structure is always 64 bytes in size, no matter the platform.

The struct Pebble structure is of a different size depending on the inclusion of the name field and the platform size.

Virtual Heap

Heap Layout

 Figure 1: Layout of a FYSOS memory allocation system.

To make it easier on the kernel, a virtual memory heap is created. The heap is a number of blocks of memory available to the kernel. The heap is used as a simple way to allocate and free virtual memory without calling the underlining physical allocator for each and every memory request, making it much faster to allocate memory.

On Kernel start up, it calls the Virtual Memory Allocator (mmap()) to allocate a single linear block of memory, 64 Meg is suggested but not required, for this heap. The memory in the heap may or may not be physically contiguous. This heap is then further managed by a Heap Allocator. All requests for memory go through this Heap Allocator.

If a memory request can be satisfied from this initial block of memory, the Heap Allocator records the allocation within its catalog and returns a pointer to a region large enough to satisfy the request. No call to the Virtual Memory Allocator or Physical Memory Allocator is needed, drastically speeding up the memory request.

If the memory request cannot be satisfied, due to lack of remaining room in the current catalog, a special alignment request, a physical address request, or other situation, the Heap Allocator may then call upon the Virtual Memory Allocator to satisfy the request. The Virtual Memory Allocator may then call upon the Physical Memory Allocator to satisfy the request.

This Virtual Heap Allocator maintains a catalog of virtual memory addresses from base address of the Bucket to the size of the heap set by the Kernel. The Kernel’s heap may set this limit to any amount. Any Heap created for a user task may set an initial limit, resizing as needed.

Heap Catalog

The Heap Catalog consists of Buckets and Pebbles. Initially there is a single Bucket of any size holding one to an arbitrary number of Pebbles within it. Each Pebble is marked used or free. If a Pebble is marked free, it may be used to satisfy a memory request. This Pebble can be split into two Pebbles so that the remaining part of the Pebble can then be used to satisfy another request.

If at any time there is not a Pebble within a Bucket to satisfy a memory request, a new Bucket may be allocated, placing free Pebbles within, to satisfy the request.

Buckets

A Bucket is allocated by the Virtual Memory Allocator, is of an arbitrary size, and is considered a contiguous number of virtually allocated bytes from start to end. The starting memory location and any contiguous byte to the end may or may not be physically contiguous, but must be virtually contiguous.

A Bucket is allocated and freed from the Virtual Memory Allocator using two functions, the first may be a function called mmap() and the second may be called mmap_free(). No other system memory function is required as far as the Heap Allocator is concerned. All Virtual Memory allocated for a Bucket is cataloged by the Virtual Memory Allocator.

A Bucket maintains only the Pebbles within it and has no awareness of any other Pebbles within another Bucket.

A Bucket may be resized at any time as long as the Pebbles within are maintained. If a Bucket has excess free Pebbles, or a single free Pebble with excess memory allocated for it, a Bucket may shrink to accommodate. If a Bucket doesn’t have enough free space within, it may expand to create an addition number of Pebbles. However, any memory added to the Bucket must be contiguously added at the end of the currently allocated memory. i.e.: A Bucket may only increase in size if the added memory is Virtually consecutively added at the end and the new sized Bucket is still completely continuous.

A Bucket has the following format and must be stored in the first bytes of the allocated space returned from the Virtual Memory Allocator.

struct Bucket
uint32_t magicThis must be equal to 0x4255434B (the 'BUCK' characters in ASCII), and it must be used to identify a valid bucket within the system.
uint32_t localFlagsFlags used for this bucket. See below. Each bucket is independent of another.
size_t largestThis field holds the size of the largest free block within this Bucket. The Heap Allocator code can compare this field with the request to see if there is a Pebble large enough to satisfy the request. This is so the Heap Allocator does not have to parse any of the Pebbles to see if one is large enough, and can move on to another Bucket if there is none. This field must be maintained after a Pebble in this Bucket is modified.
size_t sizeThis field is used to store the number of pages allocated for this Bucket. This value is used for the free function of the Virtual Memory Allocator, so it knows how many virtually contiguous pages to free.
uint32_t spinLockIn a multithreaded/tasking system, this field is used to lock this Bucket. If this bucket is locked, no value should be assumed valid until this field shows no lock. This specification does not define the technique used to lock this Bucket. It is up to the implementation to use this field to lock this Bucket.
uint8_t reserved[n]Reserved and preserved. For 64-bit platforms, n = 12. For 32-bit platforms, n = 32.
void *firstPebbleThis field points to the first Pebble in this Bucket. A Bucket must always have at least one Pebble, even if it is a free-for-use Pebble. After a call to free a Pebble, after any merging of Pebbles, if this happens to create a single Pebble within a Bucket and this Pebble is free for use, it is up to the Heap Allocator if it would like to free this Bucket from the Heap’s list. Again, making sure to not free the all Buckets.
The First Pebble, the Bucket’s firstPebble field, must not be assumed to be just after the Bucket structure. The First Pebble field may point to any Pebble within the Bucket. An advantage of this is to update the Bucket’s first Pebble field to point to the newly freed Pebble, so that when the next allocation is to take place, the first Pebble found is a free Pebble. However, this creates more work when finding a used Pebble. Also, see a later section for more reasons why you must not assume the first Pebble starts at an offset just after the Bucket Structure in the memory region.
void *previousThis field, along with the next field, create a linked list of Buckets. If a field is non-zero, there is another Bucket in that direction at that address. At least one Bucket must always exist and its previous field must be NULL. If it is the last or the only Bucket, its next field must also be NULL.
void *nextThis field, along with the previous field, create a linked list of Buckets. If a field is non-zero, there is another Bucket in that direction at that address.

The Bucket's localFlags field is defined as the following:

enum BucketLocalFlags
Symbolic namebit positionDescription
blfMethod0If set, the allocator is to use the Best Fit method when allocating a free Pebble within this Bucket. That is to use the smallest free Pebble that will satisfy the request.
If clear, the allocator must use the first free Pebble it finds in this Bucket.
blfReserved7:1reserved and preserved
blfRequestType15:8This field is equal to the enum requestType value that was used to allocate this Bucket.
blfReserved31:16reserved and preserved

Pebbles

Pebbles are stored within a bucket. A Bucket must have at least one Pebble. A Pebble is used to satisfy a memory allocation request.

A Pebble has the following format.

struct Pebble
uint32_t magicThis must be equal to 0x524F434B (the 'ROCK' characters in ASCII), and it must be used to identify a valid pebble within the system.
uint32_t localFlagsThis field is used to indicate a few attributes about this Pebble. Any flag given is for this Pebble only. No field, flags or other, are used in any other Pebble.
uint32_t reserved0reserved and preserved.
uint32_t alignmentThis field is used to pass on the Alignment value if this Pebble is to be aligned. This way, a function, such as a call to realloc(), can maintain this alignment. Allowed alignment values passed to the Heap Allocator are any power-of-two, from 1 to 1Meg, inclusive. Any value that is not a power-of-two, will be increased to the next closest power-of-two. However, the Heap Allocator’s minimal alignment will be 64. Any value below will be returned on an alignment of at least 64 bytes. A value from 64 to 1 Meg fits within a 32-bit unsigned integer, hence the size of the field. However, when using calculations on 64-bit values, remember to use correct casts to a 64-bit value to eliminate errors.
size_t sizeThis field is the number of bytes within this Pebble assigned for use. This size must be at least 64 and must be a count of 64 bytes in size. i.e.: It must be at least 64, 128, 192, 256, 320, etc. This field may not actually represent the exact number of bytes requested, but must be at a 64-byte count at or above the amount requested. This is to align the next Pebble on a 64-byte count from the end of the previous pebble. This alignment is not to be relied upon, nor assumed. See a later section for reasons why. This Size value does not include the size of the Pebble Structure described here.
char name[32]
  (optional)
This field is an optional field and may contain anything the Kernel wishes to place here. This could simply be a character string of the function or filename that called the allocator. It can also be a signature associated with a driver who has requested it. It is purely optional and used for debugging purposes. Your Heap Allocator must ignore this field other than maintaining this field throughout its life.
uint8_t reserved[n]This field is reserve and must be preserved.
64-bit: If the name field is present, n = 48 and this structure is 128 bytes in size. If the name field is omitted, n = 16 and this structure is 64 bytes is size.
32-bit: If the name field is present, this field is omitted. If the name field is omitted, this field must be present, and n = 32.
void *parentThis field points to the Parent Bucket this Pebble resides in. All Pebbles, free or not, must maintain this pointer. See the section about merging pebbles on an important safety catch.
void *previousThis field, along with the next field, create a linked list of Pebbles. If a field is non-zero, there is a Pebble in that direction at the address. The first listed Pebble must have a Previous field of NULL and the last listed Pebble must have a Next field of NULL.
All Pebbles must remain within the allocated memory of the Bucket. i.e.: When adding a Pebble, it must be within an address that is contained within the memory allocated for the Bucket. No Pebble may be allocated outside of its intended Bucket and no Pebble may point to another Bucket’s Pebble.
void *nextThis field, along with the previous field, create a linked list of Pebbles.

The Pebble's localFlags field is defined as the following:

enum PebleLocalFlags
Symbolic namebit positionDescription
plfState0If set, this Pebble is used.
If clear, this Pebble is free for use.
plfAligned1If set, the alignment field is used, else it is ignored.
plfCleared2If set, this Pebble's memory has been cleared to zeros.
plfReserved31:3reserved and preserved

Splitting Pebbles

When Splitting a Pebble to allocate a portion of the memory allocated to it, you must add at least one Pebble in either direction, updating the two surrounding Pebbles to point to this newly added Pebble. You may only split a Pebble when there is enough room to satisfy the current allocation request and a new Pebble will have at least the 64-byte minimum available space allotted for it (sizeof(Pebble) + 64). If splitting a Pebble does not leave enough room for a new Pebble and this 64-byte space, the Pebble is not to be spit and the allocated size must remain as is. This 64-byte space is determined with a #define and can be changed. A value that is a power of two is recommended.

When using the Alignment functionality, you may split a Pebble into three contiguous Pebbles. The first will be a free Pebble simply to pad to the next Pebble, which will be the Aligned Pebble. Then this Aligned Pebble may possibly split again to free any excess space after it. This may create three Pebbles when splitting one Pebble for an aligned request. If doing so, the first and the possible third Pebble must remain available for use by another request.

Please note that when splitting in this manner, if the first Pebble will have an available size less than 64, it is recommended to align on the next available alignment, the one after the current calculated alignment, to make the first Pebble have an available space of at or more than 64 bytes. Also note that the Aligned Pebble must start 128 bytes (the size of a Pebble Structure) before the alignment value so that the available memory starts on the requested alignment.

Also, please note that if the current Pebble, before you split it into three, already is aligned on the requested alignment, there is no need to create three. Simply return the current Pebble, possibly first splitting it into two to free the excess space.

Merging Pebbles

When two consecutive Pebbles are free, your Heap Allocator should merge these Pebbles to create a single Pebble. To do this, the first Pebble needs to consume the second Pebble. All linked-list pointers must be updated for any surrounding Pebbles. The Size field in the first Pebble is now increased to the size of both Pebbles including the deleted Pebble’s header block. new_size = pebble0->size + sizeof(pebble) + pebble1->size

Be sure to watch that if you delete a Pebble by merging, the Bucket’s First Pebble pointer is still valid. i.e.: by deleting this Pebble, first make sure the Bucket wasn’t pointing to this Pebble, and adjust if needed.

Aligned Requests

When the caller is requesting a memory address that is aligned on a supplied boundary requirement, it is up to your Heap Allocator to satisfy this request. The manner in which you do is not specified here. However, here are a few examples. See a previous section on splitting a Pebble for more on accommodating Aligned requests.

  1. Your Heap Allocator may adjust a Pebble’s location so that the byte just after the Pebble Structure satisfies the alignment request. To do this, you forgo the 64-byte alignment request specified in a previous section and place a Pebble so that it is just before the aligned location. To do this, you must have an original free Pebble large enough to split and place a new Pebble within the allocated space so that this new Pebble’s memory region is now aligned.
  2. You may insert a new Bucket into the Heap, so that the newly created first Pebble is now aligned on the requested alignment. However, this makes it so that your code cannot assume the first Pebble is just after the memory used for the Bucket. This means that there may be unused space between the memory used to store the Bucket data and the first Pebble. However, this is an easy way to allocate aligned and/or physically aligned memory. See the next section for more on this use.

Physical Requests

Physical contiguous memory requests must also be satisfied by the Heap Allocator. Physically allocated Buckets are allocated, stored, and freed exactly like Virtually allocated Buckets, only that the localFlags field is adjusted to indicate which is which. When a new Bucket is created, the Heap Allocator passes a flag to the Virtual Memory Allocator to return memory that is Physically and Contiguous allocated. The Heap Allocator requests the Virtual Memory Allocator to make sure and allocate Physical Memory addresses that are contiguous. There is no difference in the Heap allocation scheme between Virtually stored Buckets and Physically stored Buckets, other than the localFlags member of the Bucket indicating the difference.

Heap Initialization

Generally, the owner--usually the kernel but later a user app--will call the underlining paging system to allocate a virtually contiguous block of memory, creating one or more buckets and each bucket having one or more pebbles. When the owner then makes a memory request, the allocator will scroll through the bucket(s) and pebble(s) until it finds a satisfactory type of memory block.

In general, this allocation request will simply be a size in bytes and the allocator will return a virtually contiguous block of memory at least the size of the request.

However, as a kernel, a request for physically contiguous memory may be requested. This is where another Bucket can be created within the same heap or a new heap.

Each heap is created using the underlining mmap() style system call. If the system is using memory paging, the call should return a virtually contiguous block of memory, or if requested, a physically contiguous block of memory.

If the underlining system does not support memory paging, a physical contiguous block of memory will be returned for both types of request.

This allocation scheme does not require the underlining system to support paging. The mmap() call can simply return a physical address of the first byte of memory and a size.

Allocation requests

The owner of the heap will make a request for a specific type of memory. This can be anything from a normal allocation, to a more specific request such as physically contiguous and possible aligned memory request.

A driver should check each bucket within the heap to see if any bucket satisfies this type of request. If none are found, a driver may add a bucket to the linked list, first extending the size of the heap or making room within the heap, or a new heap can be allocated and initialized.

enum requestType
Symbolic nameValueDescription
rtVirtual(1<<0)This attribute, with the exception of the rtClear and rtAligned attributes, is assumed to be the only attribute given and can be any memory region, contiguous or not, physically allocated or linear. This will be the most common type of memory allocated. Most allocation will use this type, so this must be the most efficiently coded allocation. If this flag is not set, the rtPhysical flag must be set. This flag is set by default.
rtPhysical(1<<1)This attribute may be combined with most other attributes and instructs the Allocator that all memory for this request must be physically allocated. i.e.: the memory must be physically present and use a physical address, not a linear address for returned memory region. All memory in this region must also be physically contiguous. If this flag is not set, the rtVirtual flag is assumed to be set.
rtClear(1<<2)This attribute may be combined with any other attribute and simply instructs the Allocator to clear the memory region(s) to zero before returning to the caller. The act of clearing it may have been done previously, just so long as the memory is clear at point of request. i.e.: A kernel task can be created to clear memory in the background, when idle, marking regions as clear, allowing the Allocator to simply return.
rtLow1Meg1(1<<3)This attribute may be combined with most other attributes and instructs the Allocator that all memory for this request must be below the 0x00000000_00100000 (1 Meg) memory mark.
rtLow16Meg1(1<<4)This attribute may be combined with most other attributes and instructs the Allocator that all memory for this request must be below the 0x00000000_01000000 (16 Meg) memory mark.
rtLow4Gig1(1<<5)This attribute may be combined with most other attributes and instructs the Allocator that all memory for this request must be below the 0x00000001_00000000 (4 Gig) memory mark.
rtLowAny1(1<<6)This attribute may be combined with most other attributes and instructs the Allocator that all memory for this request can be anywhere in the physical or virtual address space. This flag is set by default.
rtAligned(1<<6)This attribute may be combined with most other attributes and instructs the Allocator that the requested memory must be aligned. The Virtual Heap Allocator must find an available Pebble, matching the request types, or create a new Bucket that allows for this alignment.
1Only one rtLow* bit can be set at any given time.

A suggested calling prototype might look like this:

/* Allocate size bytes, w/o Alignment, Request Flags, Caller name/debug info */
void *kmalloc(size_t size, uint32_t Alignment, uint32_t rtFlags, const char *debugName);

A caller might use similar calls as this:

/* Allocate 64k, no alignment, anywhere in the virtual address space, "a long time ago" */
void *fobar = kmalloc(65536, 0, rtVirtual, "What is the");
/* Allocate 1Meg, 4k page alignment, anywhere in the physical address space below 4Gig, "in a galaxy far," */
void *fobar = kmalloc(1 * 1024 * 1024, 4096, rtPhysical | rtAligned | rtLow4Gig, "core temperature");
/* Allocate 4k page, 64-byte alignment, anywhere in the physical address space and clear it, "far, far away...."*/
void *fobar = kmalloc(4096, 64, rtPhysical | rtAligned | rtLowAny | rtClear, "of a tauntaun?");
/* Allocate 32k, 32-byte alignment, anywhere in the virtual address space and clear it, "let the wookie win"*/
void *fobar = kmalloc(32768, 32, rtVirtual | rtAligned | rtClear, "-- Luke warm --");

if (fobar == NULL) {
  /* Do some kind of error */
  return ERROR_NO_MEMORY;
}

Requirements

The following is a list of notes and/or requirements.

End of the FYSOS Memory Allocation System specification