// Per-process state structproc { structspinlocklock;// 自旋锁(1) // p->lock must be held when using these: 因为可能有多个进程同时访问某个进程的这些状态变量 enumprocstatestate;// Process state structproc *parent;// Parent process void *chan; // If non-zero, sleeping on chan(2) int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent's wait int pid; // Process ID
// these are private to the process, so p->lock need not be held. 所以只有进程自己可以进行访问,不需要锁,最多同时被一个进程写 uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table structtrapframe *trapframe;// data page for trampoline.S 用于陷阱处理 structcontextcontext;// swtch() here to run process 用于内核上下文切换的被保存的寄存器。 structfile *ofile[NOFILE];// Open files structinode *cwd;// Current directory char name[16]; // Process name (debugging) };
_entry的指令设置了一个栈区,这样xv6就可以运行C代码。Xv6在start. c (kernel/start.c:11)文件中为初始栈stack0声明了空间。由于RISC-V上的栈是向下扩展的,所以_entry的代码将栈顶地址stack0+4096加载到栈顶指针寄存器sp中。现在内核有了栈区,_entry便调用C代码start(kernel/start.c:21)。
# qemu -kernel loads the kernel at 0x80000000 # and causes each CPU to jump there. # kernel.ld causes the following code to # be placed at 0x80000000. .section .text _entry: # set up a stack for C. # stack0 is declared in start.c, # with a 4096-byte stack per CPU. # sp = stack0 + (hartid * 4096) la sp, stack0 // load address 将sp加载为stack0的地址 li a0, 1024*4 // load immediate 将a0设置为4096(4KB) csrr a1, mhartid // 从 mhartid 寄存器读取当前硬件线程ID(HART ID),并将其存储在 a1 中。 addi a1, a1, 1 // 保证栈偏移量不为0,不能向负地址减少 mul a0, a0, a1 // 计算出了当前CPU的栈偏移量 add sp, sp, a0 // 将计算出的栈偏移量加到栈指针 sp 上,从而为每个CPU分配独立的栈空间 # jump to start() in start.c call start spin: j spin // 说明start程序返回了,那么进入无限循环,防止CPU继续执行无效指令
// entry.S needs one stack per CPU. __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
// scratch area for timer interrupt, one per CPU. uint64 mscratch0[NCPU * 32];
// assembly code in kernelvec.S for machine-mode timer interrupt. externvoidtimervec();
// entry.S jumps here in machine mode on stack0. void start() { // set M Previous Privilege mode to Supervisor, for mret. unsignedlong x = r_mstatus();// 读取mstatus寄存器的值 x &= ~MSTATUS_MPP_MASK;//清除MPP字段,表示机器模式之前的特权模式 x |= MSTATUS_MPP_S;//设置MPP字段为S(Supervisor mode) w_mstatus(x);//写会mstatus
// set M Exception Program Counter to main, for mret. // requires gcc -mcmodel=medany w_mepc((uint64)main);//将 mepc 寄存器设置为 main 函数的地址。mepc 寄存器用于存储异常返回时的程序计数器(PC)值
// disable paging for now. w_satp(0);//禁用虚拟地址转换
// delegate all interrupts and exceptions to supervisor mode. w_medeleg(0xffff);//medeleg 寄存器用于指定哪些异常类型应该由监督模式处理 w_mideleg(0xffff);//mideleg 寄存器用于指定哪些中断类型应该由监督模式处理 // 0xffff表示所有的异常类型和中断类型都委托给监督模式处理 w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// ask for clock interrupts. timerinit();
// keep each CPU's hartid in its tp register, for cpuid(). int id = r_mhartid(); w_tp(id);
// switch to supervisor mode and jump to main(). asmvolatile("mret"); //mret执行返回,返回到先前状态,由于start函数将前模式改为了管理模式且返回地址改为了main,因此mret将返回到main函数,并以管理模式运行 }
在main(kernel/main.c:11)初始化几个设备和子系统后,便通过调用userinit (kernel/proc.c:212)创建第一个进程,第一个进程执行一个用RISC-V程序集写的小型程序:initcode. S (**user/initcode.S:**1),它通过调用exec系统调用重新进入内核。正如我们在第1章中看到的,exec用一个新程序(本例中为 /init)替换当前进程的内存和寄存器。一旦内核完成exec,它就返回/init进程中的用户空间。如果需要,init(user/init.c:15)将创建一个新的控制台设备文件,然后以文件描述符0、1和2打开它。然后它在控制台上启动一个shell。系统就这样启动了。
// Fetch the nul-terminated string at addr from the current process. // Returns length of string, not including nul, or -1 for error. int fetchstr(uint64 addr, char *buf, int max) { structproc *p = myproc(); int err = copyinstr(p->pagetable, buf, addr, max); if(err < 0) return err; returnstrlen(buf); }
// Fetch the nth 32-bit system call argument. int argint(int n, int *ip) { *ip = argraw(n); return0; }
// Retrieve an argument as a pointer. // Doesn't check for legality, since // copyin/copyout will do that. int argaddr(int n, uint64 *ip) { *ip = argraw(n); return0; }
// Fetch the nth word-sized system call argument as a null-terminated string. // Copies into buf, at most max. // Returns string length if OK (including nul), -1 if error. int argstr(int n, char *buf, int max) { uint64 addr; if(argaddr(n, &addr) < 0) return-1; returnfetchstr(addr, buf, max); } // extern 关键字表示这些函数是在其他地方定义的,当前文件只需要知道它们的存在和签名即可 // 此处extern只是对于变量的声明而不是定义,因为只能有一次定义 // 将变量/函数声明为extern就可以在另一个文件中用extern定义变量/函数,并且在所提前声明的文件中使用 extern uint64 sys_chdir(void); extern uint64 sys_close(void); extern uint64 sys_dup(void); extern uint64 sys_exec(void); extern uint64 sys_exit(void); extern uint64 sys_fork(void); extern uint64 sys_fstat(void); extern uint64 sys_getpid(void); extern uint64 sys_kill(void); extern uint64 sys_link(void); extern uint64 sys_mkdir(void); extern uint64 sys_mknod(void); extern uint64 sys_open(void); extern uint64 sys_pipe(void); extern uint64 sys_read(void); extern uint64 sys_sbrk(void); extern uint64 sys_sleep(void); extern uint64 sys_unlink(void); extern uint64 sys_wait(void); extern uint64 sys_write(void); extern uint64 sys_uptime(void);
// Copy from user to kernel. // Copy len bytes to dst from virtual address srcva in a given page table. // Return 0 on success, -1 on error. /* @param pagetable:用户空间的页表 dst:内核空间的目标地址 srcva:用户空间的源地址(此处是一个虚拟地址virtual address) len:要复制的字节数 */ int copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len) { uint64 n, va0, pa0; //n:当前页中要复制的字节数。 //va0:当前页的起始虚拟地址。(va: virtual address) //pa0:当前页的物理地址。(pa:physical address)
// Return the address of the PTE in page table pagetable // that corresponds to virtual address va. If alloc!=0, // create any required page-table pages. // // The risc-v Sv39 scheme has three levels of page-table // pages. A page-table page contains 512 64-bit PTEs. // A 64-bit virtual address is split into five fields: // 39..63 -- must be zero. // 30..38 -- 9 bits of level-2 index. 因为512=2^9所以需要9位保存页表索引 // 21..29 -- 9 bits of level-1 index. // 12..20 -- 9 bits of level-0 index. // 0..11 -- 12 bits of byte offset within the page. /* @param pagetable:页表的根节点 va:要查找的虚拟地址 alloc:是否在找不到页表项时创建新的页表页 */ pte_t * walk(pagetable_t pagetable, uint64 va, int alloc) { if(va >= MAXVA)//确保虚拟地址在有效的范围内 panic("walk"); /* 有三级页表,分别对应L2,L1和L0层 L2 层索引:用于查找 L1 页表的基地址 L1 层索引:用于查找 L0 页表的基地址 L0 层索引:用于查找物理页框的基地址 */ for(int level = 2; level > 0; level--) { pte_t *pte = &pagetable[PX(level, va)];//计算当前层次的页表索引,获取对应的的页表项指针 /* // extract the three 9-bit page table indices from a virtual address. #define PXMASK 0x1FF // 9 bits 用于提取最右边的9位(低9位) #define PXSHIFT(level) (PGSHIFT+(9*(level))) 计算虚拟地址宏对应层次的页表索引的偏移量 这里所说的“偏移量”是位数的偏移量,并且后面提到的参数在代码段最上方的注释中有写明 PGSHIFT是页内偏移的位移量,为12位 9*(level)g根据层次level计算额外的位移量,因为每一级页表索引占用9位 #define PX(level, va) ((((uint64) (va)) >> PXSHIFT(level)) & PXMASK) 将虚拟地址右移特定位数以使得所需的对应页表索引位位于最低的9位 然后与PXMASK相 & 得到最低的9位得到页表索引 */ if(*pte & PTE_V) { pagetable = (pagetable_t)PTE2PA(*pte); // #define PTE2PA(pte) (((pte) >> 10) << 12) } else { if(!alloc || (pagetable = (pde_t*)kalloc()) == 0) // 使用kalloc分配新的页表页 return0; memset(pagetable, 0, PGSIZE);//初始化新的页表页 *pte = PA2PTE(pagetable) | PTE_V;//将新分配的页表页的物理地址转换为页表项格式,并设置有效位 PTE_V // shift a physical address to the right place for a PTE. //#define PA2PTE(pa) ((((uint64)pa) >> 12) << 10) // 先去除页内偏移量然后预留10位来表示属性信息 } } // 遍历完成后,pagetable 指向 L0 层的页表,返回对应的页表项指针 return &pagetable[PX(0, va)];//返回 L0 层的页表项指针 }
fetchchstr()函数
1 2 3 4 5 6 7 8 9 10 11 12 13
// Fetch the nul-terminated string at addr from the current process. // 获取一个以nul结尾的字符串 // Returns length of string, not including nul, or -1 for error. int fetchstr(uint64 addr, char *buf, int max) { structproc *p = myproc(); int err = copyinstr(p->pagetable, buf, addr, max); // copyinstr()函数的逻辑和上面copyin函数的逻辑基本相同,只添加了一个是否发现'\0'字符的判断 if(err < 0) return err; returnstrlen(buf);//返回字符串的长度 }
// Fetch the nth 32-bit system call argument. // 取回 第n个 32位的系统调用的参数 // e.g. ssize_t sys_write(int fd, const void *buf, size_t count); // n=0是第一个参数fd,n=1...以此类推 int argint(int n, int *ip) { *ip = argraw(n); return0; }
argaddr()函数
1 2 3 4 5 6 7 8 9
// Retrieve an argument as a pointer. 将参数作为指针值返回 // Doesn't check for legality, since 不检查指针的合法性,因为copin、copyout函数会进行检查 // copyin/copyout will do that. int argaddr(int n, uint64 *ip) { *ip = argraw(n); return0; }
argstr()函数
1 2 3 4 5 6 7 8 9 10 11 12
// Fetch the nth word-sized system call argument as a null-terminated string. // 需要注意这里是将参数作为一个和字长的长度相同的字符串 // Copies into buf, at most max. // Returns string length if OK (including nul), -1 if error. int argstr(int n, char *buf, int max) { uint64 addr; if(argaddr(n, &addr) < 0) // 检查是否合法 return-1; returnfetchstr(addr, buf, max); }
syscall()函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
void syscall(void) { int num; structproc *p = myproc();
// Per-CPU state. // 每个CPU的状态 structcpu { structproc *proc; // The process running on this cpu, or null. structcontext context; // swtch() here to enter scheduler(). // 在 swtch() 函数中使用这个变量来进入调度器 int noff; // Depth of push_off() nesting. 计数器 int intena; // Were interrupts enabled before push_off()? // 其实就是保存中断嵌套中第一次中断前是否允许中断 };
// p->lock must be held when using these: enumprocstate state; // Process state structproc *parent; // Parent process void *chan; // If non-zero, sleeping on chan int killed; // If non-zero, have been killed int xstate; // Exit status to be returned to parent's wait int pid; // Process ID
// these are private to the process, so p->lock need not be held. uint64 kstack; // Virtual address of kernel stack uint64 sz; // Size of process memory (bytes) pagetable_t pagetable; // User page table structtrapframe *trapframe; // data page for trampoline.S structcontext context; // swtch() here to run process structfile *ofile[NOFILE]; // Open files structinode *cwd; // Current directory char name[16]; // Process name (debugging) };
// Allocate one 4096-byte page of physical memory. // Returns a pointer that the kernel can use. // Returns 0 if the memory cannot be allocated. void * kalloc(void) { structrun *r; /* struct run { struct run *next; }; 其实就是一个链表,不过没有数据域,每个节点代表一个可用的内存页 为什么不需要数据域呢,因为只需要通过run结构体来管理和链接这些内存页 每个内存页的前几个字节用于存储run结构体,其余部分用于存储实际的数据 内存页的实际数据存储在run结构体之后的部分,可以认为是隐式的数据域 */
//Acquire the lock. // Loops (spins) until the lock is acquired. void acquire(struct spinlock *lk) { push_off(); // disable interrupts to avoid deadlock. if(holding(lk)) // 查看是否自身已经持有锁 /* // Check whether this cpu is holding the lock. // Interrupts must be off. int holding(struct spinlock *lk) { int r; r = (lk->locked && lk->cpu == mycpu()); return r; } */ panic("acquire");
// Tell the C compiler and the processor to not move loads or stores // past this point, to ensure that the critical section's memory // references happen strictly after the lock is acquired. // On RISC-V, this emits a fence instruction. __sync_synchronize(); //用于插入完整内存屏障指令,防止 CPU 乱序执行指令 //这个函数没有参数,只是作为一个内存屏障指令的占位符存在,可以防止编译器优化代码,同时保证 CPU 按照指定的顺序执行指令 // Record info about lock acquisition for holding() and debugging. lk->cpu = mycpu(); }
// Tell the C compiler and the CPU to not move loads or stores // past this point, to ensure that all the stores in the critical // section are visible to other CPUs before the lock is released, // and that loads in the critical section occur strictly before // the lock is released. // On RISC-V, this emits a fence instruction. __sync_synchronize();
// Release the lock, equivalent to lk->locked = 0. // This code doesn't use a C assignment, since the C standard // implies that an assignment might be implemented with // multiple store instructions. // On RISC-V, sync_lock_release turns into an atomic swap: // s1 = &lk->locked // amoswap.w zero, zero, (s1) __sync_lock_release(&lk->locked);
pop_off(); }
kvmmap函数()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// add a mapping to the kernel page table. 向内核页表添加一个映射 // only used when booting. // does not flush TLB or enable paging. 不刷新TLB或启动分页 /* @param kernel_pagetable: 内核页表 va: 虚拟地址 sz: 映射区域的大小 pa: 物理地址 perm: 访问权限 */ void kvmmap(uint64 va, uint64 pa, uint64 sz, int perm) { if(mappages(kernel_pagetable, va, sz, pa, perm) != 0) panic("kvmmap"); }
// Create PTEs for virtual addresses starting at va that refer to // physical addresses starting at pa. va and size might not // be page-aligned. Returns 0 on success, -1 if walk() couldn't // allocate a needed page-table page. int mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm) { uint64 a, last; pte_t *pte;
// Must be called with interrupts disabled, // to prevent race with process being moved // to a different CPU. 必须禁用中断,防止进程被调度到另一个cpu上 int cpuid() { int id = r_tp();//读取tp寄存器的值 // tp寄存器用于存储线程指针,指向当前线程的上下文信息(context) return id; }
由于后面的函数太多了,所以比较简单的函数就不再作说明
allocpid()函数
1 2 3 4 5 6 7 8 9 10 11
int allocpid(){ int pid; acquire(&pid_lock);//必须要上锁, pid = nextpid; nextpid = nextpid + 1; release(&pid_lock);
/ 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. //用于在进程表中查找一个未使用的进程结构,并初始化该进程所需的状态,以便其能够在内核中运行 staticstructproc* allocproc(void) { structproc *p; // 在进程表中查找一个未使用的进程结构 for(p = proc; p < &proc[NPROC]; p++) { acquire(&p->lock);//获取进程锁 if(p->state == UNUSED) {//找到未使用的进程 goto found; } else { release(&p->lock);//释放 } } return0;
/ Free the page of physical memory pointed at by v, // which normally should have been returned by a // call to kalloc(). (The exception is when // initializing the allocator; see kinit above.) // 一般用于释放由kalloc分配的物理内存页面 void kfree(void *pa) { structrun *r; // 检查传入的地址是否合法 // 首先地址必须是页对齐的,地址必须是PGSIZE的倍数 // 地址必须在end和PHYSTOP之间,其中end是内核结束地址,PHYSTOP是物理内存的上限 // the kernel expects there to be RAM // for use by the kernel and user pages // from physical address 0x80000000 to PHYSTOP. // #define KERNBASE 0x80000000L // 硬件设备通常会被映射到较低的地址空间范围,所以在这里内核加载到0x80000000处 // #define PHYSTOP (KERNBASE + 128*1024*1024) // 内存为128MB // KERNBASE到PHYSTOP是内核和用户页面的物理地址范围 if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP) panic("kfree");
// Fill with junk to catch dangling refs. //将释放的页面填充为全1,在后续使用时更容易发现悬空引用(即对已释放内存的非法访问) memset(pa, 1, PGSIZE);
r = (struct run*)pa;//将释放的页面地址转换为run类型 // 前文有提到过每个内存页的前几个字节用于存储run结构体,其余部分用于存储实际的数据 // 所以内存页的开始地址可以认为指向一个页,也可以认为指向一个run结构体,其地址都是相同的
// Set up first user process. void userinit(void) { structproc *p;
p = allocproc();// 分配进程 initproc = p; // allocate one user page and copy init's instructions // and data into it. uvminit(p->pagetable, initcode, sizeof(initcode));//initcode用于执行"/init"程序,如上 p->sz = PGSIZE;// 将进程的大小设置为一个页
// prepare for the very first "return" from kernel to user. p->trapframe->epc = 0; // user program counter 用户程序入口为0 p->trapframe->sp = PGSIZE; // user stack pointer 指向页面顶部
safestrcpy(p->name, "initcode", sizeof(p->name));// 为进程命名 p->cwd = namei("/");// 表示进程的当前工作目录是根目录(cwd= current work directory) // namei函数查找并返回根目录的 inode 结构
p->state = RUNNABLE;
release(&p->lock); }
uvminit()函数 user virtual memory init
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Load the user initcode into address 0 of pagetable, // for the very first process. // sz must be less than a page. void uvminit(pagetable_t pagetable, uchar *src, uint sz) { char *mem;
if(sz >= PGSIZE) panic("inituvm: more than a page"); mem = kalloc();// 分配一个页面大小的内存块 memset(mem, 0, PGSIZE); mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U); // 将内存块映射到进程的页表中,设置一些权限标志 memmove(mem, src, sz);//将initcode复制到分配的内存块中 }
// Create a new process, copying the parent. // Sets up child kernel stack to return as if from fork() system call. int fork(void) { int i, pid; structproc *np; structproc *p = myproc();
// Pass p's abandoned children to init. // Caller must hold p->lock. // 用于把所有孤儿进程托管到init进程下,不会的导致其他问题但是会占用进程表导致进程表满 void reparent(struct proc *p) { structproc *pp;
for(pp = proc; pp < &proc[NPROC]; pp++){ // 检查当前进程 pp 的父进程是否为 p。 // 这里没有对 pp->lock 进行加锁,因为加锁可能会导致死锁。 // 具体来说,如果 pp 或其子进程也在 exit 函数中并且即将尝试锁定 p,则会导致死锁 if(pp->parent == p){ // pp->parent can't change between the check and the acquire() // because only the parent changes it, and we're the parent. acquire(&pp->lock); pp->parent = initproc; // we should wake up init here, but that would require // initproc->lock, which would be a deadlock, since we hold // the lock on one of init's children (pp). this is why // exit() always wakes init (before acquiring any locks). // 比如说如果initproc需要获取pp->lock那么就会出问题,因为initproc会等待 pp->lock // 而我们又在等待initproc->lock // 因此,唤醒 initproc 的操作通常在exit函数中进行,且在获取任何锁之前 release(&pp->lock); /* 不在检查pp->parent时加锁,以避免与pp或其子进程在exit函数中尝试锁定p时产生死锁。 在修改pp->parent时加锁,确保修改操作的原子性和一致性。 唤醒initproc的操作在exit函数中进行,且在获取任何锁之前,以避免死锁 */ } } }
// Exit the current process. Does not return.退出当前进程但是不返回 // An exited process remains in the zombie state一个退出了的进程在僵尸状态 // until its parent calls wait().直到其父亲调用了wait /* @param status:进程退出时的状态值,即在使用时给它一个无符号的整型数,该数将会作为进程的退出状态 并且要在0-255范围内,否则将自动默认为未定义退出状态值 */
// we might re-parent a child to init. we can't be precise about // waking up init, since we can't acquire its lock once we've // acquired any other proc lock. so wake up init whether that's // necessary or not. init may miss this wakeup, but that seems // harmless. // 获取initproc的锁,唤醒initproc,以便它可以接收孤儿进程 // 由于并发控制的需要,一旦获取了某个进程的锁,就不能再获取其他进程的锁,以避免死锁 // 因此,在已经获取了当前进程或其他进程的锁的情况下,无法精确地获取 init 进程的锁 acquire(&initproc->lock); wakeup1(initproc);// wakeup1专门用于唤醒slepping in wait()的进程p,使用前必须获取p的lock release(&initproc->lock); /* initproc的主要职责是初始化用户态环境,并启动其他必要的系统进程和服务 因此,initproc会有很多子进程,这些子进程通常包括系统服务、守护进程和其他用户态程序 initproc在以下情况下可能会进入睡眠状态: 1.等待子进程退出: 在调用wait系统调用时。 2.等待系统事件: 在等待特定事件发生时。 3.空闲状态: 在没有任务需要处理时。 */ // 获取 p->parent 的副本,以确保我们解锁的是同一个父进程。 // 以防我们的父进程在我们等待父进程锁时将我们交给 init。 // 这样做可能会与一个正在退出的父进程产生竞争条件, /* 竞争条件: 在获取父进程锁的过程中,父进程可能会退出 如果父进程在当前进程等待锁时退出,可能会导致当前进程的父进程引用发生变化 当前进程 p 正在等待获取父进程 original_parent 的锁 在等待期间,父进程 original_parent 退出,并将当前进程 p 重新分配给 initproc 当前进程 p 终于获取到了锁,但此时父进程已经不再是原来的 original_parent,而是 initproc */ // 但结果只会是一个无害的虚假唤醒,唤醒一个已经退出或错误的进程; // 进程结构体永远不会被重新分配为其他用途。 // 为了确保在后续操作中锁定的是正确的父进程,即使父进程在这段时间内已经被重新分配给initproc acquire(&p->lock); structproc *original_parent = p->parent; release(&p->lock); // we need the parent's lock in order to wake it up from wait(). // the parent-then-child rule says we have to lock it first. acquire(&original_parent->lock); // 获取父进程的锁,以便可以安全地唤醒父进程
acquire(&p->lock);
// Give any children to init. reparent(p);// 将p的子进程重新分配给initproc // 因为p此刻马上要退出了.所以其还没有退出的子进程就会变成孤儿进程,需要进行托管
// Parent might be sleeping in wait(). wakeup1(original_parent);// 唤醒父进程,使其从wait函数中返回
// called at the end of each FS system call. // commits if this was the last outstanding operation. // 在所有正在进行的操作完成后触发一次日志提交 void end_op(void) { int do_commit = 0;
acquire(&log.lock); log.outstanding -= 1; // 减少正在进行的操作计数 if(log.committing) // 不应该在一个日志提交过程中尝试结束另一个操作 panic("log.committing"); if(log.outstanding == 0){ do_commit = 1; log.committing = 1;// 开始提交 } else { // begin_op() may be waiting for log space, // and decrementing log.outstanding has decreased // the amount of reserved space. // 如果还有其他操作正在进行,唤醒可能因日志空间不足而等待的线程 // 因为减少log.outstanding可能会释放足够的日志空间 wakeup(&log); } release(&log.lock);
if(do_commit){ // call commit w/o holding locks, since not allowed // to sleep with locks. // 这里不持有任何锁,因为commit函数可能需要睡眠,而持有锁时不能睡眠 commit();// 进行日志提交 acquire(&log.lock); // 在日志提交完成后重新获取锁 log.committing = 0; // 标记日志提交结束 wakeup(&log);// 唤醒可能因等待日志提交结束而阻塞的线程 release(&log.lock); } }
// Wait for a child process to exit and return its pid. // Return -1 if this process has no children. int wait(uint64 addr) { structproc *np; int havekids, pid; structproc *p = myproc();
// hold p->lock for the whole time to avoid lost // wakeups from a child's exit(). // 获取当前进程 p 的锁,确保在整个 wait 函数执行过程中不会丢失来自子进程 exit 的唤醒信号 acquire(&p->lock);
for(;;){ // Scan through table looking for exited children. // 无限循环不断检查是否有子进程退出 havekids = 0;// 现在无子进程 for(np = proc; np < &proc[NPROC]; np++){ // this code uses np->parent without holding np->lock. // acquiring the lock first would cause a deadlock, // since np might be an ancestor, and we already hold p->lock. // 根据父子进程规则,必须先获取父进程的锁才能获取子进程的锁,所以在读取过程中不可以加锁 if(np->parent == p){ // np->parent can't change between the check and the acquire() // because only the parent changes it, and we're the parent. // 在下一行代码的acquire()和上面的检查之间np的父进程不会变化 // 因为只有父进程可以对其进行修改,而p就是其父进程 // 这里应该是用于说明在下面acquire()的锁就是上面检查到的np,其父进程不会改变 acquire(&np->lock); havekids = 1; // 表示当前进程有子进程 if(np->state == ZOMBIE){ // Found one. pid = np->pid; if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate, sizeof(np->xstate)) < 0) { // 如果addr不为0,使用copyout将子进程的退出状态复制到用户空间地址addr // 如果参数addr的值不是NULL,wait就会把子进程退出时的状态取出并存入其中,这是一个整数值 // 如果 copyout 失败,释放所有锁并返回 -1 release(&np->lock); release(&p->lock); return-1; } // 调用freeproc释放子进程的资源 freeproc(np); release(&np->lock); release(&p->lock); return pid;// 释放锁,返回子进程的pid } release(&np->lock); } }
// No point waiting if we don't have any children. // 当前进程没有子进程,或者当前进程被杀死,释放当前进程的锁并返回-1 if(!havekids || p->killed){ release(&p->lock); return-1; } // Wait for a child to exit. // 调用sleep使当前进程进入睡眠状态,等待子进程退出 sleep(p, &p->lock); //DOC: wait-sleep } }
// Per-CPU process scheduler. // Each CPU calls scheduler() after setting itself up. // Scheduler never returns. It loops, doing: // - choose a process to run. // - swtch to start running that process. // - eventually that process transfers control // via swtch back to the scheduler. // 每个CPU在初始化完成后都会调用这个调度器。调度器的主要任务是在所有进程中选择一个可运行的进程 // 并切换到该进程执行,直到该进程自愿放弃CPU控制权或被中断打断,再回到调度器继续选择下一个可运行的进程 void scheduler(void) { structproc *p; structcpu *c = mycpu(); c->proc = 0; // 初始化为0表示现在没有运行任何进程 for(;;){ // 使用一个无限循环来不断选择和运行进程 // Avoid deadlock by ensuring that devices can interrupt. // 启用中断,确保设备可以中断当前进程,避免死锁 intr_on(); int found = 0; // 表示是否找到了可运行的进程 for(p = proc; p < &proc[NPROC]; p++) { acquire(&p->lock);// 先获取锁防止别的进程进行修改 if(p->state == RUNNABLE) { // Switch to chosen process. It is the process's job // to release its lock and then reacquire it // before jumping back to us. p->state = RUNNING; c->proc = p;// 将当前CPU的当前进程指针c->proc设置为该进程p swtch(&c->context, &p->context);// 使用swtch函数切换到该进程的上下文,开始执行该进程 /* CPU 上下文切换就是先把前一个任务的 CPU 上下文(也就是 CPU 寄存器和程序计数器)保存起来 然后加载新任务的上下文到这些寄存器和程序计数器,最后再跳转到程序计数器所指的新位置,运行新任务 而这些保存下来的上下文,会存储在系统内核中,并在任务重新调度执行时再次加载进来 这样就能保证任务原来的状态不受影响,让任务看起来还是连续运行 切换上下文 == 运行新的程序 */ // 在xv6中是由一个汇编语言程序写成的 // void swtch(struct context *old, struct context *new) // 后面的是要切换去运行的进程,前面的是现在正在运行的进程
// Process is done running for now. // It should have changed its p->state before coming back. // 当进程执行完毕后,返回到调度器,将 c->proc 设置为 0 c->proc = 0;
// Atomically release lock and sleep on chan. // Reacquires lock when awakened. 当被唤醒的时候需要锁 // 因为我们需要在更改当前进程的状态之前确保持有 p->lock,以避免竞争条件 void sleep(void *chan, struct spinlock *lk) { structproc *p = myproc(); // Must acquire p->lock in order to // change p->state and then call sched. // Once we hold p->lock, we can be // guaranteed that we won't miss any wakeup // (wakeup locks p->lock), // so it's okay to release lk. // 持有锁可以保证我们不错过任何wakeup,因为wakeup需要获取进程的锁 if(lk != &p->lock){ //DOC: sleeplock0 acquire(&p->lock); //DOC: sleeplock1 release(lk); //如果传入的锁 lk 不是当前进程的锁 p->lock,则需要先获取当前进程的锁 p->lock }
// Go to sleep. p->chan = chan; p->state = SLEEPING;
// A fork child's very first scheduling by scheduler() // will swtch to forkret. // 是在新创建的子进程第一次被调度器调度时调用 void forkret(void) { staticint first = 1; // static关键字使得first变量在整个程序的生命周期中只初始化一次,并且在每次调用forkret时都保留其值。 // firs 变量用于标记是否是第一次调用 forkret
// Still holding p->lock from scheduler. release(&myproc()->lock);// 释放当前进程的锁 p->lock,以便其他线程可以访问该进程的数据结构 // 在调用 forkret 时,当前进程仍然持有 p->lock(即当前进程的锁) // 这是因为在调度器 scheduler 中调用 swtch 切换到新进程时,锁还没有释放
if (first) { // File system initialization must be run in the context of a // regular process (e.g., because it calls sleep), and thus cannot // be run from main(). first = 0;// 将 first 设为 0,表示已经初始化过文件系统 fsinit(ROOTDEV);// 调用 fsinit(ROOTDEV) 初始化文件系统 // 文件系统初始化需要在普通进程的上下文中进行,因为它可能调用 sleep 等函数 // 而这些函数不能在 main 函数中直接调用 }