三、xv6 for 2025 内存实验专项(COW,Lazy_Allocation,Superpage)
三、xv6的内存实验专项
1. 写时复制 (Copy-on-Write, COW)
核心机制:
- Fork 优化:在执行 fork 系统调用时,不再立即为子进程拷贝物理内存,而是将父子进程的用户页表中原本可写的页面(PTE_W)统一改为只读。
- 状态标记:利用 PTE 中的软件保留位(PTE_RSW)将该页标记为“COW 待复制”状态。
实现要点:
- COW 权限位:引入专门的 COW 标志位。当触发 usertrap(写异常)时,内核通过该位判断错误原因。若该页标记为 COW,则清除该标志,并为进程分配新的物理页,拷贝内容后建立独立的读写映射。
- 引用计数:为每个物理内存块引入引用计数。当多个进程的 PTE 指向同一个物理块时,计数增加。只有当所有相关进程都释放了对该页的引用(计数归零)时,内核才会真正回收该物理块。
2. 延迟分配 (Lazy Allocation, LA)
核心机制:
- 虚假承诺:在 sbrk 系统调用中,内核仅增加进程的规模限制(p->sz),代表“声称”已分配内存,但实际并未在页表中建立映射,也未分配物理内存。
- 按需分配:只有当 CPU 真正访问(读/写/执行)该地址并触发缺页异常时,内核才会介入分配物理内存。
实现要点:
- PTE 状态:在异常发生前,对应的 PTE 条目实际上是空的。
- 异常拦截:内核在 trap 处理程序中识别出该非法访问位于合法的 sz 范围内,随后动态完成内存申请与映射补全。
3. 超级页 (Superpage)
核心机制:
- 页大小提升:引入 PTE_S 作为超级页标志。普通页的大小通常为 4KB(注:原文 2KB 疑似笔误,标准 RISC-V 为 4KB),而超级页的大小提升至 2MB。
- 对齐要求:超级页必须严格按照 2MB 边界对齐。
技术优势:
- TLB 效率:通过单一的 PTE 条目覆盖更大的内存范围,显著减少 TLB(地址转换缓存)的缺失率,提升大内存访问性能。
1. 物理块初始化,kalloc和kfree
run在这代表物理块节点,因为struct run*是pa强转得到
struct run {
struct run* next;
};
struct {
struct spinlock lock;
struct run* freelist;
} kmem;
struct {
struct spinlock reflock;
uint8 ref[PNUM];
} refs;
在引入 COW (Copy-on-Write) 机制后,内存的释放逻辑由“直接回收”转变为基于引用计数的延迟回收。
- 回收触发机制:kfree 函数不再直接释放物理内存,而是先将目标物理页的引用计数减 1。
- 释放判定:只有当引用计数降至 0 时,才表明该物理块已不再被任何进程(父进程或子进程)使用,此时内核才会执行真正的释放操作。
- 初始化逻辑:在系统启动阶段,freerange 会将所有物理页的初始引用计数设为 1。随后,通过对每个物理块调用 kfree,利用上述“减一”逻辑将其计数归零,从而完成物理块的初始化——即将其挂载到空闲链表(Free List)的头节点,并更新链表入口。
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.
uint64 index = (uint64)pa / PGSIZE;
// 引用计数-1,若减为0,释放
acquire(&refs.reflock);
--refs.ref[index];
if (refs.ref[index] > 0) {
release(&refs.reflock);
return;
}
release(&refs.reflock);
r = (struct run*)pa;
memset(pa, 1, PGSIZE);
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
void freerange(void* pa_start, void* pa_end) {
char* p;
p = (char*)PGROUNDUP((uint64)pa_start);
for (int i = 0; i < PNUM; i++) {
refs.ref[i] = 1;
}
for (; p + PGSIZE <= (char*)pa_end; p += PGSIZE) {
kfree(p);
}
}
若分配页,说明物理块才分配出去,引用计数从0变为1
void* kalloc(void) {
struct run* r;
acquire(&kmem.lock);
r = kmem.freelist;
if (r) kmem.freelist = r->next;
release(&kmem.lock);
uint64 index;
if (r) {
index = (uint64)r / PGSIZE;
acquire(&refs.reflock);
refs.ref[index] = 1;
release(&refs.reflock);
} else
return 0;
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
2. COW and Lazy_Allocation
首先是kfork函数和sys_sbrk函数。kfork这里分配内存是调用了uvmcopy,将父进程的页表复制给子进程;而sys_sbrk则是只加尺寸不分配物理块。
int kfork(void) {
int i, pid;
struct proc* np;
struct proc* p = myproc();
// Allocate process.
if ((np = allocproc()) == 0) {
return -1;
}
// Copy user memory from parent to child.
if (uvmcopy(p->pagetable, np->pagetable, p->sz) < 0) {
freeproc(np);
release(&np->lock);
return -1;
}
np->sz = p->sz;
...
}
uint64 sys_sbrk(void) {
uint64 addr;
int t;
int n;
argint(0, &n);
argint(1, &t);
addr = myproc()->sz;
if (t == SBRK_EAGER || n < 0) {
if (growproc(n) < 0) {
return -1;
}
} else {
// Lazily allocate memory for this process: increase its memory
// size but don't allocate memory. If the processes uses the
// memory, vmfault() will allocate it.
if (addr + n < addr) return -1;
myproc()->sz += n;
}
return addr;
}
uvmcopy 核心逻辑:
在 fork 过程中,uvmcopy 遍历父进程页表。对于所有具有可写权限(PTE_W)的页面,内核不再分配新内存,而是采取以下步骤:
- 权限剥离与标记:清除子进程 PTE 中的 PTE_W 位,并同步添加 PTE_C 标志。这确保了未来子进程的写操作都能被硬件捕获并触发缺页异常。
- 建立共享映射:使子进程的页表项指向该现有物理块。
- 更新引用计数:完成映射后,该物理块的引用计数加 1。此时,该物理内存处于“多进程共享”状态,直至某一方触发 COW 拷贝或进程退出。
int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) {
pte_t* pte;
uint64 pa, i;
uint flags;
char* mem;
for (i = 0; i < sz; i += PGSIZE) {
if ((pte = walk(old, i, 0)) == 0) continue; // page table entry hasn't been allocated
if ((*pte & PTE_V) == 0) continue; // physical page hasn't been allocated
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if (flags & PTE_W) flags = (flags | PTE_C) & ~PTE_W;
*pte = PA2PTE(pa) | flags;
if (mappages(new, i, PGSIZE, pa, flags) != 0) {
goto err;
}
addref(pa);
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
读时不会进行对PTE_C页的物理块分配,只有在写时才会,而对PTE_W权限但是缺页、PTE_C | ~PTE_W权限的地址写入时,会在用户态触发写时缺页错误,分别进行多页惰页分配和写时拷贝,由kernel下的usertrap进行异常的处理:
uint64 usertrap(void) {
if (r_scause() == 8) {
...
} else if ((which_dev = devintr()) != 0) {
// ok
//
} else if (r_scause() == 15) {
uint64 va = r_stval();
if (va >= p->sz) {
setkilled(p);
goto err;
}
pte_t* pte = walk(p->pagetable, va, 1);
// 如果是取消映射了以后呢?就不能访问了,这个时候*pte应该是0?
// 判断是否是缺页错误,即未分配页,未设置pte
if (!(*pte)) {
// 写时缺页,分配页
char* mem = kalloc();
if (mem == 0) {
setkilled(p);
}
if (mappages(p->pagetable, va, PGSIZE, (uint64)mem, PTE_U | PTE_R | PTE_W) != 0) {
kfree((void*)mem);
setkilled(p);
}
}
// 写时遇到错误,判断是否是cow页
else if (*pte & PTE_C) {
// 只可能是写错误,并且是pte_c
// 写时拷贝
if (cowalloc(p->pagetable, va) == 0) {
setkilled(p);
goto err;
}
} else {
// 如果不是cow页的话,也是杀死进程
setkilled(p);
goto err;
}
} else {
printf("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
printf(" sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
setkilled(p);
}
err:
if (killed(p)) kexit(-1);
...
}
首先判断访问的地址是否合理,合理的话在看所在页是否分配物理块,若未分配,则分配物理块,并建立新映射(Lazy_Allocation);若已分配,判断是否为PTE_C块,若是则说明是未分配物理块的可写内存区域,为其分配一个物理块(COW)。其他错误无需分配物理块,直接杀死进程。
此外,copyout是kernel态下对user空间的写入,也会触发PTE_C的判断
int copyout(pagetable_t pagetable, uint64 dstva, char* src, uint64 len) {
uint64 n, va0, pa0;
pte_t* pte;
// 修改copyout,向用户空间写数据时,若遇到cow,分配页
while (len > 0) {
va0 = PGROUNDDOWN(dstva);
if (va0 >= MAXVA) {
return -1;
}
pte = walk(pagetable, va0, 0);
if (pte == 0 || (*pte & PTE_V) == 0 || (*pte & PTE_U) == 0) {
return -1; // 无效页表项
}
pa0 = walkaddr(pagetable, va0);
// 判断写入的页是否为cow
if (ISCOW(*pte)) {
pa0 = cowalloc(pagetable, va0);
pte = walk(pagetable, va0, 0);
}
// 判断有无写权限,并写入
if (!(PTE_FLAGS(*pte) & PTE_W)) return -1;
n = PGSIZE - (dstva - va0);
if (n > len) n = len;
memmove((void*)(pa0 + (dstva - va0)), src, n);
len -= n;
src += n;
dstva = va0 + PGSIZE;
}
return 0;
}
专门为PTE_C页分配物理块的函数cowalloc,cowalloc需要将原先父进程PTE_W块引用计数-1,因为子进程将获得自己的物理块。但是这里有两种情况,当前子进程是唯一映射到这个物理块的进程,那么不需要再分配并复制物理块数据了,可以直接将这个物理块映射给子进程,将子进程权限改掉即可;另外就是并非唯一映射的进程,那么就得分配一个物理块。
uint64 cowalloc(pagetable_t pagetable, uint64 va) {
pte_t* pte = walk(pagetable, va, 0);
uint flags = (PTE_FLAGS(*pte) & ~PTE_C) | PTE_W;
if (!ISCOW(*pte)) {
panic("cowalloc fail : not a cow");
}
uint64 pa = PTE2PA(*pte);
uint64 mem;
// 减小引用计数,当引用计数减为0,只改变状态位,再将引用计数++,避免并发进程影响
// 当引用计数不为0,分配内存
uint64 ref = subref(pa);
if (ref == 0) {
*pte = PA2PTE(pa) | flags;
addref(pa);
return pa;
}
// 若并非最后进程,分配
if ((mem = (uint64)kalloc()) == 0) {
panic("cowalloc : kalloc fail");
}
memmove((void*)mem, (char*)pa, PGSIZE);
*pte = PA2PTE(mem) | flags;
uint64 test = walkaddr(pagetable, va);
return mem;
}
3. Superpage
struct {
struct spinlock lock;
struct run* freelist;
struct run* superreserve;
} kmem;
void freerange(void* pa_start, void* pa_end) {
char* p;
p = (char*)SUPERPGROUNDUP((uint64)pa_start);
for (int i = 0; i < SUPERNUM; i++, p += SUPERPGSIZE)
superfree(p);
p = (char*)PGROUNDUP((uint64)p);
for (; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
对于superpage,采用的方法是添加一个superpage链表,预留SUPERNUN个超级块。
void* superalloc(void) {
struct run* r;
acquire(&kmem.lock);
r = kmem.superreserve;
if (r) kmem.superreserve = r->next;
release(&kmem.lock);
if (r) memset((char*)r, 5, SUPERPGSIZE); // fill with junk
return (void*)r;
}
void superfree(void* pa) {
struct run* r;
if ((char*)pa < end || (uint64)pa >= PHYSTOP) panic("superfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, SUPERPGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.superreserve;
kmem.superreserve = r;
release(&kmem.lock);
}
添加函数superalloc和superfree,用于处理超级块的分配和回收。
以上是superpage物理块的分配与回收,但是难的点在于分配和回收时的内存对齐。我们先追踪请求时的函数调用:sys_sbrk->growproc->uvmalloc
uint64 uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm) {
if (newsz < oldsz) return oldsz;
// 页对齐,从这个位置开始
oldsz = PGROUNDUP(oldsz);
uint64 a = oldsz;
uint64 superbegin = SUPERPGROUNDUP(oldsz), superend = SUPERPGROUNDDOWN(newsz);
if (superbegin < superend) {
// 先分配普通页,若需要分配超级页,则分到超级页对齐的地方就停止,开始分配超级页
// 分配完超级页以后,继续分配普通页直到分配完毕
for (; a < superbegin; a += PGSIZE)
if (pagemalloc(pagetable, a, PGSIZE, xperm) != 0) goto err;
for (; a < superend; a += SUPERPGSIZE) {
if (pagemalloc(pagetable, a, SUPERPGSIZE, xperm | PTE_S) != 0) {
goto err;
}
}
}
for (; a < newsz; a += PGSIZE)
if (pagemalloc(pagetable, a, PGSIZE, xperm) != 0) goto err;
return newsz;
err:
// 分配页失败,则从oldsz开始释放页直到地址a的位置,返回0
uvmdealloc(pagetable, a, oldsz);
return 0;
}
int growproc(int n) {
// paddr((uint64)n);
uint64 sz;
struct proc *p = myproc();
sz = p->sz;
if (n > 0) {
if ((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
return -1;
}
} else if (n < 0) {
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}
超级页分配范围的确定
当内存申请范围从 oldsz 扩展到 newsz 时,内核并不会盲目分配超级页。为了确保分配的精确性,只有处于 SUPERPGROUNDUP(oldsz) 到 SUPERPGROUNDDOWN(newsz) 之间的区域才会被考虑用于映射超级块。通过这种方式,我们能够确定一个名为 superbegin 和 superend 的边界。只有当 superend > superbegin 时,才说明在该申请范围内存在分配超级块的必要空间。(如果申请了一个超级块大小的内存结果仍然可能是普通页分配,这是因为首先一定要是superpgsize对齐,其次如果不在oldsz到newsz中间分配superpg,很可能会在末尾会造成更大的内存浪费,因为原先的pgsize转变为了superpgsize,粒度更粗了)
对齐与碎片化的考量
需要注意的是,即使申请的内存总量达到了超级页的大小,最终也可能仅以普通页的形式分配。这首先是因为超级页必须严格满足 superpgsize 地址对齐;其次,如果分配的超级页未能完全落在 oldsz 到 newsz 的中间区间,由于超级页的分配粒度比普通页粗得多(2MB对比4KB),在末尾强行分配超级页反而可能导致比普通页映射更严重的内存浪费。
分段分配的实现路径
在实际映射过程中,分配动作被拆分为三个阶段:首先分配从 oldsz 初始位置到 superbegin 之间的普通页;接着分配从 superbegin 到 superend 之间的超级块;最后分配从 superend 到 newsz 剩余部分的普通页。如果在预判阶段发现没有必要或无法分配超级页,则系统会退回到常规逻辑,直接完成 oldsz 到 newsz 之间的普通页分配。这种分段处理模式既保证了地址空间的连续性,又实现了大页与小页的混合高效管理。
这里引入了pagemalloc函数,如果要手动调用kalloc和superalloc会比较麻烦,所以将这两个做了一个封装。
uint64 pagemalloc(pagetable_t pagetable, uint64 a, uint64 pgsz, int xperm) {
char* mem;
if (pgsz == SUPERPGSIZE) {
if ((mem = superalloc()) == 0) {
return 1;
}
memset(mem, 0, pgsz);
if (mappages(pagetable, a, pgsz, (uint64)mem, PTE_R | PTE_U | xperm) != 0) {
superfree(mem);
uvmdealloc(pagetable, a + pgsz, a);
return 1;
}
} else {
if ((mem = kalloc()) == 0) {
return 1;
}
memset(mem, 0, pgsz);
if (mappages(pagetable, a, pgsz, (uint64)mem, PTE_R | PTE_U | xperm) != 0) {
kfree(mem);
uvmdealloc(pagetable, a + pgsz, a);
return 1;
}
}
return 0;
}
在超级页(Superpage)的分配逻辑中,xperm 权限位引入了 PTE_S 标记。从页表结构的角度看,超级页的引入并未改变页表树的中间层级架构,其核心影响仅作用于最终的叶子节点。中间节点依然负责存放指向下一级页表的页表项,而叶子节点则直接完成物理地址的映射。对于硬件而言,超级页与普通页在地址映射的本质上并无差别,PTE_S 标志位仅仅是提供了一种身份判定,指示硬件在地址转换时采用不同的解析规则。
然而,超级页映射对物理空间管理的影响是巨大的。如果将所有的末级页表项都映射为超级页,单次映射所覆盖的物理空间将从 4KB 扩展至 2MB,增长了 512 倍。在 xv6 采用的虚拟内存机制中,标准的 39 位虚拟地址被划分为三级索引:VPN2(9)、VPN1(9)、VPN0(9) 以及 12 位的偏移量 offset。在常规查找流程中,CPU 会根据每一级 VPN 在对应的页表中逐层检索,直到在最底层找到对应的物理页。页表项(PTE)的高位存储着物理页号,低位则用于存放各类权限与状态标志。
超级页的关键特征在于它“跳过”了最底层的偏移细分。对于普通页,其偏移量固定为 12 位;而超级页则将原本属于 VPN0 的 9 位索引空间与底层的 12 位偏移量合并,构成了一个共计 21 位的超大偏移量。这意味着超级页的映射直接在页表树的倒数第二级(Level 1)就宣告结束并直接指向物理地址,其逻辑层级实际上比普通页高出了一级。通过将 VPN0 纳入偏移量,超级页显著减少了地址转换时的访存次数。
对于mappages,作用是建立从va开始,size大小的虚拟内存区域进行映射,映射到从pa开始size大小的物理块。
int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) {
uint64 a, pgsz;
pte_t* pte;
if ((va % PGSIZE) != 0) panic("mappages: va not aligned");
if ((size % PGSIZE) != 0) panic("mappages: size not aligned");
if (size == 0) panic("mappages: size");
a = va;
while (a < va + size) {
if (perm & PTE_S) {
pgsz = SUPERPGSIZE;
if ((pte = superwalk(pagetable, a, 1)) == 0) return -1;
} else {
pgsz = PGSIZE;
if ((pte = walk(pagetable, a, 1)) == 0) return -1;
}
if (*pte & PTE_V) {
panic("mappages: remap");
}
*pte = PA2PTE(pa) | perm | PTE_V;
a += pgsz;
pa += pgsz;
}
return 0;
}
这里因为superpg和page在页表中级数的差异,引入了superwalk,看看superwalk。
pte_t* superwalk(pagetable_t pagetable, uint64 va, int alloc) {
if (va >= MAXVA) panic("superwalk");
pte_t* pte = &pagetable[PX(2, va)];
if (*pte & PTE_V) {
pagetable = (pagetable_t)PTE2PA(*pte);
} else {
if (!alloc || (pagetable = (pde_t*)kalloc()) == 0) return 0;
memset(pagetable, 0, PGSIZE);
*pte = PA2PTE(pagetable) | PTE_V;
}
return &pagetable[PX(1, va)];
}
superwalk首先拿到VPN2,在当前顶级页表中找到下一级页表地址,再拿到VPN1从二级页表中拿到superpg对应的pte。
以上是superpg的分配,接下来是superpg的回收,以及回收一部分时superpg如何被分解成多个小pg。
void uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free) {
uint64 a, totalsz = npages * PGSIZE;
pte_t* pte;
int sz = PGSIZE;
if ((va % PGSIZE) != 0) panic("uvmunmap: not aligned");
for (a = va; a < va + totalsz; a += sz) {
if ((pte = walk(pagetable, a, 0)) == 0) // leaf page table entry allocated?
continue;
if ((*pte & PTE_V) == 0) // has physical page been allocated?
continue;
if (*pte & PTE_S) {
sz = SUPERPGSIZE;
uint64 begin = SUPERPGROUNDDOWN(a + 1);
if (begin < va && begin + SUPERPGSIZE > va) {
// 超级页被截断,则进行降级
super2page(pagetable, begin);
pte = walk(pagetable, a, 0);
sz = PGSIZE;
}
} else
sz = PGSIZE;
if (PTE_FLAGS(*pte) == PTE_V) panic("uvmunmap: not a leaf");
if (do_free) {
uint64 pa = PTE2PA(*pte);
if (sz == SUPERPGSIZE)
superfree((void*)pa);
else
kfree((void*)pa);
}
*pte = 0;
}
}
uint64 uvmdealloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz) {
if (newsz >= oldsz) return oldsz;
if (PGROUNDUP(newsz) < PGROUNDUP(oldsz)) {
int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
uvmunmap(pagetable, PGROUNDUP(newsz), npages, 1);
}
return newsz;
}
uvmdealloc仍然是释放从PGROUNDUP(newsz)开始,大小为npages*pgsize的内存空间。在uvmunmap中,会对这片内存进行映射的取消与相对应物理块的释放,其中,对于超级页的处理,分为两种情况:只是截断部分,需要降级处理,最终释放pgsize大小当前地址所在页;超级页完全处于这片内存空间中,需要全部释放掉,无需降级处理。
void super2page(pagetable_t pagetable, uint64 begin) {
char* mem;
pte_t* pte = walk(pagetable, begin, 0);
uint perms = PTE_FLAGS(*pte) & (~(PTE_S));
uint64 pa = PTE2PA(*pte);
uint64 base = pa;
*pte = 0;
for (uint64 a = begin; a < SUPERPGSIZE + begin; a += PGSIZE, pa += PGSIZE) {
mem = kalloc();
memmove(mem, (char*)pa, PGSIZE);
mappages(pagetable, a, PGSIZE, (uint64)mem, perms);
}
superfree((void*)base);
}
对于降级函数如上。使用walk获取superpg的pte,再从superbegin开始开始拷贝超级块数据到普通块,建立va到pa的映射,最后superfree掉超级块。
int uvmcopy(pagetable_t old, pagetable_t new, uint64 sz) {
// pt("copy");
pte_t* pte;
uint64 pa, i;
uint flags;
char* mem;
int szinc = PGSIZE;
for (i = 0; i < sz; i += szinc) {
if ((pte = walk(old, i, 0)) == 0) continue;
if ((*pte & PTE_V) == 0) {
continue;
}
if (((*pte) & PTE_S) == PTE_S) {
szinc = SUPERPGSIZE;
flags |= PTE_S;
} else
szinc = PGSIZE;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
if (szinc == SUPERPGSIZE) {
if ((mem = superalloc()) == 0) goto err;
memmove(mem, (char*)pa, SUPERPGSIZE);
if (mappages(new, i, SUPERPGSIZE, (uint64)mem, flags) != 0) {
superfree(mem);
}
} else {
if ((mem = kalloc()) == 0) goto err;
memmove(mem, (char*)pa, PGSIZE);
if (mappages(new, i, PGSIZE, (uint64)mem, flags) != 0) {
kfree(mem);
goto err;
}
}
}
return 0;
err:
uvmunmap(new, 0, i / PGSIZE, 1);
return -1;
}
最后是uvmcopy部分。fork时会让子进程拷贝父进程的页表以及物理块数据,这里就也要有对超级块的拷贝,基本与原先的uvmcopy类似,只是多加了一条分枝判定是不是superpage。

浙公网安备 33010602011771号