操作系统是如何管理物理内存的?以xv6系统为例

318 阅读3分钟

在xv6中,与物理内存分配相关的代码都在proc.c/kalloc.c文件中,主要实现了三个功能:物理内存分配器初始化;分配物理内存空间;释放物理内存空间。

前置知识点

为什么操作系统内核能使用物理内存

我们知道系统内核和用户进程都是使用虚拟内存的,为什么操作系统内核能操作物理内存呢?

首先,我们来回顾下虚拟内存系统是如何工作的:
在设置好stap寄存器后,MMU(memory manage unit)就会开始使用stap寄存器指向的页表来对CPU传送过来的指令地址进行转换,再到内存中寻址。

20201110194457471.png

而系统内核使用的映射,大多是恒等映射(如下图所示),因此我们在系统内核中使用的虚拟内存地址其实与物理地址相同的。

pic_kernel_table.png

空闲物理内存在内核中的表示

先说三个细节:

  1. 在RISC-V中,一个地址使用64 bits表示,也即8 bytes,所以struct run的大小就是8 bytes;
  2. 分配与释放物理内存空间的单位是一个页面,在RISC-V中,一个页面的大小是4 kb,也即4096 bytes;

为了方便,我假设RAM大小为3*4kb = 12Kb,即有3个物理页。在初始时,三个页面都是空闲页。pa0,pa1,pa2分别是三个物理页的首地址。 Screenshot from 2023-07-20 19-51-52.png

我们需要解决两个问题,一是如何表示一个空闲物理页,二是如何表示所有的空闲物理页。
对于第一个问题,由于每个物理页的大小是相同的,因此我们只需要使用每个物理页的首地址,就可以表示出该物理页(首地址往后4 kb是同一页)。
而对于第二个问题,可以使用链表来将所有空闲的物理页串起来。

一般的链表可以使用如下代码实现:

struct ListNode {
  int val;
  struct ListNode* next;
};

在xv6中,所有的空闲物理内存记录在头节点为kmem.freelist的链表中,链表的存储的数据类型是struct run,struct run中只有一个成员struct run的指针。

struct run {
  struct run *next;
};

struct {
  struct spinlock lock;
  struct run *freelist;
} kmem;

具体来说,我们可以在每个空闲物理页的前八个字节存放一个地址,这个地址指向了下一个空闲页的首地址。而kmem.freelist就指向这个链表的头结点。 Screenshot from 2023-07-20 19-48-37.png

物理内存分配器初始化与物理内存释放

在物理内存分配器初始化是需要对物理内进行释放,因此这里对两者一并介绍。

首先来看看xv6是如何释放一个物理页的。

void kfree(void *pa) {
  struct run *r;

  // 检查物理地址pa 是否是页大小的整数倍 与 是否在RAM的地址范围内
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // 将pa表示的一个物理页填充满垃圾数据,以方便捕捉垂悬引用
  memset(pa, 1, PGSIZE);
  
  // 将r指向一个物理页的起始地址pa
  r = (struct run*)pa;

  acquire(&kmem.lock);
  // 在空闲物理页的开头放入下一个空闲物理页的首地址
  r->next = kmem.freelist;
  // 将空闲物理页链表的头结点设置为r
  kmem.freelist = r;
  release(&kmem.lock);
}

而初始化其实就是遍历RAM(KERNBASE ~ PHYBASE)中的每个页,并一一使用kfree()进行释放。初始化后的freelist应该包括了RAM中的所有物理页。

void kinit() {
  initlock(&kmem.lock, "kmem");
  freerange(end, (void*)PHYSTOP);
}

void freerange(void *pa_start, void *pa_end) {
  char *p;
  p = (char*)PGROUNDUP((uint64)pa_start);
  for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    kfree(p);
}

物理内存分配

而物理内存的分配其实也十分简单,就是取当前头结点,将头结点从freelist中删除,并返回该空闲物理页的首地址。

void *kalloc(void) {
  struct run *r;

  acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  release(&kmem.lock);

  if(r)
    memset((char*)r, 5, PGSIZE); // fill with junk
  return (void*)r;
}