kernel/memlayout.h

这段代码是一个RISC-V处理器上的内存布局和地址映射相关的配置。RISC-V是一种开放的指令集架构(ISA),在很多领域得到了广泛应用。以下是这段代码的解释:

  1. 这段代码首先定义了一些硬件寄存器的物理地址:
    • UART0(0x10000000L):串行通信接口UART的基地址。
    • VIRTIO0(0x10001000):虚拟I/O设备的基地址。
    • CLINT(0x2000000L):核心局部中断器(Core Local Interruptor)的基地址。
    • PLIC(0x0c000000L):平台级中断控制器(Platform-Level Interrupt Controller)的基地址。
  2. 之后,定义了与中断相关的寄存器地址和宏函数。
  3. 定义了内核使用的物理内存区域:
    • KERNBASE(0x80000000L):内核程序的起始地址。
    • PHYSTOP:内核使用的物理内存的结束地址。
  4. 定义了内核栈和用户栈的内存布局。
  5. 用户内存布局定义如下:
    • 首先是文本段、原始数据段、BSS段。
    • 固定大小的栈。
    • 可扩展的堆。
    • 用户系统调用(USYSCALL)区域。
    • 中断帧(TRAPFRAME)区域,用于存储进程的中断帧。
    • 跳板区域(TRAMPOLINE),与内核空间共享。

这些配置用于设置处理器上的内存布局和地址映射。在实际系统中,硬件组件的地址、中断号等可能根据具体实现而有所不同。这段代码为一个基于RISC-V的操作系统内核提供了一个内存布局的模板。

kernel/vm.c

给定的代码实现了在 RISC-V 架构上运行的 xv6 操作系统的页表管理。

kvminit 函数通过创建内核直接映射页表并进行各种映射来初始化内核页表。它为 UART0 设备、virtio 磁盘接口、内核文本和数据段、陷阱入口/出口跳板、进程内核栈以及物理本地中断控制器 (PLIC) 分配和映射内存区域。

kvminithart 函数将硬件页表寄存器切换到内核的页表,并启用分页机制。

walk 函数返回与虚拟地址 va 对应的页面 pagetable 中 PTE 的地址。如果 alloc!=0,则会创建所需的页面表页面。

walkaddr 函数查找虚拟地址,返回物理地址或 0(如果未映射)。此函数仅可用于查找用户页面。

mappages函数为从va开始引用起始物理地址pa的虚拟地址创建PTE。va和size可能不是对齐的。成功时返回0,如果 walk() 无法分配所需的页面表页面,则返回-1。

uvmunmap函数从va开始移除npages个映射。 va必须对齐页。 映射必须存在。 可选地,它释放物理内存。

uvmcreate函数创建一个空的用户页表。 如果内存不足,则返回0。

uvmfirst函数将用户initcode加载到第一个进程的pagetable地址0中。 sz必须小于一页。

uvmalloc函数分配PTE和物理内存以使进程从oldsz增长到newsz,这不需要对齐页面大小。 它返回新大小或错误时返回0。

uvmdealloc函数取消分配用户页面以将进程大小从oldsz调整为newsz. oldsz和newsz无需对齐页面大小,而且newsz无需小于oldsz. oldsz可以大于实际进程大小。 它返回新的进程大小。

freewalk函数递归释放页表页面。所有叶映射必须已经被删除。

uvmfree函数先释放用户内存页面,然后再释放页表页面。

uvmcopy函数将父进程的页表中的内存复制到子进程的页表中。它同时复制了页表和物理内存。成功返回0,失败返回-1。在失败时会释放任何分配的页面。

uvmclear函数标记一个PTE对于用户访问无效。它由exec用于用户堆栈保护页面。

copyout函数从内核向用户空间复制内存。它将长度为len字节的数据从src复制到给定页表中虚拟地址dstva处。成功返回0,错误返回-1.

copyin函数从用户空间向内核复制内存。它将长度为len字节的数据从给定页表中虚拟地址srcva处复制到dst中。成功返回0,错误返回-1.

copyinstr函数从用户空间向内核复制以null结尾字符串(即C风格字符串)。它将最多max个字节或直到遇到’\0’停止,并将其从给定页表中虚拟地址srcva处开始进行操作并写入dst缓冲区中 。成功则返回0,否则返回-1.

kernel/kalloc.c

这段代码实现了一个物理内存分配器,用于分配给用户进程、内核栈、页表页面和管道缓冲区。分配器使用 4096 字节(4KB)的整数倍页面。

代码中包含的主要结构和函数如下:

  1. 结构 run:表示一个可用的内存块,包含一个指向下一个可用内存块的指针。
  2. 结构 kmem:管理可用内存块链表的结构,包含一个自旋锁和一个指向可用内存块链表的指针。
  3. 函数 kinit():初始化内存分配器。设置 kmem 结构的自旋锁,然后调用 freerange() 函数,将内核结束地址(end)到物理内存结束地址(PHYSTOP)之间的内存空间初始化为可用内存块。
  4. 函数 freerange(void *pa_start, void *pa_end):将给定范围内的物理内存地址空间初始化为可用内存块,并将它们添加到 kmem 的可用内存块链表中。
  5. 函数 kfree(void *pa):释放由 kalloc() 分配的物理内存页面。将要释放的内存页面填充为特定值(这里是 1),以捕获悬空引用。然后将页面添加回 kmem 的可用内存块链表中。
  6. 函数 kalloc(void):分配一个 4096 字节(4KB)的物理内存页面。从 kmem 的可用内存块链表中取一个内存块,并将其从链表中移除。如果成功分配到内存块,则将其填充为特定值(这里是 5),并将指针返回给内核。

这段代码提供了一个简单的物理内存管理功能,以满足内核和用户进程的内存分配需求。

Speed up system calls (easy)

一些操作系统(例如Linux)通过在用户空间和内核之间共享只读区域中的数据来加速某些系统调用。这消除了执行这些系统调用时需要进行内核交叉的需求。为了帮助您学习如何将映射插入到页表中,您的第一个任务是为xv6实现getpid()系统调用的此优化。
当创建每个进程时,在 USYSCALL 处映射一个只读页面(在memlayout.h中定义的虚拟地址)。在该页面开头存储一个 struct usyscall(也在 memlayout.h 中定义),并将其初始化以存储当前进程的PID。对于本实验室,已经提供了用户空间上的 ugetpid() 并且会自动使用 USYSCALL 映射。如果运行 pgtbltestugetpid 测试案例通过,则您将获得此实验室部分积分。
一些建议:

  • 您可以在 kernel/proc.cproc_pagetable() 中执行映射。
  • 选择允许用户空间仅读取页面的权限位。
  • 您可能会发现 mappages() 是有用工具。
  • 不要忘记在 allocproc() 中分配和初始化页面。
  • 确保在 freeproc() 中释放该页。