惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
人人都是产品经理
人人都是产品经理
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
V2EX
博客园 - 三生石上(FineUI控件)
Martin Fowler
Martin Fowler
WordPress大学
WordPress大学
D
Docker
S
SegmentFault 最新的问题
博客园 - 聂微东
美团技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Last Week in AI
Last Week in AI
M
MIT News - Artificial intelligence
F
Fortinet All Blogs
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The GitHub Blog
The GitHub Blog
GbyAI
GbyAI
L
LangChain Blog
Vercel News
Vercel News
博客园 - 叶小钗
MongoDB | Blog
MongoDB | Blog
Stack Overflow Blog
Stack Overflow Blog
H
Help Net Security
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
The Cloudflare Blog
Engineering at Meta
Engineering at Meta
T
Threat Research - Cisco Blogs
T
Threatpost
Scott Helme
Scott Helme
T
Tailwind CSS Blog
Latest news
Latest news
Stack Overflow Blog
Stack Overflow Blog
Blog — PlanetScale
Blog — PlanetScale
The Register - Security
The Register - Security
罗磊的独立博客
P
Proofpoint News Feed
腾讯CDC
S
Schneier on Security
雷峰网
雷峰网
A
About on SuperTechFans
T
Tenable Blog
F
Full Disclosure
Cyberwarzone
Cyberwarzone
博客园_首页
有赞技术团队
有赞技术团队
K
Kaspersky official blog

文章列表

游戏玩后感:ReLief:献给亲爱的你 我的周边(谷子)分享 游戏玩后感:Kanon 简谱:致真实的你 《Rust中常见的有关生命周期的误解》学习笔记 简谱:StarMap 简谱:かく咲きたらばいと恋ひめやも 简谱:东风 简谱:无法诉说的思念 简谱:Girlish 游戏玩后感:时钟机关的Layline 简谱:风之琶音 简谱:星空的记忆 简谱:因为遇见了你 简谱:月童 番茄简谱脚本转调器 游戏玩后感:青空下的约定:Refine 游戏玩后感:在这苍穹展翅 书籍读后感:控制论与科学方法论 游戏玩后感:恋爱表达式 游戏玩后感:樱之诗 MLIR-tutorial学习笔记 游戏玩后感:潜伏之赤途 游戏玩后感:纯爱咖啡厅:帕露菲重制版 游戏玩后感:智以泪聚 游戏玩后感:初雪樱 游戏玩后感:告别回忆:从今以后 游戏玩后感:梦灯花 游戏玩后感:金辉恋曲四重奏 游戏玩后感:五彩斑斓的世界 昇腾310P使用记录 游戏玩后感:AIR 游戏玩后感:弹丸论破 游戏玩后感:流景之海的艾佩莉亚 Xilinx_HLS上板过程记录 游戏玩后感:告别回忆2 游戏玩后感:恋爱绮谭 Faiss和Rapidsai_Raft使用记录 游戏玩后感:近月少女的礼仪 游戏玩后感:樱色之云,绯色之恋 游戏玩后感:幸运草的约定 游戏玩后感:星之梦、候鸟和丸子与银河龙 游戏玩后感:白色相簿2 Windows上使用VTune分析PyTorchExtension调用的Cpp程序 SpinalHDL上板过程记录 游戏玩后感:仰望夜空的星辰 最简单的算卦方法之一:梅花易数法 游戏玩后感:苍之彼方的四重奏 krkr引擎解包工具介绍 自定义CUDA实现PyTorch算子的四种简单方法 游戏玩后感:星空的记忆 游戏玩后感:9nine 游戏玩后感:AtriMyDearMoments 游戏玩后感:极限脱出 游戏玩后感:魔女的夜宴 SSH实现多跳代理 动漫观后感:向山进发 flv重封装H264、AAC流 动漫观后感:夏日重现 CSP模板 游戏玩后感:海沙风云 动漫观后感:灵能百分百 游戏玩后感:交响乐之雨 游戏玩后感:爱上火车LastRun 游戏玩后感:LittleBustersEX 游戏玩后感:SummerPockets 游戏玩后感:逆转裁判 Ultra96V2开发板简单使用 SpinalWorkshop实验笔记(三) SpinalWorkshop实验笔记(二) SpinalWorkshop实验笔记(一) PYNQ开发板上使用USB声卡+OSS兼容层播放音频 TestOS移植K210开发板 rCore-Tutorial-Book-v3学习笔记(七) 动漫观后感:凉宫春日的忧郁 rCore-Tutorial-Book-v3学习笔记(♭七) rCore-Tutorial-Book-v3学习笔记(六) rCore-Tutorial-Book-v3学习笔记(五) rCore-Tutorial-Book-v3学习笔记(四) rCore-Tutorial-Book-v3学习笔记(三) rCore-Tutorial-Book-v3学习笔记(二) rCore-Tutorial-Book-v3学习笔记(一) 游戏玩后感:RewritePlus MIT-6.S081-2020实验(xv6-riscv64)十一:net MIT-6.S081-2020实验(xv6-riscv64)十:mmap MIT-6.S081-2020实验(xv6-riscv64)九:fs MIT-6.S081-2020实验(xv6-riscv64)八:lock MIT-6.S081-2020实验(xv6-riscv64)七:thread MIT-6.S081-2020实验(xv6-riscv64)六:cow MIT-6.S081-2020实验(xv6-riscv64)五:lazy MIT-6.S081-2020实验(xv6-riscv64)四:traps MIT-6.S081-2020实验(xv6-riscv64)二:syscall 动漫观后感:吹响吧上低音号 MIT-6.S081-2020实验(xv6-riscv64)一:util 快速生成网络mp4视频缩略图技术 用plantuml画图示例 QQ缩略图和大图不同实现 Python制作字符图片 动漫观后感:命运石之门 Unity3D+Post_Processing_Stack_V2自定义后处理效果研究
MIT-6.S081-2020实验(xv6-riscv64)三:pgtbl
VnYzm · 2020-12-31 · via

实验文档

概述

这次实验主要涉及虚拟内存的管理,重点是和页表相关的操作。个人觉得难点主要还是在调试方面,因为一旦写到什么非法内存或者哪里内存泄漏了,基本只能抓瞎。我也是参考了github上别人的代码才最终完成了实验。

内容

这个任务比较简单,只要仿照freewalk递归遍历就行了。

void printwalk(pagetable_t pt, int dep) {
    for(int i = 0; i < 512; i++){
        pte_t pte = pt[i];
        if (pte & PTE_V) {
            for (int j = 0; j < dep - 1; j++) printf(".. ");
            printf("..%d: pte %p ", i, pte);
            uint64 child = PTE2PA(pte);
            printf("pa %p\n", child);
            if ((pte & (PTE_R|PTE_W|PTE_X)) == 0)
                printwalk((pagetable_t)child, dep + 1);
        }
    }
}
void vmprint(pagetable_t pt) {
    printf("page table %p\n", pt);
    printwalk(pt, 1);
}

A kernel page table per process

这个任务需要给每个进程添加一个独立的内核页表,两个任务的总体目的是让每个进程独立拥有一个同时映射了用户内存区和内核内存区的内核页表。这样进程在进入内核态后,可以直接在自己这个内核页表中的用户内存区和内核内存区之间传递数据,不需要经过页表切换。首先我除了给proc结构体添加了kpagetable外,额外加了一个kstackpa表示kstack的物理地址,这一步不是必须的,因为结构体里已经保存了kstack的虚拟地址了,在用之前walk一遍也不是不行。加了之后初始化在申请kstack的时候就顺便保存了物理地址:

      char *pa = kalloc();
      if(pa == 0)
        panic("kalloc");
      p->kstackpa = pa;

然后是allocproc,需要申请kpagetable并对其进行映射:

  p->kpagetable = proc_kpagetable();
  if (p->kpagetable == 0) {
      freeproc(p);
      release(&p->lock);
      return 0;
  }

  if (mappages(p->kpagetable, (uint64)p->kstack, PGSIZE,
               (uint64)p->kstackpa, PTE_R | PTE_W) != 0) {
      freeproc(p);
      release(&p->lock);
      return 0;
  }

proc_kpagetable的实现我写在vm.c里,借鉴了kvminit函数:

pagetable_t proc_kpagetable(void) {
    pagetable_t kpagetable = (pagetable_t) kalloc();
    memset(kpagetable, 0, PGSIZE);

    if (mappages(kpagetable, UART0, PGSIZE, UART0, PTE_R | PTE_W) != 0) return 0;
    if (mappages(kpagetable, VIRTIO0, PGSIZE, VIRTIO0, PTE_R | PTE_W) != 0) return 0;
    if (mappages(kpagetable, PLIC, 0x400000, PLIC, PTE_R | PTE_W) != 0) return 0;
    if (mappages(kpagetable, KERNBASE, (uint64)etext-KERNBASE, KERNBASE, PTE_R | PTE_X) != 0) return 0;
    if (mappages(kpagetable, (uint64)etext, PHYSTOP-(uint64)etext, (uint64)etext, PTE_R | PTE_W) != 0) return 0;
    if (mappages(kpagetable, TRAMPOLINE, PGSIZE, (uint64)trampoline, PTE_R | PTE_X) != 0) return 0;

    return kpagetable;
}

值得注意的是这个CLINT没有被映射,我也不知道这个区域代表什么什么意思,但实验文档中提到:

However, this scheme does limit the maximum size of a user process to be less than the kernel’s lowest virtual address. After the kernel has booted, that address is 0xC000000 in xv6, the address of the PLIC registers;

memlayout.h中CLINT对应的常数是0x2000000,比0xC000000小,按照文档的指示是可以被用户区覆盖的,所以没有映射(映射了可能后面再映射用户内存会报remap错误)。

Update2021.6.29:这个CLINT用来存储发生时钟中断时的一些额外信息,由于存取这些信息的过程都发生在机器态,不受页表控制,所以这个区域无需映射(甚至我认为原版的内核页表也不需要映射这个区域)。

scheduler函数中切换进程后需要切换satp寄存器为这个进程的内核页表(因为现在在内核态)并刷新TLB,这一步还是比较简单的:

        p->state = RUNNING;
        c->proc = p;
        w_satp(MAKE_SATP(p->kpagetable));
        sfence_vma();
        swtch(&c->context, &p->context);

最后是freeproc,基本也是仿照对用户页表的操作依样画葫芦:

  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  if (p->kpagetable)
      proc_kfreepagetable(p->kpagetable);
  p->pagetable = 0;
  p->kpagetable = 0;

proc_kfreepagetable我也写在vm.c里,基本上是仿照freewalk函数写的,但是freewalk函数要求把最底层页表的映射全部解除了才能调用,否则会报错。我嫌麻烦就直接一步了,遇到最底层就不递归,直接只释放页表:

void proc_kfreepagetable(pagetable_t pagetable) {
    for(int i = 0; i < 512; i++){
        pte_t pte = pagetable[i];
        if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
            uint64 child = PTE2PA(pte);
            proc_kfreepagetable((pagetable_t)child);
            pagetable[i] = 0;
        }
    }
    kfree((void*)pagetable);
}

Simplify copyin/copyinstr

这个任务要求实现对copyin和copyinstr函数的完全替代,实际上这两个函数就是上面所说的直接在进程自己的内核页表中的用户内存区和内核内存区之间传递数据,基本就一个简单的memcpy操作,而且实验文件也已经给了,不用你实现,真正要你做的是在fork、exec、sbrk三个函数中实现内核页表的管理操作。

先看fork函数,fork函数里复制了用户页表,那就依葫芦画瓢,也把内核页表复制一份:

  if(kvmcopy(np->pagetable, np->kpagetable, 0, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }

注意这里因为两个进程kstack的物理地址不同,所以不能是两个进程的内核页表互相复制,而应该是新进程的内核页表复制自己的用户页表,因为新进程在申请内核页表时内核区已经映射完毕了,所以只需复制用户区即可,这里的复制指浅拷贝,即不是拷贝物理内存而是让两个页表指向同一个物理地址。

kvmcopy函数在vm.c里,基本可以调已有的函数:

int kvmcopy(pagetable_t old, pagetable_t new, uint64 st, uint64 en) {
    pte_t *pte;
    uint64 pa, i;
    uint flags;

    if (en > PLIC) return -1;

    st = PGROUNDUP(st);

    for(i = st; i < en; i += PGSIZE) {
        if((pte = walk(old, i, 0)) == 0)
            panic("kvmcopy: pte should exist");
        if((*pte & PTE_V) == 0)
            panic("kvmcopy: page not present");
        pa = PTE2PA(*pte);
        flags = PTE_FLAGS(*pte) & (~PTE_U);
        if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0) goto err;
    }
    return 0;
err:
    uvmunmap(new, 0, i / PGSIZE, 0);
    return -1;
}

补个自己的错误,就是忘了st = PGROUNDUP(st) 一句,这句很重要,因为对物理内存的操作都是以页为单位的,如果这句忘了,这个没对齐的地址可能就会落到之前某个已经映射过的页中间,导致重映射错误。害我调试了一个晚上……

然后是exec函数,这里就有点坑了,我们观察到原函数在处理用户页表的时候是先开辟一个新的用户页表,然后该映射映射,再把老的用户页表释放掉,很自然的也会把这番操作套到内核页表上。但是,这样会爆空间!我被这个卡了很久,后来看了实验的测试程序,发现测试非常极限,会先不断申请空间直到空闲空间只剩一丁点的时候运行你的exec函数,这时你要是先开辟一个新的内核页表,老的内核页表还在,自然爆空间。看了别人的代码才知道正确做法是直接把内核页表的用户区全部解除映射,再重新映射上新的用户页表,这样有两个好处,一来省空间;二来不用释放老的内核页表,注意这个删除不是随便删就了事的,因为你当前是在内核态,这个进程正用着这个老页表,删了它直接翻车,还得先把satp切换为新页表并刷新TLB,这个我也调了很久,用上面的方法就不需要考虑这个问题。所以说内存这种东西,尽可能重用,谨慎删除:

  oldpagetable = p->pagetable;
  p->pagetable = pagetable;
  kvmdealloc(p->kpagetable, p->sz, 0);
  if (kvmcopy(p->pagetable, p->kpagetable, 0, sz) < 0) goto bad;

开辟新内核页表的写法比上面还麻烦很多,正确写法空间效率和代码效率均强,真的服气。kvmdealloc函数代码如下,如前所述,只要解除映射即可:

uint64 kvmdealloc(pagetable_t kpagetable, uint64 oldsz, uint64 newsz) {
    if(newsz >= oldsz)
        return oldsz;

    if(PGROUNDUP(newsz) < PGROUNDUP(oldsz)){
        int npages = (PGROUNDUP(oldsz) - PGROUNDUP(newsz)) / PGSIZE;
        uvmunmap(kpagetable, PGROUNDUP(newsz), npages, 0);
    }
    return newsz;
}

sbrk函数直接调用proc.c里的growproc函数,所以直接改这个函数。类似fork函数,内核页表只要随着用户页表来动就行,用户内存扩大,它就复制扩大的部分,用户内存缩小,它就对应解除缩小部分的映射:

  if(n > 0){
    if((sz = uvmalloc(p->pagetable, sz, sz + n)) == 0) {
      return -1;
    }
    if (kvmcopy(p->pagetable, p->kpagetable, p->sz, p->sz + n) != 0) {
        return -1;
    }
  } else if(n < 0){
    sz = uvmdealloc(p->pagetable, sz, sz + n);
    kvmdealloc(p->kpagetable, p->sz, p->sz + n);
  }

总结一下,这个实验难度其实是非常高的,我也因此写加调了好几天。我本人一开始的做法是照葫芦画瓢,直接修改uvmalloc和uvmdealloc函数让其同时处理用户页表和内核页表,结果这种设计到最后调不下去了。原因在于内核页表实际上就是对用户页表的一个引用,所以直接用一个浅拷贝函数kvmcopy就可以轻松直接地完成大量操作,再加上一个解除绑定的kvmdealloc,这种设计就非常简洁且容易调试,但是需要思考。这也说明架构和设计极其重要。