XV6 main函数中的页表代码详解

#if和#ifndef用法

借鉴:

C语言#if、##ifdef、#ifndef的用法详解,C语言条件编译详解 (biancheng.net)

在xv6的kernel/main.c中出现了这种用法,在其他多个.c文件里也出现了

为何要用这种用法:

假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?

这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。

Windows 有专有的宏_WIN32,Linux 有专有的宏__linux__,以现有的知识,我们很容易就想到了 if else,请看下面的代码:

#include <stdio.h>
int main(){
    if(_WIN32){
        system("color 0c");
        printf("http://c.biancheng.net\n");
    }else if(__linux__){
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    }else{
        printf("http://c.biancheng.net\n");
    }
    return 0;
}

但这段代码是错误的,在 Windows 下提示 linux 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对上面的代码进行改进:

#include <stdio.h>
int main(){
    #if _WIN32
        system("color 0c");
        printf("http://c.biancheng.net\n");
    #elif __linux__
        printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
    #else
        printf("http://c.biancheng.net\n");
    #endif
    return 0;
}

这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。

#if 用法的一般格式为:它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。

#if 整型常量表达式1
    程序段1
#elif 整型常量表达式2
    程序段2
#elif 整型常量表达式3
    程序段3
#else
    程序段4
#endif
        
// #elif 和 #else 也可以省略 ,但是#if和#endif是一定要的

#ifdef 用法的一般格式为如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。

#ifdef  宏名
    程序段1
#else
    程序段2
#endif
        
// 也可以省略为:
#ifdef  宏名
    程序段
#endif
        
// 如下面这段程序:
#include <stdio.h>
#include <stdlib.h>
int main(){
    #ifdef _DEBUG
        printf("正在使用 Debug 模式编译程序...\n");
    #else
        printf("正在使用 Release 模式编译程序...\n");
    #endif
    system("pause");
    return 0;
}

#ifndef 用法的一般格式为:与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。

#ifndef 宏名
    程序段1 
#else 
    程序段2 
#endif

最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。

// 两个宏都存在时编译代码A,否则编译代码B:
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
    #if (defined NUM1 && defined NUM2)
        //代码A
        printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
    #else
        //代码B
        printf("Error\n");
    #endif
    return 0;
}

宝图

一直用到这个图,所以取名宝图。

初始化freelist代码

这里的内容都看宝图中的右图。

首先在kernel/kernel开启debug,在main.c的kinit()处打断点:

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
  if(cpuid() == 0){
    consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
    statsinit();
#endif
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    // 打断点
    kinit();         // physical page allocator
   ...       
}

step into kinit(),进入kalloc.c:

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

跳过initlock()(看代码应该就是初始化锁的功能,和页表没啥关系),step into第二个函数,还是在kalloc.c中:

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

PGROUNDUP() :

这个函数的功能是向上对齐页大小,xv6中一页是4kb,也就是0x1000。假设现在pa_start是0x1020,为了保证每一页都是同等大小,就需要对齐。而这个函数就是用来这样做的,PGROUNDUP(pa_start) 会对齐为 0x2000。

pa_start && pa_end :

接着我们看一下freerange的参数。首先我们应该知道freerange这个函数的功能是把当前所有物理界面给分成一页一页,加入到 freelist中。

pa_start在kinit()中传入的是end,可以看到end的定义是: extern外部定义,在kernel.ld中定义。

extern char end[]; // first address after kernel.                  
                   // defined by kernel.ld.

转到kernel.ld查看:可以看到这里巴拉巴拉处理了一堆东西,存了又存。可以看宝图的0x80000000后面的kernel text部分。

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  . = 0x80000000;

  .text : {
    /* 这里就是图中的0x800.. 之上的kernel text区域,比如现在是:0x80002555*/
    *(.text .text.*)
    . = ALIGN(0x1000); /* 对齐一页4kb(4096=0x1000),对齐后就是:0x80003000,0x3000 = 4096*3,ALIGN就是这个作用,就是说如果我们需要两页多来存储这些数据,我们就强行对齐成三页*/
    /*trampoline分配空间并对齐一页*/
    _trampoline = .;
    *(trampsec) /*put*/
    . = ALIGN(0x1000);
    /*这里的意思是trampoline只能用一个页存,超过一个页就会报错*/
    ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
    PROVIDE(etext = .);
  }

  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }
/* 这个end就是在kalloc.c里面的那个end数组*/
  PROVIDE(end = .);
}

所以,通过上面的kernel.ld我们可以看到,pa_start(也就是end)应该是大于0x80000000的,这也跟宝图对应上。我们查看gdb的pa_start地址,可以看到为:pa_start:0x80027020 。 也就是初始化上面那些东西用了27页多

然后再回到freerange的代码: 因为pa_start大小不一定是整数页,所以需要对齐,通过gdb我们可以看到 p = 0x80028000。进入for循环,再通过gdb可以看到 pa_end = 0x88000000。其实这个pa_end就是宝图的PHYSTOP(虽然比他小,但就是这个),这也说明了xv6其实只有128m的内存可以使用。

for循环就是从 p 开始,每次增长一页,一直到PHYSTOP-PAGESIZE(也就是宝图的freememory底部到顶部)。

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

然后转到kfree()查看:

struct run {
  struct run *next;
};

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

void
kfree(void *pa)
{
  struct run *r;
	// 确保pa是整数页倍且不越界
  if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    panic("kfree");

  // Fill with junk to catch dangling refs.
    // 将每一页内容全初始化为1
  memset(pa, 1, PGSIZE);
	// 使得run本身就是pa的地址所在
  r = (struct run*)pa;

  acquire(&kmem.lock);
  // add to the list front
    // 将地址加入到freelist的头部
  r->next = kmem.freelist;
  kmem.freelist = r;
  release(&kmem.lock);
}

这就是整个初始化freelist的代码,结束完for的时候,我们的physical page就分配结束了。此时freelist的首页就是0x87fff000。

kvminit()函数

结束完kinit()就进入了kvminit()函数:

void
main()
{
  if(cpuid() == 0){
    consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
    statsinit();
#endif
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
      // 打断点
    kvminit();       // create kernel page table

kvminit(),这个函数会设置好kernel的地址空间。kvminit的代码(在kernel/vm.c中):

kvmmap设置了虚拟地址va到物理地址pa的等价映射,看宝图的KernelBase以下部分

void
kvminit()
{
  kernel_pagetable = (pagetable_t) kalloc(); 
  memset(kernel_pagetable, 0, PGSIZE);

    
  kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
  kvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
  kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);
  kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);
  kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
  kvmmap((uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
  kvmmap(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}

然后我们看kalloc():

打一下kalloc()的地址可以发现他是在0x80000b20,因为我们刚刚在kernel.ld中把 0x8000000到0x80027020的地址给用了,所以kalloc()也已经被加载了。

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

// 分配一页空间
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;
}

通过解读freelist那里我们已经知道,kmem的freelist里面存着所有空闲的物理地址,且都是一页一页的。所以kalloc要做的事就是检查是否还有内存,如果 r !=NULL说明还有页数,就可以把第一页分配给它。

通过gdb我们可以看到 kernel_pagetable的地址是0x87fff000,也就是freelist的第一页。

然后再看一下kvmmap():

void
kvmmap(uint64 va, uint64 pa, uint64 sz, int perm)
{
  if(mappages(kernel_pagetable, va, sz, pa, perm) != 0)
    panic("kvmmap");
}

mappages()的功能就是把va映射到pa,添加到页表中,还有赋予权限之类的:

int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
  uint64 a, last;
  pte_t *pte;

  a = PGROUNDDOWN(va);
  last = PGROUNDDOWN(va + size - 1);
  for(;;){
      // walk()函数模仿了硬件查找三级页表的过程,可以看我的4.11写的
      // walk()函数是比较重要比较屌的,在lab也会用到,所以要看明白。
    if((pte = walk(pagetable, a, 1)) == 0)
      return -1;
    if(*pte & PTE_V)
      panic("remap");
    *pte = PA2PTE(pa) | perm | PTE_V;
    if(a == last)
      break;
    a += PGSIZE;
    pa += PGSIZE;
  }
  return 0;
}

pte_t *
walk(pagetable_t pagetable, uint64 va, int alloc) // walk 模仿RISC-V分页硬件查找虚拟地址的PTE
{
  if(va >= MAXVA)
    panic("walk");

  for(int level = 2; level > 0; level--) {
    pte_t *pte = &pagetable[PX(level, va)];
    if(*pte & PTE_V) {
      pagetable = (pagetable_t)PTE2PA(*pte);
    } else { // 如果PTE无效,那么所需的物理页还没有被分配
      if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) // 如果alloc参数被设置true,walk会分配一个新的页表页,并把它的物理地址放在PTE中
        return 0;
      memset(pagetable, 0, PGSIZE);
      *pte = PA2PTE(pagetable) | PTE_V;
    }
  }
  return &pagetable[PX(0, va)]; // 返回PTE在树的最低层的地址
}

配合这个图会好理解很多,就是返回下图的第三级页表对应的pte。alloc=1代表如果不存在对应的page directory就分配一页给它。

kvminithart()函数

回到main,结束完kvminit()之后,就来到了kvminithart()函数:

首先就是w_satp(),这个函数使用了c语言的内嵌汇编,将kernel_pagetable的地址写入了satp寄存器,然后通过sfence_vma() flush TLB。每次更新satp寄存器都应该出现刷新TLB,这样才不会读到旧数据导致出错。

void
kvminithart()
{
  w_satp(MAKE_SATP(kernel_pagetable));
  sfence_vma();
}

static inline void 
w_satp(uint64 x)
{
  asm volatile("csrw satp, %0" : : "r" (x));
}

// flush the TLB.
static inline void
sfence_vma()
{
  // the zero, zero means flush all TLB entries.
  asm volatile("sfence.vma zero, zero");
}

procinit()函数

void
main()
{
  if(cpuid() == 0){
    consoleinit();
#if defined(LAB_PGTBL) || defined(LAB_LOCK)
    statsinit();
#endif
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();         // physical page allocator
    kvminit();       // create kernel page table
    kvminithart();   // turn on paging
    procinit();      // process table
      .....
}

进入procinit():通过注释可以看到这个函数的功能应该是:在启动时初始化进程表。理解procinit()的功能对lab3也有帮助。

// param.h
#define NPROC        64  // maximum number of processes

// initialize the proc table at boot time.
void
procinit(void)
{
  struct proc *p;
  
  initlock(&pid_lock, "nextpid");
    // 给所有的进程分配kernel stack并添加到内核页表。所有的内核栈都在procinit中设置。
  for(p = proc; p < &proc[NPROC]; p++) {
      initlock(&p->lock, "proc");

      // Allocate a page for the process's kernel stack.
      // Map it high in memory, followed by an invalid
      // guard page.
      char *pa = kalloc();
      if(pa == 0)
        panic("kalloc");
      uint64 va = KSTACK((int) (p - proc));
      kvmmap(va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
      p->kstack = va;
  }
  kvminithart();
}

Last updated