0.briefly speaking
点击跳转到上一篇博客
好,现在进入下一个话题,就是物理内存分配器(kernel/kalloc.c)。在简单介绍完内核态的物理内存分配器之后,之后简单带过一下两个头文件riscv.h和memorylayout.h这两个头文件,因为它们都比较特殊,直接阅读可能会失去整体性。
1.kernel/memorylayout.h (79 rows) <-----------(简单概括)
2.kernel/vm.c (434 rows)
3.kernel/kalloc.c (82 rows) <-----------(这篇博客要阅读的代码)
4.kernel/exec.c (154 rows)
5.kernel/riscv.h (366 rows) <-----------(简单概括)
3.kernel/kalloc.c
3.1 end[]
和vm.c一样,这里的开头是一个全局变量的引用声明,end标明了自由物理内存的开始位置。
// 译:内核之后的第一个地址,由kernel.ld定义
extern char end[]; // first address after kernel.
// defined by kernel.ld.
既然注释说到了end标号在kernel.ld中定义,不妨去看看:)
/*......*/
/*以上部分省略*/
.bss : {
. = ALIGN(16);
*(.sbss .sbss.*) /* do not need to distinguish this from .bss */
. = ALIGN(16);
*(.bss .bss.*)
}
PROVIDE(end = .);
在kernel.ld:43处定义了end标号,它定义在所有内核代码和数据段之后,对照一下内核地址空间就可以更加方便地认识到这一点了:
3.2 struct run & kmem
这两个结构体向我们展示了在内核中空闲物理内存以怎样的方式组织起来,其实就是一个简单的链表,这个链表将一个个空闲的页面串在了一起。关于这种组织方式,xv6 book中有更加详细的描述,每个run结构体存储在对应空闲页面中,占用着一个指针的大小,指向它的后继者,而freelist指针则指向整个空闲链表开头,如下图所示,空心圆点表示空指针。
源码如下:
// run结构体就是一个指向自身的指针,用于指向下一个空闲页表开始位置
struct run {
struct run *next;
};
// 管理物理内存的结构
// 有一把锁lock保证访问时的互斥性
// 以及一个指向
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
3.3 kfree函数
首先来看kfree函数,这是因为freerange函数和kinit函数调用了它,其实kfree做的事情非常简单,就是使用头插法将页面回收到空闲链表中。
// Free the page of physical memory pointed at by pa,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
// 译:释放pa指向的页的物理内存,它通常是由调用kalloc返回的
// 特殊情况是初始化分配器时,见上面的kinit函数
void
kfree(void *pa)
{
struct run *r;
// 如果要释放的内存不是页对齐的,或者不在自由内存范围内,陷入panic
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
// 将要回收的这一页填满无用的数据
// 这一步主要是为了防止在本页内存释放之后仍有进程尝试访问之,无用数据会导致进程快速崩溃
// 这在xv6 book中有所解释
memset(pa, 1, PGSIZE);
// 将pa强制转型为run类型的指针,准备回收到链表中
r = (struct run*)pa;
// 头插法,将回收的页作为链表第一项插入到空闲链表中
// 注意使用锁机制来保持动作的安全性
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
3.4 freerange函数
这个函数用来以页为单位释放[pa_start, pa_end]这个范围内的物理内存。pa_start和pa_end函数不一定要是完全页对齐的,这个函数首先会使用PGROUNDUP宏将页面强制对齐,然后逐页释放到终点页面。
思考一个问题,为什么要用向上取整的函数?
——这是一种保守策略,本质还是担心将有用的页面释放,甚至于看for循环中的终止条件:p + PGSIZE <= (char*)pa_end,也是遵循同样的保守策略。因为调用者传入的地址可能是非对齐的,这个地址可能正好处于某个页面的中部,此地址以下的地址空间可能还有有用的数据,如果向下对齐可能会将有用的页面一起释放,从而导致错误。
——另外一个重要的原因是,页(page)是内存管理的最小单位,因此PTE只能指向一个对齐的页,所以释放和申请内存时内存也必须是页对齐的。
void
freerange(void *pa_start, void *pa_end)
{
char *p;
// 向上对齐,防止释放有用的页面
p = (char*)PGROUNDUP((uint64)pa_start);
// 逐页释放到终点页面,注意终止条件p + PGSIZE <= pa_end
// 这本质上也加入了保护措施,防止释放有用页
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
3.5 kinit
kinit函数是在内核启动时对物理内存分配器进行初始化的函数,只有经过kinit之后内存管理器才有内存可以分配。
void
kinit()
{
initlock(&kmem.lock, "kmem");
// 释放从end到PHYSTOP之间的所有物理内存
// 回收进空闲链表freelist中
freerange(end, (void*)PHYSTOP);
}
另外,一个操作系统应该是从硬件信息中直接获悉系统的内存,但是xv6直接假定内存只有128MB。所以PHYSTOP的定义如下,这包含所有内核代码和数据以及可用的RAM大小:
#define KERNBASE 0x80000000L
#define PHYSTOP (KERNBASE + 128*1024*1024)
3.6 kalloc函数
kalloc函数专门负责分配一页未用的物理内存并返回,主要操作就是从空闲链表的头部摘下一个节点并返回。
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
// 加锁保证操作安全
acquire(&kmem.lock);
// 取下链表头部的第一个节点,即第一个空闲页
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
// 如果r不为空,表示成功分配到了内存
// 将其填满随机数据后返回
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
4.kernel/memorylayout.h & kernel/riscv.h
这两个文件不打算在此展开,因为它们都是一些细碎的宏定义和嵌入式汇编语句,其中memorylayout.h头文件中定义了和地址空间布局相关的一些宏,riscv.h则定义了很多嵌入式汇编语句。这些我们在后面阅读代码时再适时地切入可能效果会更好。
那么有关Xv6的虚拟内存部分代码,到这里算是告一段落了,下一次我们该研究陷阱机制了:)