Xv6源码分析——内存管理

        内存管理主要分为两大部分,第一部分是内核的物理内存分配程序,以便内核可以分配内存并稍后释放它。 分配器将以4096字节为单位进行操作,称为页面。内核会维护记录哪些物理页面是空闲的和哪些已分配的数据结构,以及每个页面的进程数量,以及如何分配和释放内存页面。内存管理的第二个组成部分是虚拟内存,它将内核和用户软件使用的虚拟地址映射到物理内存中的地址。 当指令使用内存时,x86硬件的内存管理单元(MMU)执行映射,查询一组页表。

预备知识

页表

        页表是为了便于在内存中找到进程的每个页面所对应的物理块,系统为每个进程建立了一张页表,记录页面在内存中对应的物理块号。XV6主要使用页表来复用地址空间并保护内存。

地址转换硬件

        x86指令(包括用户和内核)直接使用的是虚拟地址,而物理内存使用物理地址进行索引,从虚拟地址到物理地址的转换是由硬件完成的。x86页表由一级的页目录和二级的页表项组成,每个页目录项有1024个连续的页表项(每个页表项4B,刚好占用4kb的空间,也就是一页),页目录项也是连续的,一共有1024个页目录项。CR3是页目录基地址寄存器,保存页目录表的物理地址,因为页目录表是页对齐的,所以CR3只有高20位是有效的。地址转换如下图所示:

 内存初始化

        boot将内核代码放到物理地址低地址的0x100000处,为了使内核运行,entry建立了一个页表将虚拟地址0x80000000(KERNBASE)映射到从0x0开始的物理地址。页表为main.c文件中的enterpgdir数组,其中虚拟地址低4M映射物理地址低4M,(因为启动多处理器的时候还需要从低地址启动)虚拟地址[KERNBASE,KERNBASE+4MB)映射到物理地址[0,4MB)

__attribute__((__aligned__(PGSIZE)))
pde_t entrypgdir[NPDENTRIES] = {
  // Map VA's [0, 4MB) to PA's [0, 4MB)
  [0] = (0) | PTE_P | PTE_W | PTE_PS,
  // Map VA's [KERNBASE, KERNBASE+4MB) to PA's [0, 4MB)
  [KERNBASE>>PDXSHIFT] = (0) | PTE_P | PTE_W | PTE_PS,
};


};

PTE_P:表示此页已经在内存中,PTE_W:可写,PTE_PS:页面大小?

// Page table/directory entry flags.
#define PTE_P           0x001   // Present
#define PTE_W           0x002   // Writeable
#define PTE_PS          0x080   // Page Size

来看entry的代码:

entry:
  # Turn on page size extension for 4Mbyte pages
  #设置cr4,使用4M页,这样创建的页表比较简单
  movl    %cr4, %eax
  orl     $(CR4_PSE), %eax
  movl    %eax, %cr4
  # Set page directory 将 entrypgdir 的物理地址载入到控制寄存器 %cr3 中
  movl    $(V2P_WO(entrypgdir)), %eax
  movl    %eax, %cr3
  # Turn on paging. 开启分页
  movl    %cr0, %eax
  orl     $(CR0_PG|CR0_WP), %eax
  movl    %eax, %cr0

  # Set up the stack pointer.创建CPU栈
  movl $(stack + KSTACKSIZE), %esp

  # Jump to main(), and switch to executing at
  # high addresses. The indirect call is needed because
  # the assembler produces a PC-relative instruction
  # for a direct jump.
  mov $main, %eax
  jmp *%eax
#开辟stack区域,大小为KSTACKSIZE
.comm stack, KSTACKSIZE

cr3寄存器中的数必须是物理地址,因为现在还没有页表,不能进行地址转换。通过宏定义V2P_W0来得到物理地址

#define V2P_WO(x) ((x) - KERNBASE)    // same as V2P, but without casts

 PG  分页(CR0的31位)置1启用分页,置0不启用分页。当禁用分页时,所有的线性地址都可以当作物理地址对待。

 WP 写保护(CR0的位16)置1时禁止管理级的过程往用户级只读页中写,置0时允许管理级的过程往用户级只读页中写。
        它将栈指针 %esp 指向被用作栈的一段内存。所有的符号包括 stack 都在高地址,所以当低地址的映射被移除时,栈仍然是可用的。最后 entry 跳转到高地址的 main 代码中。 必须使用间接跳转,否则汇编器会生成 PC 相关的直接跳转(PC-relative direct jump),而该跳转会运行在内存低地址处的 main。 main 不会返回,因为栈上并没有返回 PC 值。之后内核就运行在高地址处的函数 main中了。

物理内存初始化以及管理:

        main函数通过调用kinit1和kinit2来初始化物理内存,区别是kinit1调用时候用的还是之前的页表,只能初始化4m的空间,这时候多核cpu还没有启动所以没有设置锁机制。在建立了完整的页表之后用kinit2初始化剩下的物理内存。

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;
}

        xv6通过freelist数据结构来记录哪些物理页面是可以被分配的,kinit1和kinit2通过调用freerang来把空闲页加到freelist,PTE只能引用页对齐的物理地址,所以freerange通过PGROUNDUP来确保只释放页对齐的物理地址。PGROUNDUP(sz)的功能就是当sz不是页的倍数时进一位使其为页的倍数

#define PGROUNDUP(sz)  (((sz)+PGSIZE-1) & ~(PGSIZE-1))

kfree开始把被释放的内存填满字节1,目的是使在释放后再有代码使用这块内存(通过野指针非法使用内存)时读到的是垃圾数据,从而使这段代码尽快终止。然后kfree将v转换为指向struct run的指针,在r-> next中记录空闲列表的原来的头,并将空闲列表头设置为r。 kalloc删除并返回空闲列表中的第一个元素。

void
freerange(void *vstart, void *vend)
{
  char *p;
  p = (char*)PGROUNDUP((uint)vstart);
  for(; p + PGSIZE <= (char*)vend; p += PGSIZE)
    kfree(p);
}
//PAGEBREAK: 21
// Free the page of physical memory pointed at by v,
// which normally should have been returned by a
// call to kalloc().  (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(char *v)
{
  struct run *r;

  if((uint)v % PGSIZE || v < end || V2P(v) >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
  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);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
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;
}

进程地址空间

           entry建立的页表已经足够内核的C代码开始运行了,然而main函数直接通过kvmalloc建立了新页表,每个进程都有一张独立的页表,xv6通过页表硬件在进程切换时切换页表。switchkvm将页表切换成内核页表。

// Allocate one page table for the machine for the kernel address
// space for scheduler processes.
void
kvmalloc(void)
{
  kpgdir = setupkvm();
  switchkvm();
}

在setupkvm中,先通过kalloc分配一物理块作为页目录,然后调用mappages来按照kmap将内核虚拟地址空间映射到物理地址空间。

// This table defines the kernel's mappings, which are present in
// every process's page table.
static struct kmap {
  void *virt;
  uint phys_start;
  uint phys_end;
  int perm;
} kmap[] = {
 { (void*)KERNBASE, 0,             EXTMEM,    PTE_W}, // I/O space
 { (void*)KERNLINK, V2P(KERNLINK), V2P(data), 0},     // kern text+rodata
 { (void*)data,     V2P(data),     PHYSTOP,   PTE_W}, // kern data+memory
 { (void*)DEVSPACE, DEVSPACE,      0,         PTE_W}, // more devices
};
  


// Return the address of the PTE in page table pgdir
// that corresponds to virtual address va.  If alloc!=0,
// create any required page table pages.
static pte_t *
walkpgdir(pde_t *pgdir, const void *va, int alloc)
{
  pde_t *pde;
  pte_t *pgtab;

  pde = &pgdir[PDX(va)];//前10项 找到在页目录中的位置
  if(*pde & PTE_P){
    pgtab = (pte_t*)P2V(PTE_ADDR(*pde));
  } else {
    if(!alloc || (pgtab = (pte_t*)kalloc()) == 0)//分配页表
      return 0;
    // Make sure all those PTE_P bits are zero.
    memset(pgtab, 0, PGSIZE);
    // The permissions here are overly generous, but they can
    // be further restricted by the permissions in the page table
    // entries, if necessary.
    *pde = V2P(pgtab) | PTE_P | PTE_W | PTE_U;//设置权限
  }
  return &pgtab[PTX(va)];
}
 
#define PGSHIFT         12      // log2(PGSIZE)
#define PTXSHIFT        12      // offset of PTX in a linear address
#define PDXSHIFT        22      // offset of PDX in a linear address

 

switchkvm将kpgdir设置为cr3寄存器的值,这个页表仅仅在 scheduler内核线程中使用。

// Switch h/w page table register to the kernel-only page table,
// for when no process is running.
void
switchkvm(void)
{
  lcr3(V2P(kpgdir));   // switch to the kernel page table
}

页表和内核栈都是每个进程独有的,xv6使用结构体proc将它们统一起来,在进程切换的时候,他们也往往随着进程切换而切换,内核中模拟出了一个内核线程,它独占内核栈和内核页表kpgdir,它是所有进程调度的基础。

switchuvm通过传入的proc结构负责切换相关的进程独有的数据结构,其中包括TSS相关的操作,然后将进程特有的页表载入cr3寄存器,完成设置进程相关的虚拟地址空间环境。

// Switch TSS and h/w page table to correspond to process 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();
  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;
  // setting IOPL=0 in eflags *and* iomb beyond the tss segment limit
  // forbids I/O instructions (e.g., inb and outb) from user space
  mycpu()->ts.iomb = (ushort) 0xFFFF;
  ltr(SEG_TSS << 3);
  lcr3(V2P(p->pgdir));  // switch to process's address space
  popcli();
}

进程的页表在使用前往往需要初始化,其中必须包含内核代码的映射,这样进程在进入内核时便不需要再次切换页表,进程使用虚拟地址空间的低地址部分,高地址部分留给内核,设置页表时通过调用setupkvm、allocuvm、deallocuvm接口完成相关操作。

猜你喜欢

转载自blog.csdn.net/qq_36116842/article/details/79890745