• xv6源码阅读——虚拟内存


    目录

    说明

    内核地址空间

    进程地址空间

    kernel/main.c

    kinit()函数

    freerange()

    kvminit()函数

    kalloc()函数

    kvmmap()函数

    proc_mapstacks()函数

    kvminithart()函数

    procinit()函数


    说明

    阅读的代码是xv6-riscv版本的

    内核地址空间

    Xv6为每个进程维护一个页表,用以描述每个进程的用户地址空间,外加一个单独描述内核地址空间的页表。内核配置其地址空间的布局,以允许自己以可预测的虚拟地址访问物理内存和各种硬件资源。下图显示了这种布局如何将内核虚拟地址映射到物理地址。文件(kernel/memlayout.h) 声明了xv6内核内存布局的常量。

    QEMU模拟了一台计算机,它包括从物理地址0x80000000开始并至少到0x86400000结束的RAM(物理内存),xv6称结束地址为PHYSTOP。QEMU模拟还包括I/O设备,如磁盘接口。QEMU将设备接口作为内存映射控制寄存器暴露给软件,这些寄存器位于物理地址空间0x80000000以下。内核可以通过读取/写入这些特殊的物理地址与设备交互;这种读取和写入与设备硬件而不是RAM通信。

    进程地址空间

    每个进程都有一个单独的页表,当xv6在进程之间切换时,也会更改页表。如图2.3所示,一个进程的用户内存从虚拟地址零开始,可以增长到MAXVA (kernel/riscv.h:348),原则上允许一个进程内存寻址空间为256G。

      

    当进程向xv6请求更多的用户内存时,xv6首先使用kalloc来分配物理页面。然后,它将PTE添加到进程的页表中,指向新的物理页面。Xv6在这些PTE中设置PTE_WPTE_XPTE_RPTE_UPTE_V标志。大多数进程不使用整个用户地址空间;xv6在未使用的PTE中留空PTE_V

    我们在这里看到了一些使用页表的很好的例子。首先,不同进程的页表将用户地址转换为物理内存的不同页面,这样每个进程都拥有私有内存。第二,每个进程看到的自己的内存空间都是以0地址起始的连续虚拟地址,而进程的物理内存可以是非连续的。第三,内核在用户地址空间的顶部映射一个带有蹦床(trampoline)代码的页面,这样在所有地址空间都可以看到一个单独的物理内存页面。

    kernel/main.c

    当启动xv6时,在main.c当中会对页表进行初始化,创建,启用等操作

    1. // start() jumps here in supervisor mode on all CPUs.
    2. void
    3. main()
    4. {
    5. 、、、、、、、、、、、、//省略
    6. kinit(); // 初始化物理页
    7. kvminit(); // 创建内核页表
    8. kvminithart(); // 打开分页机制
    9. procinit(); //为每个进程分配一个内核栈
    10. 、、、、、、、、、、、、//省略
    11. scheduler();
    12. }

    kinit()函数

    在该函数当中会调用freerange()函数将范围内的物理地址进行分页

    1. void
    2. kinit()
    3. {
    4. initlock(&kmem.lock, "kmem"); //初始化锁
    5. freerange(end, (void*)PHYSTOP);//将end----PHYSTOP范围内的地址进行分页
    6. }

    分配出来的页是拿链表的形式保存下来,每当需要分配页表时,只需要从这个链表中获取就可以,

    释放页表后,再将页表插入到该链表当中。结构体声明如下

    1. struct run {
    2. struct run *next;
    3. };
    4. struct {
    5. struct spinlock lock;
    6. struct run *freelist;
    7. } kmem;

    freerange()

    下来我们看一下freerange()函数是怎样操作的

    1. void
    2. freerange(void *pa_start, void *pa_end)
    3. {
    4. char *p;
    5. p = (char*)PGROUNDUP((uint64)pa_start);
    6. for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
    7. kfree(p);
    8. }

    一个页的大小为PGSIZE(1024),我们通过循环拿到范围内每个页表的首地址

    然后调用kfree()函数

    1. // Free the page of physical memory pointed at by v,
    2. // which normally should have been returned by a
    3. // call to kalloc(). (The exception is when
    4. // initializing the allocator; see kinit above.)
    5. void
    6. kfree(void *pa)
    7. {
    8. struct run *r;
    9. if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
    10. panic("kfree");
    11. // Fill with junk to catch dangling refs.
    12. memset(pa, 1, PGSIZE);
    13. r = (struct run*)pa;
    14. acquire(&kmem.lock);
    15. r->next = kmem.freelist;
    16. kmem.freelist = r;
    17. release(&kmem.lock);
    18. }
    1. 在kfree()函数当中先查看,改首地址是否内存对齐
    2. 将内存中的每一个字节设置为1。这将导致使用释放后的内存的代码(使用“悬空引用”)读取到垃圾信息而不是旧的有效内容,从而希望这样的代码更快崩溃。

    3. 然后kfree将页面前置(头插法)到空闲列表中:它将pa转换为一个指向struct run的指针r,在r->next中记录空闲列表的旧开始,并将空闲列表设置为等于r

    注意:在进行此类操作时,要上锁,防止同时访问

    kvminit()函数

    在kvminit中使用 kvmmake (kernel/vm.c:20) 创建内核的页表。此调用发生在 xv6 启用 RISC-V 上的分页之前,因此地址直接引用物理内存。

    1. // Initialize the one kernel_pagetable
    2. void kvminit(void)
    3. {
    4. kernel_pagetable = kvmmake();
    5. }

    我们看一下kvmmake()函数

    1. // Make a direct-map page table for the kernel.
    2. pagetable_t
    3. kvmmake(void)
    4. {
    5. pagetable_t kpgtbl;
    6. kpgtbl = (pagetable_t)kalloc();
    7. memset(kpgtbl, 0, PGSIZE);
    8. // uart registers
    9. kvmmap(kpgtbl, UART0, UART0, PGSIZE, PTE_R | PTE_W);
    10. // virtio mmio disk interface
    11. kvmmap(kpgtbl, VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
    12. // PLIC
    13. kvmmap(kpgtbl, PLIC, PLIC, 0x400000, PTE_R | PTE_W);
    14. // map kernel text executable and read-only.
    15. kvmmap(kpgtbl, KERNBASE, KERNBASE, (uint64)etext - KERNBASE, PTE_R | PTE_X);
    16. // map kernel data and the physical RAM we'll make use of.
    17. kvmmap(kpgtbl, (uint64)etext, (uint64)etext, PHYSTOP - (uint64)etext, PTE_R | PTE_W);
    18. // map the trampoline for trap entry/exit to
    19. // the highest virtual address in the kernel.
    20. kvmmap(kpgtbl, TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
    21. // map kernel stacks
    22. proc_mapstacks(kpgtbl);
    23. return kpgtbl;
    24. }

    1. kvmmake 首先调用kalloc()函数分配一个物理内存页来保存根页表页。
    2. 然后调用kvmmap()函数构建映射关系。包括内核的指令和数据、物理内存的上限到 PHYSTOP,并包括实际上是设备的内存。
    3. Proc_mapstacks (kernel/proc.c:33) 为每个进程分配页表,并且完成映射。

    下面我们在深入看看

    kalloc()函数

    1. // Allocate one 4096-byte page of physical memory.
    2. // Returns a pointer that the kernel can use.
    3. // Returns 0 if the memory cannot be allocated.
    4. void *
    5. kalloc(void)
    6. {
    7. struct run *r;
    8. acquire(&kmem.lock);
    9. r = kmem.freelist;
    10. if(r)
    11. kmem.freelist = r->next;
    12. release(&kmem.lock);
    13. if(r)
    14. memset((char*)r, 5, PGSIZE); // fill with junk
    15. return (void*)r;
    16. }

    该函数作用就是分配一个空闲页表,从kmem空闲链表中拿取

    kvmmap()函数

    1. // add a mapping to the kernel page table.
    2. // only used when booting.
    3. // does not flush TLB or enable paging.
    4. void kvmmap(pagetable_t kpgtbl, uint64 va, uint64 pa, uint64 sz, int perm)
    5. {
    6. if (mappages(kpgtbl, va, sz, pa, perm) != 0)
    7. panic("kvmmap");
    8. }

    kvmmap(kernel/vm.c:127)调用mappages(kernel/vm.c:138),mappages将范围虚拟地址到同等范围物理地址的映射装载到一个页表中。它以页面大小为间隔,为范围内的每个虚拟地址单独执行此操作。对于要映射的每个虚拟地址,mappages调用walk来查找该地址的PTE地址。然后,它初始化PTE以保存相关的物理页号、所需权限(PTE_WPTE_X和/或PTE_R)以及用于标记PTE有效的PTE_V(kernel/vm.c:153)。

    1. // Create PTEs for virtual addresses starting at va that refer to
    2. // physical addresses starting at pa. va and size might not
    3. // be page-aligned. Returns 0 on success, -1 if walk() couldn't
    4. // allocate a needed page-table page.
    5. int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
    6. {
    7. uint64 a, last;
    8. pte_t *pte;
    9. if (size == 0)
    10. panic("mappages: size");
    11. a = PGROUNDDOWN(va);
    12. last = PGROUNDDOWN(va + size - 1);
    13. for (;;)
    14. {
    15. if ((pte = walk(pagetable, a, 1)) == 0)
    16. return -1;
    17. if (*pte & PTE_V)
    18. panic("mappages: remap");
    19. *pte = PA2PTE(pa) | perm | PTE_V;
    20. if (a == last)
    21. break;
    22. a += PGSIZE;
    23. pa += PGSIZE;
    24. }
    25. return 0;
    26. }

    proc_mapstacks()函数

    1. // Allocate a page for each process's kernel stack.
    2. // Map it high in memory, followed by an invalid
    3. // guard page.
    4. void proc_mapstacks(pagetable_t kpgtbl)
    5. {
    6. struct proc *p;
    7. for (p = proc; p < &proc[NPROC]; p++)
    8. {
    9. char *pa = kalloc();
    10. if (pa == 0)
    11. panic("kalloc");
    12. uint64 va = KSTACK((int)(p - proc));
    13. kvmmap(kpgtbl, va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
    14. }
    15. }

    这个函数作用就是给每一个进程分配个页表,然后将内核栈映射到高地址,保护页不进行映射

    kvminithart()函数

    1. // Switch h/w page table register to the kernel's page table,
    2. // and enable paging.
    3. void kvminithart()
    4. {
    5. w_satp(MAKE_SATP(kernel_pagetable));
    6. sfence_vma();
    7. }

    kvminithart (kernel/vm.c:53)来安装内核页表。它将根页表页的物理地址写入寄存器satp。之后,CPU将使用内核页表转换地址。由于内核使用标识映射,下一条指令的当前虚拟地址将映射到正确的物理内存地址。

    procinit()函数

    1. // initialize the proc table at boot time.
    2. void procinit(void)
    3. {
    4. struct proc *p;
    5. initlock(&pid_lock, "nextpid");
    6. initlock(&wait_lock, "wait_lock");
    7. for (p = proc; p < &proc[NPROC]; p++)
    8. {
    9. initlock(&p->lock, "proc");
    10. p->kstack = KSTACK((int)(p - proc));
    11. }
    12. }

    将p->kstack指向之前映射好的页面

  • 相关阅读:
    力扣解法汇总1224-最大相等频率
    JS高级 之 ES5 实现继承
    Kafka部署、原理和使用介绍
    [Linux] 1.Linux的简介
    IntelliJ IDEA 记学习笔 Maven自动导包 Auto Import
    管理员已经阻止软件运行bug的解决
    【深度学习】 Python 和 NumPy 系列教程(七):Python函数
    C++大学教程(第七版)Chapter14.2函数模板-fig14_01
    基于生成对抗网络探索潜在空间的医学图像融合算法
    在Windows下Com库的原理以及它与系统的关系?- CoCreateInstance解析
  • 原文地址:https://blog.csdn.net/m0_61705102/article/details/126745166