Physical Memory Manager
Last update: 2002-04-06
Copyright © 2002 Luca Abeni
Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Free Documentation License, Version 1.2 or any later version published by the Free Software Foundation; with no Invariant Sections, with no Front-Cover Texts, and with no Back-Cover Texts. A copy of the license is included by reference.
The Physical Memory Manager (PMM from now) is the part of the FreeDOS-32 kernel that manages system physical memory in order to allocate and free memory areas dynamically.
Initialization
The PMM manages sets of memory areas, i.e. contiguous ranges of memory addresses, called memory pools. At the moment two distinct memory pools are defined: one for memory below 1 MiB and another for memory above 1 MiB. Currently only the second memory pool is used, while the memory below 1 MiB, currently managed by OSLib functions, will become the DOS memory pool. The second memory pool will be probably splitted in two distinct ones: one for DMA memory (below 16 MiB) and one for all other memory (above 16 MiB).
Available memory
The MultiBoot boot loader (either GRUB or X.exe) detects the amount of memory present on the machine, and passes this information to the kernel, thus the PMM doesn't have to do this task itself. The MultiBoot Information (MBI) structure contains two fields, representing the amount of free memory in the first MB and above the first MB. Moreover, it can provide a memory map, that should be used if the system has memory "holes", such as ROM of some physical device, or the VBE LFB. From this available memory, we must remove the memory used by the kernel itself and by MultiBoot modules.
Setting the PMM up
The memory manager is initialized with 0 available memory. If a memory map is not provided, we can add a free memory area with the size provided by the MBI, whereas if a memory map is provided, we can add all the free memory regions from the map. Next we remove the memory region used by the kernel (using two symbols telling us the first and the last memory location used by the kernel) and memory regions used by the MultiBoot modules loaded (the MBI provides the starting address and the size for each module).
Interface
The following functions are provided by the PMM to manipulate memory pools:
void pmm_init(struct mempool *mp);
Initializes the memory pool descriptor with zero available memory.
void pmm_free(struct mempool *mp, DWORD base_addr, DWORD size);
Deallocates a memory area from base_addr to base_addr+size, adding a memory area to the specified pool. This function is also used by the memory initialization code to add the system memory to the free memory pool.
DWORD pmm_alloc(struct mempool *mp, DWORD size);
Allocates a memory area of the specified size, returning the start address on success or 0 on failure.
int pmm_alloc_address(struct mempool *mp, DWORD base_addr, DWORD size);
Allocates a memory area from base_addr to base_addr+size, removing a memory area from the specified pool. Returns a number < 0 on failure, >= 0 on success. This function is also used by the memory initialization code to remove the memory regions used by the kernel and by the modules from the free memory pool.
Important note: the PMM functions should never be called directly by anyone. The following wrappers shall be used instead:
int mem_free(DWORD base, DWORD size); int mem_get_region(DWORD base, DWORD size); DWORD mem_get(DWORD amount);
These are the only functions that can interact with the PMM. This is done in order to hide the concept of memory pool inside the kernel. If the memory manager structure is changed, only these wrappers will need modifications. All the kernel components that use the PMM through these wrappers will not have to be modified in order to cope with this changes.
Internals
Each memory pool consists of a list of free memory areas. When a memory area is allocated it is removed from the list, while when a memory area is freed it is added to the list.
PMM data structures
A free memory area is described by the following 8-bytes structure:
struct memheader {
struct memheader *next;
DWORD size;
};
where next is a pointer to the header of the next memory area in the list
and size is the size of the memory area. The header overwrites the first 8 bytes
of the memory area. Hence, the area described by a struct memheader *h goes
from address h to address h + h->size.
A memory pool is described by the following structure:
struct mempool {
DWORD free;
struct memheader *first;
};
where free is amount of free memory in the pool (not really needed, but it
can be probably useful) and first is the pointer to the head of a list of memory
area headers.
How pmm_init works
pmm_init initializes the memory pool descriptor, setting
free to 0 and the first pointer to NULL.
How pmm_free works
pmm_free adds a memory area (specified by parameters)
to the memory pool. The memory area is described by its base address and its
size. In theory, this function should simply insert a new element in the memory
pool list:
- the area header overlaps the first 8 bytes of the area:
h = (struct memheader *) base; - fills the size field in the header:
h->size = size; - inserts the header
in the list:
h->next = mp->first; mp->first = h;
In practice, the code
is a little more complex, because the memory pool list is ordered by increasing
base address, and pmm_free() also checks if the added memory can
be added to some memory area that is already in the list. Also a sanity check
is performed to avoid freeing memory that is already free.
How pmm_alloc works
pmm_alloc searches the memory pool list for the first
area with a size that is bigger than the requested amount of memory. When such
a memory area is found, its size is decreased by the requested amount of memory,
and the required memory is allocated from it. Note that the memory is allocated
at the end of the area, in order to simplify the code (in the most common case
the list is not modified, and only the size field of the memory header has to
be changed).
How pmm_alloc_address works
pmm_alloc_address is similar to pmm_alloc,
but it allocates the memory at a specified address. For this reason pmm_alloc_address is more complex than pmm_alloc; in fact it has to browse the memory pool list
searching for an area that contains the requested memory: if ((b <= base) && ((b
+ p->size) >= (base + size))) {
Moreover, the memory cannot be allocated at the end of the area, hence the list has to be modified in a more complex way (in general, the allocated memory divides a memory area in two parts, hence a new memory area has to be created...).
Considerations
Looking at the previous description, you can note three things:
pmm_freeis different from the libcfree()function, becausepmm_freetakes the amount of memory to free as an input. This is not a problem for a memory allocator used in the kernel.- If a memory area smaller than 8 bytes (the size
of
struct memheader) has to be put in the memory pool list, we can have memory corruption. This hopefully is a very uncommon situation, hence I avoided the problem by simply leaving the area unallocated (look at theWARNINGmessages in the code...). In practice, we lose up to 8 bytes of memory each time that this situation occurs. - The
pmm_alloc_addressefficiency depends on how the memory pool list is ordered. This list is currently ordered by base address, in order to simplify the implementation ofpmm_free(with a different order, it would be more complex to collapse continous memory areas), but some day we will have to test if some other ordering strategy can improve the memory allocation performance. In order to do that, only thepmm_freefunction has to be modified. Currently, I am thinking about two possible strategies: smallest area first or biggest area first.