背景介绍
背景¶
操作系统是一个软件,也需要通过某种机制加载并运行它。对于LoongArch32的计算机来说,上电复位最初启动的是一个BIOS软件(例如PMON),该BIOS软件能够支持从网络加载ELF格式的操作系统内核,从而开始启动我们已经编译好的uCore内核。
而对于QEMU虚拟机而言,我们可以直接使用-kernel
来指定我们需要加载的内核的ELF文件,从而直接完成了内核的载入过程,并直接从ELF的入口点开始启动。
BIOS启动过程¶
当计算机加电后,一般不直接执行操作系统,而是执行系统初始化软件完成基本IO初始化和引导加载功能。简单地说,系统初始化软件就是在操作系统内核运行之前运行的一段小软件。通过这段小软件,我们可以初始化硬件设备、建立系统的内存空间映射图,从而将系统的软硬件环境带到一个合适的状态,以便为最终调用操作系统内核准备好正确的环境。最终引导加载程序把操作系统内核映像加载到RAM中,并将系统控制权传递给它。
对于绝大多数计算机系统而言,操作系统和应用软件是存放在磁盘(硬盘/软盘)、光盘、EPROM、ROM、Flash等可在掉电后继续保存数据的存储介质上。计算机启动后,CPU一开始会到一个特定的地址开始执行指令,这个特定的地址存放了系统初始化软件,负责完成计算机基本的IO初始化,这是系统加电后运行的第一段软件代码。
对于LoongArch32体系结构而言,真实的硬件上电后会开始启动BIOS,该BIOS可以被自己刷写。而在ChipLab教学计算机中采用的是PMON2000作为BIOS,它具有网络功能,可以通过网卡从网络上使用tftp协议载入ELF格式的操作系统内核加载到内存,然后从ELF的入口点启动。
而我们实验采用的QEMU环境,则是抛弃了BIOS这一过程。在QEMU上直接使用-kernel
参数指定内核的ELF文件,本质上就是完成了BIOS所做的加载内核的过程,直接从ELF文件的入口点开始启动操作系统。
操作系统启动过程¶
当bootloader通过把ucore在系统加载到内存后,就转跳到ucore操作系统在内存中的入口位置(kern/start.S中的start的地址),这样ucore就接管了整个控制权。当前的ucore功能很简单,只完成基本的内存管理和外设中断管理。ucore主要完成的工作包括:
- 配置DMW,使得操作系统拥有可用的地址空间(位于
kern/init/entry.S
,其它部分均为C语言程序,整体位于kern/init/init.c
) - 初始化终端;
- 显示字符串;
- 设置例外两个例外向量;
- 执行while(1)死循环。
以后的实验中会大量涉及各个函数直接的调用关系,以及由于中断处理导致的异步现象,可能对大家实现操作系统和改正其中的错误有很大影响。而理解好函数调用关系的建立机制和中断处理机制,对后续实验会有很大帮助。
地址空间¶
uCore中的地址空间涉及两种地址:
-
逻辑地址(Logical Address,应用程序员看到的地址,在操作系统原理上称为虚拟地址,以后提到虚拟地址就是指逻辑地址)
-
物理地址(Physical Address, 实际的物理内存地址)。
(1) 逻辑地址空间
从应用程序的角度看,逻辑地址空间就是应用程序员编程所用到的地址空间,比如下面的程序片段:
int val=100;
int * point=&val;
其中指针变量point中存储的即是一个逻辑地址。
(2) 物理地址空间
从操作系统的角度看,CPU、内存硬件(通常说的“内存条”)和各种外设是它主要管理的硬件资源而内存硬件和外设分布在物理地址空间中。物理地址空间就是一个“大数组”,CPU通过索引(物理地址)来访问这个“大数组”中的内容。物理地址是指CPU提交到内存总线上用于访问计算机内存和外设的最终地址。
物理地址空间的大小取决于CPU实现的物理地址位数,LoongArch32计算机中,CPU的物理地址空间取决于处理器配置的PALEN。而对于外设,则是固定配置于0x1f000000~0x1fffffff。例如我们如果配置QEMU的内存为256M,那么物理地址空间如下:
+------------------+ <- 0xFFFFFFFF (4GB)
| 无效空间 |
+------------------+ <- 0x1FFFFFFF (512M)
| IO外设地址空间 |
+------------------+ <- 0x1F000000 (496M)
| 无效空间 |
+------------------+ <- 0x0FFFFFFF (256M)
| 有效内存 |
+------------------+ <- 0x00000000
中断与异常¶
操作系统需要对计算机系统中的各种外设进行管理,这就需要CPU和外设能够相互通信才行。一般外设的速度远慢于CPU的速度。如果让操作系统通过CPU“主动关心”外设的事件,即采用通常的轮询(polling)机制,则太浪费CPU资源了。所以需要操作系统和CPU能够一起提供某种机制,让外设在需要操作系统处理外设相关事件的时候,能够“主动通知”操作系统,即打断操作系统和应用的正常执行,让操作系统完成外设的相关处理,然后再恢复操作系统和应用的正常执行。在操作系统中,这种机制称为中断机制。中断机制给操作系统提供了处理意外情况的能力,同时它也是实现进程/线程抢占式调度的一个重要基石。但中断的引入会导致对操作系统的理解更加困难。
在LoongArch32架构中,中断属于异常(Exception)的一种,uCore内核目前处理的异常包括以下类型:
-
中断 (EX_IRQ,CSR.ESTAT.Ecode=0)
-
Load操作页无效 (EX_TLBL,CSR.ESTAT.Ecode=1)
-
Store操作页无效(EX_TLBS,CSR.ESTAT.Ecode=2)
-
TLB 重填 (EX_TLBR,CSR.ESTAT.Ecode=31)
-
指令不存在 (EX_RI,CSR.ESTAT.Ecode=13)
-
指令特权等级错误(EX_IPE,CSR.ESTAT.Ecode=14)
-
系统调用 (EX_SYS,CSR.ESTAT.Ecode=11)
-
地址错误例外 (EX_ADE,CSR.ESTAT.Ecode=8) 例如地址没有对齐
LoongArch32架构的处理器也提供了两个例外入口。分别是常规例外与TLB例外。由于TLB例外涉及重填页表的工作,因此必须为物理地址。而常规例外入口则可以根据目前处理器的运行状态选择使用虚拟地址或物理地址。这两个例外入口也存储在CSR寄存器中,名称分别为CSR.EBASE与CSR.RFBASE。uCore对于CSR.RFBASE的处理采用了两层跳转的方式,本次实验我们暂时不考虑RFBASE的处理过程。
时钟中断¶
LoongArch32的CSR提供了一个时钟定时器,操作系统可以通过CSR指令配置该定时器,从而实现硬件产生时钟中断,使得操作系统能够进行分时调度。
这一部分大家请参考龙芯架构32位精简版参考手册_v1.02.pdf的 7.6定时器相关控制状态寄存器 ,以及 7.4.5例外状态(ESTAT) 的介绍。
串口中断¶
我们的QEMU以及Chiplab SoC上都有一个NS16550a兼容的串口控制器,该控制器在很多种情况下都会有中断产生(大家可以自行查阅ns16550有关资料),在本实验中我们暂时只需要知道,串口有输入时会产生中断。
在LoongArch32的QEMU以及Chiplab SoC上,串口连接的中断为HWI[0]。
例外产生¶
当例外产生时,处理器 硬件 会进行如下操作(这些操作由硬件完成,不会出现在操作系统代码中):
-
将CSR.CRMD的PLV、IE分别存到CSR.PRMD的PPLV和IE中,然后将CSR.CRMD的PLV置为0,IE置为0。
-
将触发例外指令的PC值记录到CSR.ERA中
-
跳转到例外入口处取值。(如果是TLB相关例外跳转到CSR.RFBASE,否则为CSR.EBASE)
然后将PC跳转到对应的例外入口地址处,交给软件完成例外的处理操作。
Note
更加详细的信息请同学们自行阅读龙芯架构32位精简版参考手册_v1.02.pdf。关于中断产生过程,可以阅读该文档的6.1.4部分。
Note
小思考:为什么进入异常处理时要关闭中断?
这里给出参考答案:
若不关闭,考虑以下两种情况:
-
异常类型是中断
处理器由于中断异常跳转到了异常入口点,此时中断信号还没有响应使其清除,处理器继续满足中断产生条件,则继续跳转中断入口点,会导致死循环。
-
异常类型不是中断
若在异常处理时发生中断(例如外设有数据到达、定时器产生时钟中断等情况),会导致异常上下文被破坏,从而导致上一个异常应该保存的状态还没存下来,下一个异常继续产生,从而无法恢复原来的现场。
(同学们可以思考uCore中的trapframe保存时再次出现一个中断会出现什么情况?)
Note
小思考:为什么TLB重填异常采用了关闭映射地址翻译模式,采用直接地址翻译模式的设计?
同学们可以做完Lab2后尝试回答此问题。
该问题有多个角度,同学们可以考虑分解后的以下子问题:
- 如果内存超过512M,且页表会放在物理内存任意位置,DMW方案还是否可行?
-
TLB Miss相对于页无效/页修改是频率高得多的事件,如果希望TLB Miss时OS能够尽快完成TLB重填,应该如何改写uCore代码?
同学们可以尝试阅读Linux中对于同样是软件控制TLB的MIPS架构的代码。
操作系统响应异常¶
对于uCore,CSR.EBASE在内核初始化阶段(位于kern/init/init.c
的setup_exception_vector
函数)会被初始化为exception_handler
符号对应的指针(位于kern/trap/exceptions.S
)。
在kern/trap/exceptions.S
中,会完成以下操作:
-
将t0、t1保存到CSR的KS0、KS1中,以便我们可以使用这两个GPR来存储一些数据而不必破坏例外现场。
-
判断异常是否发生在用户态,若是,切换栈为内核栈。
-
在栈上开辟足够存储
trapframe
数据结构的空间(位于kern/trap/loongarch_trapframe.h
),将异常发生现场按照trapframe结构保存在栈中。注:其中的pushreg结构只保存30个GPR寄存器,而不是32个。
这是因为0号寄存器横为0不可写,因此不需要保存,1号寄存器在ABI中意义是RA,已经保存在了trapframe中的tf_ra。
需要注意的是,ra寄存器与CSR.ERA意义不同。ra寄存器在ABI上存储的是程序运行中函数return的返回地址,而CSR.ERA存储的是例外产生时还没提交的PC地址。
-
数据保存后,将a0寄存器保存为指向trapframe的指针
根据ABI定义,a0保存的是函数调用的第一个参数,这样C语言编写的loongarch_trap函数读取该参数即读取a0的值。
-
跳转到loongarch_trap函数
之后的C语言代码编写的部分不详细展开,请同学们自行阅读代码。
Note
小思考:为什么需要切换为内核栈?直接让内核跑在用户程序的栈上是否可行?
考虑以下角度: 1. 内核栈泄露给用户程序是否导致安全问题? 2. 用户程序跑在虚拟地址,读写用户栈可能会产生什么问题?(可以做完后续Lab后再思考该子问题)
例外处理返回¶
当loongarch_trap函数结束后,由于我们跳转时写了ra寄存器,此时会回到跳转前的位置。
该位置也就是kern/trap/exception.S
的jirl到loongarch_trap的下一条指令,对应的是exception_return符号的开始。
在这里,我们将之前保存的trapframe中的信息从栈上还原,然后调用ertn指令,该指令会完成如下操作:
-
将CSR.PRMD中的PPLV、PIE值回复到CSR.CRMD的PLV、IE中。
-
跳转到CSR.ERA所记录的地址处取指。
Note
小思考:我们在loongarch_trap函数中继续调用了trap_dispatch,此时是否会改变ra寄存器的值?如果改变了,原本的ra会被存储在什么位置?
同学们可以回忆程序运行时的栈是做什么用的,以及阅读LoongArch32的ABI文档以及其他架构的ABI文档。