gdb调试

  • make CPUS=1 qemu-gdb
  • new Terminal:gdb-multiarch kernel/kernel
  • set architecture riscv:rv64
  • target remote localhost:26000

第二章

2.5进程概述

  • xv6的隔离单位是进程,进程抽象防止一个进程破坏或访问另一个进程的内存、cpu、文件描述符等,同时也起到保护内核的作用。
  • 为实现强隔离,进程为程序提供了一块私有空间,其它进程无法操作。xv6使用页表机制实现进程隔离(硬件实现)。riscv页表将虚拟地址转换成物理地址。
  • xv6为每个进程维护单独的页表,为该进程分配地址空间,从0-maxva为:代码区、数据区、栈区、堆区(用于,malloc),riscv的指针有64位(64bit) = 8byte,硬件在页表中查找虚拟地址只使用39bit,xv6只使用38bit,因此最大地址为2的38次方-1。
  • 进程最重要的内核状态:1. 页表 p->pagetable 2. 内核堆栈p->kstack 3. 运行状态p->state,显示进程是否已经被分配、准备运行/正在运行/等待IO或退出
  • 每个进程都由线程构成,线程可以挂起。线程的大部分状态存储在线程的栈区。
  • 每个进程都有两个栈:用户栈和内核栈。
  • 一个进程可以通过执行RISC-V的ecall指令进行系统调用,该指令提升硬件特权级别,并将程序计数器(PC)更改为内核定义的入口点
  • p->state可以查看进程目前的状态,表明进程是已分配、就绪态、运行态、等待I/O中(阻塞态)还是退出。
  • p->pagetable以RISC-V硬件所期望的格式保存进程的页表。当在用户空间执行进程时,Xv6让分页硬件使用进程的p->pagetable。一个进程的页表也可以作为已分配给该进程用于存储进程内存的物理页面地址的记录。

第三章

3.1页表机制

  • xv6采用三级页表,用户态与内核态页表分离,每个进程拥有属于自己的用户态页表,所有进程共享一个内核态页表,其中内核态页表采用直接映射获取内存映射和设备寄存器(也就是虚拟地址等于物理地址),其中蹦床页面(trampoline page)和内核栈页面不是直接映射。
  • 在进程切换时,操作系统首先会将旧进程所有寄存器的状态保存到PCB(Process Control Block进程控制块)中包括当前进程的satp寄存器值,然后会从新进程的PCB中读取satp寄存器的值,该值为一个物理地址,存储了一张页表大小为4kb,该页表有512个PTE,4096/512=8byte,8*8=64bit,(PTE中取低54位,高44位为PPN,低10位为页表的权限信息等),每个PPN代表一个物理地址,然后取虚拟地址的低39位中的高九位,2的9次方等于512对应512个PPN,表示一级页表的PPN索引,找到第一张页表中的PPN后,进行寻址找到第二张页表,用虚拟地址的搞39位的中间9位找到二级页表的PPN索引,根据PPN找到第三张页表,同理找到第三张的PPN,取第三章页表PPN的高44位(低12位都是0)加上虚拟地址的后12位找到真实的物理地址,由于页表操作需要进行多次地址访问,开销比较大,所以一般会设计一个Cache,用于存储最近访问的虚拟地址到物理地址的映射(也就是TLB快表),当虚拟地址能从Cache中找到时会直接返回实际物理地址,不进行页表访问。

3.4-3.5物理内存分配

  • xv6使用内核末尾(end)到PHYSTOP之间的物理内存进行分配,一个内存页4kb(4096byte),使用链表记录空闲页面。分配时需要从链表中删除页面(run *r = kmem.freelist->next; kmem.freelist = r->next);释放时需要将释放的页面添加到链表中(头插)
  • kinit: main函数调用kinit,kinit调用freerange(end,PHYSTOP),freerange内for循环调用kfree,kfree会释放一个物理内存块(也就是将从起始地址位置开始的4kb内存每个字节全部置为1,由memset实现),memset释放完后kfree会将这块内存r插入到kmem.freelist的头部,然后将空闲链表的头部设置为r(也就是头插法)。
  • kalloc 用于从自由列表freelist中分配一个物理内存块(4kb),首先上锁,然后run *r = kmem.freelist,如果freelist不为空,那么将freelist指向下一个空闲块(也就是当前空闲块已经被分配出去了)kmem.freelist = r->next;
  • kmem是一个匿名结构体类型的全局变量,里面有锁和freelist,只有这一个这个类型的变量,kmem这个对象用于内存的管理

vm.c

  • void kvminit()用于创建内核页表的直接映射,先kernel_pagetable = (pagetable_t) kalloc()从freelist中分配4kb给内核页表,然后memset(kernel_pagetable,0,PGSIZE)初始化内核页表地址(清理垃圾值),然后用kvmmap将寄存器等虚拟的内存地址映射到物理地址并设置其权限
  • void kvminithart()用于为当前硬件线程(hardware thread)初始化页表,具体操作为将内核页表的物理地址写入satp寄存器中,然后刷新TLB,例如又四个硬件线程(或者有四个cpu,那么每个cpu都有一个satp寄存器,在调用此函数时这四个satp寄存器中都将存入内核页表的物理地址)
  • pte_t* walk(pagetable_t pagetable, uint64 va, int alloc),核心函数,用于多级页表寻址,具体就是实现3.1的部分功能,最后返回的是在第三张表中根据虚拟地址va找到的PTE,如果alloc为1,则代表要根据虚拟地址创建出所需的页表页(具体去读源码第82行)
  • uint64 walkaddr(pagetable_t pagetable, uint64 va),用于将虚拟地址转换成物理地址,根据给定的页表和虚拟地址先调用walk找到最后一级的PTE然后用宏PTE2PA(*pte)得到并返回实际的物理地址
  • uint64 kvmpa(uint64 va)也是用于将虚拟地址转换成物理地址,只不过他只负责转换内核的虚拟地址,因为kernel_pagetable是全局变量,所以传参时不需要传他
  • int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)主要用于

懒分配

  • 当一个进程请求分配内存的时候,操作系统会在页表中为虚拟地址分配条目,并将这些条目标记为无效,当进程首次访问地址的时候,由于页表条目无效,所以会触发一个缺页故障,在页错误处理程序中,操作系统会检查导致异常的地址是否在进程空间内,并且是否被进程请求过,如果满足,操作系统就会为这个地址分配一块物理内存,然后更新页表条目,最后重新执行导致异常的指令,如此以来只有进程被使用的时候才会分配物理内存,可以有效的节约资源