成都最好的网站推广优化公司,怎么可以做自己的网站,网站备案 需要什么,做一个内容网站多少钱一、本节任务 实验环境#xff1a; 二、要点 如何防止程序破坏内核或其他进程空间#xff1f;隔离地址空间#xff0c;进程只能读写自己的内存空间。
在保证隔离的同时#xff0c;如何将多个地址空间复用到一个物理内存上#xff1f;虚拟内存/页表。操作系统通过页表来为…一、本节任务 实验环境 二、要点 如何防止程序破坏内核或其他进程空间隔离地址空间进程只能读写自己的内存空间。
在保证隔离的同时如何将多个地址空间复用到一个物理内存上虚拟内存/页表。操作系统通过页表来为每个进程提供自己的私有地址空间和内存。
2.1 分页硬件
页表为寻址提供了一个间接的层次CPU 通过虚拟地址VA访存MMU 将虚拟地址映射成实际的物理地址PA再通过实际的物理地址去访问 RAM这样做的主要目的是为了隔离isolation每个进程都能有自己的地址空间。 内核通过页表page table来告诉 MMU 如何将虚拟地址映射成物理地址。
如果我们需要每个进程都有不同的地址空间那么每个进程都需要有一个页表内核通过写 MMU 的 satp 寄存器来切换页表。在切换进程时内核也需要切换页表。
页表存在于内存中satp 寄存器里面存放了页表在内存中的物理地址MMU 通过 satp 寄存器从内存中加载页表项page table entry, PTE。
在 64 位地址的机器中有 2^64 个不同的虚拟地址假如页面大小为 4KB12 位则页表索引为高 52 位64-12低12 位则为页面偏移量。而页表的作用就是将高位的索引替换为实际物理地址的索引低位的页内偏移去访问实际的物理内存。
页表项PTE有 64 位其中 10 位保留44位作为 PPN (physical page number)10 位作为标志位 是否每个页表项PTE都直接映射到对应的物理地址否如果直接映射的话页表会非常大一般采取混合映射的方式即直接映射加多级页表。 Xv6 运行在 Sv39 RISC-V 上这意味着 xv6 只使用 64 位虚拟地址的低 39 位未使用高 25 位。也就是说RISC-V 页表逻辑上是由2^27个页表项PTEs组成的数组每个PTE都包含一个44 位的物理页码PPN和一些标志。页硬件通过虚拟地址低 39 位的高 27 位39 - 12 27来找到对应的页表项并且得到一个 56 位的物理地址其顶部 44 位来自页表项中的 PPN其底部 12 位从原始虚拟地址复制。 注在上面的单极页表中虚拟地址的 27 位只是索引并不在页表项中占用空间页表项中只包含物理页号和标志位。
在三级页表中分页硬件使用 27 位中的前 9 位来在根页表页面中选择一个 PTE中间的 9 位来在树的下一层的页表页面中选择一个 PTE并使用最下面的 9 位来选择最终的 PTE。如果中间出现页表项不存在的情况则引发缺页故障page-fault exception。 使用多级页表的好处就是可以节省页表所占用的空间比如一个进程只用到了一个页面那么除了一个根页表、一个二级页表、一个三级页表外其他的空间都可以省略等到需要用到的时候再分配。 缺点就是需要多次访存如三级页表需要访存三次取出对应的页表项。为了避免从内存中多次加载页表项RISCV CPU 使用备用转换缓冲区Translation Look-aside Buffer (TLB)来存放经常使用的页表项。
每个页表项都有对应的标志位
PTE_V 指示页表项是否存在如果没有设置对页面的引用将导致异常PTE_R 控制是否允许将指令读取到该页面PTE_W 控制是否允许将指令写入该页面PTE_X 控制 CPU 是否可以将页面的内容解释为指令并执行它们PTE_U 控制是否允许在用户模式下的指令访问该页面
这些标志和所有其他与页面硬件相关的结构都在kernel/riscv.h中定义。 2.2 内核地址空间
Xv6 为每个进程维护一个页表以描述每个进程的用户地址空间还需要有一个用于描述内核地址空间的页表。内核配置其地址空间的布局使自己能够在可预测的虚拟地址上访问物理内存和各种硬件资源。文件kernel/memlayout.h中声明了 xv6 内核布局中的一些常量如 KERNBASEPHYSTOP 等。
内核使用 “直接映射direct mapping” 获取 RAM 和内存映射的设备寄存器即虚拟地址等于物理地址。直接映射能简化内核读写内存例如当 fork 为子进程分配用户内存时分配器返回该内存的物理地址当它将父进程的用户内存复制到子进程时fork 直接将该地址用作虚拟地址。
也有几个内核虚地址是没有直接映射的
The trampoline page它被映射在虚拟地址空间的顶部用户页表具有相同的映射。一个物理页面holding the trampoline code在内核的虚拟地址空间中映射两次一次在虚拟地址空间的顶部一次通过直接映射。The kernel stack pages每个进程都有自己的内核栈再每个进程内核栈的下面会有一个 Guard page这个页面是无效的PTE_V is not set作用是如果内核栈溢出会直接发生异常避免栈溢出导致覆盖其他进程的内核栈。 虽然内核通过高内存映射使用其堆栈但内核也可以通过一个直接映射的地址访问它们。但是这种安排中无法通过 Guard page 来提供保护。
2.3 创建地址空间的代码
大多数用于操作地址空间和页表的 xv6 代码都位于 vm.ckernel/vm.c中。
其中核心的数据结构为 pagetable_t它作为一个指针指向根页表页面可能是内核页表也可能是用户进程的页表。
核心的函数为 walk 函数该函数的作用是找到虚拟地址对应的页表项和为新的映射安装页表项。vm.c 中 kvm 开头的函数操作内核页表uvm 开头的函数操作用户页表其他函数则两者都可以。copyout 和 copyin 函数拷贝数据到用户虚拟地址和从用户虚拟地址拷贝数据。
在系统启动时main 函数会调用 kvminit 函数kvminit 函数又会调用 kvmmake 函数来创建内核的页表此时 xv6 并未使能分页机制所以直接使用物理地址。kvmmake 首先分配一页的物理内存来保存根页表然后它会调用 kvmmap 来安装内核的指令和数据以及实际上是设备的内存范围的转换。proc_mapstacks 为每个进程分配一个内核堆栈。它调用 kvmmap 来在 KSTACK 生成的虚拟地址上映射每个堆栈这为 invalid stack-guard pages 留出了空间。
kvmmap 调用 mappages 来安装一系列虚拟地址到对应物理地址的映射页表项到页表中它以页为间隔对范围内的每个虚拟地址分别执行此操作。
对于每个要映射的虚拟地址mappages 调用 walk以查找该地址的 PTE 的地址。然后它会初始化PTE以保存相关的物理页码。
walk 使用每个级别的 9 位虚拟地址来查找下一级页表或最后一个页面的 PTE。如果 PTE 无效则表示尚未分配所需的页面即未建立该虚拟地址到物理地址的映射如果设置了 alloc 参数walk 将分配一个新的页表页面并将其物理地址放在 PTE 中。它返回树中最低图层中的 PTE 的地址。
上面的代码依赖于物理内存被直接映射到内核虚拟地址空间。例如当 walk 降低页表的级别时它从 PTE 中提取下一级页表的物理地址然后使用该地址作为虚拟地址来获取下一级的 PTE。
main 函数调用 kvminithart 来装载内核页表它会将根页表页面的物理地址写入 satp 寄存器在这之后 cpu 会使用内核页表来转换地址。
2.4 物理内存分配
内核必须能在运行时分配和释放页表、用户内存、内核栈、和管道 buffer 的物理内存xv6 使用内核尾部到 PHYSTOP 之间的内存作为运行时分配的内存。每次分配和释放一整个页面4096B并且使用链表结构来追踪空闲页面。 这部分代码位于 kernel/kalloc.c 中结构体 struct run 用于指向可用的页面kalloc 和 kfree 用于从 freelist 拿取或增添可用的页面从而实现物理内存的分配与回收。freelist 由一个自旋锁所保护。
main 函数会调用 kinit 来初始化从内核尾部到 PHYSTOP 之间的所有物理页面。分配器有时将地址视为整数以便对它们进行算术运算例如在 freerange 中遍历所有页面有时使用地址作为读写内存的指针例如操作存储在每个页面中的 run 结构。
2.5 进程地址空间
每个进程都拥有独立的页表并且当 xv6 切换进程时页表也要切换。
一个进程的用户内存开始于虚拟地址 0并且能够增长到 MAXVAkernel/riscv.h。并且由程序的 text 段xv6 使用 PTE_R、PTE_X 和 PTE_U 权限映射、包含程序预先初始化的数据的页面、栈的页面、堆的页面Xv6 使用权限 PTE_R、PTE_W 和 PTE_U 映射数据、栈和堆。 通过映射没有 PTE_X 的数据用户程序不能随意地跳转到程序数据中的一个地址并在该地址开始执行。
用户栈只有一个页面并显示了由exec创建的初始内容包含命令行参数的字符串以及指向它们的指针数组位于堆栈的最顶部。为了检测用户栈溢出xv6 将堆栈的正下方放置一个不可访问的保护页面guard page并且清除 PTE_U 标志。
2.6 sbrk
sbrk 是为进程收缩或增加其内存的系统调用。该系统调用由 growproc 函数实现growproc 再根据 n 是正数还是负数来使用 uvmalloc 或 uvmdealloc并且使用 mappages 函数来添加页表项到用户的页表中。
2.7 exec
exec 为系统调用它读取一个二进制或可执行文件并替换进程的用户地址空间exec 首先使用 nameikernel/exec.c:36来打开 path 指向的二进制文件然后读取文件的 ELF headerkernel/elf.h。
ELF 二进制文件由一个 ELF header、struct elfhdrkernel/elf.h紧接着一系列 program section headers struct proghdrkernel/elf.h组成。每个 progvhdr 描述了必须加载到内存中的应用程序的一部分xv6 程序有两个 program section headers一个用于指令一个用于数据。
一个 ELF 二进制文件以四字节的 “magic number” 0x7F、“E”、“L”、“F” 开头即 7f 45 4c 46。exec 会先使用 readi 函数从该文件的 inode 中读出 elfhdr然后查看 elfhdr 中的 magic 是否和 ELF_MAGIC 一致然后使用 proc_pagetable 分配一个用户页表给当前进程但并未进行用户内存空间映射只对用户空间顶部的 trampoline code 进行了映射。然后读取后续的 proghdr使用 uvmalloc 给每个段分配页面最后使用 loadseg 来导入每个段到内存中。
exec 的后续代码会先分配两个页面第一个页面作为 guard page第二个页面作为用户栈然后将 argv 中的参数先存储到栈中再将参数的地址数组 ustack 存到栈中再将 ustack 的地址存储到 p-trapframe-a1return argc 表示将 argc 存放到 p-trapframe-a0函数的返回值会存放到 a0 寄存器。最后再保存进程的映像如进程的新页表进程的入口栈指针等。 2.8 C 语言和汇编如何相互调用
调用约定Calling Convention。
Base ISA: Program counter, 32 general-purpose registers (x0--x31) ra 寄存器需要调用者保存。sp 寄存器需要被调用者保存。t0-t6 临时寄存器需要调用者保存。s0-s11 保存寄存器需要被调用者保存。a0-a7 函数参数寄存器需要调用者保存。
在 rv32 和 rv64 中int 类型都是 32 位而 long 和 指针类型在 rv32 中是 32 位在 rv64 则是 64 位。 三、Lab: page tables
3.1 Speed up system calls (easy)
一些系统调用将数据放到用户空间和内核空间之间的只读区域即内核和用户都可以访问但是用户只能读页面从而绕过内核实现加速。而本实验就是要在 trapframe 下定义一个新的用户只能读的页放在 USYSCALLUSYSCALL 是一个虚拟地址在 memlayout.h 中定义 在该页的起始位置存放一个 struct usyscall 结构体kernel/memlayout.h里面存放当前进程的 pid 因此可以提供一个 ugetpid() 函数给用户该函数直接在用户态从该结构体中读出 pid省去了 getpid() 系统调用切换到内核态的时间 实现
首先在 kernel/proc.h 中的 proc 结构体中定义结构体 usyscall 的指针 usyscall_page
// Per-process state
struct proc {struct spinlock lock;// p-lock must be held when using these:enum procstate state; // Process statevoid *chan; // If non-zero, sleeping on chanint killed; // If non-zero, have been killedint xstate; // Exit status to be returned to parents waitint pid; // Process ID// wait_lock must be held when using this:struct proc *parent; // Parent process// these are private to the process, so p-lock need not be held.uint64 kstack; // Virtual address of kernel stackuint64 sz; // Size of process memory (bytes)pagetable_t pagetable; // User page tablestruct trapframe *trapframe; // data page for trampoline.Sstruct usyscall *usyscall_page;struct context context; // swtch() here to run processstruct file *ofile[NOFILE]; // Open filesstruct inode *cwd; // Current directorychar name[16]; // Process name (debugging)
};
然后在 kernel/proc.c 的 allocproc 函数中为 usyscall_page 分配物理页面并且为 usyscall 结构体的 pid 赋值
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p-lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{struct proc *p;for(p proc; p proc[NPROC]; p) {acquire(p-lock);if(p-state UNUSED) {goto found;} else {release(p-lock);}}return 0;found:p-pid allocpid();p-state USED;// Allocate a trapframe page.if((p-trapframe (struct trapframe *)kalloc()) 0){freeproc(p);release(p-lock);return 0;}// Allocate a usyscall pageif((p-usyscall_page (struct usyscall *)kalloc()) 0){freeproc(p);release(p-lock);return 0;}p-usyscall_page-pid p-pid;// An empty user page table.p-pagetable proc_pagetable(p);if(p-pagetable 0){freeproc(p);release(p-lock);return 0;}// Set up new context to start executing at forkret,// which returns to user space.memset(p-context, 0, sizeof(p-context));p-context.ra (uint64)forkret;p-context.sp p-kstack PGSIZE;return p;
}相应地在 freeproc() 函数中需要释放该物理页面不释放的话后面的 usertest 通过不了
// free a proc structure and the data hanging from it,
// including user pages.
// p-lock must be held.
static void
freeproc(struct proc *p)
{if(p-trapframe)kfree((void*)p-trapframe);if(p-usyscall_page)kfree((void*)p-usyscall_page);p-trapframe 0;if(p-pagetable)proc_freepagetable(p-pagetable, p-sz);p-pagetable 0;p-sz 0;p-pid 0;p-parent 0;p-name[0] 0;p-chan 0;p-killed 0;p-xstate 0;p-state UNUSED;
}
在 kernel/proc.c 中的 proc_pagetable() 函数中使用 mappages() 函数创建虚拟地址 USYSCALL 到物理地址的映射并将其作为页表项PTE存入当前进程的页表中该页的权限位为 PTE_R | PTE_U即只读并且用户可以访问
// Create a user page table for a given process, with no user memory,
// but with trampoline and trapframe pages.
pagetable_t
proc_pagetable(struct proc *p)
{pagetable_t pagetable;// An empty page table.pagetable uvmcreate();if(pagetable 0)return 0;// map the trampoline code (for system call return)// at the highest user virtual address.// only the supervisor uses it, on the way// to/from user space, so not PTE_U.if(mappages(pagetable, TRAMPOLINE, PGSIZE,(uint64)trampoline, PTE_R | PTE_X) 0){uvmfree(pagetable, 0);return 0;}// map the trapframe page just below the trampoline page, for// trampoline.S.if(mappages(pagetable, TRAPFRAME, PGSIZE,(uint64)(p-trapframe), PTE_R | PTE_W) 0){uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmfree(pagetable, 0);return 0;}// 映射 USYSCALL 页面 below the trapframe pageread onlyif(mappages(pagetable, USYSCALL, PGSIZE,(uint64)(p-usyscall_page), PTE_R | PTE_U) 0){uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmunmap(pagetable, TRAPFRAME, 1, 0);uvmfree(pagetable, 0);return 0;}return pagetable;
}
相应的在 proc_freepagetable 也要解除该页的映射
// Free a processs page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{uvmunmap(pagetable, TRAMPOLINE, 1, 0);uvmunmap(pagetable, TRAPFRAME, 1, 0);uvmunmap(pagetable, USYSCALL, 1, 0);uvmfree(pagetable, sz);
}
执行 make qemu在 xv6 中运行 pgtbltest 会显示
ugetpid_test starting
ugetpid_test: OK
3.2 Print a page table (easy)
此部分需要实现一个函数 vmprint此函数能够递归打印涉及到的页表项如下图所示 这是打印 pid 1 的进程的页表首先打印页表地址然后递归打印页表项和对应的物理地址。
代码实现如下首先在 vm.c 中添加 vmprint() 函数可以参考同文件下的 freewalk 函数此函数作用为递归地释放非叶节点页表的空间
static void
vmprintpte(pagetable_t pagetable, int depth)
{for(int i 0; i 512; i){pte_t pte *(pagetable i);if((pte PTE_V) (pte (PTE_R|PTE_W|PTE_X)) 0){for(int j 0; j depth; j)printf(..);printf(%d: pte %p pa %p\n, i, pte, PTE2PA(pte));uint64 child PTE2PA(pte);vmprintpte((pagetable_t)child, depth1);}else if(pte PTE_V){// leaf pagefor(int j 0; j depth; j)printf(..);printf(%d: pte %p pa %p\n, i, pte, PTE2PA(pte));}}
}// vmprint, Recursively print page-table.
void
vmprint(pagetable_t pagetable)
{printf(page table %p\n, pagetable);vmprintpte(pagetable, 1);
}
然后到 kernel/defs.h 中增加 vmprint() 函数的定义
void vmprint(pagetable_t);
最后到 kernel/exec.c 的 exec 函数的 return argc 前加入如下代码即如果进程为初始进程则递归打印其页表
// vmprint
if(p-pid 1)
{vmprint(p-pagetable);
}
使用 make qemu 启动 xv6 即可看到打印结果 3.3 Detect which pages have been accessed (hard)
此部分需要我们实现 pgaccess() 函数该函数用来确认传入的页面是否被访问过。这个函数需要三个参数第一个参数为用户传入的页面的起始虚拟地址第二个参数为需要检查的页数第三个参数为一个地址用来返回结果将结果存储到位中(每页使用一位其中第一页对应于最低有效位)。
首先我们需要定义 PTE_A即访问位查阅 RISC-V privileged architecture manual 手册可知PTE_V 为第六位 然后在 kernel/riscv.h 中定义 PTE_V
#define PTE_V (1L 0) // valid
#define PTE_R (1L 1)
#define PTE_W (1L 2)
#define PTE_X (1L 3)
#define PTE_U (1L 4) // user can access#define PTE_A (1L 6) // access bit
pgaccess() 函数是一个系统调用其对应的内核函数为 sys_pgaccess() 实现在 kernel/sysproc.c 中代码如下
int
sys_pgaccess(void)
{// lab pgtbl: your code here.uint64 base;int len;uint64 mask;unsigned int abits 0;pte_t *pte;struct proc *p myproc();argaddr(0, base);argint(1, len);argaddr(2, mask);//vmprint(p-pagetable);//printf(%p\n, PTE2PA(*walk(p-pagetable, base, 0)));for (int i 0; i len base MAXVA; i, base PGSIZE){if ((pte walk(p-pagetable, base, 0)) ! 0 (*pte PTE_A)){abits | (1 i);*pte ~PTE_A;}}if (copyout(p-pagetable, mask, (char *)abits, sizeof(abits)) 0){return -1;}//printf(aa%d\n, abits);return 0;
}
此时执行 pgtbltest 可以看到测试用例全部通过