Programming lesson
Building a Custom Memory Manager in C++: A Step-by-Step Guide for Project Repto
Learn how to implement a memory manager in C++ for the COP4600 Project Repto, covering initialization, allocation algorithms (best fit, worst fit), hole coalescing, and integration with a console program.
Introduction to Memory Management in Operating Systems
Memory management is a core function of modern operating systems, ensuring that processes can allocate and deallocate memory efficiently without interfering with each other. In this tutorial, we'll build a custom memory manager in C++ for the fictional Project Repto, inspired by real-world OS layering. This project is perfect for students in COP4600 who want to understand how memory allocation algorithms like best fit and worst fit work under the hood. By the end, you'll have a static library that can be used in any console application.
Understanding the Memory Manager Class
The MemoryManager class is the heart of our project. It manages a contiguous block of memory, tracks holes (free blocks), and provides allocation and deallocation services. The class uses a word size for alignment and a pluggable allocator function (best fit or worst fit). Let's break down the key methods.
Constructor and Destructor
The constructor sets the word size and default allocator. The destructor must release all memory without leaks. Use delete[] for the memory block and any dynamically allocated metadata.
MemoryManager::MemoryManager(unsigned wordSize, std::function<int(int, void*)> allocator)
: m_wordSize(wordSize), m_allocator(allocator), m_memory(nullptr), m_sizeInWords(0) {}
MemoryManager::~MemoryManager() {
shutdown();
}Initialization and Shutdown
initialize(size_t sizeInWords) allocates a block of memory up to 65535 words. It must clean up any previous block. shutdown() releases the block. Note: only free the memory block itself, not lists or bitmaps returned by other functions.
void MemoryManager::initialize(size_t sizeInWords) {
shutdown();
m_sizeInWords = sizeInWords;
m_memory = new char[m_sizeInWords * m_wordSize];
// Initialize hole list: one big hole from 0 to sizeInWords-1
m_holes.clear();
m_holes.push_back({0, sizeInWords});
}Allocation Algorithms: Best Fit and Worst Fit
Two algorithms are required: bestFit and worstFit. Both take a size in words and a list of holes (returned by getList()). The list format is an array of offsets and lengths: [offset1, length1, offset2, length2, ...].
Best Fit
Best fit selects the smallest hole that can accommodate the request. This minimizes wasted space but can create many tiny holes.
int bestFit(int sizeInWords, void *list) {
int *arr = (int*)list;
int numHoles = arr[0];
int bestOffset = -1;
int bestSize = INT_MAX;
for (int i = 0; i < numHoles; i++) {
int offset = arr[1 + i*2];
int length = arr[2 + i*2];
if (length >= sizeInWords && length < bestSize) {
bestSize = length;
bestOffset = offset;
}
}
return bestOffset;
}Worst Fit
Worst fit selects the largest hole, leaving a big leftover that might be useful for future large allocations.
int worstFit(int sizeInWords, void *list) {
int *arr = (int*)list;
int numHoles = arr[0];
int worstOffset = -1;
int worstSize = -1;
for (int i = 0; i < numHoles; i++) {
int offset = arr[1 + i*2];
int length = arr[2 + i*2];
if (length >= sizeInWords && length > worstSize) {
worstSize = length;
worstOffset = offset;
}
}
return worstOffset;
}Allocate and Free Methods
allocate(size_t sizeInBytes) converts bytes to words (rounding up), calls the allocator to find a hole, and returns a pointer to the allocated memory. free(void *address) returns the block to the hole list and coalesces adjacent holes.
void* MemoryManager::allocate(size_t sizeInBytes) {
if (sizeInBytes == 0) return nullptr;
unsigned wordsNeeded = (sizeInBytes + m_wordSize - 1) / m_wordSize;
// Get hole list
void *list = getList();
int offset = m_allocator(wordsNeeded, list);
free(list); // list was allocated by getList
if (offset == -1) return nullptr;
// Remove hole from list, split if needed
// ... (implementation details)
return (char*)m_memory + offset * m_wordSize;
}
void MemoryManager::free(void *address) {
if (!address) return;
unsigned offset = ((char*)address - (char*)m_memory) / m_wordSize;
// Add hole, then coalesce
m_holes.push_back({offset, allocatedSize[address]});
coalesceHoles();
}Hole Coalescing
After freeing, adjacent holes should be merged to prevent fragmentation. Sort the hole list by offset and merge consecutive holes.
void MemoryManager::coalesceHoles() {
std::sort(m_holes.begin(), m_holes.end());
std::vector<Hole> merged;
for (auto &h : m_holes) {
if (merged.empty() || merged.back().offset + merged.back().length < h.offset)
merged.push_back(h);
else
merged.back().length += h.length;
}
m_holes = merged;
}Dumping Memory Map and Bitmap
dumpMemoryMap(char *filename) writes the hole list to a file using POSIX calls. The format is [START, LENGTH] - [START, LENGTH] .... getBitmap() returns a bit array where each bit represents a word (1 = used, 0 = free). The first two bytes are the bitmap size in words (little-endian).
int MemoryManager::dumpMemoryMap(char *filename) {
int fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd == -1) return -1;
std::string map;
for (size_t i = 0; i < m_holes.size(); i++) {
if (i > 0) map += " - ";
map += "[" + std::to_string(m_holes[i].offset) + ", " + std::to_string(m_holes[i].length) + "]";
}
write(fd, map.c_str(), map.size());
close(fd);
return 0;
}Testing and Memory Leaks
Use Valgrind or AddressSanitizer to check for leaks. Ensure every new has a matching delete. The provided testing file only checks basic functionality; you must test edge cases like allocating zero bytes, freeing invalid pointers, and exhausting memory.
Building the Static Library
Write a Makefile that compiles MemoryManager.cpp and the allocator functions into libMemoryManager.a. Do not build executables.
CC = g++
CFLAGS = -Wall -std=c++11
libMemoryManager.a: MemoryManager.o bestFit.o worstFit.o
ar rcs $@ $^
MemoryManager.o: MemoryManager.cpp MemoryManager.h
$(CC) $(CFLAGS) -c $<
bestFit.o: bestFit.cpp
$(CC) $(CFLAGS) -c $<
worstFit.o: worstFit.cpp
$(CC) $(CFLAGS) -c $<
clean:
rm -f *.o *.aExtra Credit: Avoiding new and malloc
For extra credit, implement initialize without new or malloc. You can use mmap on Linux to allocate memory directly from the OS. This demonstrates a deeper understanding of OS memory management.
#include <sys/mman.h>
void MemoryManager::initialize(size_t sizeInWords) {
shutdown();
m_sizeInWords = sizeInWords;
size_t bytes = sizeInWords * m_wordSize;
m_memory = mmap(nullptr, bytes, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (m_memory == MAP_FAILED) throw std::bad_alloc();
// Initialize holes
}Conclusion
Building a memory manager from scratch is a fantastic way to learn about OS internals, dynamic memory allocation, and fragmentation. This project mirrors real-world challenges in systems programming. By implementing best fit and worst fit algorithms, you'll see how different strategies affect performance. As you work on Project Repto, remember to test thoroughly and avoid memory leaks. Happy coding!