Exercise1 源代码阅读
1. 内存管理部分: kalloc.c vm.c 以及相关其他文件代码
kalloc.c:char * kalloc(void)负责在需要的时候为用户空间, 内核栈, 页表页以及缓冲区分配物理内存, 将物理地址转为虚拟地址返回, 物理页大小为 4k.void kfree(char * v)接收一个虚拟地址, 找对对应的物理地址进行释放. xv6 使用空闲内存的前部分作为指针域来指向下一页空闲内存, 物理内存管理是以页 (4K) 为单位进行分配的. 物理内存空间上空闲的每一页, 都有一个指针域 (虚拟地址) 指向下一个空闲页, 最后一个空闲页为 NULL , 通过这种方式, kmem 只需要保存着虚拟地址空间上的 freelist 地址即可;
- // kalloc.c
- // Physical memory allocator, intended to allocate
- // memory for user processes, kernel stacks, page table pages,
- // and pipe buffers. Allocates 4096-byte pages.
- void freerange(void *vstart, void *vend);
- extern char end[]; // first address after kernel loaded from ELF file
- struct run {
- struct run *next;
- };
- struct {
- struct spinlock lock;
- int use_lock;
- struct run *freelist;
- } kmem;
xv6 让每个进程都有独立的页表结构, 在切换进程时总是需要切换页表, switchkvm 设置 cr3 寄存器的值为 kpgdir 首地址, kpgdir 仅仅在 scheduler 内核线程中使用. 页表和内核栈都是每个进程独有的, xv6 使用结构体 proc 将它们统一起来, 在进程切换的时候, 他们也往往随着进程切换而切换, 内核中模拟出了一个内核线程, 它独占内核栈和内核页表 kpgdir, 它是所有进程调度的基础. switchuvm 通过传入的 proc 结构负责切换相关的进程独有的数据结构, 其中包括 TSS 相关的操作, 然后将进程特有的页表载入 cr3 寄存器, 完成设置进程相关的虚拟地址空间环境;
- // vm.c
- ......
- // 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
- }
- // Switch TSS and h/w page table to correspond to process p.
- void
- switchuvm(struct proc *p)
- {
- pushcli();
- CPU->gdt[SEG_TSS] = SEG16(STS_T32A, &CPU->ts, sizeof(CPU->ts)-1, 0);
- CPU->gdt[SEG_TSS].s = 0;
- CPU->ts.ss0 = SEG_KDATA <<3;
- CPU->ts.esp0 = (uint)proc->kstack + KSTACKSIZE;
- ltr(SEG_TSS <<3);
- if(p->pgdir == 0)
- panic("switchuvm: no pgdir");
- lcr3(v2p(p->pgdir)); // switch to new address space
- popcli();
- }
进程的页表在使用前往往需要初始化, 其中必须包含内核代码的映射, 这样进程在进入内核时便不需要再次切换页表, 进程使用虚拟地址空间的低地址部分, 高地址部分留给内核, 主要接口:
pde_t * setupkvm(void)通过 kalloc 分配一页内存作为页目录, 然后将按照 kmap 数据结构映射内核虚拟地址空间到物理地址空间, 期间调用了工具函数 mappages;
int allocuvm(pde_t * pgdir, uint oldsz, uint newsz)在设置页表的同时分配虚拟地址 oldsz 到 newsz 的以页为单位的内存;
int deallocuvm(pde_t * pgdir, uint oldsz, uint newsz)则将 newsz 到 oldsz 对应的虚拟地址空间内存置为空闲;
int loaduvm(pde_t * pgdir, char * addr, struct inode * ip, uint offset, uint sz)将文件系统上的 i 节点内容读取载入到相应的地址上, 通过 allocuvm 接口为用户进程分配内存和设置页表, 然后调用 loaduvm 接口将文件系统上的程序载入到内存, 便能够为 exec 系统调用提供接口, 为用户进程的正式运行做准备;
当进程销毁需要回收内存时, 调用 void freevm(pde_t * pgdir)清除用户进程相关的内存环境, 其首先调用将 0 到 KERNBASE 的虚拟地址空间回收, 然后销毁整个进程的页表;
pde_t * copyuvm(pde_t * pgdir, uint sz)负责复制一个新的页表并分配新的内存, 新的内存布局和旧的完全一样, xv6 使用这个函数作为 fork()底层实现.
Exercise2 带着问题阅读
1.XV6 初始化之后到执行 main.c 时, 内存布局是怎样的(其中已有哪些内容)?
内核代码存在于物理地址低地址的 0x100000 处, 页表为 main.c 文件中的 entrypgdir 数组, 其中虚拟地址低 4M 映射物理地址低 4M, 虚拟地址 [KERNBASE, KERNBASE+4MB) 映射到 物理地址[0, 4MB);
紧接着调用 kinit1 初始化内核末尾到物理内存 4M 的物理内存空间为未使用, 然后调用 kinit2 初始化剩余内核空间到 PHYSTOP 为未使用. kinit1 调用前使用的还是最初的页表(也就是是上面的内存布局), 所以只能初始化 4M, 同时由于后期再构建新页表时也要使用页表转换机制来找到实际存放页表的物理内存空间, 这就构成了自举问题, xv6 通过在 main 函数最开始处释放内核末尾到 4Mb 的空间来分配页表, 由于在最开始时多核 CPU 还未启动, 所以没有设置锁机制. kinit2 在内核构建了新页表后, 能够完全访问内核的虚拟地址空间, 所以在这里初始化所有物理内存, 并开始了锁机制保护空闲内存链表;
然后 main 函数通过调用 void kvmalloc(void)函数来实现内核新页表的初始化;
最后内存布局和地址空间如下: 内核末尾物理地址到物理地址 PHYSTOP 的内存空间未使用, 虚拟地址空间 KERNBASE 以上部分映射到物理内存低地址相应位置.
- // kalloc.c
- // Initialization happens in two phases.
- // 1. main() calls kinit1() while still using entrypgdir to place just
- // the pages mapped by entrypgdir on free list.
- // 2. main() calls kinit2() with the REST of the physical pages
- // after installing a full page table that maps them on all cores.
- 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;
- }
- // kmap.c
- ......
- // 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
- };
- ......
2.XV6 的动态内存管理是如何完成的? 有一个 kmem(链表), 用于管理可分配的物理内存页.(vend=0x00400000, 也就是可分配的内存页最大为 4Mb)
详见 "Exercise 1 源代码阅读" 部分, 已经作出完整解答.
3.XV6 的虚拟内存是如何初始化的? 画出 XV6 的虚拟内存布局图, 请说出每一部分对应的内容是什么. 见 memlayout.h 和 vm.c 的 kmap 上的注释?
main 函数通过调用 void kinit1(void * vstart, void * vend), void kinit2(void * vstart, void * vend), void kvmalloc(void)函数来实现内核新页表的初始化. 虚拟地址与物理地址的转换接口:
- // memlayout.h
- // Memory layout
- #define EXTMEM 0x100000 // Start of extended memory
- #define PHYSTOP 0xE000000 // Top physical memory
- #define DEVSPACE 0xFE000000 // Other devices are at high addresses
- // Key addresses for address space layout (see kmap in vm.c for layout)
- #define KERNBASE 0x80000000 // First kernel virtual address
- #define KERNLINK (KERNBASE+EXTMEM) // Address where kernel is linked
- #ifndef __ASSEMBLER__
- static inline uint v2p(void *a) {
- return ((uint) (a)) - KERNBASE;
- }
- static inline void *p2v(uint a) {
- return (void *) ((a) + KERNBASE);
- }
- #endif
- #define V2P(a) (((uint) (a)) - KERNBASE)
- #define P2V(a) (((void *) (a)) + KERNBASE)
- #define V2P_WO(x) ((x) - KERNBASE) // same as V2P, but without casts
- #define P2V_WO(x) ((x) + KERNBASE) // same as V2P, but without casts
内存布局:
4. 关于 XV6 的内存页式管理. 发生中断时, 用哪个页表? 一个内存页是多大? 页目录有多少项? 页表有多少项? 最大支持多大的内存? 画出从虚拟地址到物理地址的转换图. 在 XV6 中, 是如何将虚拟地址与物理地址映射的(调用了哪些函数实现了哪些功能)?
发生中断时, 将换入 CPU 的进程的页表首地址存入 cr3 寄存器; 一个内存页为 4k;XV6 页表采用的二级目录, 一级目录有 \(2^{10}\)条, 二级目录有 \(2^{10} * 2^{10}\)条; 页表项为 \(2^2\)Bytes, 故页表有 \(2^{12} / 2^2 = 2^{10} = 1024\)项; 最大支持 4G 内存;
物理内存页的申请与释放, 虚拟地址与物理地址如何映射等在 "Exercise 1 源代码阅读" 都已经详述了, 在此主要说下 mappages 接口, 虚拟地址 * va 与物理地址 * pa 映射 size 个字节, 同时赋予该页的权限 perm, 如下:
- // vm.c
- ......
- // Create PTEs for virtual addresses starting at va that refer to
- // physical addresses starting at pa. va and size might not
- // be page-aligned.
- static int
- mappages(pde_t *pgdir, void *va, uint size, uint pa, int perm)
- {
- char *a, *last;
- pte_t *pte;
- a = (char*)PGROUNDDOWN((uint)va);
- last = (char*)PGROUNDDOWN(((uint)va) + size - 1);
- for(;;){
- if((pte = walkpgdir(pgdir, a, 1)) == 0)
- return -1;
- if(*pte & PTE_P)
- panic("remap");
- *pte = pa | perm | PTE_P;
- if(a == last)
- break;
- a += PGSIZE;
- pa += PGSIZE;
- }
- return 0;
- }
- ......
参考文献
[1] xv6 虚拟内存 - 博客园 https://www.cnblogs.com/hehao98/p/10683588.html
[2] xv6 virtual memory-hexo http://linbo.github.io/2017/10/01/xv6-vm
[3] xv6 内存管理 - 简书 https://www.jianshu.com/p/13921afe0fde
[4] xv6 内存管理 - CSDN
来源: http://www.bubuko.com/infodetail-3087460.html