Skip to content

操作系统启动流程

从机器启动到操作系统运行的过程

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)死循环。

以后的实验中会大量涉及各个函数直接的调用关系,以及由于中断处理导致的异步现象,可能对大家实现操作系统和改正其中的错误有很大影响。而理解好函数调用关系的建立机制和中断处理机制,对后续实验会有很大帮助。

地址空间

LoongArch32的地址空间涉及两种地址: - 逻辑地址(Logical Address,应用程序员看到的地址,在操作系统原理上称为虚拟地址,以后提到虚拟地址就是指逻辑地址) - 物理地址(Physical Address, 实际的物理内存地址)。

(1) 逻辑地址空间 从应用程序的角度看,逻辑地址空间就是应用程序员编程所用到的地址空间,比如下面的程序片段: int val=100; int * point=&val;

其中指针变量point中存储的即是一个逻辑地址。

(2) 物理地址空间 从操作系统的角度看,CPU、内存硬件(通常说的“内存条”)和各种外设是它主要管理的硬件资源而内存硬件和外设分布在物理地址空间中。物理地址空间就是一个“大数组”,CPU通过索引(物理地址)来访问这个“大数组”中的内容。物理地址是指CPU提交到内存总线上用于访问计算机内存和外设的最终地址。

物理地址空间的大小取决于CPU实现的物理地址位数,LoongArch32计算机中,CPU的物理地址空间取决于处理器配置的PALEN。而对于外设,则是固定配置于0x1f000000~0x1fffffff。例如我们如果配置QEMU的内存为256M,那么物理地址空间如下:

1
2
3
4
5
6
7
8
9
+------------------+  <- 0xFFFFFFFF (4GB)
|     无效空间      |
+------------------+  <- 0x1FFFFFFF (512M)
|   IO外设地址空间   |
+------------------+  <- 0x1F000000 (496M)
|     无效空间      |
+------------------+  <- 0x0FFFFFFF (256M)
|     有效内存      |
+------------------+  <- 0x00000000

图6 LoongArch32计算机系统的物理地址空间

(3) 地址空间的翻译

LoongArch32架构地址空间有两种翻译模式:

  • 直接地址翻译模式(CSR.CRMD中的DA=1且PG=0时) 简单地使物理地址=虚拟地址的前PALEN位,后续补0。 注:PALEN与处理器配置有关。
  • 映射地址翻译模式(CSR.CRMD中的DA=0且PG=1时)
    • 先查看DMW地址映射窗口,若匹配则按照DMW配置的翻译。不匹配则继续。
    • 然后检查TLB是否存在该页表项,若匹配则按照TLB对应配置翻译,不匹配则产生TLB例外由操作系统内核根据页表完成TLB填充操作。

中断与异常

操作系统需要对计算机系统中的各种外设进行管理,这就需要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例外涉及重填页表的工作,因此必须为物理地址。而常规例外入口则可以根据目前处理器的运行状态选择使用虚拟地址或物理地址。

注意:这里我们所使用的QEMU在直接地址翻译模式下,会抹除CSR.RFBASE地址的高3位,因此我们不需要关心TLB重填时地址访问的地址的问题,可以直接修改CSR.CRMD来开启映射地址翻译模式,然后当做虚拟地址一样处理即可。

这两个例外入口也存储在CSR寄存器中,名称分别为CSR.EBASE与CSR.RFBASE。当例外产生时,处理器会进行如下操作: - 将CSR.CRMD的PLV、IE分别存到CSR.PRMD的PPLV和IE中,然后将CSR.CRMD的PLV置为0,IE置为0。 - 将触发例外指令的PC值记录到CSR.ERA中 - 跳转到例外入口处取值。(如果是TLB相关例外跳转到CSR.RFBASE,否则为CSR.EBASE)

然后将PC跳转到对应的例外入口地址处,交给软件完成例外的处理操作。

当例外处理结束后,软件应该执行ERTN从例外状态返回,该指令会完成如下操作: - 将CSR.PRMD中的PPLV、PIE值回复到CSR.CRMD的PLV、IE中。 - 跳转到CSR.ERA所记录的地址处取指。

lab1中对例外的处理实现

(1) 外设基本初始化设置

Lab1实现了中断初始化和对键盘、串口、时钟外设进行中断处理。串口的初始化函数serial_init(位于/kern/driver/console.c)中涉及中断初始化工作的很简单:

......
// 使能串口1接收字符后产生中断
outb(COM1 + COM_IER, COM_IER_RDI);
......
// 通过中断控制器使能串口1中断
pic_enable(IRQ_COM1);

时钟是一种有着特殊作用的外设,其作用并不仅仅是计时。在后续章节中将讲到,正是由于有了规律的时钟中断,才使得无论当前CPU运行在哪里,操作系统都可以在预先确定的时间点上获得CPU控制权。这样当一个应用程序运行了一定时间后,操作系统会通过时钟中断获得CPU控制权,并可把CPU资源让给更需要CPU的其他应用程序。时钟的初始化函数clock_init(位于kern/driver/clock.c中)完成了对时钟控制器的初始化:

    ......
unsigned long timer_config;
unsigned long period = 200000000;
period = period / HZ;
timer_config = period & LISA_CSR_TMCFG_TIMEVAL;
timer_config |= (LISA_CSR_TMCFG_PERIOD | LISA_CSR_TMCFG_EN);
__lcsr_csrwr(timer_config, LISA_CSR_TMCFG);
pic_enable(TIMER0_IRQ);
(2) 例外初始化设置

对于LoongArch32架构的计算机来说,操作系统初始化中断首先需要完成例外处理程序的基地址设置。这一部分程序写在了kern/init/init.c中的setup_exception_vector函数。

其中,该函数中使用的__exception_vector地址位于文件kern/trap/vectors.S。为了简化,它直接跳转到了kern/trap/exception.S中的ramExcHandle_general处。

(3) 例外的处理过程

trap函数(定义在trap.c中)是对例外进行处理的过程,所有的例外在经过中断入口函数ramExcHandle_general预处理后 (定义在 exception.S中) ,都会跳转到这里。在处理过程中,根据不同的例外类型,进行相应的处理。在相应的处理过程结束以后,trap将会返回,被中断的程序会继续运行。整个中断处理流程大致如下:

1)产生例外后,CPU硬件完成了如下操作: - 将CSR.CRMD的PLV、IE分别存到CSR.PRMD的PPLV和IE中,然后将CSR.CRMD的PLV置为0,IE置为0。 - 将触发例外指令的PC值记录到CSR.ERA中 - 跳转到例外入口处取值。(如果是TLB相关例外跳转到CSR.RFBASE,否则为CSR.EBASE)

2)经过例外向量的跳转,进入exception.S中的ramExcHandle_general

在这里会完成例外现场的保存操作,切换到内核栈,将当前处理器的所有通用寄存器压入内核栈中,并使用CSR中的KS0和KS1寄存器用来辅助保存数据(否则保存过程中必然导致一些特定寄存器的修改)。

保存的数据按照trapfame结构进行,位于kern/trap/loongarch_trapframe.h

3) 然后跳转进入kern/trap/trap.c中的loongarch_trap函数,开始了C语言程序的内核的处理。

这个函数中,会根据例外的类型完成例外的分类并进行处理,具体见kern/trap/trap.c

4) 当loongarch_trap这一函数处理完毕后(处理过程可能包含对trapframe的修改),会返回到之前的汇编程序(exception.S中),完成寄存器状态的还原,然后使用ertn指令结束例外处理,恢复程序的执行。

至此,对整个lab1中的主要部分的背景知识和实现进行了阐述。请大家能够根据前面的练习要求完成所有的练习。