Skip to content

Latest commit

 

History

History
397 lines (241 loc) · 28.1 KB

一些笔记.md

File metadata and controls

397 lines (241 loc) · 28.1 KB

xv6-riscv学习笔记

计算机体系架构的分层,操作系统开发的方法

开发者首先在主机系统(通常是 x86 或 ARM 等常见架构的电脑)上编写源代码,这些代码可以是操作系统内核、应用程序或硬件驱动等。源代码通常采用高级编程语言(如 C、C++、汇编语言等)。

交叉编译器是一个在一种平台上(通常是开发主机平台)生成另一种平台(目标硬件架构)上可以执行的二进制代码的工具链。交叉编译器会将开发主机的架构与目标架构区分开来。

在主机上,使用交叉编译工具链将源代码编译为目标平台所需的二进制文件。

编译好的二进制文件通常会被部署到目标硬件中,可能通过多种方式传输(如通过串口、网络、SD 卡等)。在目标硬件上,通常没有完整的操作系统环境,可能只运行一个引导程序(如 bootloader),该程序负责加载内核、驱动等。

目标硬件接收到二进制代码后,可以直接运行(如果是裸机环境)。在有操作系统的环境下,加载的代码将被操作系统调度和管理,执行相应的任务。

生成二进制文件的工具(如交叉编译器、链接器等)通常由硬件厂商、开源社区或第三方工具提供,并且这些工具针对特定硬件架构进行优化或定制。

也就是说,(最基础的)编译工具是先于操作系统的,反过来,在操作系统完成之后,编译工具也需要针对操作系统实现的系统调用和一些特定进行对应的优化。

为什么能用C语言来写操作系统代码,如何理解"操作系统就是C语言程序"

与其说为什么能用C语言来写操作系统代码,不如说,一个能用来开发操作系统的语言,需要有哪些特征?

    1. 直接硬件访问
    能够操作硬件资源:内核语言需要能够直接访问和控制硬件资源,如 CPU 寄存器、内存、I/O 端口、硬盘等。这要求编程语言能够生成低级的机器指令(如汇编语言)或提供与硬件直接交互的能力。

    2. 高效的内存管理
    内存控制:操作系统内核需要精确控制内存分配和回收,包括内存分页、物理内存与虚拟内存的映射、堆栈管理等。语言本身需要支持低级内存操作,如指针操作、内存分配和释放。

    3. 性能要求
    高效性:内核代码要求高效的执行,因为内核直接参与系统的调度、资源管理等核心操作。编  程语言需要支持低开销、快速执行,以确保内核响应速度。
    
    实时性:在许多情况下,操作系统内核必须满足实时性要求(例如中断响应、任务调度)。因  此,语言需要允许开发者写出高效、可预测的代码。
    
    4. 没有操作系统依赖
    无运行时依赖:内核开发语言必须能够在没有操作系统支持的环境中运行。在编写内核时,不能依赖操作系统的库函数或运行时环境(如标准库、线程库等)。这意味着语言应尽量避免使用动态内存分配、文件操作等高级功能。
    
    裸机环境支持:编程语言必须能够在“裸机”环境中工作,即没有操作系统的支持,直接在硬件上执行。
    5. 可移植性
    跨平台支持:虽然内核需要硬件控制,但也希望支持多个架构(如 x86、ARM、RISC-V)。开发语言需要能够支持不同的体系结构,并且可以通过合适的交叉编译器生成适用于不同平台的 代码。

综上,操作系统为什么常用C语言来写,就很显而易见了。

同理,操作系统程序和普通C语言程序的最大差距,也在于:操作系统程序一定不能依赖于运行时库。

xv6-riscv的一些文件格式及其作用

.c, .h, .o, .S:
    无需多言

.d:
    依赖文件,描述源文件与其依赖项之间的关系。例如 一个 .d 文件可能包含以下内容:
    main.o: main.cpp main.h utils.h

    其作用为增量编译,构建工具根据依赖关系来判断头文件是否发生了变化,从而决定是否重新编译对应的源文件。上述例子中,main.cpp main.h utils.h其中任何一个文件改变,main.o 都需要重新生成。

.ld:
    链接脚本文件,用于定义链接时,不同的代码段所装载的内存位置。在sv6-riscv中,kernel.ld非常简短,大概就是定义了整个程序的入口(entry.S),并且定义了各个代码段的别名,对齐方式等。

    kernel.ld中定义了代码段从0x80000000开始,其中代码段以4kB对齐,只读数据,数据,未初始化数据段以16B对齐,接下来就是end(内存管理中的初始地址)

MakeFile:
    无需多言,唯一需要注意的是,MakeFile中,指定了kernel和user是分开编译的。这也让两个程序的代码可以有重定义的部分。

xv6-riscv的启动过程

kernel.ld定义了整个操作系统程序的入口entry.S。

操作系统启动时,此时处于权限最高的机器模式(Machine Mode)。从entry.S进入,进行栈的分配后,然后进入start.c的start()。

在start()中,首先将mstatus的mpp位(中断返回后的特权级)设置为权限较低的监管模式(Supervisor Mode),然后设置epc(中断返回的程序执行位置)为main的函数指针,进行了一些设置后,最后执行mret,进入监管模式并且运行main。

    补充:mstatus是非常重要的寄存器,保存了cpu的状态,它的内存结构如下:

    31          23 22 |21| 20 |19|18| 17 |16|15| 14|13|12|11|10| 9| 8| 7| 6| 5| 4| 3| 2| 1| 0
    +---------------+---+---+----+---+---+----+---+---+---+---+---+---+---+---+---+---+---+---+---+
    |     ...       | U | S | H  |   | F | MSA| MIE| MPIE| SIE| SPIE|  ... | MPP | MPRV | XS | FS | MIE |
    +---------------+---+---+----+---+---+----+---+---+---+---+---+---+---+---+---+---+---+---+---+

    1. MIE (Bit 3):
        Machine Interrupt Enable. 这个位表示机器模式下的中断是否使能。
    2. MPIE (Bit 7):
        Machine Previous Interrupt Enable. 当中断被关闭时,该位保存中断使能状态,以便在中断返回时恢复。
    3. SIE (Bit 1):
        Supervisor Interrupt Enable. 这个位表示监管模式下的中断使能状态。
    4. SPIE (Bit 5):
        Supervisor Previous Interrupt Enable. 当监管中断被关闭时,该位保存以前的监管中断使能状态以便恢复。
    5. MPP (Bits 11-10):
        Machine Previous Privilege. 这两位表示mret后所需要设置的特权级别,可以是:
            00: User Mode
            01: Supervisor Mode
            11: Machine Mode
    6. MPRV (Bit 17):
        Machine Privilege Level. 这是一个控制位,指示使用不同的地址空间(例如,系统级别的页面映射)。
    7. XS (Bits 19-18):
        Extension Status. 这一对位用于状态指示,通常与支持的扩展相关。
    8. FS (Bits 22-20):
        Floating Point Status. 这部分用于表示浮点单元的状态。
    9. U (Bit 23):
        User Mode. 表示用户模式下的某些状态。
    10. S (Bit 24):
        Supervisor Mode. 表示当前处于监管模式。
    11. H (Bit 25):
    Hypervisor Mode. 表示当前处于虚拟化环境中(如果支持)。

然后进入main,根据不同的cpuid来进入不同的分支,进行初始化,然后进入schdule进行进程调度。

gcc的一些特性

1. 原子内建函数:
    见gcc手册相应内容(https://gcc.gnu.org/onlinedocs/gcc-4.1.2/gcc/Atomic-Builtins.html)

    虽然该文档是x86的,但是riscv的实现应当也大差不差(应该吧)

2. __attribute__((x))
    x:注解,表示程序员的一些承诺,允许编译器的优化。类似cpp11后的[[x]]注解

riscv架构

32个用户态的通用寄存器。详情见计组(论计组为什么是神)。

3个状态:M,S,U。
    机器模式主要用于启动和定时器中断。

    内核代码全部运行在监管模式。
    
    用户程序全部运行在用户模式。

除了通用寄存器外,还有许多控制和状态寄存器CSRs(Control and Status Registers)。这些寄存器是用户模式无法访问的。riscv支持非常多的CSR,其中有19个是很重要的。m开头表示只有机器模式可访问,而s开头表示机器和监管模式都可访问。
    1. mhartid:     hart(cpu)id
    2. m(s)status:  status register,其中有SIE(是否开中断),SPIE(中断前是否开中断),SPP(中断前的模式,S、U)。可以通过sie来暂时地关中断。
    3. m(s)tvec:   陷阱向量表
    4. m(s)epc:     previous exception PC
    5. scause:      trap cause code
    6. m(s)scratch: 指向异常处理或上下文切换的私有存储区域
    7. satp:        addr translation ptr(指向页表的指针)
    8. m(s)ie:      interrupt enable(中断标志寄存器)
    9. sip:         interrupt pending(待处理中断)
    10. medeleg:    exception delegation(指派异常下放的位掩码,实际只有16个有效位)
    10. mideleg:    exception delegation(指派中断下放的位掩码,实际只有16个有效位)
    11. pmpcfg0:    限制监管模式和用户模式对物理地址的访问
    12. pmpaadr0:   限制监管模式和用户模式对物理地址的访问 
    13. stval:      bad addr or instruction
    14. m(s)ie:     选择性地开关某个中断的位掩码寄存器

自旋锁:spinlock.c

很简单的一个实现,在获取锁时会先给对应的cpu关中断,然后用gcc的内置函数调用riscv的一个原子操作amoswap来检查是否已经被上锁,在释放时同样调用原子操作来解锁。

值得注意的是,当一个cpu上运行的进程申请了自旋锁时,它会进入关中断状态,直到该进程的所有锁都被释放后,才会重新开中断,在此期间不允许进程的调度等。

内存分配:kalloc.c

所有内存分配基于4KB大小的页。

上述提到过,可用内存的起始物理地址end来源于kernel.ld,而终止物理地址(PHYSTOP)被硬编码为0x80000000 + 128 * 1024 * 1024,即操作系统内核的代码段,数据段和可用内存,总计128MB。

内存的布局详见memlayout.h。

分配内存页(kalloc)就是简单地获得锁,然后取出kmem中空闲内存页链表freelist的表头,然后释放锁,返回指针。一整页的数据会被设置为5(junk data)。

释放内存页(kfree)的操作就是检查指针是否对齐,是否在范围内,然后将它里面的数据设置为1(junk data),然后将它插入kmen中freelist的表头。

这里的junk data似乎有点像cpp stl中卫兵值的概念,标识数据结构的边界,一定程度上防止越界访问。

内存分配的初始化就是将对齐后的起始内存和终止内存之间的内存页都进行kree的操作。

系统调用:user.h

user.h中声明了21个已有的系统调用和一些简单的库函数。
    库函数的实现在ulib.c中。
    系统调用的实现在usys.S中。它实际上是由usys.pl脚本通过perl工具生成的。

syscall.h中声明了21个系统调用号的宏。

usys.S做的事情是将系统调用号装入a7寄存器中,然后调用ecall进行系统调用。

riscv页表

xv6采用的是3级页表,上面说过,每一页的大小为硬编码的4KB。

虚拟地址(64bit, 8B)中,高25位无效,有效位数为39位,被分为9,9,9,12,分别为一级页表,二级页表,三级页表,页内地址。

页表中的每一条记录(64bit, 8B)中,高10位无效,有效位数为54位,被分为44,10,分别为物理页号和标志位。
    标志位的低位:U,X,W,R,V。

    物理页号将和虚拟地址的末12位组合成一个56位的物理地址。

详情见https://blog.csdn.net/MCQSW/article/details/135172606

riscv陷阱处理(trap processing)

riscv的陷阱(trap)被分为中断(interrupt)和异常(exception),其中中断是同步的,是对硬件或系统事件的响应,异常是异步的,是对程序内部错误或特定事件的响应。

当出现陷阱时,如果是中断,则会根据sstatus中的sie位来判断是否要处理,还是将它置为待处理,如果是异常,则不受sie控制,它一定会被捕捉和正确处理。

监管模式下的陷阱处理时,硬件做的事情:
    1. PC -> sepc: 保存陷阱前的下一条指令的地址
    2. stvec -> PC: 进入陷阱处理
    3. 陷阱原因 -> scause, 额外信息 -> stval: 保存陷阱原因和额外信息
    4. 陷阱前的模式 -> sstatus.SPIE, 陷阱前的sstatus.sie -> sstatus.spie: 保存陷阱前的模式和是否开中断
    5. sstatus.SIE = 0: 关中断
    6. mode = supervisor: 模式设置成监管模式
    7. 最后进入陷阱处理

陷阱返回的流程(sret):
    sstatus.SPIE -> sstatus.SIE, sstatus.SPP -> mode, sepc -> PC

机器模式下的陷阱处理基本一致。唯一区别是mstatus中MPP位有2位。机器模式下处理的唯一中断是计时器中断,其他中断将被委托(delegate)给监管模式。计时器中断将被转换成一个软中断,在监管模式触发一个中断。这一点还不太懂。

而在xv6的实现中,陷阱的处理是这样的:
    1. 执行上述的硬件操作,会将stvec的值装入PC中,实际上就是跳入第二步的uservec做的准备。stvec(64bit, 8byte)的值是操作系统在初始化时装入的。
    2. 进入uservec(trampoline.S中),保存用户模式的通用寄存器,加载内核的通用寄存器,将内核的页表装入satp中,跳转至usertrap.c。
    3. 运行usertrap.c,陷阱处理结束后调用usertrapret.c。
    4. 调用userret(trampoline.S中),基本就是uservec的逆过程。
    5. 最后调用sret,即执行上述硬件的一些行为。
    
    值得注意的是,这里的2和5是在用户的地址空间中完成的。另外,这一系列的处理工作流与函数的调用很像,但是实际上这些调用都是不会返回的。

xv6 memlayout

每个用户模式的虚拟地址空间(即进程)的最顶部有trampoline和trapframe。

trampoline是一块代码(包含uservec和userret),用于快速转移到特定的处理程序或特定的代码上下文中,通常用于处理中断和异常。它的权限是可读,可执行,不可被用户模式访问。

trapframe 是在发生中断或异常时用来保存处理过程中的寄存器状态和上下文的一种数据结构。它通常是一个结构体,包含了所有需要保存的寄存器值,以便在中断处理完成后能够恢复之前的执行状态。它的权限是可读,可写,用户模式不可访问。

在xv6的布局中,二者各占一页的内存(4KB)。所有的trampoline都被映射到同一个物理地址。所有的trapframe被映射到不同的物理地址。

具体的memlayout见memlayout.h。

内核空间的虚拟地址就是物理地址,不管是否开启了虚拟寻址。

proc.h

struct context:上下文结构体

riscv的寄存器分为两类,调用者保存寄存器(caller-saved registers)和被调用者保存寄存器(callee-saved registers)。上下文结构体中,只保存了被调用者保存寄存器,即由被调用者来确保不变的寄存器(s0-s11)。

struct cpu: rt,里面有当前正在运行的proc结构体,上下文结构体,关中断深度,在关中断之前是否允许中断的信息。

struct trapframe: 它是一个内存页,具体构造详见代码。

struct proc: 里面有一个枚举类state, 包括{ UNUSED, USED, SLEEPING, RUNNABLE, RUNNING, ZOMBIE },有个锁,用于访问进程状态的一致性。具体构造详见代码。值得注意的是,其中的trapframe*指向的是陷阱内存帧的物理地址,我们只会在内核模式下使用这个指针。

trampoline.S和trap.c

trampoline.S中有uservec和userret两个汇编函数,它们kernel.ld被安放在内核代码块最后的位置,且被分配了一个内存页。

uservec要做的大致就是让,将a0保存至sscratch,让a0指向trapframe,保存31个通用寄存器(除了硬件恒零的x0以外),然后根据proc.h中trapframe的内存结构读取内核栈指针,cpu号,usertrap(下文介绍)的函数指针,内核页表的指针。

需要注意的是,上述的汇编代码是运行在内核空间的虚拟地址空间的。同时,trapframe的最顶部的那些非寄存器的内存,是100%不会修改的。同时每次对内核模式程序上下文的初始化是不需要恢复上次运行内核模式程序上下文的寄存器的。

接下来就会运行trap.c中的usertrap()。

usertrap不会return。

usertrap会根据scause寄存器中的值来判断trap的类型。
    当它为8时,就是系统调用,接下来会kill进程或者调用syscall()。
    
    否则,检查它是设备中断还是计时器中断。如果是计时器中断,则调用yield,重新进行进程调度。

usertrap处理结束后会调用usertrapret。

usertrapret会在做了一些事情后调用userret。

userret(uint64 pagetable)做的就是把uservec做的事情反过来做一遍,参数pagetable就是trapframe的首地址。

userret最后会调用sret。

系统调用的参数传递是通过risc-v的调用约定来实现的。a0-a6会由硬件来自动地进行参数装填,而a7需要装系统调用号,因此xv6自带的系统调用只能支持7个参数,每个参数不能超过8B。

进程调度:proc.c和swtch.S

cpuid(),核心(hart, cpu, core)id,只是返回tp寄存器中的值。为了避免在查询过程中,进程被移到不同cpu中,必须在关中断后访问。

mycpu(),根据cpuid来获得cpu。

myproc(),根据cpuid来获得cpu,并且获得当前该cpu正在运行的进程。

yield(),它会在每次的定时器中断中被调用。把当前cpu中目前正在运行的进程从RUNNING改成RUNNABLE,即停止运行。然后调用sched()。

sched()首先做一些检查,然后保存cpu的intena字段,最后调用swtch(struct context *old, struct context *new),它在swtch.S中。

swtch所做的事情就是把除了ax和tx寄存器以外的寄存器给存放到old指向的context中,并将new指向的context给加载到寄存器中。

值得注意的是,进程的context结构体中含有ra寄存器,即函数返回的地址,而swtch改变了context后,它最后调用ret会返回到new context中的返回地址。

scheduler(),它遵循的就是简单的时间片调度,且先进先出。

值得注意的是,scheduler在找到了新的可运行进程后会直接通过swtch来保存目前的上下文并直接切换到该进程,不会return。

sleep()和wakeup()

proc结构体中有一个chan字段,是专门用于sleep的。chan是一个void*,事实上就是一个uint64,用于标识正确唤醒。

sleep(void* chan, struct spinlock* lk),这里的lk是为了确保在sleep之前,cpu不会进入进程调度,从而保证及时进入sleep。它会将当前proc的chan字段设置为chan,并将状态设置为SLEEPING。

wakeup(void* chan)就是遍历整个proc,唤醒所有SLEEPING且chan字段为chan的proc。

串行输入输出: uart.c和console.c

就是运行xv6后的命令行,是这个操作系统与外界交互的一个接口。

这是和硬件相关的,这个硬件basicly维护了两个队列,一个用于Display一个用于Keyboard输入。

串口设备有自己的寄存器,被映射到地址空间的特定地址(UART0),我们可以通过响应的地址去读写串口设别的寄存器。

uart.c中有5个函数, uartputc_sync(), uartputc(), uartstart(), uartgetc(), uartintr()。

uart通过中断来调用uartintr()来让xv6知道外界输入了,外界输出了。每次键盘输入或者准备好接收输出的时候,uart都会发出中断。如果uart得到了用户的输入,就会调用consoleintr(),否则,会唤醒所有现在在uartputc上面睡着的进程,让它们继续putc。

console.c: 对uart的更高一级抽象 

console中自己维护了一个结构体实例cons,里面有128个字节的缓冲区,有r,w,e三个指针。

consoleintr()

大致的逻辑就是用户的输入->调用uartintr()->consoleintr(),printf()->uartputc_sync(), consolewrite()->uartputc()。 

虚拟内存: vm.c

这也是内存管理的大头。

内存映射通过将根页表(三级页表)的首地址传入硬件中来进行。

所有核心将会共享同一个内存空间,即共享同一个根页表。

ptr* walk(pagetable, va, alloc): 根据根页表和虚拟地址,返回第三级页表(存放物理地址的页表)的页表项的地址。如果alloc字段为1,则会按需创建页表。创建页表调用的函数是上面的kalloc(),即分配一页内存。

页表项中的VRWXU位分别为末01234位,定义在riscv.h中,这是由riscv架构定义的。

int mappages(pagetable, va, size, pa, params): 创建va到pa的映射,params就是VRWXU位。值得注意的是,它只会进行映射,而不会进行真正的内存分配,这个特性在kvmmap和kvmmake中会用到。

kvmmap()是mappages的一个封装,且只会在启动的时候被调用。

kvmmake()首先会调用kalloc来分配一页内存,将它作为根页表,然后将UART(串行输入输出设备),VIRTIO(磁盘),PLIC(平台级终端控制器),设置成可读可写,内核代码段设置成可读可执行,将从内核代码段顶端到0x80000000 + 128MB之间的内存设置为可读可写,将trampoline代码段映射到虚拟内存空间的最顶端,并设置为可读可执行。最后,调用proc_mapstacks()来为所有的进程分配栈页。除了trampoline代码段和栈映射以外,其他的都是直接映射。并且只有proc_mapstacks中真正地使用kalloc分配了内存。其他映射的内存可以被访问,但是kalloc看来,这些内存还是未经分配的。

值得注意的是,内核可以未经kalloc访问全部的内存空间,很方便但是非常危险。

即便在同一个根页表上下文中,多个虚拟内存也可以访问指向同一个物理地址,这很有趣。

kvminit()仅仅调用了kvmmake,将kvmmake返回的页表存在kernel_pagetable中。

kvminithart()会将kernel_pagetable写入w_satp中,即监管模式下的页表。

walkaddr(pagetable, va)根据页表和虚拟地址来返回物理地址。当且仅当这个物理页是UV时才会返回,否则返回0。

uvmcreate()用于创建一个空的用户页表。它做的仅仅是分配一页内存。

uvmfirst(pagetable, src, sz)为第一个用户进程分配内存,以便将initcode给装载到0虚拟内存中。sz必须比一页内存要小。它是RWXUV所有权限都被允许的。

初始进程只有一页的内存,里面从低到高是代码,从高到低是栈,没有堆和其他的东西。

用户空间中的堆是从低到高进行拓展的。

uvmalloc(pagetable, oldsize, newsize)返回newsize。它所要做的就是一页页地分配内存,直到用户pagetable的可用地址上限大于等于newsize为止。

uvmunmap(pagetable, va, numpages, do_free)就是对这些内存页进行遍历并且解除映射。如果do_free为真,那么它会对kalloc意义上地释放页。do_free应该是在对用户栈的差别处理。

uvmdealloc就是uvmalloc的逆过程。

uvmfree是从调用uvmunmap底向上地解除映射并且kalloc意义上释放内存,并且调用freewalk来删除页表。

freewalk就是删除页表,memset并且kree这些页表。

uvmcopy是在fork中被调用的。它假设已有了一个内存空间,并且trampoline和trapframe已经被映射/分配好了,然后从下至上copy代码段/Gurad page/栈/堆。是值语义上的复制。里面有一个goto语句,不知道是哪个该死的家伙写出来的。

uvmclear传入一个虚拟地址,将这个页表项的U位置为~U,作为保护页(Guard page)。

copyin(pagetable, dst, srcva, len)将用户空间中的东西给copy到内核空间中。

copyout将内核空间中的东西给copy到用户空间中。这里的pagetable都是用户空间下的根页表,而内核空间中的内存映射是连续的,所以不需要页表。

copyinstr是copyin的一个特例,但是只要遇到'\0'就会停止copy,并且返回实际copy的长度。

进程的创建: proc.c

procinit(): 仅在boot的时候被hart 0调用一次,初始化state为UNUSED, 初始化traceMast为0(自己加的追踪号),初始化kstack为该进程的栈在内核空间中的虚拟地址。

在每时每刻,cpu要么在运行用户进程,要么在运行调度进程。如果在运行调度进程,它的proc指针为0。

procdump(): 对所有非UNUSED的proc的状态和pid和进程名进行一次输出。它可以通过ctrl + p来调用。

allocproc(): 寻找一个UNUSED的proc,分配一个trapframe,创建页表并且分配栈并将栈映射到kstack中。

freeproc(): 释放proc所占用的资源。

forkret(): 如果是第一个进程,它会启动文件系统,然后调用usertrapret返回用户空间。

proc_pagetable(p): 为该进程创建一个页表,并且将TRAMPOLINE和TRAPFRAME给映射到根页表中。

proc_freepagetable(): 为trampoline和trapframe解除映射并且删除页表。

userinit(): 调用vm中的uvmfirst,并且将initcode给传入到uvmfirst中。将p->trapframe->的epc设置为0,让它返回的时候能从0开始执行代码。initcode是一个52字节的char,它会调用user/initcode.S的代码。

fork(), exit(), wait(), kill()

fork在创建子进程时,会将子进程的ra修改为forkret,此时它在被调度时,不会返回至sched(在yield时间片中断时,swtch会保存ra为sched的中间),而是直接进入forkret,如果不是init进程,那么它就会直接usertrapret()。

exit()不会返回。当一个进程调用exit时,如果它的父进程在waiting,那么状态将被返回至父进程,并且唤醒父进程,然后由父进程清理资源。否则,进程将会进入ZOMBIE状态,直到父进程调用wait。也就是说,进程的清理一定是由父进程来进行的。如果父进程在不调用wait的前提下直接exit,那么所有的子进程将被重定位给init进程。init进程是一个一直在死循环中等待的进程,只要有init的子进程exit,它就会进行子进程的清理。因此initproc永远不会退出。

void exit(int status),它会首先关闭所有打开了的文件,关闭文件目录,将子进程(如果有的话),唤醒父进程(如果正在wait中睡眠的话)。托付给initproc,将xstate字段设置为status,将状态变为ZOMBIE。最后调用sched()。

reparent()就是上述的托付操作。它所做的就是把所有的子进程的parent字段给设置为initproc。

uint64 wait(uint64*)会查找有没有ZOMBIE的子进程,如果有的话就释放它的资源,如果没有的话,就在这个进程控制块上面sleep。子进程的xstate将被复制到参数的指针中,并且返回子进程的pid。一个wait只会释放一个子进程。

kill(pid)会找到这个进程,然后将它的kill字段置为1,然后它在下一次进入系统模式的时候(自己调用系统调用或者被时间片中断),就会被usertrap中的检查给杀死。