blob: a634da2f234b703e6fdaad4f9fabc8331a8c1541 [file] [log] [blame]
// Copyright 2023 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_TRI_PAGE_STORAGE_H_
#define ZIRCON_KERNEL_VM_INCLUDE_VM_TRI_PAGE_STORAGE_H_
#include <fbl/canary.h>
#include <vm/compression.h>
// Tri-page storage is an allocator optimized around the expectations of compressed pages. At the
// high level it works by attempting to pack up to 3 compressed pages into a single physical page.
//
// The storage works by taking a physical page, and giving it three notional slots; a left, a right
// and a middle. At most one piece of data can be stored at each slot, and data stored in one slot
// limits, or completely prevents, data stored in another slot. For example, if something of
// PAGE_SIZE/2 or larger is stored in either the left or right slot, then nothing can now be stored
// in the middle slot as it would necessarily overlap.
//
// Contents of each slot is recorded in the vm_page_t::zram.*_compress_size. For any non-zero size
// The contents of the page then are
// * [0, left_compress_size)
// * [PAGE_SIZE/2 - (mid_compress_size+1)/2, PAGE_SIZE/2 + (mid_compress_size+1)/2)
// * [PAGE_SIZE - right_compress_size, PAGE_SIZE)
// As per previous paragraph, these regions may not overlap, and so only one or two slots being used
// could cause the page to be full. Note that for the middle slot the range considered used in the
// page might be 1 byte larger as it is rounded up to the next even size for implementation
// simplicity.
//
// Pages are either empty, full, or have partial space. A full page is one where either all three
// slots are in use, or no slot could be used without overlapping with existing data. Empty pages
// have no slots in use. Partial pages are stored by taking the log2 of the largest item they could
// store in any of the remaining slots and using that as the index into bucket lists.
class VmTriPageStorage final : public VmCompressedStorage {
public:
VmTriPageStorage();
~VmTriPageStorage() override;
void Free(CompressedRef ref) override;
std::pair<ktl::optional<CompressedRef>, vm_page_t*> Store(vm_page_t* page, size_t len) override;
std::pair<const void*, size_t> CompressedData(CompressedRef ref) const override;
void Dump() const override;
MemoryUsage GetMemoryUsage() const override;
private:
// Attempts to find an existing partially used page that can satisfy an allocation of the
// specified length. Returns nullptr if none is found. Len must be in range (0,PAGE_SIZE].
vm_page_t* AllocateBucketLocked(size_t len) TA_REQ(lock_);
// Adds a page to the partially used page buckets. The page must be neither empty nor full. The
// bucket it is added to is determined based on its largest free slot.
void InsertBucketLocked(vm_page_t* page) TA_REQ(lock_);
// Removes a specific page from the buckets. It knows which bucket to remove from by the largest
// free slot, and so the slot information should not have been modified between this and
// InsertBucketLocked.
void RemovePageFromBucketLocked(vm_page_t* page) TA_REQ(lock_);
// Represents which of the three potential slots in a page that an allocation resides. Although
// can have multiple slots in use, a given allocation exists at a unique slot.
enum class PageSlot : uint64_t {
// None is explicitly defined as a 0 encoding to help detect usage errors of CompressedRefs.
None = 0,
Left = 1,
Mid = 2,
Right = 3,
// Number of slots. Used by static assertions to ensure we can bit pack everything.
NumSlots,
};
// Returns the bucket a page would be in based on its free space. Requires the page be neither
// empty nor full.
static uint64_t bucket_for_page(vm_page_t* page);
// Splits a reference into the specific page and slot. The returned slot will never be None.
static std::pair<vm_page_t*, PageSlot> DecodeRef(CompressedRef ref);
// Encodes a specific page and slot into a reference. Slot must not be None.
static CompressedRef EncodeRef(vm_page_t* page, PageSlot slot);
// Returns whether the page should be considered full and cannot store anything more.
static bool page_is_full(vm_page_t* page);
// Given a page and size, returns the 'best' slot to be used for storage. Assumes that the page is
// not empty and that there is at least one slot available of the requested size.
static PageSlot select_slot(vm_page_t* page, size_t len);
// Returns a kernel usable address for a slot of the given size in the specified page. This does
// not assume that the given slot is allocated in the page, just returns the address as if it
// were.
static void* addr_for_slot(vm_page_t* page, PageSlot slot, size_t len);
// Calculate the offset for a slot from the start of a page.
static size_t offset_for_slot(PageSlot slot, size_t len);
// Updates the book keeping the given page to set the specific slot as having an allocation of
// len.
static void set_slot_length(vm_page_t* page, PageSlot slot, uint16_t len);
// Retrieves the length of a particular slot in a page.
static size_t get_slot_length(vm_page_t* page, PageSlot slot);
// The CompressedRef's we generate need to encode a pointer to a vm_page_t, and indicate which of
// the |Slot| this reference is for. They have the following bit layout:
// 63 0
// P..PTTA..A
// This layout is variable based on the number of bits reserved by the CompressedRef, indicated
// by its kAlignBits. After that alignment bits there are 2 bits for storing the slot tag, and
// then all remaining bits store the pointer to the vm_page_t.
// To pack a 64-bit vm_page_t pointer into <64 bits, the assumption of kernel pointer having spare
// high bits always being set is leveraged.
static constexpr uint64_t kRefTagShift = CompressedRef::kAlignBits;
static constexpr uint64_t kRefTagBits = 2;
static constexpr uint64_t kRefPageShift = kRefTagShift + kRefTagBits;
static constexpr uint64_t kRefHighBits = BIT_MASK(kRefPageShift)
<< ((sizeof(uint64_t) * 8) - kRefPageShift);
// Check that all pointers in the kernel region, that is any value >KERNEL_ASPACE_BASE, will have
// the high bits always set.
static_assert((KERNEL_ASPACE_BASE & kRefHighBits) == kRefHighBits);
// Check that there are enough tag bits to encode all the possible PageSlots
static_assert(1ul << kRefTagBits >= static_cast<uint64_t>(PageSlot::NumSlots));
// For simplicity select our number of buckets such that we can use a single uint64_t bitmap to
// track availability.
static constexpr size_t kNumBuckets = sizeof(uint64_t) * 8;
// Buckets are of uniform size, with bucket N supporting an allocation of at most
// (N+1)*kBucketSize.
static constexpr uint64_t kBucketSize = PAGE_SIZE / kNumBuckets;
fbl::Canary<fbl::magic("3PS_")> canary_;
mutable DECLARE_MUTEX(VmTriPageStorage) lock_;
// Informational counter of how many items are stored, i.e. how many CompressedRef's we have
// vended.
uint64_t stored_items_ TA_GUARDED(lock_) = 0;
// The total compressed size of all the items being stored.
uint64_t total_compressed_item_size_ TA_GUARDED(lock_) = 0;
// Pages that we consider full and cannot have additional items stored in them.
list_node_t full_pages_ TA_GUARDED(lock_);
uint64_t full_pages_count_ TA_GUARDED(lock_) = 0;
// The buckets are used to track available ranges in partially used pages and are a simple
// linearly increasing size. The smallest amount of free space that is tracked before a page is
// considered full is kBucketSize and so bucket 0 tracks pages that can store at least
// kBucketSize.
// More generally any bucket K can store at least (K+1)*kBucketSize, and a page is placed in the
// largest bucket possible for its free space.
// The non_empty_buckets_ is a bitmap optimization around what buckets_ are non-empty to avoid
// linear searches.
uint64_t non_empty_buckets_ TA_GUARDED(lock_) = 0;
list_node_t buckets_[kNumBuckets] TA_GUARDED(lock_);
uint64_t bucket_pages_counts_[kNumBuckets] TA_GUARDED(lock_) = {};
};
#endif // ZIRCON_KERNEL_VM_INCLUDE_VM_TRI_PAGE_STORAGE_H_