mit lab3
kernel/vm.c解读
点击查看代码
pagetable_t kernel_pagetable;
extern char etext[]; // kernel.ld sets this to end of kernel code.
extern char trampoline[]; // trampoline.S
/*
* create a direct-map page table for the kernel.
*/
void
kvminit()
{
kernel_pagetable = (pagetable_t) kalloc();
memset(kernel_pagetable, 0, PGSIZE);
// uart registers
kvmmap(UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
kvmmap(VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
kvmmap(CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
kvmmap(PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
kvmmap((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(TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
}
点击查看代码
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(;;){
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;
}
点击查看代码
void
freewalk(pagetable_t pagetable)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
freewalk((pagetable_t)child);
pagetable[i] = 0;
} else if(pte & PTE_V){
panic("freewalk: leaf");
}
}
kfree((void*)pagetable);
}
define PTE2PA(pte) (((pte) >> 10) << 12) 从页表项中提取出物理地址的高部分,并将其左移到正确的位置,以便与页内偏移量组合成完整的物理地址
typedef uint64 *pagetable_t;
一、pagetable_t kernel_pagetable; 定义一个内核的页表
kvminit()对这个页表进行初始化,调用void kvmmap(uint64 pa, uint64 va, uint64 size, int perm)。其中将物理地址和虚拟地址相同,保证内核启动时无需地址转换
pa:物理地址(Physical Address),要映射的物理内存的起始地址。
va:虚拟地址(Virtual Address),要映射到的虚拟内存的起始地址。
size:映射的大小,以字节为单位。
perm:权限标志,指定映射的内存区域的访问权限,例如读(PTE_R)、写(PTE_W)、执行(PTE_X)等。
二、walk函数用于对页表进行遍历
pte_t * walk(pagetable_t pagetable, uint64 va, int alloc)
¥¥三级页表
将虚拟地址空间划分为多个层次,使用逐级索引的方式进行地址转换,有效减少了页表占用的内存空间。
1提取 PGD Index:从虚拟地址中提取页全局目录索引,使用该索引查找页全局目录,得到对应的 PGD Entry。
2提取 PUD Index:如果 PGD Entry 有效,从虚拟地址中提取页上级目录索引,使用该索引查找 PUD,得到对应的 PUD Entry。
3提取 PT Index:如果 PUD Entry 有效,从虚拟地址中提取页表索引,使用该索引查找页表,得到对应的 PTE。
4计算物理地址:如果 PTE 有效,将 PTE 中存储的物理页框号与虚拟地址中的页内偏移组合,得到最终的物理地址。
pte_t *pte = &pagetable[PX(level, va)]; 这是一个宏,用于根据虚拟地址 va 和当前层级 level 计算页表项的索引。
define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK)
最终返回对应虚拟地址的页表项指针
三、uint64 walkaddr(pagetable_t pagetable, uint64 va)
用于查找虚拟地址对应的物理地址,
pte = walk(pagetable, va, 0),调用 walk 函数查找虚拟地址 va 对应的页表项,alloc 参数为 0,表示不进行分配
pa = PTE2PA(*pte);最后返回对应的物理地址
define PTE2PA(pte) (((pte) >> 10) << 12) 从页表项中提取出物理地址的高部分,并将其左移到正确的位置,以便与页内偏移量组合成完整的物理地址
四、void kvmmap(uint64 va, uint64 pa, uint64 sz, int perm) kernel_pagetable中添加一个虚拟地址到物理地址的映射,内部实现调用mappages这个函数通常在操作系统内核初始化时使用
五、uint64 kvmpa(uint64 va) kernel virtual memory to physical address虚拟地址到物理地址
pte = walk(kernel_pagetable, va, 0);使用walk函数知道对于的页表项,之后调用 pa = PTE2PA(pte);转化为物理地址,最后加上偏移量
六、mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) 将va向下取整后把虚拟地址按页存储,将虚拟内存页面映射到物理内存页面
七、uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) 解除用户虚拟内存的映射
调用 walk(pagetable, a, 0) 查找对应的页表项(PTE)。
检查标记为有效(pte & PTE_V == 0),触发 panic,因为尝试解除未映射的页面。
检查是否为叶节点(PTE_FLAGS(pte) == PTE_V),若 do_free 为真,提取物理地址PTE2PA(pte),并调用 kfree 释放该物理页面。
八、pagetable_t uvmcreate() 用于创建一个新的空用户页表
pagetable = (pagetable_t) kalloc();调用kalloc函数分配一页内存后使用memset进行零填充,返回这个页表的指针
九、uvminit(pagetable_t pagetable, uchar *src, uint sz) 函数用于将用户初始化代码加载到指定页表的地址 0 处,这是操作系统为第一个进程设置内存环境的重要步骤
uchar *src:源代码指针,指向用户初始化代码的起始位置。uint sz:大小,表示用户初始化代码的大小(以字节为单位)。
使用kalloc分配一页地址,并进行memset0填充
mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);将分配的内存页映射到虚拟地址 0 处,可读、可写、可执行,并允许用户模式访问(PTE_W | PTE_R | PTE_X | PTE_U)。
最后使用memmove(mem, src, sz);把代码拷贝在虚拟地址
十、uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) 进程分配新的页面表项(PTE)和物理内存,以扩展进程的虚拟内存空间
十一、uint64 uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) 释放进程虚拟内存中从 newsz 到 oldsz 的内存页面。
先将对齐到整页,之后计算页数,调用uvmunmap函数
十二、void freewalk(pagetable_t pagetable) 参数 pagetable 表示的是页表的起始虚拟地址,而不是物理地址。函数的目的是递归地释放页表及其子页表所占用的内存。
使用i遍历页表项的所有项,如果页表项有效(PTE_V)且没有设置读、写、执行权限(PTE_R | PTE_W | PTE_X),则认为该页表项指向一个子级页表。
提取子级页表的物理地址(PTE2PA(pte)),并递归调用 freewalk 释放子级页表。
十三void uvmfree(pagetable_t pagetable, uint64 sz)
uvmunmap释放虚拟内存映射:
从虚拟地址 0 开始,释放 PGROUNDUP(sz)/PGSIZE 个页面,并释放对应的物理内存(do_free 参数为 1)。
这一步确保了用户虚拟内存的映射被解除,对应的物理内存被释放。
调用 freewalk 函数递归释放页表及其子页表所占用的内存。
freewalk 会遍历页表,释放所有相关的页表项和子页表,并最终释放顶级页表本身。
十四、int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz)
用于将父进程的页表内容复制到子进程的页表中。按页遍历虚拟地址到sz,调用walk函数查看在Old页表里面是否有i对于的页表项,使用PTE2PA,PTE_FLAGS提取物理地址和有效位。
使用kalloc分配一页物理地址给mem,并将父进程的pa复制到mem里面,最后使用mappages将虚拟地址i映射到mem,并将映射关系存在new页表
十五、void uvmclear(pagetable_t pagetable, uint64 va) 禁止用户程序访问某个页面
调用 walk(pagetable, va, 0) 查找对应虚拟地址 va 的页表项(PTE)。
*pte &= ~PTE_U禁止用户模式程序访问该页面。
十六、int copyout(pagetable_t pagetable, uint64 dstva, char *src, uint64 len)将数据从内核空间复制到用户空间
uint64 dstva:用户空间的目标虚拟地址。 char *src:内核空间的源数据指针。
画图会发现第一次复制之后已经对齐了。函数逐页将源码复制到用户空间的pa
十七、 int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)将数据从用户空间的指定虚拟地址复制到内核空间的缓冲区。
char *dst:内核空间的目标缓冲区指针。 uint64 srcva:用户空间的源虚拟地址。
十八、int copyinstr(pagetable_t pagetable, char *dst, uint64 srcva, uint64 max)
将字符串从用户空间复制到内核空间,直到遇到字符串结束符 \0 或达到最大长度限制。
kernel/kalloc文件
void freerange(void *pa_start, void *pa_end);
extern char end[]; // first address after kernel // defined by kernel.ld.
end 是一个外部变量,定义在链接脚本 kernel.ld 中,表示内核结束后的第一个地址。
struct run {
struct run *next;
};
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
每个空闲物理页被当作一个 struct run 节点,嵌入在页的起始位置| next 指针(8字节) | 空闲内存(4088字节) |
内存管理全局结构:kmem
lock:确保对空闲链表的操作是原子性的(防止竞态条件)。freelist:指向空闲链表的第一个页(即链表头)。
一、void kinit()
{
initlock(&kmem.lock, "kmem");
freerange(end, (void*)PHYSTOP);
}
freerange 将内核结束后的内存(end 到 PHYSTOP)标记为空闲。
二、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);
}
pa_start 到 pa_end 范围内的所有页释放到空闲链表。
三、void kfree(void *pa) 释放单个页
填充垃圾数据:将整个页填充为 1,用于检测悬空指针。转换为链表节点:将物理页的起始地址强制转换为 struct run 指针。
将新节点的 next 指向当前链表头。更新链表头为新节点。
四、void * kalloc(void)
获取锁:保护对空闲链表的访问。
取出链表头:
如果链表非空(r != NULL),将链表头指向下一个节点。
释放锁。
填充垃圾数据:将分配的内存页填充为 5,用于调试。
返回地址:返回分配的内存页的起始地址。
¥¥¥¥
对于vm.c理解
关键函数void kvminit()初始化全局内核页表,直接映射物理地址到相同虚拟地址(恒等映射)。内核运行时可以直接访问物理内存和设备,无需地址转换。
注意内部调用了kvmmap,没有传递页表,而调用mappages直接传入了kernel_pagetable
进程内核页表初始化函数pagetable_t pro_kpt_init() 为每个进程创建独立的内核页表,复制全局内核页表的映射(设备、内核代码等)。
内部使用的是uvmmap,和kvmmap不一样的在用多了一个变量,需传入kernelpagetable.
那么为什么要为每个进程虚拟一个内核呢?
对于传统设计,问题在于
共享内核页表:所有进程在内核态共享同一个全局内核页表。
用户地址不可用:内核页表不包含用户空间的映射,因此内核无法直接解引用用户指针(如write(fd, buf, size)中的buf),必须手动转换到物理地址。
改进方案的核心思想
独立内核页表:每个进程拥有自己的内核页表。
包含用户映射:进程的内核页表除了内核区域的恒等映射外,还包含该进程用户空间的映射。
切换机制:进程进入内核态时,切换到它的内核页表,使得用户地址在内核中有效。
内核页表的核心映射是共享的
Simplify copyin/copyinstr
传统的 copyin 和 copyinstr 每次都需要遍历用户页表来转换虚拟地址,这种操作在频繁访问用户内存时会导致明显的性能瓶颈。
在内核的页表中,直接映射用户空间的虚拟地址到物理地址,使得内核可以直接访问用户内存,而无需每次都通过软件遍历用户页表来转换地址。
点击查看代码
void
u2kvmcopy(pagetable_t pagetable, pagetable_t kernelpt, uint64 oldsz, uint64 newsz){
pte_t *pte_from, *pte_to;
oldsz = PGROUNDUP(oldsz);
for (uint64 i = oldsz; i < newsz; i += PGSIZE){
if((pte_from = walk(pagetable, i, 0)) == 0)使用 walk 函数在用户页表中查找虚拟地址 i 对应的 PTE
panic("u2kvmcopy: src pte does not exist");
if((pte_to = walk(kernelpt, i, 1)) == 0)使用 walk 函数在内核页表中查找或创建虚拟地址 i 对应的 PTE
panic("u2kvmcopy: pte walk failed");
uint64 pa = PTE2PA(*pte_from); 从用户 PTE 中提取物理地址。
uint flags = (PTE_FLAGS(*pte_from)) & (~PTE_U);从用户 PTE 中提取标志位。清除 PTE_U 标志位,确保该内存不能在用户模式下访问。
*pte_to = PA2PTE(pa) | flags;将物理地址和修改后的标志位组合成一个新的 PTE,并写入内核页表。
}
}

浙公网安备 33010602011771号