Appearance
kalloc.c
TIP
物理内存页分配器 (Physical Memory Allocator) xv6 内核使用此分配器来动态申请和释放物理内存。 它的工作单位是“页”(Page),每页大小固定为 4096 字节。 这部分内存主要用于: 1. 为用户进程分配内存空间。 2. 为每个进程创建其内核栈。 3. 存储页表 (Page Tables)。 4. 作为管道 (Pipes) 的内核缓冲区。 分配器管理着从内核代码和数据段结束位置(end)到物理内存顶端(PHYSTOP)之间的所有内存。 实现原理是维护一个空闲物理页的单向链表(通常称为 freelist)。
#include "types.h"
#include "param.h"
#include "memlayout.h"
#include "spinlock.h"
#include "riscv.h"
#include "defs.h"
void freerange(void *pa_start, void *pa_end);
extern char end[];TIP
它并不指向任何实际的数据,而是作为一个地址标记, 标识了内核代码和静态数据段结束之后第一个字节的位置。 物理内存分配器将管理从 end 到 PHYSTOP 地址之间的所有内存。
TIP
struct run 代表一个空闲的物理页。 因为页是空闲的,我们可以安全地利用其 4096 字节的空间来存储数据。 在这里,我们用它来存储指向下一个空闲页的指针, 从而将所有空闲页链接成一个单向链表。
struct run {
struct run *next;
};
TIP
内核内存分配器的核心数据结构。
struct {
struct spinlock lock;TIP
如果没有锁,多个 CPU 同时调用 kalloc() 或 kfree() 可能会导致链表损坏或数据不一致。
struct run *freelist;
} kmem;
TIP
初始化物理内存分配器。
void
kinit()
{
initlock(&kmem.lock, "kmem");TIP
调用 freerange 函数,将从内核末尾(end)到物理内存顶端(PHYSTOP) 之间的所有物理内存,逐页释放并添加到空闲链表中。
freerange(end, (void*)PHYSTOP);
}
TIP
将一段给定的物理地址范围 [pa_start, pa_end) 逐页添加到空闲链表中。
void
freerange(void *pa_start, void *pa_end)
{
char *p;TIP
首先,将起始地址向上取整到下一个页边界(PGSIZE的整数倍)。 这是为了确保我们操作的单位都是完整的页。
p = (char*)PGROUNDUP((uint64)pa_start);TIP
遍历这个地址范围内的每一页。
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
TIP
释放一个物理页。 pa 是要释放的页的起始物理地址,它通常是之前调用 kalloc() 的返回值。
void
kfree(void *pa)
{
struct run *r;
TIP
对传入的地址 pa 进行一系列健全性检查: 1. 地址必须是页对齐的 (地址是 PGSIZE 的整数倍)。 2. 地址不能位于内核代码和数据区域(即地址值必须大于 end)。 3. 地址必须在物理内存的合法范围内(即小于 PHYSTOP)。
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
TIP
将被释放的内存块用固定的值(1)填充。这是一种调试技巧,被称为“内存投毒”(Memory Poisoning)。 如果有代码在释放后仍然非法地使用这块内存(即“悬挂指针”问题), 它会读到无意义的数据(1)而不是旧的有效数据,这使得这类错误更容易被发现。
memset(pa, 1, PGSIZE);
TIP
将物理地址 pa 强制转换为 struct run* 类型,以便将其作为链表节点处理。
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
TIP
分配一页(4096字节)的物理内存。 成功时,返回一个指向该页起始地址的指针,该指针可被内核直接使用。 如果系统中所有内存页都已被分配,则返回 0 (空指针)。
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r) {TIP
如果成功分配到了页 (r 不为 null),也对其进行“内存投毒”。 将其内容填充为 5。这有助于捕获那些错误地假设新分配内存被自动清零的代码。 正确的实践是,内核代码在使用新分配的内存前,应该根据需要进行显式地初始化。
memset((char*)r, 5, PGSIZE);
}
return (void*)r;
}