XV6 Operator System: 03-The Kernel Environment
在上一节中,我详细介绍了xv6-riscv
的start
函数,里面对xv6-riscv
的内核执行特权级环境、中断环境和定时器中断进行了一系列配置。现在,让我们来看看xv6-riscv
内核态中的具体配置和实现。
Main Function
在本节中,我们只会对内核态的配置做出大概介绍,具体的介绍在后续会为每一个模块做出详细章节来讲述。
我们从xv6-riscv
的源码中也可以看出main
函数主要是对各种模块进行启用,然后调用initcode
进入到用户态中,完成整个操作系统的启动。
需要完全的理解该代码,我们需要进行一些知识的准备。
volatile type
Every access (both read and write) made through a lvalue expression of volatile-qualified type is considered an observable side effect for the purpose of optimization and is evaluated strictly according to the rules of the abstract machine (that is, all writes are completed at some time before the next sequence point). This means that within a single thread of execution, a volatile access cannot be optimized out or reordered relative to another visible side effect that is separated by a sequence point from the volatile access.
使用volatile
限定类型的左值表达式的每次访问(无论是读取还是写入)都被视为一个可观察的副作用,并且严格按照抽象机器的规则进行评估(也就是说,所有写操作都会在下一个序列点之前完成)。这意味着在单个线程的执行过程中,一个volatile
访问不能被优化掉,也不能相对于另一个由序列点分隔的可见副作用进行重新排序。
通常,编译器会优化代码以提高执行效率。例如,如果一个变量在代码中没有被显式修改,编译器可能会将其值缓存到寄存器中,以避免多次访问内存。然而,对于某些变量(如硬件寄存器、共享内存、信号处理程序中的变量等),它们的值可能在程序之外被修改。如果编译器对这些变量进行优化,可能会导致程序读取的值不是最新的,从而引发错误。volatile
关键字通知编译器不要对这些变量进行优化,每次都应从内存中读取它们的最新值。
在多线程环境中,多个线程可能会访问和修改同一个变量。使用volatile
可以确保一个线程对该变量所做的修改能立即被其他线程看到。
值得注意的:volatile
仅保证了变量的最新值读取,不提供任何同步机制。如果多个线程同时读写同一个变量,还需要使用适当的同步机制(如互斥锁)来确保操作的原子性和一致性。
在某些体系结构和编译器实现中,即使使用了volatile
,依然可能需要额外的内存屏障(memory barrier
)指令来确保正确的内存访问顺序。
__sync_synchronize
__sync_synchronize
是GCC
提供的一种内存屏障(memory barrier
或memory fence
),用于确保在多处理器系统上进行的内存操作按照程序指定的顺序执行。它在涉及多线程编程时尤其有用,能够防止编译器和CPU对内存操作进行不正确的重排序。
- 防止重排
__sync_synchronize
用于阻止编译器和CPU
对内存操作进行重排序。这样可以确保在它之前的内存操作在它之后的内存操作之前完成。这对于多线程程序中的同步非常重要,能确保不同线程对共享数据的访问顺序正确
- 确保内存可见性
- 在调用该函数之前进行的所有内存写操作在其他处理器和线程中变得可见。因此,它可以用于实现内存可见性保证,确保一个线程所做的更改对其他线程可见
1 |
|
线程1在写入data
后调用__sync_synchronize
来确保在设置flag
之前,data
的写操作已经完成并且对其他线程可见。线程2在读取到flag
为1
后调用__sync_synchronize
来确保在读取data
之前,所有之前的内存操作(包括线程1对data
的写操作)都已经完成并且对当前线程可见。
这样就使得,不论如何,线程2所看见的data
的数据总是线程1已经写入完毕后的data
数据,而不会发生线程1正在写的时候(并未写入),线程2访问data
得到了一个错误的数据。
Main Code
在了解上面的前置知识后,我们就可以逐行解析xv6-riscv
内核的大致流程了:
1 | volatile static int started = 0; |
volatile static int started = 0
:定义一个静态变量started
,用来协调各个hart
的启动。volatile
关键字确保每次访问该变量时,都从内存中读取最新的值,而不是从寄存器中读取缓存值。
在xv6-riscv
中,我们将$hartid = 0$的hart
标识为主要的执行流,主hart
执行一系列初始化函数,如consoleinit()
、kinit()
、kvminit()
等,这些函数负责初始化控制台、物理内存分配器、内核页表、进程表、trap vector、中断控制器、缓存、inode表、文件表和虚拟磁盘等。
而__sync_synchronize()
在started
变量赋值之前调用内存屏障,确保所有的初始化操作都在赋值之前完成。这样就使得其他hart
流执行时,访问到的started
数据一定是$started == 1$的。
其他的hart
流执行需要等待主要hart
流执行完毕,直到$started == 1$为止,__sync_synchronize()
使得hart
流能够看见主要hart
执行流的一切初始化操作,然后hart
执行自己的初始化函数,如kvminithart()
、trapinithart()
和plicinithart()
,这些函数负责启用分页、安装内核陷阱向量、请求设备中断等。
最终,所有hart
(包括主hart
和从hart
)都进入调度器,以开始调度和执行任务。值得注意的是,第一个任务是跳转到用户态执行initcode
。
这里给出对应的部分逻辑(在此不做过多介绍):
1 | uchar initcode[] = { |
可以看见,userinit
会在内部调用uvmfirst
函数来设置第一个任务为initcode
,然后通过scheduler
函数跳转到initcode.S
中执行。而initcode
中通过exec
程序调用了用户态的init
程序,init
程序又会调用sh
执行shell
环境,然后就正式进入了用户态,并且终端上呈现出:
1 | init: starting sh |