2023年8月2日发(作者:)
第二次代码阅读报告——process 一、 重要函数或语句的代码分析和注释 1. kfree()——将v指向的页加入到list中 void kfree(char *v) { struct run *r;//页list的元素 //当v不再1MB和PHYSTOP范围内时报错。 if(((uint) v) % PGSIZE || (uint)v < 1024*1024 || (uint)v >= PHYSTOP) panic("kfree"); //将内存中的byte都赋值为1,使得当读垃圾的时候,避免读//到上一个程序的有效值。最好是代码就此崩溃。 memset(v, 1, PGSIZE); acquire(&); //将释放的页表加入list中,并且在第一个位置。 r = (struct run *) v; r‐>next = st; st = r; release(&); } 2. kalloc()——分配页表 char* kalloc() { struct run *r; acquire(&); //返回list中的第一个页,将kmem赋值为第二个页 r = st; if(r) st = r‐>next; release(&); return (char*) r; } 3. kinit()——初始化free list的物理页 //系统假设机器有16MB的物理内存,使用内核的end到//PHYSTOP(16MB)作为初始化的空闲内存。 kinit(void) { //end是内核数据段之后的物理页 extern char end[]; initlock(&, "kmem"); //#define PGROUNDUP(sz) (((sz)+PGSIZE−1) & ~(PGSIZE−1)) //作用是将sz转化为所对应的页表的下一个页表起始地址。 //PGROUNDUP确保仅释放以页为单位的空间。 char *p = (char*)PGROUNDUP((uint)end); //将1MB‐16MB的物理地址加入到list中 for( ; p + PGSIZE ‐ 1 < (char*) PHYSTOP; p += PGSIZE) kfree(p); } 4. kvmalloc()—创建供内核使用的页表。 kvmalloc(void) { //调用setupkvm()并且用kpgdir储存返回的页表的指针 kpgdir = setupkvm(); } 5. setupkvm()——创建供内核使用的页表。 setupkvm(void) { pde_t *pgdir; //创建页目录,为页目录分配内存,失败返回0. if(!(pgdir = (pde_t *) kalloc())) return 0; //将页目录内存初始化为0 memset(pgdir, 0, PGSIZE); //将内核将会使用的内存进行映射。这些映射均将每个虚拟//地址映射到相同的物理地址。映射包括内核的指令,数据,//和到PHYSTOP的物理内存,还有IO设备。但是并不对进 //程的内存进行映射。 if( //640K B‐1MB映射IO设备 !mappages(pgdir, USERTOP, PTE_W) || //映射内核和空闲内存 !mappages(pgdir, (void *)0x100000, (void *)USERTOP, 0x60000, PHYSTOP‐0x100000, 0x100000, PTE_W) || // 映射设备. !mappages(pgdir, (void *)0xFE000000, 0x2000000, 0xFE000000, PTE_W)) return 0; return pgdir; } 6. mappages()——将一个页表的处于某个范围的虚拟地址映射到相应的物理地址。 static int mappages(pde_t *pgdir, void *la, uint size, uint pa, int perm) { //
#define PGROUNDDOWN(a) (((a)) & ~(PGSIZE−1)) //将a转化为a所在页表的起始地址。 char *a = PGROUNDDOWN(la); char *last = PGROUNDDOWN(la + size ‐ 1); //以页为单位,对每个范围中的虚拟地址进行映射 while(1){ //调用walkpgdir()找到应该对应到的PTE的地址。 pte_t *pte = walkpgdir(pgdir, a, 1); if(pte == 0) return 0; if(*pte & PTE_P) panic("remap"); //初始化PTE。 *pte = pa | perm | PTE_P; if(a == last) break; a += PGSIZE; pa += PGSIZE; } return 1; } 7. walkpgdir()——找到应该对应到的PTE的地址。 walkpgdir(pde_t *pgdir, const void *va, int create) { uint r; pde_t *pde; pte_t *pgtab; //
#define PDX(la) ((((uint) (la)) >> PDXSHIFT) & 0x3FF) // #define PDXSHIFT 22 //PDX()的作用是取la的高10位虚拟地址,用来找到PDE pde = &pgdir[PDX(va)]; if(*pde & PTE_P){ //如果PDE是有效的 //#define PTE_ADDR(pte) ((uint) (pte) & ~0xFFF) //PTE_ADDR()是将后10位的bit置为0 //pgtab中储存指向页表的指针 pgtab = (pte_t*) PTE_ADDR(*pde); } else if(!create || !(r = (uint) kalloc())) //给r分配一页 return 0; else { pgtab = (pte_t*) r; // 将pgtab指向的页均初始化为0 memset(pgtab, 0, PGSIZE); *pde = PADDR(r) | PTE_P | PTE_W | PTE_U; } //// #define PTX(la) ((((uint) (la)) >> PTXSHIFT) & 0x3FF) // #define PTXSHIFT 12 //取的是la虚拟地址的11‐20位,用来找到PTE。 //返回的是对应的PTE的地址。 return &pgtab[PTX(va)]; } 8. struct proc——进程的数据结构 struct proc { uint sz; // 进程内存大小(字节) pde_t* pgdir; //进程的页表指针 char *kstack; // 进程的内核栈,指向栈的底部 enum procstate state; // 进程的状态 volatile int pid; //进程的ID struct proc *parent; // 父进程 struct trapframe *tf; //当前系统调用的trap frame struct context *context; // 运行进程的时候转到这 void *chan; // 如果不为0,休眠 int killed; // 如果不为0,则死亡 struct file *ofile[NOFILE]; struct inode *cwd; char name[16]; }; 9. allocproc()——在进程表中分配一个proc,初始化内核线程执行所需要的某些状态 static struct proc* allocproc(void) { struct proc *p; char *sp; acquire(&); //搜索进程表,找到一个状态是UNUSED的进程。 for(p = ; p < &[NPROC]; p++) if(p‐>state == UNUSED) goto found; release(&); return 0; found: //将进程的状态设置为EMBRYO p‐>state = EMBRYO; //给予一个唯一的ID p‐>pid = nextpid++; release(&); // 分配内存的分的栈空间 if((p‐>kstack> = kalloc()) === 0){ p‐>state> = UNUSED; retturn 0; } //KSTACCKSIZE 内核栈的大大小。 sp = p‐>kstack + KSTACKSSIZE; //为trapt framee预留空间间 sp ‐= sizeof *p‐>tf; trapframee*)sp; p‐>tf = (struct t //建立新的context来执行forkret //返回到trapret sp ‐= 4; *(uint*)sp = (uint)trapret; sp ‐= sizeof *p‐>context; p‐>context = (struct context*)sp; memset(p‐>context, 0, sizeof *p‐>context); p‐>context‐>eip = (uint)forkret; return p; } 10. userinit()——执行第一个进程 userinit(void) { struct proc *p; //第一个程序保存在这里,储存二进制代码的位置和大小。 extern char_ binary_initcode_start[], _binary_initcode_size[]; p = allocproc(); initproc = p; //调用setupkvm()用来创建一个仅仅内核使用的页表。 if(!(p‐>pgdir = setupkvm())) panic("userinit: out of memory?"); // 调用inituvm()分配一页物理内存,将虚拟地址0映射 //到该内存,将二进制代码拷贝到该页中。 inituvm(p‐>pgdir, _binary_initcode_start, (int)_binary_initcode_size); p‐>sz = PGSIZE; memset(p‐>tf, 0, sizeof(*p‐>tf)); //将trap frame赋值为原始的用户模式状态 //%cs包含一个SEG_UCODE段选择器,优先级是//DPL_USER,其他的类似。 p‐>tf‐>cs = (SEG_UCODE << 3) | DPL_USER; p‐>tf‐>ds = (SEG_UDATA << 3) | DPL_USER; p‐>tf‐>es = p‐>tf‐>ds; p‐>tf‐>ss = p‐>tf‐>ds; p‐>tf‐>eflags = FL_IF; p‐>tf‐>esp = PGSIZE; p‐>tf‐>eip = 0; // initcode.S的开头 safestrcpy(p‐>name, "initcode", sizeof(p‐>name)); p‐>cwd = namei("/"); //将进程的状态赋值为RUNNABLE。 p‐>state = RUNNABLE; } 11. scheduler()——执行进程 void scheduler(void) { struct proc *p; for(;;){ sti(); acquire(&); //找到一个状态是RUNNABLE的进程(只能是initproc) for(p = ; p < &[NPROC]; p++){ if(p‐>state != RUNNABLE) continue; //将per‐cpu变量proc设置为p。 proc = p; //调用switchuvm()使硬件开始使用目标进程的页表。 switchuvm(p); p‐>state = RUNNING; //context转换到目标进程的内核线程。Swtch保存当前 //寄存器并且将保存的目标内核线程(proc‐>context)的寄 //存器装载到x86的硬件寄存器中,包括栈指针和指令指针。 swtch(&cpu‐>scheduler, proc‐>context); switchkvm(); proc = 0; } );; release(&pta } } 二、
内在页氏管理和和进程的生生命周期流流程分析 表存储的two‐levell tree结构构 1. 页表 A. 目的的:page hardwaree用页表将将线性地址址转换成物物理地址 B. 逻辑辑结构:页表在逻辑页辑上是2^220个PTE组成的数数组。每个PTE(32bit3)包含20bit的的物理地址,和一一些flag(见图)。 C. 具体体实现:页表的具体页体实现是是two‐level tree结构构。树的根根是40996byte的页目录,页目录包包含1024个PDE((类似PTEE的结构构)。PDEE指向页表表页(40096byte)。每个页表表含有1024个PTE。Page hardwaree将虚拟地地址高位的的10bit选选择一个PDE,用PPN替代。用接替接下来的101个选择择一个PTEE,并用PPNP并用替代代。如果PDE或PPTE中的任任何一个PTE_P没没有设置,则Pagge hardwaare将返回回错误。D. 细节:细PTE__P 设置后后代表有效效PTE_W 控制是否否允许指令令对页进行行写操作PTE_UP是允允许进程程使用页。 2. 进程程内存的实实现 A. 进程的结结构:每个进进程都有有一个独立立的页表进进程的内存存从0开始并且可以有160页。 B. 低位内存映射:xv6设立PTE将虚拟地址和所分配的实际地址连接起来。并且设立flag,对于少于160页的进程,xv6将未使用的页的PTE_P清空。 C. 高位内存映射:对于高于160页的虚拟地址,进程的页表将其直接映射到物理地址(这使得找到物理内存变的简单)。但是xv6没有设置PTE_U,所以只有内核可以使用它们。例如:内核可以使用它自己的指令和数据(虚拟地址和物理地址都起始在1MB处)。而且内核还可以读写数据段之后的物理内存。 D. 高位映射的具体实现:进程所用的内存从0开始,可以最高达到KERNBASE(2GB)。Xv6将高于KERNBASE:KERNBASE+PHYSTOP的虚拟地址映射到0:PHYSTOP(16MB)。 E. 细节:每个进程的页表都同时包含进程的内存和内核的内存的映射。这使得系统在进程和内核之间切换的时候不用转换页表。所以,大多数内核是没有自己的页表的。 3. 进程的内存分配 A. 内存管理:xv6维护物理内存使得可以在程序运行时动态分配内存。 B. 管理内存的位置:使用在内核数据段之后的一页物理内存。 C. 管理方式:xv6对页进行内存管理。Xv6存储一个可用页的线性链链表。分配配页时,将页从线性将性链表中删删除,清空空页时,将页页加入到线线性链表中中。 D. 动态分配配内存:当当运行sbrrk时,假假设现在进进程的大小小为12MB(0x30000)。假设Xv66找到空空闲的物理理页,地址址为0x2010000.为了保持持进程内存存的连续续性,找到到的物理页页虚拟地址为为0x30000.这时(仅在这时时)xv6使使用pagging hardwaree将一个虚虚拟地址转化到一一个不同的的物理地址址。Xv6修改PTE(包含地址从0x3000‐0x3ffff)指向物物理含虚拟地000高位的的20bit),并且设置置flag。这这样页0x201(0x2010能够使用从从0开始16MB1的连连续内存。。 进程就能程的生命周周期分析析 4. 进程 A. 通常的进进程都是父父进程通过过系统调用用fork()创创建的。foork()调用后将将创建子进进程的描述述符和进程程ID,在在子进程中中复制父进程程的进程描描述符,子进程使用子用父进程的的内存,在父在进程的地址空间中运行。 B. exec()系统调用将在子进程的地址空间中复制新的程序数据。由于共享相同的地址空间,写入新的程序数据将会导致内存的页错误,因此,在这种情况下,Linux内核将分配新的物理页给子进程。一般情况下,子进程执行自己的程序,避免了复制完整地址空间的低效率操作。 C. 当程序执行完成时,子进程通过系统调用exit()终止进程。exit()系统调用释放子进程相应的资源,并发送信号给父进程,通知子进程的终止。在这个时刻,子进程被称为僵尸进程。父进程通过系统调用wait()接收子进程的终止信号。当父进程接收到该信号,删除子进程所有的数据结构,并释放子进程的进程描述符,这个时候子进程才被完全删除。 三、 阅后心得 本次代码的阅读主要是内存的组织和进程有关的部分。其中有很多的函数是用汇编语言实现的,所以遇到了这些函数,就无法明白函数的具体实现过程。所以,要是想彻底的明白xv6系统的化,还是应该具体的学习汇编语言的。所以,我打算好好学习汇编语言,之后在回过头来看看xv6的代码,相信那时候我会明白的更多。
发布者:admin,转转请注明出处:http://www.yc00.com/web/1690957559a472799.html
评论列表(0条)