mm: implement a basic virtual memory manager
This commit is contained in:
@@ -0,0 +1,426 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
|
||||||
|
const Range = @import("../util/range.zig").Range;
|
||||||
|
|
||||||
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
|
/// Describes a single virtual memory range.
|
||||||
|
///
|
||||||
|
/// Used by `VirtualMemoryAllocator` to track allocated/used regions.
|
||||||
|
pub const VirtualMemoryRange = struct {
|
||||||
|
range: Range(u64),
|
||||||
|
|
||||||
|
prev: ?*VirtualMemoryRange = null,
|
||||||
|
next: ?*VirtualMemoryRange = null,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Virtual memory allocator implementation.
|
||||||
|
pub const VirtualMemoryAllocator = struct {
|
||||||
|
gpa: Allocator,
|
||||||
|
head: ?*VirtualMemoryRange = null,
|
||||||
|
outer_range: Range(u64),
|
||||||
|
|
||||||
|
/// One of errors returned by the allocation logic + underlying allocator error.
|
||||||
|
pub const Error = error{ already_exists, invalid_region, cannot_fit } || Allocator.Error;
|
||||||
|
|
||||||
|
/// An iterator over VM regions being freed.
|
||||||
|
pub const FreeIterator = struct {
|
||||||
|
range: Range(u64),
|
||||||
|
vma: *VirtualMemoryAllocator,
|
||||||
|
current: ?*VirtualMemoryRange,
|
||||||
|
|
||||||
|
fn next(self: *@This()) Error!?Range(u64) {
|
||||||
|
while (self.current) |n| {
|
||||||
|
if (n.range.intersect(&self.range)) |xs| {
|
||||||
|
if (xs.start == n.range.start) {
|
||||||
|
if (xs.end() == n.range.end()) {
|
||||||
|
// Whole range encompassed by requested range
|
||||||
|
// Unlink the node
|
||||||
|
if (n.next) |nn| {
|
||||||
|
nn.prev = n.prev;
|
||||||
|
}
|
||||||
|
if (n.prev) |np| {
|
||||||
|
np.next = n.next;
|
||||||
|
} else {
|
||||||
|
self.vma.head = n.next;
|
||||||
|
}
|
||||||
|
// Free it
|
||||||
|
self.current = n.next;
|
||||||
|
self.vma.gpa.destroy(n);
|
||||||
|
|
||||||
|
return xs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove space from the start
|
||||||
|
n.range.start += xs.len;
|
||||||
|
n.range.len -= xs.len;
|
||||||
|
// Does not touch the end, so can be sure this is the last node
|
||||||
|
self.current = null;
|
||||||
|
return xs;
|
||||||
|
} else if (xs.end() == n.range.end()) {
|
||||||
|
n.range.len -= xs.len;
|
||||||
|
// Continue, there might be a following node affected
|
||||||
|
self.current = n.next;
|
||||||
|
return xs;
|
||||||
|
} else {
|
||||||
|
// Insert a new node after the current one
|
||||||
|
const new_node = try self.vma.gpa.create(VirtualMemoryRange);
|
||||||
|
new_node.* = VirtualMemoryRange {
|
||||||
|
.range = .{ .start = xs.end(), .len = n.range.end() - xs.end() },
|
||||||
|
.prev = n,
|
||||||
|
.next = n.next,
|
||||||
|
};
|
||||||
|
n.range.len = xs.start - n.range.start;
|
||||||
|
if (n.next) |nn| {
|
||||||
|
nn.prev = new_node;
|
||||||
|
}
|
||||||
|
n.next = new_node;
|
||||||
|
// Requested region is fully encompassed by this one, so no intersections
|
||||||
|
// will follow
|
||||||
|
self.current = null;
|
||||||
|
return xs;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No intersect
|
||||||
|
self.current = n.next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Creates a new instance of a virtual memory allocator.
|
||||||
|
pub fn init(gpa: Allocator, outer_range: Range(u64)) @This() {
|
||||||
|
return .{
|
||||||
|
.outer_range = outer_range,
|
||||||
|
.gpa = gpa,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Allocates a free region of virtual memory of requested (`pfn_count`) size.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `cannot_fit` - if no free space found to fit the requested allocation.
|
||||||
|
/// * Underlying allocator error - if allocation of a new node fails.
|
||||||
|
pub fn allocate(self: *@This(), pfn_count: u64) Error!u64 {
|
||||||
|
// Try to fit before first entry
|
||||||
|
const gap_before_first = if (self.head) |n| (n.range.start - self.outer_range.start) else self.outer_range.len;
|
||||||
|
|
||||||
|
if (gap_before_first >= pfn_count) {
|
||||||
|
var new_node = try self.gpa.create(VirtualMemoryRange);
|
||||||
|
|
||||||
|
new_node.range = .{ .start = self.outer_range.start, .len = pfn_count };
|
||||||
|
new_node.next = self.head;
|
||||||
|
new_node.prev = null;
|
||||||
|
|
||||||
|
if (self.head) |n| {
|
||||||
|
n.prev = new_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.head = new_node;
|
||||||
|
|
||||||
|
return self.outer_range.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cannot fit before first entry, find an entry to fit after
|
||||||
|
var node = self.head;
|
||||||
|
while (node) |n| {
|
||||||
|
const gap =
|
||||||
|
if (n.next) |nn|
|
||||||
|
// Gap between this and next
|
||||||
|
(nn.range.start - n.range.end())
|
||||||
|
else
|
||||||
|
// Gap between this and the end
|
||||||
|
(self.outer_range.end() - n.range.end());
|
||||||
|
|
||||||
|
if (gap >= pfn_count) {
|
||||||
|
// Insert after this
|
||||||
|
const result = n.range.end();
|
||||||
|
var new_node = try self.gpa.create(VirtualMemoryRange);
|
||||||
|
new_node.prev = n;
|
||||||
|
new_node.next = n.next;
|
||||||
|
new_node.range = .{ .start = result, .len = pfn_count };
|
||||||
|
if (n.next) |nn| {
|
||||||
|
nn.prev = new_node;
|
||||||
|
}
|
||||||
|
n.next = new_node;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
return error.cannot_fit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts a reservation into the VM allocator.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// * `already_exists` - if the requested range intersects existing ranges.
|
||||||
|
/// * Underlying allocator error - if allocation of a new node fails.
|
||||||
|
pub fn insert(self: *@This(), region: Range(u64)) Error!void {
|
||||||
|
// Validate that the range does not escape the outer range
|
||||||
|
if (region.start < self.outer_range.start or region.end() > self.outer_range.end()) {
|
||||||
|
return error.invalid_region;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the last node which is before the region supposed to be inserted
|
||||||
|
var node = self.head;
|
||||||
|
var insert_after: ?*VirtualMemoryRange = null;
|
||||||
|
while (node) |n| {
|
||||||
|
if (n.range.intersect(®ion) != null) {
|
||||||
|
return error.already_exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (n.range.end() <= region.start) {
|
||||||
|
insert_after = n;
|
||||||
|
}
|
||||||
|
|
||||||
|
node = n.next;
|
||||||
|
}
|
||||||
|
|
||||||
|
var new_node = try self.gpa.create(VirtualMemoryRange);
|
||||||
|
|
||||||
|
new_node.range = region;
|
||||||
|
|
||||||
|
if (insert_after) |ia| {
|
||||||
|
new_node.prev = ia;
|
||||||
|
new_node.next = ia.next;
|
||||||
|
|
||||||
|
if (ia.next) |ian| {
|
||||||
|
ian.prev = new_node;
|
||||||
|
}
|
||||||
|
ia.next = new_node;
|
||||||
|
} else {
|
||||||
|
new_node.next = null;
|
||||||
|
new_node.prev = null;
|
||||||
|
|
||||||
|
self.head = new_node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deallocates (shrinks/truncates) regions intersecting the requested range.
|
||||||
|
pub fn free(self: *@This(), start_pfn: u64, pfn_count: u64) FreeIterator {
|
||||||
|
const range = Range(u64) { .start = start_pfn, .len = pfn_count };
|
||||||
|
return FreeIterator {
|
||||||
|
.current = self.head,
|
||||||
|
.vma = self,
|
||||||
|
.range = range,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test "Inserted entries in vmalloc are properly ordered" {
|
||||||
|
var vma = VirtualMemoryAllocator.init(std.testing.allocator, .{ .start = 0x1000, .len = 0x2000 });
|
||||||
|
defer {
|
||||||
|
while (vma.head) |n| {
|
||||||
|
vma.head = n.next;
|
||||||
|
std.testing.allocator.destroy(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try vma.insert(.{ .start = 0x1200, .len = 0x200 });
|
||||||
|
{
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1200, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n0.range.len);
|
||||||
|
try std.testing.expectEqual(null, n0.next);
|
||||||
|
try std.testing.expectEqual(null, n0.prev);
|
||||||
|
}
|
||||||
|
try vma.insert(.{ .start = 0x2000, .len = 0x200 });
|
||||||
|
{
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1200, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n0.range.len);
|
||||||
|
try std.testing.expectEqual(null, n0.prev);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x2000, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
try std.testing.expectEqual(null, n1.next);
|
||||||
|
}
|
||||||
|
try vma.insert(.{ .start = 0x1400, .len = 0x200 });
|
||||||
|
{
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1200, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n0.range.len);
|
||||||
|
try std.testing.expectEqual(null, n0.prev);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x1400, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
const n2 = n1.next.?;
|
||||||
|
try std.testing.expectEqual(0x2000, n2.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n2.range.len);
|
||||||
|
try std.testing.expectEqual(n1, n2.prev);
|
||||||
|
try std.testing.expectEqual(null, n2.next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Overlapping insertions are denied" {
|
||||||
|
var vma = VirtualMemoryAllocator.init(std.testing.allocator, .{ .start = 0x1000, .len = 0x1000 });
|
||||||
|
defer {
|
||||||
|
while (vma.head) |n| {
|
||||||
|
vma.head = n.next;
|
||||||
|
std.testing.allocator.destroy(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try vma.insert(.{ .start = 0x1200, .len = 0x200 });
|
||||||
|
try std.testing.expectError(error.already_exists, vma.insert(.{ .start = 0x1100, .len = 0x200 }));
|
||||||
|
try std.testing.expectError(error.already_exists, vma.insert(.{ .start = 0x1300, .len = 0x200 }));
|
||||||
|
try std.testing.expectError(error.already_exists, vma.insert(.{ .start = 0x1100, .len = 0x400 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Insertions outside of bounds are denied" {
|
||||||
|
var vma = VirtualMemoryAllocator.init(std.testing.allocator, .{ .start = 0x1000, .len = 0x1000 });
|
||||||
|
// As above...
|
||||||
|
try std.testing.expectError(error.invalid_region, vma.insert(.{ .start = 0x2200, .len = 0x200 }));
|
||||||
|
// ... so below
|
||||||
|
try std.testing.expectError(error.invalid_region, vma.insert(.{ .start = 0x200, .len = 0x200 }));
|
||||||
|
// Crosses from below
|
||||||
|
try std.testing.expectError(error.invalid_region, vma.insert(.{ .start = 0x200, .len = 0x1000 }));
|
||||||
|
// Crosses into above
|
||||||
|
try std.testing.expectError(error.invalid_region, vma.insert(.{ .start = 0x1200, .len = 0x1000 }));
|
||||||
|
// Encompasses whole
|
||||||
|
try std.testing.expectError(error.invalid_region, vma.insert(.{ .start = 0x200, .len = 0x2000 }));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "Allocations from vmalloc" {
|
||||||
|
var vma = VirtualMemoryAllocator.init(std.testing.allocator, .{ .start = 0x1000, .len = 0x1000 });
|
||||||
|
defer {
|
||||||
|
while (vma.head) |n| {
|
||||||
|
vma.head = n.next;
|
||||||
|
std.testing.allocator.destroy(n);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try vma.insert(.{ .start = 0x1200, .len = 0x200 });
|
||||||
|
try std.testing.expectEqual(0x1000, try vma.allocate(0x100));
|
||||||
|
try std.testing.expectEqual(0x1400, try vma.allocate(0x400));
|
||||||
|
try std.testing.expectEqual(0x1100, try vma.allocate(0x100));
|
||||||
|
}
|
||||||
|
|
||||||
|
test "vmalloc free" {
|
||||||
|
var vma = VirtualMemoryAllocator.init(std.testing.allocator, .{ .start = 0x1000, .len = 0x1000 });
|
||||||
|
|
||||||
|
try vma.insert(.{ .start = 0x1200, .len = 0x800 });
|
||||||
|
try vma.insert(.{ .start = 0x1A00, .len = 0x400 });
|
||||||
|
|
||||||
|
// Remove nothing
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1000, 0x200);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a chunk in the middle of a node
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1400, 0x400);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1400, r0.start);
|
||||||
|
try std.testing.expectEqual(0x400, r0.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1200, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n0.range.len);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x1800, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
const n2 = n1.next.?;
|
||||||
|
try std.testing.expectEqual(0x1A00, n2.range.start);
|
||||||
|
try std.testing.expectEqual(0x400, n2.range.len);
|
||||||
|
try std.testing.expectEqual(n1, n2.prev);
|
||||||
|
try std.testing.expectEqual(null, n2.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from the start
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1200, 0x100);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1200, r0.start);
|
||||||
|
try std.testing.expectEqual(0x100, r0.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1300, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x100, n0.range.len);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x1800, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
const n2 = n1.next.?;
|
||||||
|
try std.testing.expectEqual(0x1A00, n2.range.start);
|
||||||
|
try std.testing.expectEqual(0x400, n2.range.len);
|
||||||
|
try std.testing.expectEqual(n1, n2.prev);
|
||||||
|
try std.testing.expectEqual(null, n2.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove from the end
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1900, 0x100);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1900, r0.start);
|
||||||
|
try std.testing.expectEqual(0x100, r0.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1300, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x100, n0.range.len);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x1800, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x100, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
const n2 = n1.next.?;
|
||||||
|
try std.testing.expectEqual(0x1A00, n2.range.start);
|
||||||
|
try std.testing.expectEqual(0x400, n2.range.len);
|
||||||
|
try std.testing.expectEqual(n1, n2.prev);
|
||||||
|
try std.testing.expectEqual(null, n2.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove single full
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1000, 0x600);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1300, r0.start);
|
||||||
|
try std.testing.expectEqual(0x100, r0.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1800, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x100, n0.range.len);
|
||||||
|
const n1 = n0.next.?;
|
||||||
|
try std.testing.expectEqual(0x1A00, n1.range.start);
|
||||||
|
try std.testing.expectEqual(0x400, n1.range.len);
|
||||||
|
try std.testing.expectEqual(n0, n1.prev);
|
||||||
|
try std.testing.expectEqual(null, n1.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove one full + one partial
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0x1600, 0x600);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1800, r0.start);
|
||||||
|
try std.testing.expectEqual(0x100, r0.len);
|
||||||
|
const r1 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1A00, r1.start);
|
||||||
|
try std.testing.expectEqual(0x200, r1.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
const n0 = vma.head.?;
|
||||||
|
try std.testing.expectEqual(0x1C00, n0.range.start);
|
||||||
|
try std.testing.expectEqual(0x200, n0.range.len);
|
||||||
|
try std.testing.expectEqual(null, n0.next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove whatever remains
|
||||||
|
{
|
||||||
|
var free_it = vma.free(0, 0x20000);
|
||||||
|
const r0 = (try free_it.next()).?;
|
||||||
|
try std.testing.expectEqual(0x1C00, r0.start);
|
||||||
|
try std.testing.expectEqual(0x200, r0.len);
|
||||||
|
try std.testing.expectEqual(null, free_it.next());
|
||||||
|
|
||||||
|
try std.testing.expectEqual(null, vma.head);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user