揭秘xv6内存管理:虚拟内存如何让物理内存“无限大”?

揭秘xv6内存管理:虚拟内存如何让物理内存“无限大”?

【免费下载链接】xv6-public xv6 OS 【免费下载链接】xv6-public 项目地址: https://gitcode.com/gh_mirrors/xv/xv6-public

你是否想过,当同时打开多个应用时,计算机是如何管理有限的物理内存的?xv6操作系统通过精妙的内存管理机制,让每个进程都以为自己拥有整个内存空间。本文将带你深入了解xv6如何通过虚拟内存技术,实现物理内存与虚拟内存的完美协作,解决内存分配、进程隔离和地址转换的核心难题。读完本文,你将掌握操作系统内存管理的基本原理,以及xv6中虚拟地址到物理地址的映射机制。

内存布局:xv6的内存“地图”

xv6将整个内存空间划分为不同区域,每个区域有特定的功能和访问权限。这种划分确保了内核空间与用户空间的隔离,同时高效利用物理内存。

内存布局的关键定义在memlayout.h中,主要包括以下几个部分:

  • 用户空间:0到KERNBASE(0x80000000),供用户进程使用
  • 内核空间:KERNBASE到PHYSTOP(0xE000000),内核代码和数据
  • 设备区域:DEVSPACE(0xFE000000)及以上,用于硬件设备I/O
// memlayout.h中的关键定义
#define EXTMEM  0x100000            // 扩展内存起始地址
#define PHYSTOP 0xE000000           // 物理内存上限
#define DEVSPACE 0xFE000000         // 设备地址空间
#define KERNBASE 0x80000000         // 内核虚拟地址起始

内核通过这张“地图”,可以准确知道每块内存的用途和访问规则,为后续的内存分配和地址转换打下基础。

物理内存管理:内核的“内存管家”

物理内存管理的核心任务是高效分配和回收物理页面。xv6采用空闲链表(free list)管理未使用的物理内存,由kalloc.c实现。

空闲链表机制

xv6使用简单而高效的空闲链表管理物理内存。空闲链表是一个单向链表,每个节点代表一个空闲的物理页面。分配内存时从链表头部取下一个页面,释放时将页面插回链表。

// kalloc.c中的空闲链表结构
struct {
  struct spinlock lock;
  int use_lock;
  struct run *freelist;  // 空闲页面链表
} kmem;

struct run {
  struct run *next;
};

内存初始化

xv6启动时通过kinit1和kinit2两个函数初始化内存管理系统:

  1. kinit1:初始化内核刚启动时可用的少量内存
  2. kinit2:初始化剩余的物理内存
// kalloc.c中的初始化函数
void kinit1(void *vstart, void *vend) {
  initlock(&kmem.lock, "kmem");
  kmem.use_lock = 0;
  freerange(vstart, vend);
}

void kinit2(void *vstart, void *vend) {
  freerange(vstart, vend);
  kmem.use_lock = 1;
}

分配与释放

kalloc()和kfree()是物理内存管理的核心函数:

  • kalloc():从空闲链表分配一个4KB页面
  • kfree():释放页面并插回空闲链表,同时用特殊值填充页面以检测野指针
// 分配物理页面
char* kalloc(void) {
  struct run *r;
  
  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = kmem.freelist;
  if(r)
    kmem.freelist = r->next;
  if(kmem.use_lock)
    release(&kmem.lock);
  return (char*)r;
}

// 释放物理页面
void kfree(char *v) {
  struct run *r;
  
  if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
    panic("kfree");
  
  // 填充垃圾数据以捕获悬空引用
  memset(v, 1, PGSIZE);
  
  if(kmem.use_lock)
    acquire(&kmem.lock);
  r = (struct run*)v;
  r->next = kmem.freelist;
  kmem.freelist = r;
  if(kmem.use_lock)
    release(&kmem.lock);
}

这种简单的内存分配策略在xv6这样的教学操作系统中非常有效,既易于理解又能满足基本需求。

虚拟内存:每个进程的“私人内存”

虚拟内存是操作系统提供的一种抽象,让每个进程都认为自己拥有整个内存空间。xv6通过页表(page table)实现虚拟地址到物理地址的转换,这一机制主要在vm.cmmu.h中实现。

页表结构

xv6采用二级页表结构,符合x86架构的分页机制:

  • 页目录(Page Directory):一级表,每个条目指向一个页表
  • 页表(Page Table):二级表,每个条目指向一个物理页面

页表条目的结构定义在mmu.h中:

// mmu.h中的页表条目定义
#define PTE_P           0x001   // 存在位(页面是否在物理内存中)
#define PTE_W           0x002   // 可写位
#define PTE_U           0x004   // 用户位(用户模式可访问)

typedef uint pte_t;  // 页表条目类型

地址转换过程

虚拟地址到物理地址的转换分为三步:

  1. 从虚拟地址中提取页目录索引(PDX)
  2. 通过页目录索引找到对应的页表
  3. 从虚拟地址中提取页表索引(PTX),找到对应的物理页面
  4. 组合物理页面地址和页内偏移,得到物理地址
// mmu.h中的地址分解宏
#define PDX(va)         (((uint)(va) >> PDXSHIFT) & 0x3FF)
#define PTX(va)         (((uint)(va) >> PTXSHIFT) & 0x3FF)
#define PGADDR(d, t, o) ((uint)((d) << PDXSHIFT | (t) << PTXSHIFT | (o)))

#define PDXSHIFT        22      // 页目录索引偏移
#define PTXSHIFT        12      // 页表索引偏移

页表遍历函数

vm.c中的walkpgdir()函数实现了页表的查找功能,是地址转换的核心:

// 查找虚拟地址va对应的页表条目
static pte_t *walkpgdir(pde_t *pgdir, const void *va, int alloc) {
  pde_t *pde;
  pte_t *pgtab;
  
  pde = &pgdir[PDX(va)];
  if(*pde & PTE_P){
    pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
  } else {
    if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)
      return 0;
    // 初始化新页表
    memset(pgtab, 0, PGSIZE);
    // 设置页目录条目
    *pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;
  }
  return &pgtab[PTX(va)];
}

地址空间:进程的“内存沙盒”

每个进程都有独立的地址空间,由页表定义。xv6在进程创建和切换时管理这些地址空间,主要实现在proc.cvm.c中。

内核地址空间

内核地址空间在系统启动时通过setupkvm()函数初始化,对所有进程共享:

// 创建内核页表
pde_t* setupkvm(void) {
  pde_t *pgdir;
  struct kmap *k;
  
  if((pgdir = (pde_t*)kalloc()) == 0)
    return 0;
  memset(pgdir, 0, PGSIZE);
  
  // 映射内核各个区域
  for(k = kmap; k < &kmap[NELEM(kmap)]; k++)
    if(mappages(pgdir, k->virt, k->phys_end - k->phys_start,
                (uint)k->phys_start, k->perm) < 0) {
      freevm(pgdir);
      return 0;
    }
  return pgdir;
}

进程地址空间

每个用户进程有独立的地址空间,包含代码、数据、堆和栈等区域。xv6通过以下函数管理进程地址空间:

  • allocuvm():扩展进程地址空间
  • deallocuvm():收缩进程地址空间
  • copyuvm():复制进程地址空间(用于fork系统调用)
// 扩展进程内存
int allocuvm(pde_t *pgdir, uint oldsz, uint newsz) {
  char *mem;
  uint a;
  
  if(newsz >= KERNBASE)
    return 0;
  if(newsz < oldsz)
    return oldsz;
  
  a = PGROUNDUP(oldsz);
  for(; a < newsz; a += PGSIZE){
    mem = kalloc();
    if(mem == 0){
      cprintf("allocuvm out of memory\n");
      deallocuvm(pgdir, newsz, oldsz);
      return 0;
    }
    memset(mem, 0, PGSIZE);
    if(mappages(pgdir, (char*)a, PGSIZE, V2P(mem), PTE_W|PTE_U) < 0){
      cprintf("allocuvm out of memory (2)\n");
      deallocuvm(pgdir, newsz, oldsz);
      kfree(mem);
      return 0;
    }
  }
  return newsz;
}

进程切换时的地址空间切换

当xv6切换进程时,需要同时切换页表。这通过switchuvm()函数实现:

// 切换到进程p的地址空间
void switchuvm(struct proc *p) {
  if(p == 0)
    panic("switchuvm: no process");
  if(p->kstack == 0)
    panic("switchuvm: no kstack");
  if(p->pgdir == 0)
    panic("switchuvm: no pgdir");
  
  pushcli();
  // 设置TSS(任务状态段)
  mycpu()->gdt[SEG_TSS] = SEG16(STS_T32A, &mycpu()->ts,
                                sizeof(mycpu()->ts)-1, 0);
  mycpu()->gdt[SEG_TSS].s = 0;
  mycpu()->ts.ss0 = SEG_KDATA << 3;
  mycpu()->ts.esp0 = (uint)p->kstack + KSTACKSIZE;
  mycpu()->ts.iomb = (ushort) 0xFFFF;
  ltr(SEG_TSS << 3);
  // 切换页表
  lcr3(V2P(p->pgdir));  // 加载页目录基址寄存器
  popcli();
}

内存保护:隔离与安全的保障

xv6通过页表条目中的权限位实现内存保护,确保进程不能越权访问内存。主要的保护机制包括:

权限位控制

页表条目中的权限位定义了对页面的访问权限:

  • PTE_U:用户模式可访问
  • PTE_W:可写位
  • PTE_P:存在位

这些位在mmu.h中定义:

// mmu.h中的权限位定义
#define PTE_P           0x001   // Present
#define PTE_W           0x002   // Writeable
#define PTE_U           0x004   // User can access

内核空间保护

内核空间(KERNBASE以上)的页表条目不设置PTE_U位,确保用户进程无法访问内核内存:

// vm.c中的内核映射定义
static struct kmap {
  void *virt;
  uint phys_start;
  uint phys_end;
  int perm;
} kmap[] = {
 { (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O空间
 { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},     // 内核代码段(只读)
 { (void*)data,     V2P(data),     PHYSTOP,   PTE_W}, // 内核数据段
 { (void*)DEVSPACE, DEVSPACE,      0,         PTE_W}, // 设备空间
};

写时复制机制

虽然xv6没有实现完整的写时复制(Copy-on-Write),但fork系统调用通过复制页表实现了类似的功能,为每个子进程创建独立的地址空间:

// 复制进程地址空间
pde_t* copyuvm(pde_t *pgdir, uint sz) {
  pde_t *d;
  pte_t *pte;
  uint pa, i, flags;
  char *mem;
  
  if((d = setupkvm()) == 0)
    return 0;
  for(i = 0; i < sz; i += PGSIZE){
    if((pte = walkpgdir(pgdir, (void *) i, 0)) == 0)
      panic("copyuvm: pte should exist");
    if(!(*pte & PTE_P))
      panic("copyuvm: page not present");
    pa = PTE_ADDR(*pte);
    flags = PTE_FLAGS(*pte);
    if((mem = kalloc()) == 0)
      goto bad;
    memmove(mem, (char*)P2V(pa), PGSIZE);
    if(mappages(d, (void*)i, PGSIZE, V2P(mem), flags) < 0) {
      kfree(mem);
      goto bad;
    }
  }
  return d;
  
bad:
  freevm(d);
  return 0;
}

总结与展望

xv6的内存管理系统通过物理内存管理、虚拟内存技术和地址空间隔离,实现了多进程内存的高效利用和安全隔离。核心要点包括:

  1. 物理内存管理:通过空闲链表分配和回收物理页面
  2. 虚拟内存技术:使用二级页表实现虚拟地址到物理地址的转换
  3. 地址空间隔离:每个进程拥有独立的页表,实现内存沙盒
  4. 内存保护机制:通过页表权限位控制内存访问

xv6的内存管理虽然简单,但包含了现代操作系统内存管理的核心思想。对于深入理解操作系统工作原理,特别是内存管理部分,xv6提供了一个绝佳的学习案例。

如果你想进一步探索xv6的内存管理,可以阅读vm.ckalloc.cproc.c的完整代码,或者尝试修改内存分配算法,实现更高效的内存管理策略。

通过本文的介绍,相信你已经对xv6的内存管理机制有了清晰的认识。这种将复杂问题分解为物理内存管理、虚拟地址转换和地址空间隔离的思想,同样适用于理解其他操作系统的内存管理实现。

【免费下载链接】xv6-public xv6 OS 【免费下载链接】xv6-public 项目地址: https://gitcode.com/gh_mirrors/xv/xv6-public

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值