kernel/bio.c

  • 定义了全局块缓存数据结构 bcache, 使用 bcache->lock 进行保护. bcache 中使用一个数组 buf[NBUF] 维护了一个静态双向链表, 链表中的每个元素 struct buf 包含缓存缓冲区 (大小默认为 BSIZE = 1024) data 以及引用计数 refcnt.
  • 定义了函数 binit, 用于初始化 bcache 中的静态双向链表.
  • 定义了函数 bget, 用于从 bcache 中获取指定的物理块 (缓存命中的情况下) 或是从链表中摘下一个空闲块 (若缓存不命中). 具体操作为首先正向遍历 bcache 并检查是否缓存命中, 如果缓存不命中则反过来从链表尾反向遍历 bcache 并摘下第一个空闲的块 (因为整个链表是按照 LRU 的顺序进行维护的).
  • 定义了函数 bread, 用于从硬盘中读取一个物理块. 具体操作为调用 bget 检查缓存是否命中, 并在缓存不命中的情况下调用 virtio_disk_rw 函数进行实际读取.
  • 定义了函数 bwrite, 用于将块的缓冲区写入物理硬盘.
  • 定义了函数 brelse, 用于释放给定的块并维护 LRU 链表.
  • 定义了函数 bpin, 用于 [TODO].
  • 定义了函数 bunpin, 用于 [TODO].

kernel/exec.c

  • 定义了函数 loadseg 函数, 用于将可执行文件从文件系统中读取至指定的页面.
  • 定义了函数 exec, 用于加载一个可执行文件至当前进程中并执行. 具体操作为检查给定文件是否为 ELF 格式的可执行文件, 为该可执行文件创建一个空白的页表 (通过调用 proc_pagetable 函数), 然后不断分配页面并将可执行文件加载至所分配的页面中 (通过调用 loadseg 函数), 在载入过程中对可执行文件的合法性进行检查, 分配一个新的进程表项, 分配两个页面作为哨兵页面和用户栈页面, 将哨兵页面标记为用户不可访问以便产生保护作用, 将命令行参数 (即若干个字符串) 复制到用户栈中, 将 argcargv 复制到用户栈中, 最后使用这一新的进程内存映像替换掉旧有的进程内存映像.

kernel/file.c

  • 定义了全局变量 ftable, 作为整个操作系统的全局打开文件表.
  • 定义了函数 fileinit, 用于初始化全局打开文件表 ftable.
  • 定义了函数 filealloc, 用于从 ftable 中获取一个空闲的表项并返回.
  • 定义了函数 filedup, 用于递增 struct file 类型指针的引用计数.
  • 定义了函数 fileclose, 用于关闭给定的 struct file 指针所对应的文件. 具体操作为递减 struct file 指针的引用计数, 如果 struct file 指针的引用计数降为零, 那么还将额外递减 inode 的引用计数 (通过调用 iput 函数).
  • 定义了函数 filestat, 用于获取给定的 struct file 类型指针所对应的文件的信息. 具体操作为调用 stati 函数获取 inode 中的信息并复制到给定的 struct stat 类型的对象中, 然后调用 copyout 函数将信息复制到用户内存空间中.
  • 定义了函数 fileread, 用于从给定的文件中读取数据. 具体操作为检查文件的类型 (可能为管道, 设备, 或者普通文件), 然后调用对应的专用函数进行读取.
  • 定义了函数 filewrite, 用于向给定的文件中写入数据. 具体操作为检查文件的类型 (可能为管道, 设备, 或者普通文件), 然后调用对应的专用函数进行写入.

kernel/fs.c

  • 定义了全局变量 struct superblock sb, 表示 xv6 中唯一的硬盘设备的 super block.
  • 定义了函数 readsb, 用于从硬盘中读取 super block 的内容至全局变量 sb 中.
  • 定义了函数 fsinit, 用于初始化文件系统. 具体操作为调用 readsb 读取 super block, 然后调用 initlog (其定义位于文件 kernel/log.c 中) 初始化文件系统的日志组件.
  • 定义了函数 bzero, 用于将一整个物理块置零 (并加入当前事务).
  • 定义了函数 balloc, 用于从硬盘中分配一个空闲块. 具体操作为遍历硬盘中的 bit map 区域, 寻找空闲块, 将硬盘中对应的 bit map 块中的比特置一并写回至硬盘, 最后返回空闲块.
  • 定义了函数 bfree, 用于释放一个物理块. 具体操作为将硬盘中对应的 bit map 块中的比特置零并写回至硬盘.
  • 定义了全局变量 icache, 用于缓存 inode. 其作用类似于 bcache.
  • 定义了函数 iget, 用于从 icache 中查找目标内存 inode, 并在查找失败时分配一个空闲的 inode. 如果查找成功则递增 inode 的引用计数. 其作用类似于 bget. 参数 inum 为硬盘 inode 的索引 (硬盘中的 inode 是按照线性顺序存放的).
  • 定义了函数 ialloc, 用于从硬盘中分配一个空闲的 (硬盘) inode, 并调用 iget 在缓存中分配一个空闲的 (内存) inode. 注意此时硬盘 inode 的内容并未写入内存 inode 中.
  • 定义了函数 iupdate, 用于将内存 inode 的内容写入硬盘 inode 中并添加至事务.
  • 定义了函数 idup, 用于递增内存 inode 的引用计数.
  • 定义了函数 ilock, 用于锁定内存 inode 并将硬盘 inode 的内容读取至内存中.
  • 定义了函数 iunlock, 用于解锁内存 inode.
  • 定义了函数 iupdate, 用于将内存 inode 的内容写入硬盘 inode 中.
  • 定义了函数 itrunc, 用于释放内存 inode 所对应的文件. 具体操作为调用 bfree 函数释放文件的每一个物理块, 然后调用 iupdate 函数将清零后的内存 inode 写入硬盘中.
  • 定义了函数 iput, 用于递减内存 inode 的引用计数并在引用计数与链接数均降为零时释放物理文件.
  • 定义了函数 iunlockput, 用于对内存 inode 进行解锁然后递减其引用计数. 实际上就是连续调用了 iunlockiput 这两个函数.
  • 定义了函数 bmap, 用于将文件的相对块号映射至绝对块号.
  • 定义了函数 stati, 用于将内存 inode 中的信息复制到给定的 struct stat 类型的对象中.
  • 定义了函数 readi, 用于从文件中读取数据至给定地址.
  • 定义了函数 writei, 用于将给定地址中的数据写入文件.
  • 定义了函数 dirlookup, 用于根据给定名称从目录文件中获取硬盘 inode 索引.
  • 定义了函数 dirlink, 用于将给定的 inode 链接到给定的目录中.
  • 定义了函数 skipelem, 用于切割路径. 类似于 Python 中的 os.path.split 函数.
  • 定义了函数 namex, 用于根据给定路径获取指定分量的 inode. 可根据需求返回路径的最后一个分量或倒数第二个分量的 inode.
  • 定义了函数 namei, 用于根据给定路径获取最后一个分量的 inode. 内部调用了 namex 函数.
  • 定义了函数 nameiparent, 用于根据给定路径获取倒数第二个分量的 inode. 内部调用了 namex 函数.

kernel/log.c

  • 定义了结构体 struct logheader, 用于记录事务的信息. 成员包括:
    • int n: 本次事务涉及到的物理块的块数.
    • int block[LOGSIZE]: 整型数组, 用于存储本次事务涉及到的所有物理块的块号.
  • 定义了结构体 struct log, 用于对事务进行抽象. 成员包括:
    • struct spinlock lock: 自旋锁.
    • int start: log header 位于硬盘中的块号. log header 独占一个物理块.
    • int size: 日志区域所包含的物理块的总数. 这其中除了第一块为 log header 所占用的物理块以外, 其余所有物理块均可用于事务.
    • int outstanding: 当前正在进行的文件系统操作的数量.
    • int committing: 标志位, 表示当前事务是否正在提交.
    • int dev: 设备号.
    • struct logheader lh: 当前事务的 log header.
  • 定义了全局变量 log, 用于在内存中维护当前事务.
  • 定义了函数 read_head, 用于将硬盘中的 log header 读取至内存中的全局变量 log 中.
  • 定义了函数 install_trans, 用于将当前事务写入硬盘中.
  • 定义了函数 write_head, 用于将当前事务的 log header 写入硬盘.
  • 定义了函数 recover_from_log, 用于恢复未完成的事务. 具体操作为读取 log header (即事务), 调用 install_trans 函数写入事务, 待事务写入完成后将事务的块数置为零以将其标记为已完成, 最后写入 log header 至硬盘中.
  • 定义了函数 initlog, 用于初始化硬盘日志. 具体操作为从硬盘中读取未完成的事务的信息并初始化全局变量 log, 然后调用 recover_from_log 函数恢复未完成的事务.
  • 定义了函数 begin_op, 用于标记一次文件系统操作的开始. 具体操作为检查当前事务是否正在提交 (如果正在提交则暂时睡眠), 检查日志区域的剩余空间是否能够容纳本次文件系统操作 (如果不能则暂时睡眠), 最后递增 log.outstanding 以标志本次操作的开始.
  • 定义了函数 write_log, 用于将事务涉及到的 (修改后的位于内存中的) 所有物理块写入至 (位于硬盘中的) 日志区域中.
  • 定义了函数 commit, 用于提交事务以及执行事务. 具体操作为调用 write_logwrite_head 将当前事务写入日志区域, 然后调用 install_trans 执行事务, 执行完事务后将事务的块数置为零并再次调用 write_head 写入日志区域以标记事务的完成.
  • 定义了函数 end_op, 用于标记一次文件系统操作的结束. 具体操作为递减 log.outstanding, 如果递减至零则忽略任何正在等待的线程并直接启动提交, 否则唤醒任何正在等待的线程加入本次事务. 如果确定需要启动提交, 则调用 commit 函数提交并执行事务, 并在事务执行完成后唤醒任何正在等待的进程.
  • 定义了函数 log_write, 用于将给定的物理块加入到当前事务中. 具体操作为在当前事务中寻找给定物理块的块号, 如果找到则执行日志吸收 (log absorbtion), 否则将当前物理块加入本次事务并递增本次事务所涉及到的物理块的总数. 如果本次给定的物理块为新增的物理块那么还需要将其引用计数进行额外的一次递增以防止缓存被覆盖 (该次引用计数将在事务执行结束时递减).

kernel/main.c

  • 定义了函数 main, 用于初始化若干个设备和子系统, 然后调用 userinit 函数 (位于文件 kernel/proc.c 中).

kernel/kalloc.c

  • 定义了结构体 struct run 表示空闲物理页面链表中的单个结点.
  • 定义了全局对象 kmem, 用于存储空闲物理页面链表的表头并对空闲物理页面的分配进行同步, 其成员包括空闲物理页面链表的表头以及一个同于同步的自旋锁.
  • 定义了函数 kfree 函数用于对单个空闲物理页面进行释放. 释放的过程主要分为两步, 第一步是将该页面中的内容替换为随机比特 (具体做法是将所有字节赋值为 0x1), 以便使任何指向该区域内的悬空指针失效并尽可能快地报错, 第二步是将该页面插回空闲物理页面链表中.
  • 定义了函数 freerange 用于对一个范围内的所有空闲物理页面进行释放. 内部调用了 kfree 函数.
  • 定义了函数 kinit, 用于初始化空闲物理页面链表. 程序开始时从 end (此符号的定义见文件 kernel/kernel.ld) 到 PHYSTOP (此符号的定义见文件 kernel/memlayout.h) 为止的这一范围内的所有物理内存页面均作为空闲物理页面.
  • 定义了函数 kalloc, 用于单次分配一个页面. 具体做法是从空闲链表中抽取出一页, 向其中写入随机比特, 然后返回. 由于全局变量 kmem 的指针成员在一开始时默认初始化为空指针, 这使得链表尾总是被正确地初始化为空指针, 因此当页面耗尽时本函数将正确地返回一个空指针.

kernel/pipe.c

  • 定义了函数 pipealloc, 用于创建一个管道. 具体操作为调用 filealloc 函数分配两个打开文件表项, 然后调用 kalloc 函数分配一个物理块作为管道的承载内存, 最后对管道的数据结构进行初始化.
  • 定义了函数 pipeclose, 用于关闭一个管道.
  • 定义了函数 pipewrite, 用于向管道中写入数据, 并在缓冲区已满时将当前进程置为睡眠.
  • 定义了函数 piperead, 用于从管道中读取数据, 并在缓冲区为空时将当前进程置为睡眠.

kernel/proc.c

  • extern 了一个符号 trampoline (其定义位于文件 kernel/trampoline.S 中), 该符号的意义是作为 "蹦床 (trampoline)" 代码的起始地址标记蹦床代码所处的物理页面, 以便在函数 proc_pagetable (其定义位于文件 kernel/proc.c 中) 中将其映射至虚拟页面 TRAMPOLINE 中.
  • 定义了函数 procinit, 用于初始化进程表中的每一个进程项, 并为每个进程分配一个内核栈页面, 其中每个栈页面均后接一个非法的哨兵虚拟页面作为针对栈溢出的保护措施.
  • 定义了函数 cpuid, 用于读取当前线程所在的 CPU 的索引.
  • 定义了函数 mycpu, 用于返回存储着当前 CPU 的信息的数据结构的起始地址.
  • 定义了函数 myproc, 用于返回存储着当前 CPU 所运行的进程的信息的数据结构的起始地址.
  • 定义了函数 allocpid, 用于原子性地获取下一个 PID.
  • 定义了函数 proc_pagetable, 用于分配并初始化进程的页表. 具体操作为分别映射了蹦床代码所在的物理页面 trampoline 和进程的陷入栈帧所在的物理页面 p->trapframe.
  • 定义了函数 allocproc, 用于在进程表中寻找并返回一个空闲的进程表项. 具体操作为在进程表中寻找一个空闲表项, 为其分配 PID, 为陷入栈帧分配一个物理页面, 初始化页表, 设置返回地址为 forkret 函数, 最后设置栈顶指针 spp->kstack + PGSIZE, 即内核页面的末尾地址. 注意本函数成功返回的时候, 所分配的进程的锁仍未释放.
  • 定义了函数 proc_freepagetable, 用于解绑蹦床代码页面 TRAMPOLINE 和陷入栈帧页面 TRAPFRAME (因为这两个页面的物理页面的分配和释放是单独进行的), 然后释放所有物理页面和页表页面 (通过调用 uvmfree 函数).
  • 定义了函数 freeproc, 用于释放进程. 具体操作为释放陷入栈帧, 释放进程内存 (通过调用 proc_freepagetable 函数), 然后将各种进程表项中的字段置零.
  • 定义了代码段 initcode, 该代码段实际上是系统开机后的第一个进程, 该进程的作用是调用 /init 程序 (exec("/init")).
  • 定义了函数 userinit, 用于初始化系统开机后的第一个进程. 具体操作为分配空闲进程表项 (通过调用 allocproc 函数), 加载硬编码的代码段 initcode (通过调用 uvminit 函数), 初始化进程的各种上下文 (页表大小, PC, 栈指针, 进程名称, CWD 等等), 最后对进程的锁进行释放.
  • 定义了函数 growproc, 用于增加或减少进程的内存大小.
  • 定义了函数 fork, 用于创建子进程. 具体操作为分配一个新的进程表项, 复制父进程的内存映像至子进程中, 复制父进程的陷入栈帧中所保存的寄存器的旧值至子进程中, 将子进程的返回值 (总是为 0) 复制至其陷入栈帧中 (父进程的返回值将由 syscall 函数 (其定义位于文件 kernel/syscall.c 中) 复制到父进程的陷入栈帧中), 复制父进程的文件描述符表至子进程中 (此举将对每个文件描述符的引用计数加一), 最后将子进程的运行状态设置为 RUNNABLE.
  • 定义了函数 sched, 用于从用户进程的上下文中切换回调度器的上下文. 具体操作为通过调用 swtch 函数 (其定义位于文件 kernel/swtch.S 中) 保存当前程序的上下文 (包含返回地址) 并切换为调度器的上下文, 然后返回调度器中调用 swtch 函数的位置.
  • 定义了函数 scheduler, 用于不断循环并切换至可运行的进程. 具体操作为遍历进程表, 寻找可调度的进程, 并通过调用 swtch 函数切换至该进程的上下文中继续执行.
  • 定义了函数 sleep, 用于将当前进程的状态置为 "睡眠 (SLEEPING)" 并将执行权让出给调度器. 相当于 C++ 标准库的条件变量的 cond.wait() 方法.
  • 定义了函数 wait, 用于循环等待当前进程的某个子进程退出. 具体操作为遍历系统进程表, 找到当前进程的子进程, 检查其是否处于僵尸态, 复制其退出状态码, 最后返回子进程的 PID. 若当前进程不存在任何子进程则直接返回. 若当前进程存在子进程但均在正常运行则通过调用 sleep 函数睡眠在其自身的睡眠队列中, 并等待子进程唤醒当前进程.
  • 定义了函数 wakeup, 用于唤醒给定的睡眠队列中的所有进程. 相当于 C++ 标准库的条件变量的 cond.notify_all() 方法.
  • 定义了函数 wakeup1, 用于唤醒给定的进程. 该进程能够被唤醒当且仅当该进程睡眠在其自身的睡眠队列中.
  • 定义了函数 reparent, 用于将给定进程的所有子进程挂到 initproc 进程下.
  • 定义了函数 exit, 用于退出当前进程. 具体操作为关闭当前进程的所有打开文件, 将当前进程的所有子进程挂到 initproc 进程下 (通过调用 reparent 函数), 唤醒父进程, 最后跳转回调度器.
  • 定义了函数 yield, 用于主动将当前进程的执行权让出给调度器.
  • 定义了函数 forkret, 用于子进程从内核态返回用户态. 具体操作为释放进程锁并调用 usertrapret 函数 (其定义位于文件 kernel/trap.c 中). 此外如果当前为开机以来首次调用 forkret 函数 (一般是 /init 中调用 forkexec 执行 sh 程序的时候), 那么还会对文件系统进行初始化 (通过调用 fsinit 函数, 其定义位于文件 kernel/fs.c 中).
  • 定义了函数 kill, 用于更改指定进程的进程表项中的 killed 字段, 将其置为 1.
  • 定义了函数 either_copyout, 用于 [TODO].
  • 定义了函数 either_copyin, 用于 [TODO].
  • 定义了函数 procdump, 用于 [TODO].

kernel/spinlock.c

  • 定义了函数 initlock, 用于初始化一个自旋锁.
  • 定义了函数 acquire, 用于获取一个自旋锁.
  • 定义了函数 release, 用于释放一个自旋锁.
  • 定义了函数 holding, 用于检查当前 CPU 是否持有指定的自旋锁. 调用本函数前必须确保 CPU 禁用了中断, 以保证本函数的原子性.
  • 定义了函数 push_off, 用于禁用当前 CPU 的中断, 并记录本函数的嵌套调用深度. 如果本次调用为第一层调用, 那么还会记录调用前 CPU 中断的启用信息.
  • 定义了函数 pop_off, 用于弹出一层 push_off 函数的调用深度, 并在完全弹出时恢复 CPU 中断.

kernel/start.c

  • 定义了函数 timerinit, 用于为机器模式的定时器中断的捕获逻辑进行初始化. 具体操作为设置时间片长度 (QEMU 下约为 1/10 秒), 初始化与 timervec 函数相关的数据, 最后使能机器模式下对定时器中断的接收.
  • 定义了函数 start, 用于上下文切换并跳转至 main 函数. 具体操作为将 MPP 设置为内核模式, 设置返回地址为 main 函数的入口地址, 使能内核模式对硬件中断, 定时器中断, 以及软件中断的接收, 将所有中断和异常统统从机器模式转发给内核模式, 调用 timerinit 函数, 将 CPU ID 存入寄存器 tp 中以便 cpuid 函数 (其定义位于文件 kernel/proc.c 中) 能够查询, 最后调用 mret 指令返回至内核模式并跳转至 main 函数.

kernel/syscall.c

  • 定义了函数 fetchaddr, 用于获取当前进程, 并通过调用函数 copyin 从当前进程的页表中的虚拟地址 addr 处读取一个 uint64 类型的值到目标指针 ip 处.
  • 定义了函数 fetchstr, 作用和 fetchaddr 类似, 区别是所读取的数据为字符串, 并且所调用的函数为 copyinstr 而不是 copyin.
  • 定义了函数 argraw, 用于从陷入栈帧中以 uint64 的形式获取并返回系统调用的第 n 个参数.
  • 定义了函数 argint, 用于从陷入栈帧中以整型 int 形式获取并返回系统调用的第 n 个参数.
  • 定义了函数 argaddr, 用于从陷入栈帧中以指针 uint64 形式获取并返回系统调用的第 n 个参数.
  • 定义了函数 argstr, 用于将陷入栈帧中存储的系统调用的第 n 个参数作为起始地址以字符串形式将数据复制到目的地址中.
  • 定义了系统调用表数组 syscalls, 其中存储了每个系统调用的实际实现的入口地址 (例如第 SYS_fork 个元素就是 sys_fork 函数指针). 各个 sys_xxx 函数的具体实现散落在文件 kernel/sysfile.ckernel/sysproc.c 中.
  • 定义了函数 syscall, 用于从陷入栈帧中获取寄存器 a7 中存储的系统调用的整数代码, 然后调用该系统调用, 最后将系统调用的返回值存储在陷入栈帧中的寄存器 a0 中.

kernel/sysfile.c

  • 定义了函数 argfd, 用于获取传入给系统调用的文件描述符参数, 并从当前进程的文件描述符表中获取该文件描述符所对应的 struct file 类型指针.
  • 定义了函数 fdalloc, 用于从当前进程中分配一个文件描述符表项来保存所传入的 struct file 类型指针, 并返回所分配的文件描述符.
  • 定义了系统调用 sys_dup, 用于在当前进程的文件描述符表中克隆给定的文件描述符. 具体操作为在当前进程的文件描述符表中找到给定的文件描述符所对应的 struct file 类型对象指针, 然后分配一个新的文件描述符指向该 struct file 指针, 最后调用 filedup 函数递增 struct file 指针的引用计数.
  • 定义了系统调用 sys_read, 用于从文件中读取数据. 实际上就是函数 fileread (其定义位于文件 kernel/file.c 中) 的一个包装函数.
  • 定义了系统调用 sys_write, 用于向文件中写入数据. 实际上就是函数 filewrite (其定义位于文件 kernel/file.c 中) 的一个包装函数.
  • 定义了系统调用 sys_close, 用于关闭给定的文件描述符所对应的文件. 具体操作为查找给定的文件描述符在当前进程中所对应的 struct file 指针, 将其置为零, 然后调用 fileclose 函数关闭给定的文件.
  • 定义了系统调用 sys_fstat, 用于获取文件的元数据. 实际上就是函数 filestat (其定义位于文件 kernel/file.c 中) 的一个包装函数.
  • 定义了系统调用 sys_link, 用于创建一个硬链接 new 来指向 old 所指向的文件. 具体操作为调用 namei 函数来确定 old 所指向的文件存在并且是普通文件, 递增 inode 的链接计数, 调用 nameiparent 函数来确定 new 所在的目录, 最后调用 dirlink 新增一个目录项指向 old 所对应的文件的 inode.
  • 定义了函数 isdirempty, 用于确定一个目录是否为空 (即是否仅含有 ... 这两个文件的链接).
  • 定义了系统调用 sys_unlink, 用于删除一个目录中的指定链接. 具体操作为调用 nameiparent 函数找到倒数第二个路径部件的 inode, 调用 dirlookup 函数找到指定文件的 inode, 清空目录中对应于该文件的链接, 递减指定文件的被链接数 ip->nlink, 如果该文件为目录则同时递减倒数第二个路径部件的被链接数 (因为子目录总是含有一个 .. 链接指向其父目录, 因此删除子目录的同时也将递减父目录的被链接数).
  • 定义了函数 create, 用于创建一个新文件. 具体操作为首先检查该文件是否已经存在, 如果存在并且该文件为普通文件则直接返回, 如果存在但该文件不是普通文件则返回零, 然后调用 ialloc 函数从硬盘中分配一个空闲的 inode 给该文件, 如果该文件为目录则还需要额外创建 ... 两个固定链接, 最后调用 dirlink 函数将该 inode 链接至父目录中.
  • 定义了系统调用 sys_open, 用于打开一个文件. 具体操作为寻找给定路径对应的 inode (并在未能找到的时候视用户的需求创建新 inode), 调用 filealloc 函数 (其定义位于文件 kernel/file.c 中) 在操作系统全局的打开文件表中分配一个新的 struct file 表项, 调用 fdalloc 函数在当前进程的文件描述符表中分配一个新的文件描述符表项, 最后视用户的需求调用 itrunc 函数对文件进行截断.
  • 定义了系统调用 sys_mkdir, 用于创建新目录. 实际上内部调用的就是 create 函数.
  • 定义了系统调用 sys_mknod, 用于创建新设备文件. 实际上内部调用的就是 create 函数.
  • 定义了系统调用 sys_chdir, 用于更改当前进程的当前工作目录 (CWD). 具体操作为调用 namei 查找给定路径所对应的 inode (这一过程中将会递增该文件的引用计数), 然后将当前进程的当前工作目录 p->cwd 更改为该文件.
  • 定义了系统调用 sys_exec, 用于载入新程序并执行. 具体操作为获取用户传入的命令行参数, 然后调用实际的工作函数 exec (其定义位于文件 kernel/exec.c 中).
  • 定义了系统调用 sys_pipe, 用于创建管道并分配两个文件描述符.

kernel/trap.c

  • 定义了全局变量 ticks, 用于处理定时器中断.
  • 定义了函数 trapinithart, 用于设置 stvec 寄存器为 kernelvec 函数 (其定义位于文件 kernel/kernelvec.S 中) 的首地址.
  • 定义了函数 usertrapret, 用于设置此后的陷入 (包括系统调用, 硬件/软件中断, 以及异常) 将会用到的各种信息, 并从内核态切换回用户态. 具体操作为将寄存器 stvec 设置为函数 uservec 的入口地址 (其定义位于文件 kernel/trampoline.S 中) 用于重定向陷入的跳转位置, 设置当前进程的陷入栈帧中的各类上下文信息, 恢复用户态的权限 (更改执行模式寄存器 SPP 为用户态, 并重新使能中断), 最后调用 userret 函数 (其定义位于文件 kernel/trampoline.S 中) 正式从内核态切换回用户态.
  • 定义了函数 clockintr, 用于处理定时器中断. 具体操作为调用 wakeup 函数 (其定义位于文件 kernel/proc.c 中) 唤醒所有等待在全局变量 ticks 上的进程.
  • 定义了函数 devintr, 用于处理外部硬件中断和定时器中断 (所引发的软件中断). 具体操作为如果是硬件中断则调用若干函数来进行处理, 如果是定时器中断则调用函数 clockintr 来进行处理, 最后返回一个标志来反映本次中断的类型.
  • 定义了函数 usertrap, 用于对用户模式下的中断和异常进行路由. 具体操作为设置 stvec 寄存器为 kernelvec 函数的首地址 (注意到这其实就是函数 trapinithart 的行为), 保存用户的 PC, 并检查中断来源, 如果是系统调用则调用 syscall 函数 (其定义位于文件 kernel/syscall.c 中), 如果是外部设备中断或者定时器中断 (所引发的软件中断) 则交给 devintr 函数处理, 如果是异常 (包括缺页异常) 则打印该异常并杀死当前进程, 此外如果是定时器中断还将调用 yield 函数来让出执行权, 最后调用 usertrapret 函数来返回.
  • 定义了函数 kerneltrap, 用于内核模式下的中断和异常进行路由. 其逻辑与 usertrap 函数大体上一致, 区别是不需要对系统调用进行路由 (系统调用仅在用户模式下由用户发起) 以及不需要内核模式与用户模式的切换 (因为本来就是在内核模式中接受中断的).

kernel/vm.c

  • 定义了全局变量 kernel_pagetable, 其类型为 pagetable_t (定义在文件 kernel/riscv.h 中), 表示内核页表的起始地址.
  • 定义了函数 kvminit, 用于初始化内核页表. 具体操作为分配根页表, 映射 I/O 设备, 映射内核代码, 映射内核数据和空闲物理页面, 最后映射蹦床代码页面.
  • 定义了函数 kvminithart, 用于将硬件页表寄存器的内容切换为内核页表地址, 并 flush 快表 TLB.
  • 定义了函数 walk, 用于从三级页表开始根据虚拟内存地址一步一步获取到一级页表中对应于物理页面的 pte 的地址 (注意不是物理页面的地址而是 pte 的地址). 如果遍历的过程中遇到空的页表项, 本函数的用户可以通过传入额外的参数来决定是否分配页表页面.
  • 定义了函数 mappages, 用于将指定范围内的虚拟内存页面逐个映射至指定范围内的物理内存页面, 其中三级页表可以是任意的, 不一定是内核页表. 其大致逻辑是通过调用 walk 函数获取到一级 pte 的地址, 确定该 pte 仍未被关联至任何物理页面, 然后将该 pte 关联至当前物理页面.
  • 定义了函数 kvmmap, 其功能与 mappages 基本相同, 唯一的区别是本函数所使用的三级页表固定为内核页表.
  • 定义了函数 uvmcreate, 用于分配一个物理页面作为用户空间页表的根页面.
  • 定义了函数 uvminit, 用于为系统的第一个进程分配首地址为零的虚拟页面并加载初始化代码 initcode. 仅被调用一次.
  • 定义了函数 uvmunmap, 用于将指定范围内的虚拟页面与物理页面解绑 (具体操作是将对应的 pte 置零), 并且可以选择同时释放物理页面.
  • 定义了函数 freewalk, 用于递归地释放所有页表页面. 调用本函数前必须确保所有物理页面已经被释放.
  • 定义了函数 uvmfree, 用于释放所有分配给用户的物理页面 (通过调用 uvmunmap 函数), 并紧接着释放所有用户页表页面 (通过调用 freewalk 函数).
  • 定义了函数 uvmcopy, 用于复制一整个进程的内存映像 (包括所有页表页面和所有物理页面).
  • 定义了函数 uvmdealloc, 用于将进程的物理页面总大小从较大的旧大小 oldsz 缩小至较小的新大小 newsz. 具体操作为通过调用 uvmunmap 函数间接进行释放.
  • 定义了函数 uvmalloc, 用于将进程的物理页面总大小从较小的旧大小 oldsz 增大至较大的新大小 newsz.
  • 定义了函数 uvmclear, 用于将一个 PTE 标记为用户不可访问的. 主要用于 exec 函数中.
  • 定义了函数 walkaddr, 用于将用户空间虚拟地址转换为物理地址 (使用进程页表).
  • 定义了函数 kvmpa, 用于将内核栈虚拟地址转换为物理地址 (使用内核页表).
  • 定义了函数 copyout, 用于将内核空间中的数据根据用户页表复制到用户空间中.
  • 定义了函数 copyin, 用于将用户空间中的数据根据用户页表复制到内核空间中.
  • 定义了函数 copyinstr, 用于将用户空间中的字符串根据用户页表复制到内核空间中. 特点是遇到 \0 便会停止复制.

user/init.c

  • 定义了函数 init, 用于执行初始化控制台并启动一个 shell. 具体操作为分配三个文件描述符, 即 stdin, stdout, 以及 stderr, 然后进入外层无限循环创建子进程以便运行 shell, 创建成功之后将会进入内层无限循环等待所有被挂到 init 下的孤儿子进程结束. init 即系统开机以来所执行的首个进程.

kernel/memlayout.h [TODO]

  • 定义了宏 KERNBASE (其值固定为 0x80000000L) 和 PHYSTOP (其值固定为 KERNBASE 加上 128 MB), 两者均为物理地址, 两者所构成的范围由且仅由内核代码以及所有可用的物理页面组成.
  • 定义了宏 TRAMPOLINE, 表示为蹦床代码分配的虚拟页面的起始地址, xv6 将地址最大的虚拟页面分配给了它. 为蹦床代码分配的虚拟页面的起始地址必须是页表无关的, 也就是说用户页表中的蹦床代码的虚拟页面和内核页表中的必须是相同的, 否则我们无法切换页表, 因为切换页表就意味着 PC 失效.
  • 定义了宏 KSTACK, 用于将进程的物理页面映射至对应的内核虚拟页面 (所有内核虚拟页面从 TRAMPOLINE 开始向低地址方向生长, 其中每个内核虚拟页面均后接一个非法的哨兵虚拟页面作为针对栈溢出的保护措施).
  • 定义了宏 TRAPFRAME, 表示为陷入栈帧分配的虚拟页面的起始地址, xv6 将地址次大的虚拟页面分配给了它. 陷入栈帧只对用户空间有意义.

kernel/param.h

  • 定义了一堆与系统资源相关的阈值, 例如最大进程数, 最大 CPU 数等等.

kernel/proc.h

  • 定义了类型 struct proc, 用于对进程模型进行抽象. 该类型的成员包括:
    1. struct spinlock lock: 当前进程的自旋锁.
    2. 公有成员 (需要使用进程锁 lock 进行同步):
      1. enum procstate state: 当前进程的执行状态.
      2. struct proc *parent: 当前进程的父进程.
      3. void *chan: 如果非零则表示当前进程阻塞, 其值为所位于的阻塞队列的序号.
      4. int killed: 如果非零则表示当前进程已被 kill 掉.
      5. int xstate: 当前进程的退出状态, 将返回给父进程的 wait 调用.
      6. int pid: 当前进程的 PID.
    3. 私有成员 (不需要同步):
      1. uint64 kstack: 当前进程的内核栈的虚拟地址.
      2. uint64 sz: 当前进程所占用的内存大小 (单位: 字节).
      3. pagetable_t pagetable: 当前进程的页表.
      4. struct trapframe *trapframe: 指向陷入栈帧, 陷入栈帧用于在用户模式和内核模式之间进行上下文切换时.
      5. struct context context: 调度上下文, 用于内核模式下在不同进程之间进行上下文切换时.
      6. struct file *ofile[NOFILE]: 当前进程的打开文件表 (默认最大条目数为 16).
      7. struct inode *cwd: 当前进程的当前工作目录.
      8. char name[16]: 当前进程的名称 (用于调试).
  • 定义了类型 struct cpu, 用于对 CPU 模型进行抽象. 该类型的成员包括:
    1. struct proc *proc: 运行在当前 CPU 上的进程.
    2. struct context context: 调度上下文, 用于内核模式下在不同进程之间进行上下文切换时.
    3. int noff: 在当前 CPU 上嵌套调用 push_off 函数 (位于 kernel/spinlock.c:88) 的深度.
    4. int intena: 标志位, 指示在当前 CPU 上最近一次的第一层 push_off 调用前是否启用了中断.
  • 最后 extern 了一个 CPU 数组: struct cpu cpus[NCPU].

kernel/riscv.h [TODO]

  • 定义了函数 r_sstatus, 用于获取 RISC-V 寄存器 sstatus 的值.
  • 定义了函数 w_sstatus, 用于向 RISC-V 寄存器 sstatus 中写入值.
  • 定义了函数 intr_get, 用于获取当前 CPU 是否启用了中断 (SSTATUS_SIE 即 "supervisor interrupt enabled").
  • 定义了函数 intr_off, 用于禁用当前 CPU 的中断.
  • 定义了宏 PGSIZEPGSHIFT 分别表示物理页面大小 (4096 字节) 及其关于 2 的指数 (等于 12, 因为 $2^12 = 4096$).
  • 定义了宏 PGROUNDUPPGROUNDDOWN 分别用于对虚拟地址按物理页面大小向上和向下进行舍入.
  • 定义了宏 PTE_V, PTE_R, PTE_W, PTE_X, 以及 PTE_U 表示页表项所对应的物理页面的状态信息以及访问权限. 这五个标志位分别占据页表项 pte 的最低的五个比特.
  • 定义了宏 PA2PTEPTE2PA 用于在页表项 pte 和该页表项所对应的物理页面的首地址之间进行转换. 其中页表项 pte 的低 10 位被用于存储一些必要的标志位, 而第 11 位至第 30 位用于存储物理页面的标记 (因为空闲物理页面从 0x80000000 开始 (长 32 位), 并且只有 128 MB (或 27 位偏移), 因此物理页面标记至少需要 32 - 12 = 20 位来存储).
  • 定义了宏 PTE_FLAGS 用于从页表项 pte 中提取所有标志位.
  • 定义了宏 PXMASK, PXSHIFT, 以及 PX 用于从虚拟地址中抽取总共三个不同级别的页表的偏移值.
  • 定义了宏 MAXVA, 表示虚拟内存地址的最大合法值加一.
  • 定义了类型 pte_t, 表示单个页表项. 其类型为 uint64, 因此单个页表项占据 8 个字节, 而一个物理页面的大小为 4096 字节, 因此单个页表页面中所能包含的最大页表项个数为 512, 在虚拟内存地址中对应的偏移长度为 9 比特. 由于总共有三个不同级别的页表, 因此一个 64 位的虚拟内存地址由 5 个部分构成: 0-11 位为页内偏移, 12-20 为一级页表偏移, 21-29 为二级页表偏移, 30-38 为三级页表偏移, 39-63 位固定为零.
  • 定义了类型 pagetable_t, 表示页表的起始地址 (页表的默认大小为 512).

kernel/spinlock.h

  • 定义了类型 struct spinlock, 用于对自旋锁模型进行抽象. 该类型的成员包括:
    1. uint locked: 标志位, 表示该自旋锁是否被获取.
    2. char *name: 该自旋锁的名称 (用于调试).
    3. struct cpu *cpu: 持有该自旋锁的 CPU (用于调试).

kernel/syscall.h

  • 定义了每个系统调用的整数代码 (例如 SYS_fork -> 1), 这个整数代码将用于索引系统调用表 syscalls 中的条目.

user/user.h

  • 上半部分定义了系统调用接口 (例如 int fork(void);), 下半部分定义了用户常用的库函数 (例如 printfmalloc, 其实现位于源文件 user/ulib.c 中).

user/usys.pl

  • 这是一个 perl 脚本文件, 其中使用了 perl 来现场生成 RISC-V 汇编文件 user/usys.S.
  • user/usys.S 中定义了各个系统调用的实际入口 (例如 .global fork), 后接该系统调用的实际函数体. 每个函数体的工作都是将系统调用的整数代码 (例如 SYS_fork) 放入寄存器 a7 中然后调用 ecall 指令.

kernel/kernel.ld

  • 这是一个链接器脚本文件, 用于操控链接过程以及配置所生成的二进制文件的内存布局等等.
  • 特别是该文件将 SECTIONS 的起始地址设置为了 0x80000000, 这将使得 kernel/entry.S 中定义的 _entry: 入口落在这个地址上. 之所以要这么做是因为 qemu 的 -kernel 选项第一步总是固定跳转至这个地址.
  • 此外该文件中还定义了表示整个二进制文件的结束地址的符号 end.

kernel/entry.S

  • 本文件中的汇编代码用于初始化系统自开机以来的第一个栈帧. 具体操作为设置堆栈指针寄存器 spstack0 + (hartid * 4096) 来为 xv6 初始化栈帧, 然后调用 start 函数 (其定义位于文件 kernel/start.c 中).

kernel/kernelvec.S

  • 本文件中的汇编代码用于处理内核模式下的中断 (interrupts) 和异常 (exceptions), 以及机器模式 (machine mode) 下的定时器中断 (timer interrupts).
  • 定义了函数 kernelvec, 用于处理内核模式下的中断和异常. 具体操作为将所有寄存器的值备份至内核栈帧 (即 p->trapframe->kernel_sp, 其值为 p->kstack + PGSIZE, 在函数 usertrapret 中被设置) 中, 调用 kerneltrap 函数 (其定义位于文件 trap.c 中), 等到 kerneltrap 函数返回后再使用 sret 指令进行返回.
    • kernelvec 函数由 trapinithart 函数 (其定义位于文件 kernel/trap.c 中) 进行注册, 注册至 stvec CSR (Control and Status Register) 寄存器中.
  • 定义了函数 timervec, 用于处理机器模式下的定时器中断. 具体操作为备份寄存器的值, 设置专用的寄存器以准备下一次定时器中断, 使用 SIP (Supervisor Interrupt Pending) 寄存器将本次定时器中断转化为一次软件中断, 恢复寄存器的值, 最后使用 mret 指令进行返回.
    • timervec 函数由 timerinit 函数 (其定义位于文件 kernel/start.c 中) 进行注册, 注册至 mtvec CSR 寄存器中.

kernel/swtch.S

  • 本文件中的汇编代码用于对当前 CPU 的执行流的上下文进行切换.
  • 定义了函数 swtch, 用于对当前 CPU 的执行流的上下文进行切换. 具体操作为将当前执行流的上下文存储至第一个 struct context 类型 (其定义位于文件 kernel/proc.h 中) 的上下文结构体参数中, 然后将第二个 struct context 类型的上下文结构体参数中的上下文载入到当前执行流中, 最后直接执行指令 ret 进行跳转. 当前执行流的上下文包括:
    • ra: 即 swtch 函数的调用者的下一条指令的地址.
    • sp: 栈顶指针.
    • s0-s11: callee-save 寄存器.
  • 一个 struct context 类型的结构体包含的上下文包括返回地址 ra 和栈顶指针 sp, 以及 "被调用者保存 (callee-save)" 的寄存器 s0-s11.

kernel/trampoline.S

  • 本文件中的汇编代码用于在用户空间与内核空间之间进行上下文切换并执行跳转. 代码段的起始地址由符号 trampoline 进行标记, 该符号在文件 kernel/proc.c 中被使用.
  • 本文件编译形成的二进制代码块由链接器脚本文件 kernel/kernel.ld 负责对齐, 具体操作为通过对符号 trampsec 关于物理页面进行对齐 (ALIGN(0x1000)).
  • 定义了函数 uservec, 用于处理用户模式下的陷入. 具体操作为交换寄存器 a0sscratch 的值, 获取到 TRAPFRAME 的值 (该值就是陷入栈帧 p->trapframe 所对应的虚拟页面的起始地址), 将除 a0 以外的所有用户寄存器中的旧值备份到陷入栈帧中, 将 sscratch 的值 (即 a0 的旧值) 备份到陷入栈帧中, 恢复内核栈帧指针 spp->trapframe->kernel_sp (其值为 p->kstack + PGSIZE, 在函数 usertrapret 中被设置), 恢复寄存器 tp 为 CPU ID p->trapframe->kernel_hartid, 加载函数 usertrap 的地址 p->trapframe->kernel_trap, 恢复页表指针 satp 为内核页表 p->trapframe->kernel_satp (其值为内核页表根页面的首地址, 在函数 usertrapret 中被设置), 最后跳转至函数 usertrap (其定义位于文件 kernel/trap.c 中).
    • uservec 函数由 usertrapret 函数 (其定义位于文件 kernel/trap.c 中) 进行注册, 注册至 stvec CSR 寄存器中.
    • 由于 usertrapret 函数将会在子进程从 fork 返回时被调用, 这就确保了 uservec 函数总是能够被顺利注册.
  • 定义了函数 userret, 用于从内核模式恢复为用户模式. 具体操作为恢复页表指针 satp 为用户页表, 将寄存器 a0 的旧值加载至 sscratch 中, 将除 a0 以外的所有用户寄存器的旧值恢复至对应寄存器中, 交换寄存器 a0sscratch 的值, 恢复寄存器 a0 的旧值, 最后返回至用户模式并恢复用户 PC (程序计数器).

user/initcode.S

  • 本文件中的汇编代码用于开机时首次加载 /init 程序, 具体操作为将字符串 "/init" 以及代表 exec 系统调用的整数代码 SYS_EXEC (位于文件 kernel/syscall.h 中) 放置在寄存器中然后执行 ecall 指令.
  • 如果 exec 失败, 则从 exec 返回后将会进入一个无限循环中, 不断尝试调用 exit 系统调用 (由于此举同样需要执行 ecall 指令, 因此同样有可能失败, 因此使用的是无限循环).