目录
阅读的代码是xv6-riscv版本的
Xv6为每个进程维护一个页表,用以描述每个进程的用户地址空间,外加一个单独描述内核地址空间的页表。内核配置其地址空间的布局,以允许自己以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了这种布局如何将内核虚拟地址映射到物理地址。文件(kernel/memlayout.h) 声明了xv6内核内存布局的常量。
QEMU模拟了一台计算机,它包括从物理地址
0x80000000
开始并至少到0x86400000
结束的RAM(物理内存),xv6称结束地址为PHYSTOP
。QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间0x80000000
以下。内核可以通过读取/写入这些特殊的物理地址与设备交互;这种读取和写入与设备硬件而不是RAM通信。
每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G。
当进程向xv6请求更多的用户内存时,xv6首先使用
kalloc
来分配物理页面。然后,它将PTE添加到进程的页表中,指向新的物理页面。Xv6在这些PTE中设置PTE_W
、PTE_X
、PTE_R
、PTE_U
和PTE_V
标志。大多数进程不使用整个用户地址空间;xv6在未使用的PTE中留空PTE_V
。我们在这里看到了一些使用页表的很好的例子。首先,不同进程的页表将用户地址转换为物理内存的不同页面,这样每个进程都拥有私有内存。第二,每个进程看到的自己的内存空间都是以0地址起始的连续虚拟地址,而进程的物理内存可以是非连续的。第三,内核在用户地址空间的顶部映射一个带有蹦床(trampoline)代码的页面,这样在所有地址空间都可以看到一个单独的物理内存页面。
当启动xv6时,在main.c当中会对页表进行初始化,创建,启用等操作
- // start() jumps here in supervisor mode on all CPUs.
- void
- main()
- {
- 、、、、、、、、、、、、//省略
- kinit(); // 初始化物理页
- kvminit(); // 创建内核页表
- kvminithart(); // 打开分页机制
- procinit(); //为每个进程分配一个内核栈
- 、、、、、、、、、、、、//省略
- scheduler();
- }
在该函数当中会调用freerange()函数将范围内的物理地址进行分页
- void
- kinit()
- {
- initlock(&kmem.lock, "kmem"); //初始化锁
- freerange(end, (void*)PHYSTOP);//将end----PHYSTOP范围内的地址进行分页
- }
分配出来的页是拿链表的形式保存下来,每当需要分配页表时,只需要从这个链表中获取就可以,
释放页表后,再将页表插入到该链表当中。结构体声明如下
- struct run {
- struct run *next;
- };
-
- struct {
- struct spinlock lock;
- struct run *freelist;
- } kmem;
下来我们看一下freerange()函数是怎样操作的
- 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);
- }
一个页的大小为PGSIZE(1024),我们通过循环拿到范围内每个页表的首地址
然后调用kfree()函数
- // 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(void *pa)
- {
- struct run *r;
-
- if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
- panic("kfree");
-
- // Fill with junk to catch dangling refs.
- memset(pa, 1, PGSIZE);
-
- r = (struct run*)pa;
-
- acquire(&kmem.lock);
- r->next = kmem.freelist;
- kmem.freelist = r;
- release(&kmem.lock);
- }
将内存中的每一个字节设置为1。这将导致使用释放后的内存的代码(使用“悬空引用”)读取到垃圾信息而不是旧的有效内容,从而希望这样的代码更快崩溃。
然后kfree
将页面前置(头插法)到空闲列表中:它将pa
转换为一个指向struct run
的指针r
,在r->next
中记录空闲列表的旧开始,并将空闲列表设置为等于r
。
注意:在进行此类操作时,要上锁,防止同时访问
在kvminit中使用 kvmmake
(kernel/vm.c:20) 创建内核的页表。此调用发生在 xv6 启用 RISC-V 上的分页之前,因此地址直接引用物理内存。
- // Initialize the one kernel_pagetable
- void kvminit(void)
- {
- kernel_pagetable = kvmmake();
- }
我们看一下kvmmake()函数
- // Make a direct-map page table for the kernel.
- pagetable_t
- kvmmake(void)
- {
- pagetable_t kpgtbl;
-
- kpgtbl = (pagetable_t)kalloc();
- memset(kpgtbl, 0, PGSIZE);
-
- // uart registers
- kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
-
- // virtio mmio disk interface
- kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
-
- // PLIC
- kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
-
- // map kernel text executable and read-only.
- kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);
-
- // map kernel data and the physical RAM we'll make use of.
- kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);
-
- // map the trampoline for trap entry/exit to
- // the highest virtual address in the kernel.
- kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
-
- // map kernel stacks
- proc_mapstacks(kpgtbl);
-
- return kpgtbl;
- }
kvmmake
首先调用kalloc()函数分配一个物理内存页来保存根页表页。PHYSTOP
,并包括实际上是设备的内存。Proc_mapstacks
(kernel/proc.c:33) 为每个进程分配页表,并且完成映射。下面我们在深入看看
- // 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);
-
- if(r)
- memset((char*)r, 5, PGSIZE); // fill with junk
- return (void*)r;
- }
该函数作用就是分配一个空闲页表,从kmem空闲链表中拿取
- // add a mapping to the kernel page table.
- // only used when booting.
- // does not flush TLB or enable paging.
- void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
- {
- if (mappages(kpgtbl, va, sz, pa, perm) != 0)
- panic("kvmmap");
- }
kvmmap
(kernel/vm.c:127)调用mappages
(kernel/vm.c:138),mappages
将范围虚拟地址到同等范围物理地址的映射装载到一个页表中。它以页面大小为间隔,为范围内的每个虚拟地址单独执行此操作。对于要映射的每个虚拟地址,mappages
调用walk
来查找该地址的PTE地址。然后,它初始化PTE以保存相关的物理页号、所需权限(PTE_W
、PTE_X
和/或PTE_R
)以及用于标记PTE有效的PTE_V
(kernel/vm.c:153)。
- // Create PTEs for virtual addresses starting at va that refer to
- // physical addresses starting at pa. va and size might not
- // be page-aligned. Returns 0 on success, -1 if walk() couldn't
- // allocate a needed page-table page.
- int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
- {
- uint64 a, last;
- pte_t *pte;
-
- if (size == 0)
- panic("mappages: size");
-
- a = PGROUNDDOWN(va);
- last = PGROUNDDOWN(va + size - 1);
- for (;;)
- {
- if ((pte = walk(pagetable, a, 1)) == 0)
- return -1;
- if (*pte & PTE_V)
- panic("mappages: remap");
- *pte = PA2PTE(pa) | perm | PTE_V;
- if (a == last)
- break;
- a += PGSIZE;
- pa += PGSIZE;
- }
- return 0;
- }
- // Allocate a page for each process's kernel stack.
- // Map it high in memory, followed by an invalid
- // guard page.
- void proc_mapstacks(pagetable_t kpgtbl)
- {
- struct proc *p;
-
- for (p = proc; p < &proc[NPROC]; p++)
- {
- char *pa = kalloc();
- if (pa == 0)
- panic("kalloc");
- uint64 va = KSTACK((int)(p - proc));
- kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
- }
- }
这个函数作用就是给每一个进程分配个页表,然后将内核栈映射到高地址,保护页不进行映射
- // Switch h/w page table register to the kernel's page table,
-
- // and enable paging.
-
- void kvminithart()
-
- {
-
- w_satp(MAKE_SATP(kernel_pagetable));
-
- sfence_vma();
-
- }
kvminithart
(kernel/vm.c:53)来安装内核页表。它将根页表页的物理地址写入寄存器satp
。之后,CPU将使用内核页表转换地址。由于内核使用标识映射,下一条指令的当前虚拟地址将映射到正确的物理内存地址。
- // initialize the proc table at boot time.
- void procinit(void)
- {
- struct proc *p;
-
- initlock(&pid_lock, "nextpid");
- initlock(&wait_lock, "wait_lock");
- for (p = proc; p < &proc[NPROC]; p++)
- {
- initlock(&p->lock, "proc");
- p->kstack = KSTACK((int)(p - proc));
- }
- }
将p->kstack指向之前映射好的页面