调用图#
初始化流程#
页表#
系统调用#
中断#
多核处理器与锁#
进程间切换#
休眠唤醒#
Lost wakeup 的过程解析
运行在 core 1
上的 sleep
函数将当前进程设置为 SLEEPING
状态前,被运行在 core 2
上的 wakeup
函数抢先设置成了 RUNNABLE
状态,并因此丢失了 wakeup
操作。
因为按照正常的顺序,调度器必须发现有一个 SLEEPING
状态的函数,才可以将其设置为 RUNNABLE
状态,而现在的顺序是颠倒的。
根本原因在于 sleep
函数将进程设置为 SLEEPING
和 release sleep lock
不是原子性的,这给了其他进程(在两个操作的时隙间)有了趁虚而入的机会。我们需要加一个 condition lock
将 sleep
的 relase lock
和 SLEEPING
包装成原子性的操作。进而控制多线程的执行顺序。
修复后的结果应该是如下所示:
文件系统#
日志系统#
制作文件系统#
Makefile#
首先分析 Makefile 文件,查看它会编译哪些文件,程序入口在哪里:
这段代码是一个 Makefile 文件,用于编译和运行 xv6 操作系统的内核和用户空间程序。它定义了一系列的目标和依赖关系,以及一些变量和规则。
首先,它包含了名为
conf/lab.mk
的文件,这个文件可能包含了一些与实验相关的设置。然后,它定义了一系列的对象文件 (
OBJS
),这些文件是内核的一部分,包括了不同的功能模块。接下来,根据实验的不同,它可能会添加一些特定的对象文件到
OBJS
列表中。例如,如果实验是关于页表的,那么vmcopyin.o
对象文件就会被添加到OBJS
列表中。然后,它定义了一些工具链的前缀 (
TOOLPREFIX
) 和一些编译器、汇编器、链接器等工具的路径。如果没有设置TOOLPREFIX
,那么它会尝试自动推断出正确的工具链前缀。接下来,它定义了一些编译选项 (
CFLAGS
) 和链接选项 (LDFLAGS
),这些选项包括了一些与实验相关的宏定义 (XCFLAGS
)。然后,它定义了一些用户空间程序 (
UPROGS
),这些程序是用户空间的一部分,包括了一些测试程序。接下来,它定义了一些用户空间库文件 (
ULIB
),这些文件是用户空间程序运行时需要的库文件。然后,它定义了一些目标,这些目标用于编译内核和用户空间程序。例如,
$K/kernel
目标用于编译内核程序,而_ %
目标用于编译用户空间程序。最后,它定义了一些清理和打包的目标,用于删除生成的文件和创建提交的 tarball。
Kernel#
kernel.ld#
这段代码是一个链接器脚本,用于定义 RISC-V 架构的内核的内存布局和段。它使用了 GNU 链接器的语法,并包含了一系列的段定义和指令。
首先,
OUTPUT_ARCH("riscv")
指定了输出文件的目标架构是 RISC-V。然后,ENTRY(_entry)
将入口点设置为_entry
函数。接下来,
SECTIONS
块定义了各个段的属性。每个段都有一个起始地址(通过. = <address>
指定),并且可以包含一组输入段(通过*(.sectionname)
指定)。.text
段包含了代码段,包括.text
和.text.*
两个输入段。它还包括了一个名为trampoline
的特殊段,用于存放跳转到内核的代码。这个段的大小被设定为 0x1000 字节,以确保它正好占据一页(4KB)的空间。.rodata
段包含只读数据段,包括.srodata
和.rodata
两个输入段。.data
段包含可读写数据段,包括.sdata
和.data
两个输入段。.bss
段包含未初始化的全局变量,包括.sbss
和.bss
两个输入段。最后,
PROVIDE(end = .)
指令将end
符号设置为当前位置,即所有段结束的位置。
总的来说,这段代码定义了内核的内存布局,并确保了关键的段如代码段、只读数据段、可读写数据段和 BSS 段都按照预期的地址和大小进行排列。
entry.S#
这段代码是用汇编语言编写的,主要用于在 QEMU 模拟器中设置内核的启动和初始化。
_entry
标签定义了一个函数,这个函数是程序的入口点。当 QEMU 启动时,它会加载内核到 0x80000000 地址处,并让每个 CPU 跳转到这个地址。 kernel.ld
文件将这个函数放在了 0x80000000 地址处。
在这个函数中,首先设置了一个栈指针 (sp
),它指向一个名为 stack0
的变量,这是在 start.c
文件中声明的,每个 CPU 都有 4096 字节的栈空间。然后,根据当前 CPU 的 ID (通过 mhartid
寄存器获取),计算出栈指针应该偏移的值,并更新栈指针。
最后,调用 start
函数,这个函数是在 start.c
文件中定义的,负责进一步的系统初始化工作。如果 start
函数返回,那么程序会进入一个无限循环,即 spin
标签所指向的位置,防止程序退出。
总的来说,这段代码的作用是初始化 QEMU 模拟器中的内核环境,并调用 start
函数来完成系统的启动和初始化。
swtch.S#
这段代码是用 RISC-V 汇编语言编写的,定义了一个名为 swtch
的函数,用于实现上下文切换。
swtch
函数接受两个参数: old
和 new
,它们都是指向 context
结构体的指针。这个结构体可能包含了寄存器的值,以便在上下文切换时保存和恢复这些寄存器的状态。
在 swtch
函数内部,首先将当前寄存器的值保存在 old
指向的内存位置中。然后,从 new
指向的内存位置加载新的寄存器值。最后,通过 ret
指令返回,使得 CPU 从新加载的寄存器状态继续执行。
注意,这里使用的寄存器是特定于 RISC-V 架构的,如 ra
、sp
、s0
到 s11
等。此外, a0
和 a1
是函数调用的第一个和第二个参数,分别对应 old
和 new
。
kernelvec.S#
这段代码是用 RISC-V 汇编语言编写的,主要用于处理中断和异常。它定义了两个函数: kernelvec
和 timervec
。
kernelvec
函数的作用是在内核模式下处理中断和异常。当发生中断或异常时,该函数会被调用。它首先将寄存器保存在栈中,然后调用 kerneltrap
函数来处理中断或异常,最后从栈中恢复寄存器并返回。
timervec
函数的作用是处理机器模式下的定时器中断。它首先读取保存在 mscratch
寄存器中的数据,然后设置下一次定时器中断的时间,并触发一个软件中断。最后,它从栈中恢复寄存器并返回。
这段代码还包含一些宏定义和其他辅助函数,但它们在此处没有给出。这些宏定义和函数可能在其他部分的代码中定义。
trampoline.S#
这段代码是用 RISC-V 汇编语言编写的,主要用于在用户空间和内核空间之间进行上下文切换。它定义了几个函数,包括 trampoline
、uservec
和 userret
。
trampoline
函数是中断处理的入口点,当用户空间程序发生中断时,CPU 会跳转到这个函数。在这个函数中,它保存了用户寄存器的状态,并将控制权交给usertrap
函数。uservec
函数是用户空间的中断处理函数。它首先将a0
寄存器和sscratch
寄存器的内容交换,然后将用户寄存器的值保存在TRAPFRAME
中,并设置了一些必要的参数,如内核堆栈指针、内核线程 ID 等。最后,它调用usertrap
函数来处理中断。userret
函数是从内核空间返回用户空间的函数。它首先切换到用户页表,然后恢复用户寄存器的状态,并将TRAPFRAME
的地址保存在sscratch
寄存器中。最后,它通过sret
指令返回到用户空间,继续执行被中断的程序。
这段代码还包含一些宏定义和其他辅助函数,但它们在此处没有给出。这些宏定义和函数可能在其他部分的代码中定义。
start.c#
这段代码是用 C 语言编写的,主要用于初始化 RISC-V 架构的内核。它包括了一些头文件和函数定义。
首先,
stack0
是一个字符数组,用于为每个 CPU 分配一个栈空间。mscratch0
是一个 64 位整型数组,用于存储定时器中断的临时数据。timervec
函数是在kernelvec.S
文件中定义的,用于处理机器模式下的定时器中断。start
函数是程序的入口点,它设置了一些寄存器的值,以便在进入内核后能够正确地处理中断和异常。它还调用了timerinit
函数来初始化定时器中断。最后,它通过asm volatile("mret")
指令切换到监督模式并跳转到main
函数。timerinit
函数负责配置定时器中断。它设置了 CLINT 定时器的比较值,并将相关信息保存在mscratch0
数组中。然后,它设置了机器模式的中断向量表 (mtvec
),并启用了机器模式的中断。
总的来说,这段代码的主要任务是初始化 RISC-V 架构的内核,包括设置中断处理、分配栈空间等。
main.c#
这段代码是用 C 语言编写的,主要用于初始化 xv6 操作系统的内核部分。它包括了一些头文件和函数定义。
main
函数是程序的入口点,它在所有 CPU 上以监督模式启动。首先,它检查当前 CPU 的 ID(通过cpuid()
函数获取)是否为 0,如果是,则表示这是第一个启动的 CPU。在这个情况下,它会执行一系列的初始化操作,如初始化控制台、内存分配器、进程表、中断处理等。然后,它设置了一个标记started
,表示内核已经启动。如果当前 CPU 的 ID 不是 0,那么它会等待直到
started
被设置为 1,这意味着第一个 CPU 已经完成了初始化。之后,它会执行与第一个 CPU 类似的初始化操作,但跳过了那些只需要在第一个 CPU 上执行的步骤。最后,它调用scheduler
函数来启动调度器,开始处理进程和中断。
这段代码还包含了一些条件编译指令,根据不同的实验需求,可能会启用或禁用某些功能。例如,如果定义了 LAB_PGTBL
或 LAB_LOCK
宏,那么会初始化统计信息;如果定义了 LAB_NET
宏,那么会初始化网络相关的功能。
console.c#
这段代码是用 C 语言编写的,主要用于处理控制台输入输出。它实现了对 UART(通用异步接收/发送器)的控制,包括读取和写入字符到控制台。代码中定义了一些特殊的输入字符,如回车、退格、删除整行等,并提供了相应的处理逻辑。
consputc
函数用于将一个字符发送到 UART,如果字符是回退键,则会先发送两个字符:退格和空格,然后再发送另一个退格,以实现覆盖的效果。consolewrite
函数用于将数据从用户空间复制到内核空间,并通过调用uartputc
函数将数据发送到 UART。consoleread
函数用于从控制台读取数据,它会等待直到有输入数据到达,然后将数据复制到用户提供的缓冲区中。如果读取到了换行符或文件结束符,则表示一行输入已经完成,函数会返回。consoleintr
函数是一个中断处理函数,当 UART 接收到输入字符时会被调用。它根据不同的输入字符执行相应的处理逻辑,例如处理回退键、删除整行等操作。consoleinit
函数用于初始化控制台,包括初始化锁和 UART,并将consoleread
和consolewrite
函数与系统调用相关联。
stats.c#
这段代码是用 C 语言编写的,主要用于处理统计信息。它包括了一些头文件和函数定义。
stats
结构体定义了一个缓冲区,用于存储统计信息。statscopyin
和statslock
函数负责将统计信息复制到缓冲区中。statswrite
函数是一个空函数,它返回 -1,表示不允许向这个设备写入数据。statsread
函数用于从缓冲区读取统计信息。如果缓冲区为空,它会调用statscopyin
或statslock
函数来填充缓冲区。然后,它会尝试将缓冲区的内容复制到用户提供的缓冲区中,并更新偏移量。如果没有更多的数据可读,它会重置缓冲区的大小和偏移量。statsinit
函数用于初始化统计信息设备,包括初始化锁和设置读写函数。
这段代码还包含一些条件编译指令,根据不同的实验需求,可能会启用或禁用某些功能。例如,如果定义了 LAB_PGTBL
宏,那么会使用 statscopyin
函数来获取统计信息;如果定义了 LAB_LOCK
宏,那么会使用 statslock
函数来获取统计信息。
printf.c#
这段代码是用 C 语言编写的,主要用于格式化控制台输出。它包括了一些头文件和函数定义。
printint
函数用于将整数转换为字符串并打印到控制台。printptr
函数用于将指针地址转换为十六进制形式并打印到控制台。printf
函数是程序中最常用的打印函数,它接受一个格式化字符串和一系列参数。它会根据格式化字符串中的占位符(如%d
、%x
、%p
、%s
等)来决定如何处理后续的参数。如果遇到未知的格式化字符,它会原样打印出来。panic
函数用于在发生错误或异常时打印错误信息,并进入无限循环,以防止程序继续执行。printfinit
函数用于初始化打印锁,确保在多核环境下控制台输出的线程安全。
kalloc.c#
这段代码是用 C 语言编写的,主要用于管理物理内存。它包括了一些头文件和函数定义。
kinit
函数初始化内存分配器,并调用freerange
函数来将内核结束后的物理地址范围标记为可用。freerange
函数接受一个起始物理地址和一个结束物理地址,然后将这个范围内的所有物理页面标记为可用。kfree
函数释放一个已经分配的物理页面。它首先检查传入的地址是否符合要求(即是否为 4096 字节对齐且在有效范围内),然后将其加入到空闲列表中。kalloc
函数从空闲列表中分配一个物理页面。如果空闲列表不为空,它会返回一个可用的物理页面的地址;否则,它会返回 0,表示没有可用的物理内存。
这段代码还包含了一些宏定义和其他辅助函数,但它们在此处没有给出。这些宏定义和函数可能在其他部分的代码中定义。
vm.c#
这段代码是用 C 语言编写的,主要用于管理虚拟内存和页表。它包括了一些头文件和函数定义。
kvminit
函数创建了一个直接映射的内核页表,并将 UART、VirtIO 磁盘接口、CLINT 和 PLIC 等设备的物理地址映射到虚拟地址空间中。kvminithart
函数切换硬件页表寄存器以使用内核页表,并刷新 TLB。walk
函数遍历页表以找到与给定虚拟地址对应的页表项(PTE)。如果需要分配新的页表页,则会调用kalloc
函数来分配内存。walkaddr
函数类似于walk
函数,但只返回与给定虚拟地址对应的物理地址。kvmmap
和kvmmap2
函数分别用于将内核的虚拟地址映射到物理地址。kvmpa
函数将内核的虚拟地址转换为物理地址。mappages
函数用于将一系列连续的虚拟地址映射到一系列连续的物理地址。uvmunmap
函数用于解除一系列虚拟地址的映射,并可选择释放对应的物理内存。uvmcreate
函数用于创建一个空的用户页表。kvmcreate
函数用于复制内核页表,并返回一个新的页表。uvminit
函数用于将用户初始化代码加载到用户页表的起始地址 0 处。uvmalloc
和uvmdealloc
函数分别用于分配和释放用户内存页。uvmfree
函数用于释放用户页表及其所有关联的物理内存页。kvmfree
函数用于释放内核页表及其所有关联的物理内存页。uvmcopy
函数用于将一个父进程的页表内容复制到子进程的页表中。uvmclear
函数用于清除用户访问权限的页表项。copyout
和copyin
函数分别用于从内核向用户复制数据和从用户向内核复制数据。copyinstr
函数用于从用户向内核复制字符串。vmprint
函数用于打印页表的内容。
proc.c#
这段代码是操作系统内核的一部分,用于管理进程。它定义了一些结构体和函数来创建、销毁、复制和切换进程。
procinit
函数初始化进程表,为每个进程分配一个内核栈页面。cpuid
函数返回当前 CPU 的 ID。mycpu
函数返回当前 CPU 的cpu
结构体指针。myproc
函数返回当前进程的proc
结构体指针。allocpid
函数分配一个新的进程 ID。allocproc
函数在进程表中查找一个未使用的进程,并对其进行初始化。freeproc
函数释放一个进程及其相关资源。proc_pagetable
函数为进程创建一个用户页表。proc_freepagetable
函数释放进程的用户页表及其关联的物理内存。userinit
函数设置第一个用户进程。growproc
函数根据需要增长或缩小进程的用户内存。fork
函数创建一个新进程,复制父进程的用户内存。reparent
函数将子进程的父进程设置为 initproc。exit
函数退出当前进程,将其状态设置为 ZOMBIE,并唤醒其父进程。wait
函数等待一个子进程退出,并返回其进程 ID。scheduler
函数是一个循环,不断选择一个可运行的进程并切换到它。sched
函数让出 CPU 给调度器。yield
函数让出 CPU,允许其他进程运行。sleep
函数让进程睡眠,直到被其他进程唤醒。wakeup
函数唤醒所有正在等待特定通道的进程。kill
函数杀死具有特定 PID 的进程。either_copyout
和either_copyin
函数分别用于从用户空间或内核空间复制数据。procdump
函数打印当前所有进程的状态。
proc.h#
这段代码定义了一些结构体和全局变量,用于在 xv6 操作系统中管理进程的上下文和状态。
struct context
结构体包含了保存的寄存器状态,用于在内核上下文切换时保存和恢复寄存器的值。struct cpu
结构体表示每个 CPU 的状态,包括当前运行的进程、上下文信息以及一些标志位。extern struct cpu cpus[NCPU]
声明了一个数组,用于存储每个 CPU 的状态。struct trapframe
结构体用于保存用户空间程序陷入内核时的寄存器状态,以便在内核中进行处理。enum procstate
是一个枚举类型,定义了进程可能的状态:未使用、睡眠、可运行、正在运行和僵尸状态。struct proc
结构体表示一个进程的状态和信息,包括锁、状态、父进程、等待的通道、是否被杀死、退出状态、进程 ID、内核栈地址、进程内存大小、用户页表、陷阱帧指针、上下文信息、打开的文件描述符、当前工作目录和进程名称。
这些结构体和全局变量共同构成了 xv6 操作系统中进程管理的基础。它们用于跟踪和控制进程的生命周期和执行环境。
trap.c#
这段代码是操作系统内核的一部分,用于处理中断和异常。它定义了一些函数来初始化中断处理、设置中断向量表、处理用户空间的陷阱、以及处理时钟中断等。
trapinit
函数初始化一个用于同步的自旋锁tickslock
,并将ticks
变量设置为 0。trapinithart
函数设置中断向量表 (stvec
),使其指向kernelvec
函数。usertrap
函数处理来自用户空间的陷阱,包括系统调用、中断和异常。它根据不同的原因执行不同的操作,比如系统调用、设备中断或其他异常。usertrapret
函数返回到用户空间,恢复用户寄存器的值,并跳转到trampoline.S
中的userret
函数。kerneltrap
函数处理来自内核代码的陷阱,包括外部中断和软件中断。它根据不同的原因执行不同的操作,比如设备中断或其他异常。clockintr
函数处理时钟中断,增加ticks
变量的值,并唤醒等待该事件的进程。devintr
函数检查是否有外部设备中断发生,如果是,则调用相应的处理函数。如果不是,则返回 0 表示未识别的中断。
plic.c#
这段代码是操作系统内核的一部分,用于初始化和管理 RISC-V 架构的 PLIC(Platform Level Interrupt Controller)。PLIC 是一个硬件组件,用于管理中断请求 (IRQ),并将它们分配给处理器核心。
plicinit
函数设置了一些中断优先级,以确保 UART0 和 VIRTIO0 设备的中断能够被启用。plicinithart
函数为当前处理器核心设置了中断使能位,并设置了该核心在 S 模式下的优先级阈值。plic_claim
函数从 PLIC 获取当前处理器核心应该服务的中断号。plic_complete
函数通知 PLIC 已经完成了对特定中断的服务。
bio.c#
这段代码实现了一个缓冲区缓存(buffer cache),用于缓存磁盘块的内容。缓冲区缓存是一个链表,包含了 buf
结构体的缓存副本。缓存减少了对磁盘的读取次数,并为使用多个进程的磁盘块提供了一个同步点。
binit
函数初始化缓冲区缓存,创建一个双向链表,并初始化每个缓冲区的锁。bget
函数在缓冲区缓存中查找特定设备和块号的缓冲区。如果找到,则增加其引用计数并返回;如果未找到,则回收最近最少使用的(LRU)未使用的缓冲区。bread
函数调用bget
来获取一个缓冲区,然后从磁盘读取数据到缓冲区中。如果缓冲区已经有效,则直接返回。bwrite
函数将缓冲区的内容写入磁盘。必须持有缓冲区的锁。brelse
函数释放一个已锁定的缓冲区。它会将缓冲区移动到最常使用的列表的头部。bpin
和bunpin
函数分别增加和减少缓冲区的引用计数。
fs.c#
这段代码是操作系统内核的一部分,用于实现文件系统的各种功能。它包括了对块、日志、文件和目录的管理。以下是对代码中各个函数的解释:
readsb
: 读取超级块,将磁盘上的超级块信息加载到内存中的sb
结构体中。fsinit
: 初始化文件系统,包括读取超级块并初始化日志。bzero
: 将指定设备上的指定块清零。balloc
: 分配一个新的数据块,如果没有可用的块,则会抛出错误。bfree
: 释放指定设备上的指定块。ialloc
: 分配一个新的 inode,并将其类型设置为指定的类型。iupdate
: 将修改后的 inode 写回磁盘。iget
: 获取指定设备上指定 inode 编号的 inode。idup
: 增加 inode 的引用计数。ilock
: 锁定指定的 inode。iunlock
: 解锁指定的 inode。iput
: 减少 inode 的引用计数,并在必要时释放 inode。iunlockput
: 解锁并释放 inode。bmap
: 获取指定 inode 的第 n 个块的地址,如果不存在则分配一个新块。itrunc
: 截断指定 inode 的内容。stati
: 从 inode 复制状态信息到stat
结构体。readi
: 从指定 inode 的指定偏移量读取数据。writei
: 向指定 inode 的指定偏移量写入数据。namecmp
: 比较两个文件名是否相等。dirlookup
: 在目录中查找指定文件名的 inode。dirlink
: 在目录中创建一个新的目录项。skipelem
: 从路径中提取下一个路径元素。namex
: 根据路径名查找或创建 inode。namei
: 根据路径名查找 inode。nameiparent
: 根据路径名查找父目录的 inode。
file.c#
这段代码是关于文件描述符相关系统调用的支持函数。它包括了初始化文件表、分配文件结构、复制文件结构引用、关闭文件、获取文件元数据、读取文件和写入文件的功能。
fileinit
函数用于初始化文件表,包括一个自旋锁来保护对文件表的访问。filealloc
函数用于分配一个新的文件结构,如果没有可用的文件结构,则返回 0。filedup
函数用于增加文件结构的引用计数。fileclose
函数用于关闭文件,减少引用计数,并在引用计数为 0 时释放资源。filestat
函数用于获取文件的元数据,并将其复制到用户空间的地址addr
。fileread
函数用于从文件中读取数据,将数据复制到用户空间的地址addr
。filewrite
函数用于向文件中写入数据,从用户空间的地址addr
读取数据。
这些函数提供了对文件描述符的基本操作,包括分配、复制、关闭、读取和写入等。它们通过操作文件表来管理文件描述符,确保了对文件的正确管理和操作。
virtio_disk.c#
这段代码是操作系统内核的一部分,用于驱动 QEMU 的 virtio 磁盘设备。它使用 QEMU 的 MMIO 接口与 virtio 设备进行通信。代码中定义了一个 disk
结构体,其中包含了虚拟磁盘的相关信息和状态。
virtio_disk_init
函数用于初始化虚拟磁盘设备,包括检查设备的魔数、版本号、设备 ID 和供应商 ID,以及设置设备的特性。它还初始化了队列 0,并分配了必要的内存空间。alloc_desc
和free_desc
函数分别用于分配和释放描述符。alloc3_desc
函数用于分配三个连续的描述符。virtio_disk_rw
函数用于执行磁盘读写操作。它首先分配三个描述符,然后根据读写标志来填充描述符的内容。接着,它将描述符的地址和长度写入到相应的寄存器中,并通知设备开始处理请求。最后,它会等待设备完成请求后再返回。virtio_disk_intr
函数是一个中断处理函数,用于处理来自虚拟磁盘设备的中断。当设备完成一个请求时,它会调用这个函数来处理中断。在该函数中,它会检查每个请求的状态,并唤醒等待该请求完成的进程。
uart.c#
这段代码是针对 16550a UART(通用异步接收/发送器)的低级驱动程序。它包含了初始化 UART、向输出缓冲区添加字符、从输入缓冲区读取字符以及处理 UART 中断的功能。
uartinit
函数用于初始化 UART,包括禁用中断、设置波特率、配置线控制寄存器和使能传输和接收中断。uartputc
函数用于将一个字符添加到输出缓冲区,并在必要时启动 UART 的发送过程。如果输出缓冲区已满,该函数会阻塞直到有空间可用。uartputc_sync
函数是一个同步版本的uartputc
,用于在不使用中断的情况下向 UART 发送字符。它会等待直到 UART 的传输保持寄存器为空,然后发送字符。uartstart
函数检查输出缓冲区是否有待发送的字符,并尝试将它们发送到 UART。如果输出缓冲区为空或 UART 忙于发送,则函数不会执行任何操作。uartgetc
函数用于从 UART 读取一个字符。如果没有字符可读,则返回 -1。uartintr
函数是一个中断处理函数,用于处理 UART 中断。它会读取接收到的字符并调用consoleintr
来处理这些字符,同时也会调用uartstart
来处理输出缓冲区中的字符。
这些函数共同实现了对 16550a UART 的基本操作,包括初始化、数据传输和中断处理。
spinlock.c#
这段代码实现了一个自旋锁(spinlock),用于在多核处理器环境中实现互斥访问。自旋锁是一种忙等待的同步机制,它通过不断检查锁的状态来避免线程上下文切换的开销。
initlock
函数用于初始化一个自旋锁,设置其名称、初始状态为未锁定,以及记录当前持有锁的 CPU 编号。(备注:如果只有一个核且只有一个进程,根本不需要锁)acquire
函数用于获取自旋锁。它首先禁用中断以避免死锁,然后使用原子操作__sync_lock_test_and_set
来尝试获取锁。如果锁已经被其他线程持有,则该函数将循环等待直到锁可用。成功获取锁后,它会使用__sync_synchronize
来确保内存操作的顺序性。release
函数用于释放自旋锁。它首先检查当前线程是否持有锁,然后使用__sync_synchronize
来确保内存操作的顺序性,最后使用__sync_lock_release
来释放锁。holding
函数用于检查当前线程是否持有锁。它返回一个布尔值,表示锁是否被当前线程持有。push_off
和pop_off
函数用于管理中断的启用/禁用状态。push_off
函数禁用中断并保存当前的中断状态,而pop_off
函数根据嵌套层级的计数决定是否重新启用中断。
这些函数提供了一种简单且有效的同步机制,适用于需要保护共享资源的多线程程序。它们通过自旋等待而不是阻塞线程来减少线程调度和上下文切换的开销,但可能会导致 CPU 资源浪费。
sysfile.c#
这段代码是 xv6 操作系统中文件系统相关的系统调用的实现。它包括了对文件描述符的管理、文件的读写、关闭、获取状态信息、创建和删除链接、打开文件、创建目录、创建特殊设备节点、改变当前工作目录、执行程序和管道操作等功能。
argfd
函数用于获取用户空间传递的文件描述符,并返回对应的文件结构体。fdalloc
函数用于为一个文件分配一个新的文件描述符。sys_dup
函数用于复制一个现有的文件描述符。sys_read
函数用于从文件中读取数据。sys_write
函数用于向文件中写入数据。sys_close
函数用于关闭一个文件描述符。sys_fstat
函数用于获取文件的状态信息。sys_link
函数用于创建一个指向同一 inode 的新链接。isdirempty
函数用于检查目录是否为空。sys_unlink
函数用于删除一个链接。create
函数用于创建一个新的文件或目录。sys_open
函数用于打开一个文件或设备。sys_mkdir
函数用于创建一个新目录。sys_mknod
函数用于创建一个特殊的设备节点。sys_chdir
函数用于改变当前工作目录。sys_exec
函数用于执行一个程序。sys_pipe
函数用于创建一个管道。
这些系统调用提供了对文件系统的基本操作,使得用户可以通过系统调用来进行文件的读写、管理文件描述符、执行程序等操作。
exec.c#
这段代码是 xv6 操作系统中执行程序的核心部分。它负责加载和执行一个 ELF 格式的可执行文件。
exec
函数是执行程序的入口点。它首先打开指定的文件,并检查其 ELF 头部是否有效。然后,它为程序分配内存,并将程序的各个段加载到内存中。接着,它设置用户栈,并准备传递给程序的参数。最后,它更新进程的页表、堆栈指针和程序名称,并返回参数数量。如果在任何步骤发生错误,它会释放已分配的资源并返回 -1。loadseg
函数用于将程序的一个段加载到内存中。它遍历段中的每一页,并从文件中读取数据以填充这些页。如果成功,它返回 0;否则返回 -1。在
loadseg
函数中,使用了walkaddr
函数来获取虚拟地址对应的物理地址。这是通过页表查找实现的。在
exec
函数中,使用了safestrcpy
函数来安全地复制字符串,避免缓冲区溢出。在
exec
函数中,还包含了一些调试信息的打印,比如当进程 ID 为 1 时,打印进程的页表信息。
这个代码片段实现了从文件系统中加载程序并将其执行的功能。它涉及到了内存管理、文件 I/O 和进程控制等多个方面。
sleeplock.c#
这段代码定义了用于实现睡眠锁(sleeplock)的数据结构和相关函数。睡眠锁是一种同步机制,它允许进程在等待某个条件成立时进入睡眠状态,而不是忙等待。
struct sleeplock
结构体包含了一个基本的自旋锁(spinlock)和一些额外的字段,如锁的名称、是否被持有以及持有锁的进程 ID。initsleeplock
函数用于初始化一个睡眠锁,设置其名称、初始状态为未锁定,以及记录当前持有锁的进程 ID。acquiresleep
函数用于获取一个睡眠锁。如果锁已经被其他进程持有,则当前进程将进入睡眠状态,直到锁被释放。releasesleep
函数用于释放一个睡眠锁。它会唤醒因等待该锁而处于睡眠状态的进程。holdingsleep
函数用于检查当前进程是否持有指定的睡眠锁。
这些函数提供了对睡眠锁的基本操作,使得进程可以安全地等待特定条件的成立,而不会浪费 CPU 资源。
log.c#
这段代码实现了一个简单的日志系统,用于在文件系统中记录更改。它允许并发执行的文件系统系统调用。
struct logheader
结构体定义了日志头部,包含了块的数量和块号数组。struct log
结构体定义了日志本身,包括自旋锁、起始块号、大小、正在进行的系统调用数量、是否正在提交以及设备 ID 等信息。initlog
函数初始化日志,读取超级块中的日志信息,并从日志中恢复数据。install_trans
函数将已提交的块从日志复制到它们的实际位置。read_head
函数从磁盘读取日志头部到内存。write_head
函数将内存中的日志头部写入磁盘。recover_from_log
函数用于从日志中恢复数据,如果有未提交的更改,则将其应用到磁盘上。begin_op
函数在每个文件系统系统调用的开始处被调用,用于增加正在进行的系统调用数量,并在必要时等待提交。end_op
函数在每个文件系统系统调用的结束处被调用,用于减少正在进行的系统调用数量,并在所有操作完成后提交更改。write_log
函数将修改后的块从缓存复制到日志。commit
函数负责将日志中的更改写入磁盘。log_write
函数用于记录缓冲区中的更改,并将其添加到日志中。
这个日志系统的主要功能是确保文件系统的更改是原子的,即使在系统崩溃的情况下也能保持一致性。它通过将更改记录到日志中,并在需要时将这些更改应用到磁盘上来实现这一目标。
mkfs.c#
这段代码是一个简单的文件系统创建工具,它接受一个或多个文件作为输入,并将这些文件的内容写入到一个新的文件系统镜像中。这个文件系统镜像可以被操作系统加载和使用。
代码首先检查命令行参数的数量,如果没有提供足够的参数,则打印出错误信息并退出程序。然后,代码尝试打开一个名为 fs.img 的文件,用于存储文件系统镜像。如果打开失败,则打印出错误信息并退出程序。
接下来,代码初始化了一些变量和数据结构,包括超级块、空白数据块等。然后,代码遍历所有的磁盘块,并将它们写入到文件系统镜像中。
在主函数中,代码首先分配了一个 inode 给根目录,并为其添加了 “.” 和 “..” 两个条目。然后,代码遍历所有输入的文件,为每个文件分配一个 inode,并将文件内容写入到对应的 inode 中。最后,代码更新根目录的 inode 大小,并将超级块写入到文件系统镜像的正确位置。
整个过程中,代码通过调用一系列函数来实现文件系统的创建和写入操作。这些函数包括读写磁盘块、分配和释放磁盘块、读写 inode 等。
以下是按照函数功能对代码进行的分解:
xshort
和xint
函数:这两个函数用于将主机字节序转换为小端字节序,以便在不同的平台之间传输数据时保持一致性。main
函数:这是程序的入口点。它首先检查命令行参数的数量,然后打开或创建一个文件系统镜像文件。接着,它初始化超级块和其他必要的数据结构,并将它们写入到磁盘中。然后,它遍历输入的文件列表,为每个文件分配一个 inode,并将其内容写入到磁盘中。最后,它更新根目录的 inode 大小,并将超级块写入到磁盘的正确位置。wsect
函数:这个函数用于将一个磁盘块的内容写入到文件系统镜像中。它接受一个块号和一个指向数据的指针作为参数。winode
函数:这个函数用于将一个 inode 的内容写入到文件系统镜像中。它接受一个 inode 号和一个指向 inode 数据的指针作为参数。rinode
函数:这个函数用于从文件系统镜像中读取一个 inode 的内容。它接受一个 inode 号和一个指向 inode 数据的指针作为参数。rsect
函数:这个函数用于从文件系统镜像中读取一个磁盘块的内容。它接受一个块号和一个指向数据的指针作为参数。ialloc
函数:这个函数用于分配一个新的 inode。它接受一个 inode 类型作为参数,并返回新分配的 inode 号。balloc
函数:这个函数用于分配一个新的磁盘块。它接受一个整数作为参数,表示已经使用的磁盘块数量。iappend
函数:这个函数用于将数据追加到一个 inode 中。它接受一个 inode 号、一个指向数据的指针和数据长度作为参数。