Overhaul memory allocation model

This commit makes several fundamental changes to memory handling:

- the frame allocator is now only an allocator for free frames, and does
  not track used frames.
- the frame allocator now stores its free list inside the free frames
  themselves, as a hybrid stack/span model.
  - This has the implication that all frames must currently fit within
    the offset area.
- kutil has a new allocator interface, which is the only allowed way for
  any code outside of src/kernel to allocate. Code under src/kernel
  _may_ use new/delete, but should prefer the allocator interface.
- the heap manager has become heap_allocator, which is merely an
  implementation of kutil::allocator which doles out sections of a given
  address range.
- the heap manager now only writes block headers when necessary,
  avoiding page faults until they're actually needed
- page_manager now has a page fault handler, which checks with the
  address_manager to see if the address is known, and provides a frame
  mapping if it is, allowing heap manager to work with its entire
  address size from the start. (Currently 32GiB.)
This commit is contained in:
Justin C. Miller
2019-04-16 01:13:09 -07:00
parent fd1adc0262
commit 6302e8b73a
33 changed files with 782 additions and 1010 deletions

View File

@@ -6,6 +6,7 @@
#include <stdint.h>
#include "kutil/address_manager.h"
#include "kutil/allocator.h"
#include "catch.hpp"
using namespace kutil;
@@ -14,9 +15,18 @@ static const size_t max_block = 1ull << 36;
static const size_t start = max_block;
static const size_t GB = 1ull << 30;
class malloc_allocator :
public kutil::allocator
{
public:
virtual void * allocate(size_t n) override { return malloc(n); }
virtual void free(void *p) override { free(p); }
};
TEST_CASE( "Buddy addresses tests", "[address buddy]" )
{
address_manager am;
malloc_allocator alloc;
address_manager am(alloc);
am.add_regions(start, max_block * 2);
// Blocks should be:

View File

@@ -1,45 +0,0 @@
#include "kutil/frame_allocator.h"
#include "catch.hpp"
using namespace kutil;
TEST_CASE( "Frame allocator tests", "[memory frame]" )
{
frame_block_list free;
frame_block_list used;
frame_block_list cache;
auto *f = new frame_block_list::item_type;
f->address = 0x1000;
f->count = 1;
f->flags = kutil::frame_block_flags::none;
free.sorted_insert(f);
auto *g = new frame_block_list::item_type;
g->address = 0x2000;
g->count = 1;
g->flags = kutil::frame_block_flags::none;
free.sorted_insert(g);
frame_allocator fa(std::move(cache));
fa.init(std::move(free), std::move(used));
fa.consolidate_blocks();
uintptr_t a = 0;
size_t c = fa.allocate(2, &a);
CHECK( a == 0x1000 );
CHECK( c == 2 );
fa.free(a, 2);
a = 0;
fa.consolidate_blocks();
c = fa.allocate(2, &a);
CHECK( a == 0x1000 );
CHECK( c == 2 );
delete f;
delete g;
}

View File

@@ -0,0 +1,170 @@
#include <chrono>
#include <random>
#include <vector>
#include <signal.h>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include <sys/mman.h>
#include "kutil/memory.h"
#include "kutil/heap_allocator.h"
#include "catch.hpp"
using namespace kutil;
const size_t hs = 0x10; // header size
const size_t max_block = 1 << 22;
int signalled = 0;
void *signalled_at = nullptr;
void *mem_base = nullptr;
std::vector<size_t> sizes = {
16000, 8000, 4000, 4000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 150,
150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 48, 48, 48, 13 };
void segfault_handler(int signum, siginfo_t *info, void *ctxp)
{
signalled += 1;
signalled_at = info->si_addr;
mprotect(signalled_at, max_block, PROT_READ|PROT_WRITE);
}
TEST_CASE( "Buddy blocks tests", "[memory buddy]" )
{
using clock = std::chrono::system_clock;
unsigned seed = clock::now().time_since_epoch().count();
std::default_random_engine rng(seed);
mem_base = aligned_alloc(max_block, max_block * 4);
// Catch segfaults so we can track memory access
struct sigaction sigact;
memset(&sigact, 0, sizeof(sigact));
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = SA_NODEFER|SA_SIGINFO;
sigact.sa_sigaction = segfault_handler;
sigaction(SIGSEGV, &sigact, nullptr);
// Protect our memory arena so we trigger out fault handler
REQUIRE( mprotect(mem_base, max_block*4, PROT_NONE) == 0 );
heap_allocator mm(
reinterpret_cast<uintptr_t>(mem_base),
max_block * 4);
// Initial creation should not have allocated
CHECK( signalled == 0 );
signalled = 0;
// Allocating should signal just at the first page.
void *p = mm.allocate(max_block - hs);
CHECK( p == offset_pointer(mem_base, hs) );
CHECK( signalled == 1 );
CHECK( signalled_at == mem_base );
signalled = 0;
// Freeing and allocating should not allocate
mm.free(p);
p = mm.allocate(max_block - hs);
CHECK( p == offset_pointer(mem_base, hs) );
CHECK( signalled == 0 );
signalled = 0;
mm.free(p);
CHECK( signalled == 0 );
signalled = 0;
// Blocks should be:
// 22: 0-4M
std::vector<void *> allocs(6);
for (int i = 0; i < 6; ++i)
allocs[i] = mm.allocate(150); // size 8
// Should not have grown
CHECK( signalled == 0 );
signalled = 0;
// Blocks should be:
// 22: [0-4M]
// 21: [0-2M], 2-4M
// 20: [0-1M], 1-2M
// 19: [0-512K], 512K-1M
// 18: [0-256K], 256-512K
// 17: [0-128K], 128-256K
// 16: [0-64K], 64-128K
// 15: [0-32K], 32K-64K
// 14: [0-16K], 16K-32K
// 13: [0-8K], 8K-16K
// 12: [0-4K], 4K-8K
// 11: [0-2K], 2K-4K
// 10: [0-1K, 1-2K]
// 9: [0, 512, 1024], 1536
// 8: [0, 256, 512, 768, 1024, 1280]
// We have free memory at 1526 and 2K, but we should get 4K
void *big = mm.allocate(4000); // size 12
CHECK( signalled == 0 );
signalled = 0;
REQUIRE( big == offset_pointer(mem_base, 4096 + hs) );
mm.free(big);
// free up 512
mm.free(allocs[3]);
mm.free(allocs[4]);
// Blocks should be:
// ...
// 9: [0, 512, 1024], 1536
// 8: [0, 256, 512], 768, 1024, [1280]
// A request for a 512-block should not cross the buddy divide
big = mm.allocate(500); // size 9
REQUIRE( big >= offset_pointer(mem_base, 1536 + hs) );
mm.free(big);
mm.free(allocs[0]);
mm.free(allocs[1]);
mm.free(allocs[2]);
mm.free(allocs[5]);
allocs.clear();
std::shuffle(sizes.begin(), sizes.end(), rng);
allocs.reserve(sizes.size());
for (size_t size : sizes)
allocs.push_back(mm.allocate(size));
std::shuffle(allocs.begin(), allocs.end(), rng);
for (void *p: allocs)
mm.free(p);
allocs.clear();
big = mm.allocate(max_block / 2 + 1);
// If everything was freed / joined correctly, that should not have allocated
CHECK( signalled == 0 );
signalled = 0;
// And we should have gotten back the start of memory
CHECK( big == offset_pointer(mem_base, hs) );
// Allocating again should signal at the next page.
void *p2 = mm.allocate(max_block - hs);
CHECK( p2 == offset_pointer(mem_base, max_block + hs) );
CHECK( signalled == 1 );
CHECK( signalled_at == offset_pointer(mem_base, max_block) );
signalled = 0;
mm.free(p2);
CHECK( signalled == 0 );
signalled = 0;
free(mem_base);
}

View File

@@ -1,191 +0,0 @@
#include <chrono>
#include <random>
#include <vector>
#include <stddef.h>
#include <stdlib.h>
#include <stdint.h>
#include "kutil/memory.h"
#include "kutil/heap_manager.h"
#include "catch.hpp"
using namespace kutil;
static std::vector<void *> memory;
static size_t total_alloc_size = 0;
static size_t total_alloc_calls = 0;
const size_t hs = 0x10; // header size
const size_t max_block = 1 << 16;
std::vector<size_t> sizes = {
16000, 8000, 4000, 4000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 150,
150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 150, 48, 48, 48, 13 };
void * grow_callback(size_t length)
{
total_alloc_calls += 1;
total_alloc_size += length;
void *p = aligned_alloc(max_block, length * 2);
memory.push_back(p);
return p;
}
void free_memory()
{
for (void *p : memory) ::free(p);
memory.clear();
total_alloc_size = 0;
total_alloc_calls = 0;
}
TEST_CASE( "Buddy blocks tests", "[memory buddy]" )
{
using clock = std::chrono::system_clock;
unsigned seed = clock::now().time_since_epoch().count();
std::default_random_engine rng(seed);
heap_manager mm(grow_callback);
// The ctor should have allocated an initial block
CHECK( total_alloc_size == max_block );
CHECK( total_alloc_calls == 1 );
// Blocks should be:
// 16: 0-64K
std::vector<void *> allocs(6);
for (int i = 0; i < 6; ++i)
allocs[i] = mm.allocate(150); // size 8
// Should not have grown
CHECK( total_alloc_size == max_block );
CHECK( total_alloc_calls == 1 );
CHECK( memory[0] != nullptr );
// Blocks should be:
// 16: [0-64K]
// 15: [0-32K], 32K-64K
// 14: [0-16K], 16K-32K
// 13: [0-8K], 8K-16K
// 12: [0-4K], 4K-8K
// 11: [0-2K], 2K-4K
// 10: [0-1K, 1-2K]
// 9: [0, 512, 1024], 1536
// 8: [0, 256, 512, 768, 1024, 1280]
// We have free memory at 1526 and 2K, but we should get 4K
void *big = mm.allocate(4000); // size 12
REQUIRE( big == offset_pointer(memory[0], 4096 + hs) );
mm.free(big);
// free up 512
mm.free(allocs[3]);
mm.free(allocs[4]);
// Blocks should be:
// ...
// 9: [0, 512, 1024], 1536
// 8: [0, 256, 512], 768, 1024, [1280]
// A request for a 512-block should not cross the buddy divide
big = mm.allocate(500); // size 9
REQUIRE( big >= offset_pointer(memory[0], 1536 + hs) );
mm.free(big);
mm.free(allocs[0]);
mm.free(allocs[1]);
mm.free(allocs[2]);
mm.free(allocs[5]);
allocs.clear();
std::shuffle(sizes.begin(), sizes.end(), rng);
allocs.reserve(sizes.size());
for (size_t size : sizes)
allocs.push_back(mm.allocate(size));
std::shuffle(allocs.begin(), allocs.end(), rng);
for (void *p: allocs)
mm.free(p);
allocs.clear();
big = mm.allocate(64000);
// If everything was freed / joined correctly, that should not have allocated
CHECK( total_alloc_size == max_block );
CHECK( total_alloc_calls == 1 );
// And we should have gotten back the start of memory
CHECK( big == offset_pointer(memory[0], hs) );
free_memory();
}
bool check_in_memory(void *p)
{
for (void *mem : memory)
if (p >= mem && p <= offset_pointer(mem, max_block))
return true;
return false;
}
TEST_CASE( "Non-contiguous blocks tests", "[memory buddy]" )
{
using clock = std::chrono::system_clock;
unsigned seed = clock::now().time_since_epoch().count();
std::default_random_engine rng(seed);
heap_manager mm(grow_callback);
std::vector<void *> allocs;
const int blocks = 3;
for (int i = 0; i < blocks; ++i) {
void *p = mm.allocate(64000);
REQUIRE( memory[i] != nullptr );
REQUIRE( p == offset_pointer(memory[i], hs) );
allocs.push_back(p);
}
CHECK( total_alloc_size == max_block * blocks );
CHECK( total_alloc_calls == blocks );
for (void *p : allocs)
mm.free(p);
allocs.clear();
allocs.reserve(sizes.size() * blocks);
for (int i = 0; i < blocks; ++i) {
std::shuffle(sizes.begin(), sizes.end(), rng);
for (size_t size : sizes)
allocs.push_back(mm.allocate(size));
}
for (void *p : allocs)
CHECK( check_in_memory(p) );
std::shuffle(allocs.begin(), allocs.end(), rng);
for (void *p: allocs)
mm.free(p);
allocs.clear();
CHECK( total_alloc_size == max_block * blocks );
CHECK( total_alloc_calls == blocks );
for (int i = 0; i < blocks; ++i)
allocs.push_back(mm.allocate(64000));
// If everything was freed / joined correctly, that should not have allocated
CHECK( total_alloc_size == max_block * blocks );
CHECK( total_alloc_calls == blocks );
for (void *p : allocs)
CHECK( check_in_memory(p) );
free_memory();
}

View File

@@ -152,6 +152,7 @@ TEST_CASE( "Sorted list tests", "[containers list]" )
std::uniform_int_distribution<int> gen(1, 1000);
linked_list<sortableT> slist;
CHECK( slist.length() == 0 );
std::vector<list_node<sortableT>> sortables(test_list_size);
for (auto &i : sortables) {

View File

@@ -1,46 +1,2 @@
#define CATCH_CONFIG_MAIN
#include "catch.hpp"
#include <malloc.h>
void * operator new (size_t n) { return ::malloc(n); }
void * operator new[] (size_t n) { return ::malloc(n); }
void operator delete (void *p) noexcept { return ::free(p); }
void operator delete[] (void *p) noexcept { return ::free(p); }
#include "kutil/heap_manager.h"
#include "kutil/memory.h"
struct default_heap_listener :
public Catch::TestEventListenerBase
{
using TestEventListenerBase::TestEventListenerBase;
virtual void testCaseStarting(Catch::TestCaseInfo const& info) override
{
heap = new kutil::heap_manager(heap_grow_callback);
kutil::setup::set_heap(heap);
}
virtual void testCaseEnded(Catch::TestCaseStats const& stats) override
{
kutil::setup::set_heap(nullptr);
delete heap;
for (void *p : memory) ::free(p);
memory.clear();
}
static std::vector<void *> memory;
static kutil::heap_manager *heap;
static void * heap_grow_callback(size_t length) {
void *p = aligned_alloc(length, length);
memory.push_back(p);
return p;
}
};
std::vector<void *> default_heap_listener::memory;
kutil::heap_manager *default_heap_listener::heap;
CATCH_REGISTER_LISTENER( default_heap_listener );