blob: 2e408e66eda484ea2a5afcf11675275baaa11914 [file] [edit]
// Copyright 2025 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#ifndef ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_SLAB_ALLOCATOR_H_
#define ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_SLAB_ALLOCATOR_H_
#include <lib/page/size.h>
#include <pow2.h>
#include <ktl/algorithm.h>
#include <ktl/iterator.h>
#include <ktl/span.h>
#include <vm/page.h>
#include <vm/physmap.h>
#include <vm/pmm.h>
// Simple slab allocator that uses a vm_page_t as its slab to perform allocations out of. This makes
// the allocator only suitable for small allocations, preferably ones that divide evenly into a
// page.
//
// All per-slab metadata is stored in the vm_page_t itself and, by default, the only dependency of
// the allocator is the PMM to allocate and free pages from. The heap is not needed for any other
// metadata allocations.
//
// Free regions are tracked in a two level list with each slab having an internal list of free
// regions, and the allocator itself having a list of slabs that have at least one free region.
//
// Slabs that become fully empty are able to be returned to the PMM, although allocations cannot be
// moved between slabs, so fragmentation can still occur.
//
// The allocator can be specialized to control the actual allocation and freeing of the slabs
// themselves.
//
// This class it not thread safe.
template <size_t AllocSize>
class PageSlabAllocator {
public:
PageSlabAllocator() = default;
~PageSlabAllocator() {
ASSERT(full_slabs_.is_empty());
ASSERT(available_slabs_.is_empty());
}
// Allocates an area of uninitialized memory of AllocSize and returns a pointer to it, or nullptr
// on error.
void* allocate_bytes() { return reinterpret_cast<void*>(Allocate()); }
// Allocates an area of uninitialized memory capable of holding a single object of type |T|. This
// is largely a convenience wrapper around |allocate_bytes| that validates T is compatible with
// the size and alignment of the allocations.
// Returns a nullptr on error.
template <typename T>
T* allocate_object() {
// T must fit inside the size of the allocation.
static_assert(sizeof(T) <= AllocSize);
// Allocations must have at least equivalent alignment.
static_assert(alignof(T) <= kEntryAlign);
return reinterpret_cast<T*>(allocate_bytes());
}
// Deallocates the storage referenced by |ptr|, which must be a pointer obtained by an earlier
// call to |allocate_object| or |allocate_bytes|.
void deallocate_bytes(void* ptr) { Free(ptr); }
// Typed convenience wrapper around |deallocate_bytes|. Assumes the object at |ptr| has already
// been destructed.
template <typename T>
void deallocate_object(T* ptr) {
Free(ptr);
}
size_t AllocatedSlabs() const { return allocated_slabs_; }
// Helper to return the number of slabs this allocator would allocate to store the specified
// number of allocations.
static constexpr uint32_t SlabsRequired(uint32_t num_allocs) {
return fbl::round_up(num_allocs, kAllocsPerSlab) / kAllocsPerSlab;
}
protected:
union Entry {
Entry() : next(0) {}
~Entry() {}
uint32_t next;
char storage[AllocSize];
};
static_assert(sizeof(Entry) == AllocSize);
static constexpr uint32_t kEntryAlign = 1u << ktl::countr_zero(sizeof(Entry));
// Allocations needs to fit in a page.
static_assert(AllocSize < kPageSize);
static constexpr uint32_t kAllocsPerSlab = kPageSize / AllocSize;
static constexpr uint32_t kEndOfList = UINT32_MAX;
ktl::pair<vm_page_t*, uint32_t> AllocToSlab(const void* ptr) const {
const uint32_t offset = reinterpret_cast<uintptr_t>(ptr) % kPageSize;
vm_page_t* page = PaddrToPage(physmap_to_paddr(ptr));
ASSERT(page && page->state() == vm_page_state::SLAB);
DEBUG_ASSERT(page->queue_node.InContainer());
DEBUG_ASSERT(offset % AllocSize == 0);
return {page, offset / AllocSize};
}
static Entry* GetEntry(vm_page_t* slab, uint32_t index) {
ASSERT(slab->state() == vm_page_state::SLAB);
DEBUG_ASSERT(index < kAllocsPerSlab);
return reinterpret_cast<Entry*>(paddr_to_physmap(slab->paddr())) + index;
}
void DebugFreeAllSlabs() {
for (auto& p : full_slabs_) {
profile_track_free(p.slab.profile_cookie, kPageSize);
}
for (auto& p : available_slabs_) {
profile_track_free(p.slab.profile_cookie, kPageSize);
}
Pmm::Node().FreeList(&full_slabs_);
Pmm::Node().FreeList(&available_slabs_);
}
virtual vm_page_t* AllocSlab() {
vm_page_t* page = Pmm::Node().AllocPage(0).value_or(nullptr);
if (page) {
page->set_state(vm_page_state::SLAB);
// There is enough space to store a cookie per allocation in the slab, so amortize it and
// record a per slab cookie. On average this should have every different call site using this
// allocator to get proportional blame.
page->slab.profile_cookie = profile_track_alloc(kPageSize);
}
return page;
}
virtual void FreeSlab(vm_page_t* slab) {
DEBUG_ASSERT(slab->state() == vm_page_state::SLAB);
profile_track_free(slab->slab.profile_cookie, kPageSize);
Pmm::Node().FreePage(slab);
}
virtual vm_page_t* PaddrToPage(paddr_t paddr) const { return Pmm::Node().PaddrToPage(paddr); }
private:
bool AddSlab() {
// Allocate a new slab.
vm_page_t* slab = AllocSlab();
if (!slab) {
return false;
}
slab->slab.free_slot = kEndOfList;
slab->slab.peak_allocated = 0;
slab->slab.allocated = 0;
// Insert it into the available slabs.
available_slabs_.push_front(slab);
allocated_slabs_++;
return true;
}
Entry* Allocate() {
// See if there are any slabs available.
if (available_slabs_.is_empty()) {
if (!AddSlab()) {
return nullptr;
}
}
vm_page_t* page = &available_slabs_.front();
Entry* entry;
if (page->slab.free_slot == kEndOfList) {
DEBUG_ASSERT(page->slab.peak_allocated < kAllocsPerSlab);
entry = GetEntry(page, page->slab.peak_allocated);
page->slab.peak_allocated++;
} else {
entry = GetEntry(page, page->slab.free_slot);
page->slab.free_slot = entry->next;
}
page->slab.allocated++;
if (page->slab.free_slot == kEndOfList && page->slab.peak_allocated == kAllocsPerSlab) {
DEBUG_ASSERT(page->slab.allocated == kAllocsPerSlab);
available_slabs_.erase(*page);
full_slabs_.push_front(page);
} else {
DEBUG_ASSERT(page->slab.allocated < kAllocsPerSlab);
}
return entry;
}
void Free(void* ptr) {
// Lookup the slab this was allocated in.
auto [slab, index] = AllocToSlab(ptr);
// This will only catch the most egregious kinds of double-frees, but is better than nothing.
DEBUG_ASSERT(slab->slab.free_slot != index);
DEBUG_ASSERT(slab->slab.allocated > 0);
if (slab->slab.allocated == 1) {
// Slab has become empty, can free it.
available_slabs_.erase(*slab);
allocated_slabs_--;
FreeSlab(slab);
return;
}
if (slab->slab.allocated == kAllocsPerSlab) {
// Slab is going from full to having space available, move to the correct list. We place at
// the back of the list to encourage allocations, which happen on the head, to fill up a page
// instead of constantly bouncing allocations into different, partially full, pages.
full_slabs_.erase(*slab);
available_slabs_.push_back(slab);
}
slab->slab.allocated--;
// Update the free list for this slab.
Entry* entry = GetEntry(slab, index);
entry->next = slab->slab.free_slot;
slab->slab.free_slot = index;
}
VmPageDoublyLinkedList full_slabs_;
VmPageDoublyLinkedList available_slabs_;
// Track the total number of allocated slabs (i.e. pages), both full and available.
size_t allocated_slabs_ = 0;
};
// Common base for a specialized PageSlabAllocator that can assign a numerical ID to each
// allocation, in lieu of the pointer, and convert between. This base implements the conversion
// functions under the assumption that the derived type is assigning each allocated slab a unique
// ID, such that id*kAllocsPerSlab fits within a uint32_t.
//
// The size of the ID is fixed at 32-bits, and not variable, since a 16-bit limit is unlikely to be
// useful in practice, and a 64-bit limit has no value since the pointer is already a 64-bit ID.
template <size_t AllocSize>
class BaseIdSlabAllocator : public PageSlabAllocator<AllocSize> {
public:
static constexpr uint32_t kAllocsPerSlab = PageSlabAllocator<AllocSize>::kAllocsPerSlab;
BaseIdSlabAllocator() = default;
~BaseIdSlabAllocator() = default;
uint32_t AllocToId(const void* ptr) const {
auto [slab, slab_index] = PageSlabAllocator<AllocSize>::AllocToSlab(ptr);
uint32_t id = (slab->slab.id * kAllocsPerSlab) + slab_index;
return id;
}
void* IdToAlloc(uint32_t id) const {
uint32_t slab_id = id / kAllocsPerSlab;
uint32_t slab_index = id % kAllocsPerSlab;
vm_page_t* slab = IdToSlab(slab_id);
const uintptr_t base =
reinterpret_cast<uintptr_t>(PageSlabAllocator<AllocSize>::GetEntry(slab, 0));
const uintptr_t alloc = base + slab_index * AllocSize;
return reinterpret_cast<void*>(alloc);
}
private:
virtual vm_page_t* IdToSlab(uint32_t slab_id) const = 0;
};
// An implementation of the BaseIdSlabAllocator that supports a fixed amount of slabs, as determined
// by MaxAllocs, and statically pre-allocates all the metadata for those slabs. The metadata is 8
// bytes per slab.
//
// This allocator guarantees that all IDs are within [0, MaxAllocs). To achieve this it is a
// restriction that the maximum number of requested allocations be a multiple of the number of
// allocations per slab.
//
// Due to the static overhead per potential slab required this object can become quite large as
// MaxAllocs grows. Users should check the resulting size of the object, based on the provided
// allocation size and MaxAllocs, to ensure it is within acceptable limits.
//
// Most likely this allocator is not what you are looking for, and you want the IdSlabAllocator.
template <size_t AllocSize, size_t MaxAllocs>
class FixedIdSlabAllocator final : public BaseIdSlabAllocator<AllocSize> {
public:
static_assert(MaxAllocs % BaseIdSlabAllocator<AllocSize>::kAllocsPerSlab == 0);
FixedIdSlabAllocator() = default;
~FixedIdSlabAllocator() {
ASSERT(ktl::all_of(slabs_.begin(), slabs_.end(), [](vm_page_t* p) { return p == nullptr; }));
}
private:
vm_page_t* AllocSlab() override {
// Find an unused slot/id.
auto it = ktl::find(slabs_.begin(), slabs_.end(), nullptr);
if (it == slabs_.end()) {
return nullptr;
}
vm_page_t* slab = BaseIdSlabAllocator<AllocSize>::AllocSlab();
if (!slab) {
return nullptr;
}
const uint32_t id = static_cast<uint32_t>(ktl::distance(slabs_.begin(), it));
slab->slab.id = id;
*it = slab;
return slab;
}
void FreeSlab(vm_page_t* slab) override {
DEBUG_ASSERT(slab->state() == vm_page_state::SLAB);
DEBUG_ASSERT(slabs_.at(slab->slab.id) == slab);
slabs_.at(slab->slab.id) = nullptr;
BaseIdSlabAllocator<AllocSize>::FreeSlab(slab);
}
vm_page_t* IdToSlab(uint32_t slab_id) const override {
vm_page_t* slab = slabs_.at(slab_id);
DEBUG_ASSERT(slab);
return slab;
}
ktl::array<vm_page_t*, BaseIdSlabAllocator<AllocSize>::SlabsRequired(MaxAllocs)> slabs_ = {
nullptr};
};
// Slab allocator that can convert each allocation to/from a numerical ID, with the added guarantee
// that all IDs fall within [0, MaxAllocs). This implies that only up to MaxAllocs can be
// allocated at one time. To achieve this it is a restriction that the maximum number of requested
// allocations be a common multiple of the number of allocations per slab and per ID slab.
//
// The properties of this allocator are otherwise the same as the PageSlabAllocator, with the only
// dependency being direct PMM allocations, and slabs able to be cleaned up if they become fully
// free.
//
// This slab allocator uses the FixedIdSlabAllocator internally to allocate slab IDs. This works as
// follows:
// 1. Whenever this slab allocator allocates a slab, the `vm_page_t*` pointing to that slab is
// stored in the FixedIdSlabAllocator.
// 2. By construction, each slab in the FixedIdSlabAllocator has an ID that we can convert into a
// slab. Thus, using the AllocToId function allows us to convert the `vm_page_t**` into a stable
// ID that we can assign as the slab's ID in this allocator.
// We do this to minimize the amount of metadata needed to store the IDs. Notice that using a
// FixedIdSlabAllocator directly would increase the allocation size by 8 bytes for every slab we
// allocate. By introducing this level of indirection, we are able to add a slab to the
// FixedIdSlabAllocator only when we have (8 / kPageSize) slabs allocated in this allocator. We can
// fit (sizeof(T) / kPageSize) allocations in each slab in this allocator, so this means that the
// size of the underlying FixedIdSlabAllocator will grow at a rate of
// (8 * sizeof(T)) / (kPageSize * kPageSize) bytes per allocation added.
//
// The rate of growth of the static metadata can be further reduced by adding additional levels of
// indirection for the slab id allocation by overriding the SLAB template argument.
template <size_t AllocSize, size_t MaxAllocs,
typename SLAB = FixedIdSlabAllocator<
sizeof(vm_page_t*), BaseIdSlabAllocator<AllocSize>::SlabsRequired(MaxAllocs)>>
class IdSlabAllocator final : public BaseIdSlabAllocator<AllocSize> {
public:
static_assert(MaxAllocs % BaseIdSlabAllocator<AllocSize>::kAllocsPerSlab == 0);
IdSlabAllocator() = default;
~IdSlabAllocator() = default;
// Total memory usage, in bytes, including any unused portions of slabs, not including the size of
// |this|.
size_t MemoryUsage() const {
return (slab_id_allocator_.AllocatedSlabs() +
BaseIdSlabAllocator<AllocSize>::AllocatedSlabs()) *
kPageSize;
}
private:
vm_page_t* AllocSlab() override {
// Allocate a new slot to store the slab from the id allocator.
vm_page_t** slab_ref = slab_id_allocator_.template allocate_object<vm_page_t*>();
if (!slab_ref) {
return nullptr;
}
vm_page_t* slab = BaseIdSlabAllocator<AllocSize>::AllocSlab();
if (!slab) {
slab_id_allocator_.deallocate_object(slab_ref);
return nullptr;
}
*slab_ref = slab;
// The object id of our slot is our slab id, allowing us to go from id->vm_page_t* later.
const uint32_t slab_id = slab_id_allocator_.AllocToId(slab_ref);
slab->slab.id = slab_id;
return slab;
}
void FreeSlab(vm_page_t* slab) override {
// Return the slot used to store slab to the slab id allocator.
vm_page_t** slab_ref =
reinterpret_cast<vm_page_t**>(slab_id_allocator_.IdToAlloc(slab->slab.id));
DEBUG_ASSERT(*slab_ref == slab);
*slab_ref = nullptr;
slab_id_allocator_.deallocate_object(slab_ref);
BaseIdSlabAllocator<AllocSize>::FreeSlab(slab);
}
vm_page_t* IdToSlab(uint32_t slab_id) const override {
return *reinterpret_cast<vm_page_t**>(slab_id_allocator_.IdToAlloc(slab_id));
}
// The slab id allocator both allocates our unique slab ids, and provides the storage / mapping
// from slab_id->vm_page_t*. Importantly it guarantees that its IDs are from [0..kMaxSlabs), which
// we need to then guarantee that our final IDs are from [0..MaxAllocs).
SLAB slab_id_allocator_;
};
// An example of overriding the SLAB argument to provide an additional level of indirection for the
// ID allocation, limiting the growth of metadata by another multiple of the kPageSize.
template <size_t AllocSize, size_t MaxAllocs>
using TwoLevelIdSlabAllocator = IdSlabAllocator<
AllocSize, MaxAllocs,
IdSlabAllocator<sizeof(vm_page_t*), BaseIdSlabAllocator<AllocSize>::SlabsRequired(MaxAllocs)>>;
#endif // ZIRCON_KERNEL_VM_INCLUDE_VM_PAGE_SLAB_ALLOCATOR_H_