硬件依赖层是嵌入式系统软件架构的底层,负责与底层硬件进行交互和通信,以控制和管理硬件资源。硬件依赖层包括设备驱动程序、硬件抽象层等组件。
在嵌入式系统中,硬件依赖层直接与硬件资源进行交互,对系统性能和功能具有很大的影响。通过硬件依赖层,软件可以直接控制和管理硬件资源,如CPU、内存、外围设备等资源,从而实现软件系统的各项功能。同时,硬件依赖层还可以对硬件资源进行抽象和封装处理,以简化软件开发和维护的工作。
虽然虚拟化技术在桌面系统领域取得了极大的进展,但在嵌入式领域的发展受到了诸多限制。
1.虚拟化受限的因素
虚拟化技术在嵌入式领域主要受到以下4个因素的限制。
1)在硬件资源配置方面,嵌入式系统通常面临资源受限的局面,包括处理能力、内存和存储容量的限制。而虚拟化技术通常需要较多的计算和内存资源来管理虚拟机,这可能会导致嵌入式系统性能下降。
2)在处理器选型方面,应用在嵌入式领域的处理器架构对硬件虚拟化的支持是非常保守的。比如应用在嵌入式领域的MCU(Micro Control Unit,微控制单元)系列一般是32位的ARMv7架构,不支持硬件虚拟化。即使是多核处理器,为了节省成本,通常也不包含硬件虚拟化扩展组件。
3)在软件设计方面,当应用程序需要满足实时需求时,引入Hypervisor后,应用程序、操作系统以及Hypervisor的设计均需要满足实时需求。
4)在硬件设计方面,单核/多核处理器的底层架构设计会直接影响整个系统的实时性,其中流水线和高速缓存是影响系统实时性的两个主要因素。
同时,考虑到Hypervisor是所有分区应用程序的公共软件层,为了使不同安全级别的分区应用共存于系统中,Hypervisor的安全级别必须是整个安全关键系统中最高的。
2.分区包含的硬件
分区是Hypervisor创建的运行时环境,又称虚拟机或域(Domain),用于执行用户代码,并使得分区中的用户代码像在原生硬件平台上执行一样。在分区环境下,需要进行硬件抽象处理的资源包含以下6种。
1)某些特殊的CPU寄存器资源,比如Intel X86 处理器中的CR3、GDTR(Global Descriptor Table Register,全局描述符表寄存器)、IDTR(Interrupt Descriptor Table Register,中断描述符表寄存器)等。
2)硬件中断控制器。
3)硬件时钟和定时器。
4)基于分页的MMU硬件资源。
5)X86平台通过I/O端口地址管理I/O设备。
6)高速缓存管理。
分区通过Hypervisor提供的超级调用服务来使用虚拟化的资源。比如分区需要设置定时器时,不能直接访问硬件定时器资源,可以通过使用Hypervisor提供的定时器服务来实现定时功能。
在分区环境下,以下3种硬件资源是不需要被虚拟化的。
1)分配给该分区的内存地址空间:分区可以直接访问。
2)非特权指令:可以直接在原生pCPU上运行。比如,一个分区代码执行一条加指令(ADD)时可以直接在pCPU上运行,不需要Hypervisor的参与。
3)硬件高速缓存:高速缓存的使用对Hypervisor来说是透明的,这和在原生硬件环境下高速缓存的使用对操作系统透明是类似的。
在多核处理器硬件平台,实现Hypervisor的虚拟化技术需要考虑一些特殊情况。比如,Hypervisor在解决与缓存管理和信息安全相关的问题时,为了避免分区缓存的泄密隐患,分区被调度时通常采取刷新缓存的方式来避免敏感信息泄露。又比如,不同处理器核心对共享内存的访问会引入竞争问题,导致系统响应时间不确定。对此,虚拟化层只能缓解,不能彻底解决,需要系统在分区层通过更复杂的计算方式来估算WCET,来确定临时的解决方案。
处理器驱动主要负责初始化工作,包括设置处理器时钟、中断控制器、内存控制器等。本小节主要介绍与处理器时钟、中断控制器初始化相关的两类驱动,以实现PRTOS获取CPU主频和设置CPU中断向量。本小节之所以不涉及内存控制器的初始化,是因为在Intel X86平台使用GRUB(GRand Unified Bootloader,大统一启动加载器)来加载PRTOS系统映像时,已经对内存控制器做了初始化,PRTOS无须对内存控制器再次初始化。
1.获取CPU主频
在Intel X86硬件平台,可借助64位的TSC(Time Stamp Counter,时间戳定时器)和Intel 8253(也称为Intel 8254)PIT(可编程中断定时器)的计时通道2,来计算当前CPU的主频。
TSC可以对驱动CPU的时钟脉冲进行计数。Intel 8253芯片的时钟输入频率是1 193 180Hz。通过设定一定的初始计数值LATCH(默认值为65 535),就能控制该芯片的输出频率(默认为1 193 180/65 535Hz)。例如,假定LATCH=1 193 180/100,则能保证输出频率为100Hz,即周期为10ms。
脉冲的精度是1/(CPU主频)。比如,CPU的主频是500MHz,那么时钟脉冲的精度就是2ns。
可通过设置Intel 8253 PIT的计时通道2让定时器工作在模式0下。之所以选择通道2,是因为通道2的输出电平可以通过I/O端口0x61的第5位读取。在模式0下,当计数器的值递减到0时,通道2的输出持续处于高电平状态,并且计数器只计数一遍,便于读取通道2的计数器值递减到0时的状态。这样我们就可以在通道2的计数值从LATCH递减到0的时间段内,将TSC记录的时钟脉冲次数乘以(1 193 180/LATCH),计算得到的结果即CPU当前工作频率。CPU当前工作频率的具体计算过程如代码清单3-1所示。
//源码路径:core/kernel/x86/processor.c
01 #define CLOCK_TICK_RATE 1193180 //时钟频率Hz
02 #define PIT_CH2 0x42
03 #define PIT_MODE 0x43
04 #define CALIBRATE_MULT 100
05 #define CALIBRATE_CYCLES CLOCK_TICK_RATE / CALIBRATE_MULT
06
07 __VBOOT prtos_u32_t calculate_cpu_freq(void) {
08 prtos_u64_t c_start, c_stop, delta;
09
10 out_byte((in_byte(0x61) & ~0x02) | 0x01, 0x61);
11 out_byte(0xb0, PIT_MODE); //二进制, 模式0, LSB/MSB, 通道2
12 out_byte(CALIBRATE_CYCLES & 0xff, PIT_CH2); //低8位写入
13 out_byte(CALIBRATE_CYCLES >> 8, PIT_CH2); //高8位写入
14 c_start = read_tsc_load_low();
15 delta = read_tsc_load_low();
16 in_byte(0x61);
17 delta = read_tsc_load_low() - delta;
18 while ((in_byte(0x61) & 0x20) == 0)
19 ;
20 c_stop = read_tsc_load_low();
21
22 return (c_stop - (c_start + delta)) * CALIBRATE_MULT;
23 }
提示: 这里的源码路径是相对于PRTOS源码根目录的位置,即https://github.com/prtos-project/prtos-hypervisor。后续源码路径均为相对于这个根目录的路径。
2.设置CPU中断向量
在介绍中断向量初始化之前,我们先介绍中断种类和Intel X86处理器的中断向量表。
(1)中断种类
中断的来源有两种:一种是由CPU外部产生的,另一种是CPU在执行程序过程中产生的。外部中断就是通常所讲的“中断”。对执行中的软件来说,外部中断是异步的,CPU(或者软件)对外部中断的响应完全是被动的,当然软件可以通过关中断指令关闭CPU的响应(这里不考虑系统重置等不可屏蔽中断)。而CPU在执行程序过程中产生的中断往往是由专设的指令有意产生的,这种主动的中断被称为陷阱。除此之外,还可能存在预期之外的中断,一般是同步的,被称为异常。例如程序中的除法指令(DIV),当除数为0时,就会发生一次同步异常。
不管是外部产生的中断,还是内部产生的陷阱或异常,CPU的响应过程基本一致,即在执行完当前指令之后或者在执行当前指令的中途,根据中断源提供的中断向量在内存中找到相应的服务程序入口并调用该服务程序。外部中断的向量是由软件或硬件设置好了的,陷阱向量是在自陷指令中发出的,其他各种异常的向量则是在CPU的硬件结构中预先设定的。这些不同的情况因中断向量号的不同而被分开。根据中断类型的不同Hypervisor,挂载的中断处理程序也不同。PRTOS的中断处理类型如图3-1所示。
图3-1 PRTOS的中断处理类型
(2)Intel X86处理器的中断向量表
在X86处理器中,中断向量表中的表项称为“门”,意思是当中断发生时必须先通过这些门,才能进入相应的服务程序。这里的门并不仅是为中断而设的,只要想切换CPU的运行状态(如从用户Ring 3进入系统Ring 0),就需要通过一道门。而从用户模式进入系统态的途径也并不只限于中断(或者异常,或者陷阱),还可以通过子程序调用指令CALL来达到目的(PRTOS的超级调用就是通过子程序调用指令CALL实现的)。而且当中断发生时,不但可以切换CPU的运行状态并转入中断服务程序,还可以安排一次分区切换(即分区上下文切换),立即切换到另一个分区。
根据用途和目的的不同,X86 CPU的门共分为4种:任务门、中断门、陷阱门以及调用门。PRTOS只初始化中断门和陷阱门。中断门、陷阱门均指向一个子程序,必须结合使用段选择子和段内偏移来确定这个子程序的位置。
中断门和陷阱门在使用上的区别不在于中断是由外部产生还是由CPU本身产生的,而在于通过中断门进入中断服务程序时,CPU会自动将中断关闭(关中断),即将CPU中的标志寄存器(EFLAGS)的IF标志位清0,以防嵌套中断的发生;而通过陷阱门进入服务程序时,则维持IF标志位不变。这就是中断门和陷阱门的唯一区别。不管是什么门,都通过段选择子指向一个存储段。段选择子的作用与普通的段寄存器一样。在保护模式下,段寄存器的内容并不直接指向一个段的起始地址,而是指向由GDTR或LDTR确定的某个段描述表中的一个表项。至于到底是由GDTR还是由LDTR所指向的段描述表,则取决于段选择子中的TI标志位。在PRTOS中只使用全局段描述表GDT。对中断门和陷阱门来说,段描述表中的相应表项是一个代码段描述符表项。
CPU通过中断门找到一个代码段描述符表项,并进而转入相应的中断处理程序。之后,CPU要将当前EFLAGS寄存器的内容以及返回地址压入栈,返回地址由段寄存器CS的内容和取指令指针EIP的内容共同组成。如果中断是由异常引起的,则还要将表示异常原因的出错代码也压入栈。进一步地,如果中断服务程序的运行级别不同(即目标代码段的DPL与中断发生时的CPL不同),还得更换栈。X86的任务状态段(Task State Segment,TSS)描述符结构中除包含所有常规的寄存器内容外,还有3对额外的栈指针(SS和ESP)。这3组栈指针分别对应CPU在目标代码段中的运行级别Ring 0、Ring 1和Ring 2。CPU根据寄存器TR的内容找到当前的TSS结构,并根据目标代码段的DPL从TSS结构中取出新的栈指针(SS加ESP),装入段寄存器(Segment Register,SS)和栈指针寄存器(Extended Stack Pointer,ESP),从而达到更换栈的目的。在这种情况下,CPU不但要将EFLAGS、返回地址以及出错代码压入栈,还要将原来的栈指针也压入栈(新栈)。
(3)Intel X86处理器中断向量表的定义及初始化
在Intel X86 硬件平台,PRTOS中断向量表的定义如代码清单3-2所示。
//源码路径:core/kernel/x86/head.S
01 #include <linkage.h>
02 #include <arch/irqs.h>
03 #include <arch/asm_offsets.h>
04 #include <arch/segments.h>
05 #include <arch/prtos_def.h>
06 ...
07 .data
08 PAGE_ALIGN
09 .word 0
10 ENTRY(idt_desc) //PRTOS中断描述符表
11 .word IDT_ENTRIES*8-1
12 .long _VIRT2PHYS(hyp_idt_table)
13 ...
14 ENTRY(hyp_idt_table) //PRTOS中断向量表的定义
15 .zero IDT_ENTRIES*8
16
中断向量表的初始化如代码清单3-3所示。
//源码路径:core/kernel/x86/irqs.c
01 void setup_x86_idt(void) {
02 //setup_x86_idt()函数的具体实现,请参考PRTOS对应的源码文件
34 }
setup_x86_idt()函数的主要功能如下。
1)完成外部中断向量服务程序的初始化(这里假设有16个外部中断)。
2)实现X86 CPU预留的19个陷阱门和异常门描述选项的初始化。
Hypervisor的关键功能之一是提供虚拟时钟服务。PRTOS中的虚拟时钟可以为每个分区提供时钟计时,并独立于主机系统时钟,旨在允许虚拟机运行自己的操作系统和应用程序,并协调分区的内部任务,且不会干扰其他分区。虚拟时钟通过截获硬件时钟事件并模拟与时间相关的事件(例如使用定时器中断和时钟滴答)来实现。当分区请求虚拟时钟服务(例如获取当前时间)时,虚拟时钟服务会读取硬件时钟的实时值,并根据分区调度等因素进行调整计算,再返回虚拟时间。在PRTOS系统中,分区通过调用PRTOS提供的虚拟时钟接口获取当前的时间戳,用于满足以下5种需求。
1)分区系统可以发现某个陷入死循环(由编程错误引起)的任务,并做出相应处理。
2)在分区实时系统中,按要求的时间间隔,为实时控制设备输出正确的时间信号。
3)PRTOS调度程序按照事先给定的时间定时唤醒对应的分区。
4)PRTOS内核记录外部事件发生的时间间隔。
5)PRTOS系统记录用户和系统所需要的绝对时间。
PRTOS使用数据结构hw_clock_t来管理硬件时钟,具体实现请参考源码core/include/ktimer.h。硬件时钟是全局时钟,不管是单处理器硬件平台,还是SMP硬件平台,都只使用一个全局硬件时钟。针对Intel X86 硬件平台的3种不同的时钟硬件,PRTOS提供了3种时钟驱动,分别是Intel 8253时钟驱动、TSC时钟驱动、HPET(High Precision Event Timer,高精度事件时钟)时钟驱动,如图3-2所示。
图3-2 PRTOS的3种时钟驱动
1.Intel 8253时钟驱动
Intel 8253是一种常见的可编程间隔定时器,常用作计算机系统中的时钟驱动,用于生成精确的时间间隔和周期性中断。
Intel 8253通常由系统软件通过编程来配置和控制,包含3个独立的计数器通道,并且每个通道都可以用作定时器或计数器。每个通道都有一个16位计数器,可以根据需要进行加载和读取。
PRTOS可以将Intel 8253用作系统的时钟源,通过设定计数器的初始值和工作模式生成固定的时钟间隔,用于操作系统的调度和计时。在X86单核硬件平台上,PRTOS将Intel 8253 PIT的通道0作为计时通道,通道0的定时器设置为(Binary, Mode 2, LSB/MSB),即周期为1ms的周期触发模式,并定义一个全局结构struct pit_clock_data来记录PRTOS启动后的定时中断发生次数,再结合当前通道0中计数器的值,可以计算出PRTOS自启动后到当前时刻的精确到微秒的时间戳。具体实现参考PRTOS源码core/kernel/x86/pit.c。
2.TSC时钟驱动
TSC是一个64位的寄存器,从Intel Pentium开始,在所有的X86平台上均会提供。它存放的是CPU从启动以来执行的时钟周期,因此可以用来精确地测量程序的执行时间。TSC由处理器硬件提供,因此它的计时操作比使用软件定时器要快得多,这使得TSC成为性能分析和调试工具中的一个重要组件。在某些情况下,使用TSC进行时间测量可以提高精度,并且不会受到操作系统时钟频率调整的影响。
要使用TSC,需要使用相关的CPU指令来读取TSC寄存器的值。例如,在X86架构中,可以使用rdtsc指令来读取TSC寄存器的值。rdtsc指令将TSC寄存器的值读取到EDX:EAX寄存器中(高32位保存在EDX寄存器中,低32位保存在EAX寄存器中)。由于TSC是对驱动CPU的时钟脉冲进行计数的,因此TSC的频率就是CPU的时钟频率。基于TSC的时钟驱动实现,请参考PRTOS源码core/kernel/x86/tsc.c。
提示: TSC也存在一些限制。由于TSC基于CPU主频(记录CPU的时钟脉冲),因此在多核CPU或CPU频率变化的情况下,不同处理器核心或不同CPU之间的TSC可能不同步,导致计时不准确。为了解决这个问题,一些处理器提供了TSC同步机制,例如Intel的TSC同步引擎(TSC Sync Engine)和AMD的TSC同步模式(TSC Sync Mode)。另外,TSC还可能受到频率变化、睡眠模式和动态频率调整等因素的影响。比如,空闲的操作系统内核可能会调用HALT指令,使处理器完全停止,直到接收到外部中断被唤醒,在此期间TSC停止计数。
3.HPET时钟驱动
HPET是一种高精度定时器。它是一种系统级别的硬件设备,通常用于代替早期的定时器,如Intel 8253 PIT。与早期的定时器相比,HPET具有更高的分辨率和更准确的时钟频率,并且可以更精确地测量和记录系统事件和时间间隔。HPET通常由系统主板上的芯片提供支持,并且可以在BIOS中进行配置。具体实现可参考PRTOS源码core/kernel/x86/hpet.c。
提示: 在X86单处理器硬件平台中,PRTOS用Intel 8253 PIT或者TSC定时器作为时钟源;在X86多处理器硬件平台中,PRTOS用HEPT定时器作为时钟源。
PRTOS的定时器组件用于分区调度、追踪分区中的时间以及处理虚拟机中的事件。类似PRTOS的时钟硬件,PRTOS也为硬件定时器定义了一组驱动。
PRTOS使用定时器驱动数据结构hw_timer_t来管理硬件定时器,具体定义请参考源码core/include/ktimer.h。硬件定时器资源属于Per-CPU资源。不管是单处理器硬件平台还是多处理器硬件平台,硬件定时器和pCPU都是一一对应关系,每个pCPU独占一个硬件定时器。PRTOS的hw_timer_t定时器接口基于3种不同的定时器硬件提供了3种类型的定时器驱动,分别是Intel 8253定时器驱动、HPET定时器驱动和LAPIC(Local Advanced Programmable Interrupt Controller,本地高级可编程中断控制器)定时器驱动,如图3-3所示。
提示: Per-CPU资源是每个CPU专用的资源,只有所属的CPU才可以访问。
图3-3 PRTOS的3种定时器驱动
1.Intel 8253定时器驱动
如果PRTOS选中Intel 8253 PIT作为硬件时钟源(即CONFIG_PC_PIT_CLOCK宏将被定义),PIT的通道1定时器工作在周期性触发模式,PRTOS使用全局变量pit_clock_data来记录时钟中断的触发次数,以辅助实现PRTOS的时钟驱动。Intel 8253 PIT定时器驱动的实现,请参考PRTOS源码core/kernel/x86/pit.c。
提示: 32位X86单核处理器硬件平台(基于QEMU或者VMware Workstation创建)均采用Intel 8253作为定时器硬件。
2.HPET定时器驱动
HPET定时器驱动用于操作和管理HPET硬件定时器。使用HPET定时器驱动可以在PRTOS中实现高精度的定时功能,从而满足实时性要求高的应用程序和系统的需求。在PRTOS内核中,HPET的管理机制和Intel 8253(或Intel 8254)PIT类似,具体实现可参考源码core/kernel/x86/hpet.c。
3.LAPIC定时器驱动
LAPIC定时器是集成在pCPU中的本地定时器,用于提供处理器级别的定时和中断功能。LAPIC定时器驱动用于操作和管理LAPIC定时器,它对于实现处理器级别的定时和中断功能非常重要,在操作系统和应用程序中应用广泛,用于实现定时任务、计时、事件触发和性能测量等功能。LAPIC定时器的具体实现可参考PRTOS源码core/kernel/x86/lapic_timer.c。
提示: 硬件定时器是Per-CPU专用的硬件资源。在单处理器硬件平台上,无论选择Intel 8253 PIT定时器,还是选择HPET硬件定时器,定时器资源都是Per-CPU类型的;在多处理器硬件平台上,LAPIC定时器集成在CPU内部。当PRTOS配置成支持SMP模式时,只能选择LAPIC定时器。
中断控制器是一种集成电路,可以将来自多个设备的中断信号合并为单个中断信号,并将其传递给CPU。这样,CPU就可以以一种有效的方式处理中断请求,而不必处理来自每个设备的中断信号。中断控制器还可以为每个设备分配一个中断优先级,以确保高优先级的中断请求被优先处理。
中断是硬件和软件交互的一种机制,可以说PRTOS系统在某种程度上是由中断来驱动的。一个中断的处理会经历设备、中断控制器、CPU这3个阶段,由设备来产生中断信号,由中断控制器来翻译信号,由CPU来实际处理信号。
PRTOS内核中管理中断控制器硬件的是中断控制器驱动,中断控制器驱动中的数据结构hw_irq_ctrl_t用于管理与中断控制器连接的各个外围设备中断线。如图3-4所示,基于不同的中断控制器(Intel 8259A PIC或者APIC),PRTOS内核提供了不同的hw_irq_ctrl_t类型的对象,不同hw_irq_ctrl_t类型的对象中封装了操作各个中断线的中断控制器驱动接口。为了便于对中断线进行操作,PRTOS还封装了一组全局API来管理各个中断线,具体实现请参考源码core/include/irqs.h。
图3-4 Intel 8259A PIC和APIC的中断控制器驱动
PRTOS X86系统在单处理器硬件平台上采用的是两片8259A PIC(Programmable Interrupt Controller,可编程中断控制器)级联的中断控制器;在多处理器硬件平台上采用的是APIC(Advanced Programmable Interrupt Controller,高级可编程中断控制器)。
提示: 本小节假设读者对8259A PIC、APIC已有基本的了解,缺乏这方面知识的读者可自行阅读相关的资料。
1.Intel 8259A PIC中断控制器驱动
单处理器硬件平台采用两片Intel 8259A PIC芯片进行级联,共管理15个外部I/O设备的中断请求(Interrupt Request,IRQ)线。这些IRQ线从Intel 8259A芯片的INT引脚连接到主Intel 8259A芯片的IRQ2引脚上。在单CPU硬件平台中,硬件中断通过Intel 8259A PIC芯片进行处理。每个Intel 8259A PIC芯片有8个IRQ线,直接和外部I/O设备相连。这些IRQ线有一个隐式的优先级,通常IRQ0具有最高的优先级,其后依次是IRQ1,IRQ2,…,IRQ7。当某个I/O设备触发IRQ请求时,如果没有同级或者更高优先级的中断请求要处理,并且这个IRQ线没有被屏蔽,Intel 8259A PIC会通知CPU中断请求的到来。否则,Intel 8259A PIC不会转发这个IRQ线上的中断请求给CPU。
对于PRTOS来说,中断信号通常分成两类:硬件中断和软件中断(异常)。每个中断由0~255之间的一个数字来标识。中断Int0~Int31(0x00-0x1F)由Intel公司预留使用,由CPU在执行指令时探测到异常情况而触发,分为故障和陷阱两类;中断Int32~Int255(0x20~0xFF)由用户自己设定。在单处理器平台中,Int32~Int47(0x20~0x2F)对应Intel 8259A中断控制芯片发出的硬件中断请求号IRQ0~IRQ15。具体实现可参考PRTOS源码core/kernel/x86/pic.c。
2.APIC中断控制器驱动
Intel X86 多处理器硬件平台兼容Intel MP(Multi-Processor,多处理器)Spec v1.4规范,采用APIC中断控制器。APIC中断控制器通常由两个部分组成:LAPIC和I/O APIC。LAPIC集成在每个处理器上,负责处理本地处理器的中断请求;I/O APIC则安装在系统芯片组上,负责处理外部设备的中断请求,并将它们分发给各个处理器。一个典型的MP硬件平台通常有一个I/O APIC和多个LAPIC,它们相互配合,形成一个中断的分发网络。
PRTOS创建了一个全局变量x86_mp_conf,用来记录多核硬件平台的CPU、中断控制器以及总线等配置信息,具体实现请参考源码core/include/x86/smp.h。兼容Intel MP Spec v1.4规范的硬件平台通常保存一个MP浮动指针结构struct mp_floating_pointer,操作系统必须按照指定的顺序搜索MP浮动指针结构。MP浮动指针结构包含一个指向MP配置表和其他MP特征信息字节的物理地址指针。这个表的存在表明当前的多核处理器架构符合MP规范。根据MP规范,该结构必须存储在下列内存位置。
1)在EBDA(Extended BIOS Data Area,扩展BIOS数据区)的第一个KB数据范围内。
2)在系统基本内存的最后一个KB内。
3)在BIOS ROM地址空间的0F0000h~0FFFFFh之间。
PRTOS中对应的实现请参考源码core/kernel/x86/mpspec.c。PRTOS检索到MP浮动指针结构后,从mpf指向的MP浮动指针结构获取MP配置表,解析MP配置表的每个表项,即可获取当前平台的所有CPU、总线、I/O APIC地址、I/O APIC IRQ线、中断源信息,如图3-5所示。
在图3-5中,PRTOS解析MP配置表后,最终获取一个MP配置信息结构x86_mp_conf。PRTOS通过x86_mp_conf配置信息来初始化I/O APIC的PRT(Programmable Redirection Table,可编程重定向表)用于中断分发。通过PRT,I/O APIC可以格式化出一条中断消息,发送给某个CPU的LAPIC,由LAPIC通知CPU进行处理。在遵循Intel MP Spec v1.4规范的硬件平台中,I/O APIC一般具有24个中断引脚,每个引脚对应一个RTE(Redirection Table Entry,重定向表项)。
图3-5 解析MP配置表获取多核配置信息
与PIC不同的是,I/O APIC的引脚没有优先级,即连接在引脚上的设备是平等的。但这并不意味着APIC系统中没有硬件优先级。设备的中断优先级由它对应的中断向量号决定,APIC将优先级控制的功能放到了LAPIC中实现。I/O APIC的24个引脚对应24个RTE,派发的目标字段采用LAPIC的逻辑ID,以便设置一组接收该引脚中断信号的LAPIC设备。
除了通过解析MP配置表获取多核配置信息外,PRTOS也支持解析符合APIC接口标准的配置表来初始化x86_mp_conf。关于PRTOS解析APIC配置表的具体实现,请参考core/kernel/x86/apic.c。
提示: 关于APIC接口标准,读者可查询相关的资料。使用VMware和QEMU搭建的Intel X86 多核处理器硬件平台既支持Intel MP Spec v1.4标准,也支持APIC接口标准。
1.页式内存管理机制
内存管理有两种方式:一种是段式内存管理,另一种是页式内存管理。Intel从80286开始使用保护模式,即段式内存管理模式。Intel 80386实现了对页式内存管理机制的支持。从Intel 80386处理器开始,后续的X86指令集在段式内存管理的基础上实现了页式内存管理。
X86的段式内存管理是将指令中结合段寄存器使用的32位逻辑地址(即CS:OFFSET方式)映射成同样是32位的物理地址。之所以称为物理地址,是因为这是真正放到地址总线上去,并将访问物理上存在的具体内存单元的地址。
但是段式内存管理机制的灵活性和效率都比较差:一方面,段是可变长度的,这就给盘区交换操作带来了不便;另一方面,如果为了增加灵活性而将一个进程的空间划分成很多小段,就势必要求在程序中频繁地改变段寄存器的内容。此外,即便将段分小,一个段描述表可以容纳8192个描述项(段选择子有13位用于索引段描述符表项),也未必能保证够用。因此,比较好的办法是采用页式内存管理。
正常情况下,页式内存管理并不需要建立在段式内存管理的基础之上,这是两种不同的机制。可是X86中保护模式的实现与段式存储密不可分,比如CPU的当前执行权限就是在有关的代码段描述符表项中规定的。因此,X86页式内存管理只能建立在段式内存管理的基础上。这也意味着页式内存管理的作用是在由段式内存管理所映射而成的地址上再加上上一层的地址映射。因此,此时段式内存管理映射而成的地址不再是物理地址了,Intel称之为线性地址。也就是说,段式内存管理先将逻辑地址映射成线性地址,然后由页式内存管理将线性地址映射成物理地址。或者,当不使用页式内存管理时,就将线性地址直接用作物理地址。
X86把线性地址空间划分成4KB大小的页面,每个页面可以被映射至物理存储空间中任意一块4KB大小的区间(边界必须与4KB对齐)。在段式内存管理中,连续的逻辑地址经过映射后在线性地址空间中还是连续的。但是在页式内存管理中,连续的线性地址经过映射后在物理空间中不一定连续(其灵活性也正在于此)。
提示: 虽然页式内存管理建立在段式内存管理的基础上,但一旦启用了页式内存管理,所有的线性地址都要经过页式映射,连GDTR与LDTR中给出的段描述表起始地址也不例外。
PRTOS为了简化设计,启动段式内存管理后,只是将4GB大小的逻辑地址空间映射到自身,作为线性地址出现,这样PRTOS的所有内核线程(kthread)共享同一个GDT,详情请参考源码core/kernel/x86/head.S中全局描述符表early_gdt_table的定义。early_gdt_table中代码段和数据段的基地址为0x0,段限长为4GB,DPL=0(DPL为描述符特权级)。也就是说,代码段和数据段都是从0地址开始的整个4GB逻辑地址空间,逻辑地址到线性地址的映射保持原值不变。
提示: 正因为PRTOS的内核代码段和数据段都是从0地址开始的整个4GB逻辑地址空间,逻辑地址到线性地址的映射保持原值不变,所以线性地址和逻辑地址具有相同的含义,程序中直接使用的逻辑地址空间和线性地址空间是等价的。
当页式内存映射启用后,以4KB大小的页为例,对32位的线性地址做进一步的地址映射,如图3-6所示。
图3-6 采用4KB物理页的页面地址映射机制
图3-6中的页目录表共有2 10 = 1024 个页目录项,每个页目录项指向一个页表,而每个页表中又有1024个页表项,寄存器CR3是指向当前页目录表的指针寄存器。从线性地址到物理地址的映射过程如下。
1)从CR3取得页目录表的基地址。
2)以线性地址中的Directory位段为下标,在页目录表中取得相应页目录项的基地址。
3)以线性地址中的Table位段为下标,在所得到的页表中取得相应的页表项。
4)将页表项中给出的页面基地址与线性地址中的Offset位段相加得到物理地址。
从X86 Pentium处理器开始,Intel引入了PSE(Page Size Extension,页面大小扩展)机制。启动PSE机制后,页目录项的PS位为1时,页的大小就成了4MB,而页表就不再使用了。这时线性地址中的低22位就全部用作4MB物理页中的偏移。这样,总的寻址能力还是没有改变,即1024×4MB = 4GB,但是映射的过程减少了一个层次,如图3-7所示。
图3-7 采用4MB物理页的页面地址映射机制
PRTOS使用4KB物理页和4MB物理页混合的方案,如图3-8所示。
采用4KB物理页和4MB物理页混合映射方案时,PRTOS内核空间采用4MB页面映射,这样可以提高TLB命中率,提升系统性能。
2.虚拟内存地址空间
在32位的X86硬件平台中,PRTOS内核占用的虚拟内存地址空间为0xFC000000~0xFFFFFFFF(即64MB大小的虚拟地址空间),其他的虚拟内存地址空间留给分区使用。具体实现请参考core/kernel/mmu/virtmm.c。
图3-8 采用4MB物理页和4KB物理页混合的页面地址映射机制
PRTOS是一个轻量级的嵌入式Ⅰ型Hypervisor,旨在提供强隔离和实时保证。PRTOS内核没有用户模式,没有shell访问,没有动态内存分配,所有辅助功能都被推送到分区客户操作系统中实现,由于PRTOS采用了极小化的实现,PRTOS控制台驱动仅包含UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)驱动和VGA(Video Graphics Array,视频图形阵列)驱动,分别用于管理UART设备和VGA设备,目的是为PRTOS做基本的调试和状态输出。
PRTOS内核提供了两个格式化输出接口(具体实现请参考core/klibc/stdio.c):
//用于PRTOS的初始化早期(PRTOS驱动框架建立之前)
extern prtos_s32_t eprintf(const char *, ...);
extern prtos_s32_t kprintf(const char *, ...); //用于PRTOS驱动框架建立之后
eprintf()在PRTOS初始化的早期使用。当输出设备初始化完成后,我们就可以调用eprintf()实现PRTOS内核状态的输出。在X86硬件平台上,可用的输出设备有UART设备和VGA设备。具体使用哪些设备进行输出,可以在make menuconfig中进行配置,如图3-9所示。
图3-9 配置输出设备
prtos_s32_t kprintf()的实现基于PRTOS的设备驱动框架,因此只有在驱动框架建立后才可以使用。 kprintf()使用的输出设备通过PRTOS内核控制台节点SystemDescription/PRTOSHypervisor@console进行配置。示例代码请参考user/bail/examples/helloworld/prtos_cf.x86.xml。
分区上下文切换指的是PRTOS根据指定的调度策略从一个分区切换到另一个分区。在PRTOS内核中,内核线程和vCPU是一一对应的。同一个分区内的不同vCPU共享内存资源和虚拟地址空间。但对调度器来说,多核分区中的vCPU和单核分区中的vCPU都是统一进行调度的。这和Linux内核中进程和线程的实现类似,同一个进程中的线程共享进程空间。但对Linux调度器来说,进程和线程是统一进行调度的。至于多核分区中的vCPU是否在同一个时刻进行切换,则取决于用户在每个物理核心上的调度策略(既可以让同一个分区的不同内核线程同时切换,也可以不同时切换)。
PRTOS分区上下文切换主要分成以下8个步骤。
步骤1:检测到时钟中断。
步骤2:保存当前分区的上下文。
步骤3:清理可能嵌套的中断服务程序。
步骤4:从配置文件定义的调度策略中选择下一个将要执行的分区。
步骤5:设置中断掩码和内存映射。
步骤6:恢复下一个分区的中断状态位。
步骤7:恢复下一个分区的虚拟时钟和虚拟定时器。
步骤8:恢复下一个分区的上下文,跳转到下一个分区中运行。
具体实现请参考源码core/kernel/sched.c,我们也会在第6章详细阐述。