在嵌入式系统、操作系统和虚拟化系统中,硬件与软件并不是互相独立的两个部分,而是互相协调,互相配合的“同伴”。一个资深的软件工程师,需要深知硬件如何与软件交互,这样才能最大化的利用硬件资源。如果只会让AI生成代码,那么不久的将来确实会被AI替代。
在实际开发一块板子时,硬件与软件负责不同的部分。
硬件负责提供:
软件则负责:
实际情况下,软件使用硬件的都需要依靠硬件提供的寄存器,而是用寄存器的方法非常简单:
(unsigned long ) 0x20100000
这里实际是直接调用硬件提供的寄存器地址,这样可以修改寄存器,从而操控硬件,这里先把数字转换为指针,这样就是使用地址,然后再修改地址的内容。
实际操作系统中常常需要软件使用寄存器完成相应的功能,例如:
这些案例背后有一个共同原则:
硬件实现机制,软件制定策略;硬件负责高频执行,软件负责配置、管理和异常处理。
在很多初学者的理解中,硬件先设计完成,软件再按照手册编写驱动。
这种理解只适合非常简单的场景。
在实际芯片项目中,硬件和软件通常需要共同定义以下内容:
硬件寄存器布局
DMA 描述符格式
中断状态和清除方式
复位和初始化顺序
地址转换规则
权限检查方式
错误上报机制
版本兼容策略比如一个网络控制器,不仅要定义“发送使能位在哪一位”,还要定义:
这些问题如果没有在硬件和软件之间定义清楚,即使硬件逻辑和软件代码各自没有语法错误,系统仍然无法正常工作。
分析一个底层硬件功能时,可以按照四个问题展开。
例如:
如果软件不了解初始状态,后续配置就可能建立在错误假设上。
软件必须了解:
寄存器手册中的“位定义”只是契约的一部分,完整契约通常还包括访问顺序、状态转换和异常处理方式。
例如:
CR0 写入分页使能位;IA32_EFER.LME;HCR_EL2.VM;硬件不会自动理解软件的最终目标。软件必须通过规定的寄存器、表项或指令,明确告诉硬件应当进入什么状态。
不能因为程序“没有立即崩溃”,就认为配置一定成功。
常见的验证方式包括:
ESR_EL2、FAR_EL2 等异常寄存器;底层初始化可以抽象为:
读取初始状态
│
▼
检查硬件能力
│
▼
准备表、栈、地址和权限
│
▼
执行寄存器或控制器切换
│
▼
读取状态并验证
│
▼
进入正常运行因此,底层初始化并不是简单的“给一个寄存器写值”,而是一个完整的状态转换协议。
严格来说,软件不能修改已经流片完成的寄存器电路。
但是在芯片、固件、驱动和操作系统共同开发时,软件工程师通常会参与寄存器接口设计,包括:
例如,一个 UART 控制寄存器可以设计为:
31 4 3 0
+-------------------------+---------+
| Reserved | DMA_EN |
| | RX_EN |
| | TX_EN |
| | UART_EN |
+-------------------------+---------+软件不仅需要知道每个位的含义,还需要知道:
所以,寄存器接口实际上包含两部分:
寄存器布局 + 访问时序只有布局,没有时序,接口仍然是不完整的。
接下来使用x86启动来说明硬件与软件的交互过程。
讨论 x86 启动时,必须区分几个不同层次:
这些协议规定的入口地址、寄存器内容、段寄存器状态和内存布局并不相同。 处理器复位状态由硬件架构规定,而操作系统实际接收到的启动状态,通常还受到 BIOS、UEFI、Bootloader 和具体启动协议的影响。
硬件复位状态、固件交接状态和操作系统启动代码设置完成后的状态,是三个不同的时刻,不能混为一谈。
Linux 32 位保护模式启动协议有自己的入口约定,例如:
ESI 指向 struct boot_params
EBP、EDI、EBX 具有协议规定的状态这些约定只适用于 Linux Boot Protocol,不能推广到 BIOS、UEFI 或其他 Bootloader。
UEFI x64 镜像入口使用 UEFI 规定的调用约定,常见参数为:
RCX = ImageHandle
RDX = EFI_SYSTEM_TABLE*这不是 Linux x86-64 程序的通用入口约定。
在调用 ExitBootServices() 之后,Boot Services 已经结束,软件不能继续依赖 Boot Services 函数指针。
寄存器 | 主要作用 |
|---|---|
| 保护模式、分页、写保护等控制 |
| 保存页错误发生时的线性地址 |
| 指向当前页表根 |
| PAE、SMEP、SMAP 等扩展功能控制 |
| 长模式、NX 等扩展功能控制 |
| 指向全局描述符表 |
| 指向中断描述符表 |
这些寄存器不是普通内存变量,需要使用处理器规定的指令访问。
例如,设置页表根:
mov cr3, eax真实代码必须根据当前执行模式和寄存器宽度选择正确的汇编形式。
读取和修改 IA32_EFER 通常使用 MSR 指令:
mov ecx, 0xC0000080 ; IA32_EFER
rdmsr
; 修改 EDX:EAX 中需要设置的字段
wrmsr真实代码还需要处理:
需要注意,CR3 中保存的是符合架构要求的页表根物理地址。在支持 PCID 等功能时,低位还可能包含额外控制信息,因此不能无条件地把整个 CR3 当成一个简单地址。
进入长模式不是设置一个位就结束了,而是多个条件共同满足的结果。
如果当前已经处于保护模式,典型准备过程可以表示为:
检查 CPU 是否支持长模式
│
▼
准备 GDT 和 64 位代码段
│
▼
准备页表,映射代码、栈和必要数据
│
▼
确保 CR0.PE = 1
│
▼
设置 CR4.PAE = 1
│
▼
把 PML4 或 PML5 根的物理地址写入 CR3
│
▼
设置 IA32_EFER.LME = 1
│
▼
设置 CR0.PG = 1
│
▼
远跳转到 64 位代码段
│
▼
进入 IA-32e 的 64-bit submode下面是一个概念化伪汇编,仅用于说明寄存器依赖关系:
; [概念伪汇编]
; 前提:
; 1. 已通过 CPUID 检查长模式支持
; 2. GDT 已准备好
; 3. 页表已映射当前代码、栈和必要数据
; 4. page_table_root 是满足对齐要求的物理地址
; 5. 当前环境已经处于或即将进入保护模式
; 如果当前仍处于实地址模式,
; 需要先按照当前启动协议建立保护模式环境。
; 具体过程此处省略。
; 开启 PAE
mov eax, cr4
or eax, CR4_PAE
mov cr4, eax
; 设置页表根
mov eax, page_table_root
mov cr3, eax
; 设置 IA32_EFER.LME
mov ecx, IA32_EFER
rdmsr
or eax, EFER_LME
wrmsr
; 开启分页
mov eax, cr0
or eax, CR0_PG
mov cr0, eax
; 通过远跳转装载 64 位代码段
jmp CODE64_SELECTOR:long_mode_entry这段代码省略了保护模式建立、GDT 装载、页表构造和异常路径,不能直接作为启动代码使用。
如果当前仍处于实地址模式,则需要先建立保护模式环境。典型过程包括:
准备 GDT
│
▼
加载 GDTR
│
▼
设置 CR0.PE
│
▼
远跳转到保护模式代码段早期 Intel 8086/8088 处理器只有 20 根地址线,即 A0 到 A19,可以访问:
2^20 = 1 MiB的地址空间。
但是,8086 使用段地址和偏移量计算地址,计算结果可能超过 20 位。
例如:
段地址:0xFFFF
偏移量:0x0010计算结果为:
0xFFFF × 16 + 0x0010 = 0x100000由于处理器只有 A0 到 A19,超出的高位无法输出,于是发生回绕:
0x100000 → 0x00000因此,早期系统中以下两个地址可能指向同一个位置:
0x000000
0x100000这是一种由硬件地址线宽度导致的行为。
80286 增加了第 21 个物理地址位,也就是 A20,可以访问 1 MiB 以上的地址。
从新硬件的角度看,这是地址能力的提升;但部分旧软件依赖 8086 的地址回绕行为。
为了兼容旧软件,IBM PC/AT 等早期平台通过外部逻辑增加了 A20 Gate:
A20 关闭:屏蔽物理地址 bit 20
A20 打开:允许物理地址 bit 20 正常参与地址生成这里需要区分三个概念:
它们不是同一个 CPU 控制寄存器。
还需要注意,A20 关闭并不意味着“所有超过 1 MiB 的地址都简单地对 1 MiB 取模”。
更准确地说,是地址的 bit 20 被强制为 0。因此,下面两类地址可能发生别名:
地址 bit 20 不同
其他地址位相同典型例子是:
0x000000 与 0x100000但不能把所有高地址都简单概括为对 1 MiB 取模。
因此,在早期开发中,软件需要实现队相关硬件的特殊化处理,一个优秀的操作系统应该能够适配大部分主流硬件,完成这些是软件工程师的责任
历史上常见的路径包括:
INT 15h, AX=2401h;0x92 的 Fast A20;通过 8042 控制器时,软件通常需要按照类似步骤操作:
等待控制器输入缓冲区空闲
│
▼
向 0x64 发送命令
│
▼
从 0x60 读取或写入数据
│
▼
修改输出端口中的 A20 控制位
│
▼
等待硬件完成传统 8042 路径通常会涉及:
向 0x64 发送输出端口命令
从 0x60 读取输出端口值
保留其他位
设置输出端口的 A20 位
将新值写回 0x60具体命令、状态位和时序必须按照目标平台实现处理,并且必须带有超时机制。
使用端口 0x92 时也不能简单写入一个固定值,因为端口的其他位可能具有不同功能。某些平台中 bit 0 可能与快速复位有关,错误修改可能导致系统复位。
因此,A20 处理流程通常是:
查询当前状态
│
├── 已经开启:直接继续
│
├── 尝试平台推荐路径
│
├── 失败后尝试兼容路径
│
└── 通过内存测试确认不同平台不一定支持所有方式,也不存在对所有机器都适用的唯一顺序。
现代 BIOS、UEFI 和虚拟机固件通常已经打开 A20,但启动代码仍然应根据具体启动环境决定是否需要检测,而不应盲目操作 8042 或端口 0x92。
IOMMU,即 Input-Output Memory Management Unit,输入输出内存管理单元,通常位于设备和内存之间。
没有 IOMMU 时:
设备 DMA 地址
│
▼
直接访问内存有 IOMMU 时:
设备 DMA 地址 / IOVA
│
▼
根据设备身份选择 domain
│
▼
IOMMU 查表
│
├── 地址和权限合法
│ │
│ ▼
│ 主机物理地址
│
└── 非法访问
│
▼
阻断并报告 faultIOMMU 处理的不是 CPU 虚拟地址,而是设备发起的 DMA 地址,通常称为:
IOMMU 通常根据以下信息进行处理:
常见实现包括:
CPU MMU 主要处理:
CPU 虚拟地址 → 物理地址IOMMU 主要处理:
设备 IOVA / DMA 地址 → 主机物理地址两者设计思想类似,但页表格式、失效机制、错误语义和访问者身份并不完全相同。
不同厂商的 IOMMU 具有相似的目标,但具体硬件结构并不相同。
常见概念包括:
PCIe Requester ID
│
▼
Root Table
│
▼
Contxt Table
│
▼
Translation Domain
│
▼
DMA 页表AMD IOMMU 使用自己的设备标识、设备表项、域和页表结构,不能直接套用 Intel VT-d 的 Root Table 或 Contxt Table 格式。
Arm SMMU 常见的概念包括:
StreamID
│
▼
Stream Table Entry
│
▼
上下文描述符
│
▼
Stage-1 / Stage-2 转换因此,不能把某一种 IOMMU 的寄存器名称和表项格式直接套用到另一种实现上。
本文只讨论基本的 DMA remapping 模型:
设备身份 → 隔离域 → IOVA → 物理地址对于 ATS、PASID、PRI、SVA、SR-IOV 等扩展,需要结合具体规范单独分析。
IOMMU 不是打开一个总开关就结束了。软件通常需要完成:
以 PCIe 设备为例,可以抽象为:
Requester ID
│
▼
设备上下文
│
▼
Translation Domain
│
▼
DMA 页表
│
▼
IOVA → 物理地址核心思想是:
软件建立映射和权限,硬件在每一次 DMA 请求到来时执行检查和转换。
假设驱动希望让设备访问一块缓冲区:
设备可见地址:0x40000000
物理地址: 0x12300000
长度: 64 KiB
权限: 设备可读、设备可写软件建立的映射可以表示为:
IOVA 0x40000000
│
▼
PA 0x12300000
│
└── 允许读写,长度 64 KiB概念性接口如下:
/*
* [概念伪代码]
*
* 这里假设 buffer 对应一段可用于 DMA 的物理内存。
* 真实系统可能面对非连续页、IOMMU 页粒度、缓存一致性、
* DMA mask、并发控制和设备描述符格式等问题。
*/
dma_addr_t map_dma_buffer(device_t *dev,
void *buffer,
size_t size,
dma_direction_t direction)
{
phys_addr_t pa = get_physical_address(buffer);
iommu_domain_t *domain = get_device_domain(dev);
iommu_map(domain,
IO_VA_BASE,
pa,
size,
direction_to_permission(direction));
iommu_flush_iotlb(domain, IO_VA_BASE, size);
return IO_VA_BASE;
}设备使用 DMA 缓冲区的生命周期通常是:
停止设备 DMA
│
▼
分配内存
│
▼
建立 IOMMU 映射
│
▼
执行 CPU/设备缓存同步
│
▼
把 IOVA 写入设备描述符
│
▼
允许 bus mastering
│
▼
启动设备 DMA
│
▼
等待设备完成
│
▼
同步数据回 CPU
│
▼
停止设备并解除映射关键原则是:
必须先建立正确映射并完成相关失效与同步,再允许设备发起 DMA。
撤销映射时则相反:
停止提交新的 DMA
│
▼
等待在途 DMA 完成
│
▼
禁止 bus mastering
│
▼
撤销映射
│
▼
执行必要的 IOTLB/设备侧缓存失效这里需要会一点虚拟化知识,可以看我之前的帖子学习虚拟化知识。
ARMv7-A 时代已经出现了 Virtualization Extensions,并提供了 Hyp mode 等虚拟化能力。
Armv8-A 则将虚拟化能力带入 AArch64 执行状态,通常使用 EL2 表示 Hypervisor 所处的异常级别。
因此,更准确的说法是:
ARMv8-A 进一步完善并扩展了 ARM 平台的硬件虚拟化能力,而不是第一次出现硬件虚拟化。
不同架构版本和处理器实现支持的功能可能不同,例如:
假设 Guest OS 认为自己运行在最高权限级别,它可能执行:
如果完全依靠软件模拟,Hypervisor 可能需要:
硬件辅助虚拟化的目标是:
让 Guest 的普通指令直接在处理器上运行,只在真正需要 Hypervisor 处理的操作上产生陷入。
这与“完全不需要软件”是两回事。
硬件主要负责:
软件仍然负责:
在 ARMv8-A 中,可以用异常级别描述不同的软件权限:
EL0:普通应用程序
EL1:操作系统内核
EL2:Hypervisor
EL3:安全监控程序或固件环境可以抽象为:
+--------------------------+
| EL0 用户应用 |
+--------------------------+
| EL1 Guest OS / Host OS |
+--------------------------+
| EL2 Hypervisor |
+--------------------------+
| EL3 Secure Monitor |
+--------------------------+需要注意:
当 Guest 执行某些受限制的操作时,硬件可以根据 HCR_EL2 等控制设置,将控制权转移到 EL2:
Guest 执行敏感操作
│
▼
硬件检查虚拟化控制
│
├── 允许直接执行
│
└── 陷入 EL2
│
▼
Hypervisor 处理但 EL2 并不会自动完成所有虚拟化工作。Hypervisor 仍然需要配置陷入策略、保存上下文并实现设备模型。
在常见的 Guest 虚拟化模型中,Guest OS 负责第一阶段地址转换:
Guest 虚拟地址 GVA
│
▼
Guest Stage-1 页表
│
▼
中间物理地址 IPAHypervisor 负责第二阶段地址转换:
中间物理地址 IPA
│
▼
Hypervisor Stage-2 页表
│
▼
真实物理地址 PA完整路径为:
GVA ──Stage-1──> IPA ──Stage-2──> PA其中:
例如,Guest 认为自己访问:
IPA:0x00000000Hypervisor 可以将它映射到:
PA:0x80000000Guest 不需要知道真实内存位于哪个物理地址。
Stage-2 相关寄存器通常包括:
VTTBR_EL2:Stage-2 页表根和 VMID 相关信息;VTCR_EL2:Stage-2 粒度、输入地址大小和其他转换参数;HCR_EL2.VM:Stage-2 地址转换相关控制;HCR_EL2.RW:较低异常级执行 AArch64 还是 AArch32。HCR_EL2.VM 和 HCR_EL2.RW 不是同一个功能。
如果 Stage-2 没有开启,就不能继续声称存在完整的 IPA → PA 二次转换。
Stage-1 和 Stage-2 的权限共同决定最终访问结果:
最终访问权限
= Stage-1 允许的权限
与 Stage-2 允许的权限的交集Stage-2 可以进一步拒绝 Guest 访问,但不能把 Stage-1 已经拒绝的访问重新放行。
VMID 主要用于翻译缓存上下文标识,不单独构成完整的安全边界。修改页表、VMID 或转换控制后,还需要按照架构要求完成屏障和 TLB 失效。
如果 Guest 访问了没有建立 Stage-2 映射的 IPA:
Guest 访问非法 IPA
│
▼
Stage-2 转换失败
│
▼
产生异常并陷入 EL2
│
▼
Hypervisor 读取故障信息
│
▼
决定映射、注入异常或终止 GuestHypervisor 可以根据策略:
Stage-2 是硬件执行的地址转换机制,但页表内容和权限策略由软件建立。
修改页表、VMID 或转换控制后,还需要按照架构要求完成:
下面是armv8虚拟化的重要寄存器
寄存器 | 作用 |
|---|---|
| EL2 异常向量基地址 |
| 虚拟化控制和异常路由 |
| Stage-2 转换控制 |
| 当前虚拟机 Stage-2 页表根和 VMID |
| EL2 自身 MMU 和缓存控制 |
| 异常综合信息 |
| 故障相关虚拟地址 |
| 某些场景下的故障 IPA 信息 |
| 异常返回地址 |
| 异常返回状态 |
不同 ARMv8-A 版本和处理器实现支持的字段可能不同。不能把某组固定十六进制值当作所有 ARM 处理器的通用配置。
还要特别注意:
VTCR_EL2 是 Stage-2 转换控制参数,不是页表根地址;VTTBR_EL2 才包含 Stage-2 页表根相关信息;SCTLR_EL2.M 控制 EL2 自身翻译环境,与 Guest 的 Stage-2 不是同一个开关;CurrentEL 是状态寄存器,不能通过写入它来切换异常级别;ERET。下面是一个概念伪代码,用于说明初始化依赖,不是可直接运行的 Hypervisor:
/*
* [概念伪代码]
*
* 省略:
* - 当前异常级和执行状态检查
* - 寄存器字段计算
* - Stage-2 页表格式
* - 页表项地址宽度和对齐检查
* - TLB 失效
* - 数据和指令屏障
* - 缓存维护
* - 异常向量保存和恢复
* - 多核同步
* - Guest 完整上下文
*/
void hypervisor_init(vm_t *vm)
{
/* 1. 先建立 EL2 栈和异常向量 */
write_sysreg(VBAR_EL2, el2_exception_vector);
setup_el2_stack();
/* 2. 准备 Guest 的 Stage-2 页表 */
stage2_map(vm->stage2_root,
guest_ipa_start,
host_pa_start,
guest_memory_size,
STAGE2_READ | STAGE2_WRITE);
/* 3. 配置 Stage-2 转换参数 */
write_sysreg(VTCR_EL2,
build_vtcr_for_platform());
/* 4. 设置当前虚拟机的 Stage-2 页表根 */
write_sysreg(VTTBR_EL2,
build_vttbr(vm->vmid, vm->stage2_root));
/* 5. 配置 Guest 的异常和特权行为 */
write_sysreg(HCR_EL2,
build_hcr_policy(vm));
/* 6. 真实实现还需要 TLBI、DSB、ISB 等架构要求的操作 */
translation_sync_barrier();
}允许 Guest 运行之前,至少应该确保以下资源已经可用:
EL2 栈
│
▼
EL2 异常向量
│
▼
Stage-2 页表
│
▼
Guest 初始寄存器状态
│
▼
虚拟中断和虚拟定时器
│
▼
异常处理和 Guest 上下文保存启动Guest时,Hypervisor 需要为 Guest 准备:
SPSR_EL2;概念上可以表示为:
/*
* [概念伪代码]
* ERET 是异常返回,不是普通函数返回。
*/
void enter_guest(vm_t *vm)
{
write_sysreg(ELR_EL2, vm->entry_pc);
write_sysreg(SPSR_EL2, vm->initial_pstate);
restore_guest_system_registers(vm);
restore_guest_general_registers(vm);
instruction_sync_barrier();
/*
* 真实代码需要使用目标架构规定的异常返回指令。
*/
eret();
}Guest 运行后,控制流通常不会像普通函数一样顺序返回,而是在发生以下事件时重新陷入 EL2:
Guest OS 必须能够看到自己的中断,否则无法正常调度任务和处理设备事件。
GIC 的虚拟化扩展可以帮助 Hypervisor 管理:
基本路径为:
物理设备产生中断
│
▼
Hypervisor 识别设备归属
│
▼
设置虚拟中断状态
│
▼
通过 GIC 虚拟接口呈现给 Guest硬件负责快速保存和呈现部分中断状态,软件负责决定:
GIC 虚拟化并不意味着 Guest 可以无条件直接使用物理 Distributor 或 Redistributor。Guest 对相关寄存器的访问可能需要陷入并由 Hypervisor 模拟。
在 GICv3 等实现中,Hypervisor 可能需要管理类似以下寄存器:
ICH_HCR_EL2
ICH_VMCR_EL2
ICH_LR<n>_EL2但具体实现和寄存器集合取决于 GIC 版本及处理器能力。
还需要注意:
HCR_EL2.IMO/FMO 主要用于中断路由;操作系统依赖定时器进行:
ARMv8-A 提供虚拟定时器相关能力,例如:
CNTVCT_EL0:虚拟计数器;CNTV_CTL_EL0:虚拟定时器控制;CNTV_CVAL_EL0:绝对虚拟比较值;CNTV_TVAL_EL0:相对虚拟计时值;CNTVOFF_EL2:虚拟计数器偏移。虚拟计数器可以抽象为:
Guest 看到的虚拟时间
≈ 物理计数器时间 - CNTVOFF_EL2在符合具体架构定义的计数器宽度和回绕规则下,通常有类似关系:
CNTVCT_EL0 = CNTPCT_EL0 - CNTVOFF_EL2没有硬件虚拟定时器时:
Guest 读取时间
│
▼
陷入 Hypervisor
│
▼
软件计算虚拟时间
│
▼
返回 Guest有硬件虚拟定时器时:
Guest 可以直接读取虚拟计数器硬件减少了频繁陷入的开销,但 Hypervisor 仍然需要管理:
定时器到期也不等于 Guest 一定立刻收到中断。还需要:
因此,虚拟定时器是硬件辅助功能,而不是完全脱离软件的自动功能。
寄存器、DMA 描述符、页表和虚拟机上下文,本质上都是软硬件之间的接口。
接口设计时应明确:
在开启分页、IOMMU 或虚拟化之前,应先准备能够处理故障的路径:
准备异常向量
│
▼
准备异常栈
│
▼
准备故障日志
│
▼
建立页表和权限
│
▼
执行失效和同步
│
▼
再开启地址转换或虚拟化否则,一旦硬件按预期产生异常,而软件没有可用的异常入口,系统可能直接死机或产生数据损坏。
不能只设计地址映射,还要同时回答:
谁可以访问?
访问哪个地址?
访问权限是什么?
映射何时生效?
映射何时撤销?
TLB/IOTLB 是否需要失效?
出现错误后如何恢复?例如,IOMMU 如果只配置了地址,却没有正确绑定设备身份,就不能形成完整的 DMA 隔离。
Stage-2 如果只映射地址,却没有设置正确的读写权限,也不能形成完整的 Guest 内存保护。
底层代码不能假设所有 CPU、芯片组和虚拟机环境都相同。
软件应当通过适当机制探测:
只有确认硬件能力后,才能决定是否启用对应功能。
硬件能力 | 首选方案 | 降级方案 |
|---|---|---|
A20 控制 | 平台推荐路径 | 8042 或兼容路径 |
IOMMU | 硬件 DMA 隔离 | 受限内存池或 Bounce Buffer |
虚拟定时器 | 硬件虚拟定时器 | Hypervisor 软件管理 |
虚拟中断 | GIC 硬件虚拟化 | 软件注入或模拟 |
设备虚拟化 | 设备直通 | 软件设备模型 |
CPU 虚拟化 | 硬件陷入和二阶段转换 | 软件模拟或更严格限制 |
降级方案通常性能较低,但可以提升系统兼容性和可移植性。
适合放到硬件中的功能通常具有以下特点:
例如:
适合放到软件中的功能通常具有以下特点:
例如:
可以概括为:
硬件实现机制,软件实现策略。
从 x86 启动阶段配置 CR0、CR3、CR4 和 IA32_EFER,到 A20 地址线的历史兼容;从 IOMMU 对 DMA 地址进行硬件隔离,到 ARMv8-A 通过 EL2、Stage-2 页表、虚拟中断和虚拟定时器支持高效虚拟化,这些技术虽然来自不同处理器和不同发展阶段,但它们遵循着相同的系统设计思想:
硬件提供能力
软件建立规则
硬件执行高频路径
软件管理资源和异常因此,软硬件协同设计的核心不是简单决定“功能放在硬件还是软件”,而是回答以下问题:
谁生成地址?
谁检查权限?
谁保存状态?
什么时候发生切换?
如何确认切换生效?
出现错误后谁负责恢复?真正优秀的底层初始化,不是把硬件简单“打开”,而是在打开之前准备好硬件将要遵守的规则,并在打开之后证明这些规则确实生效。
把高频、低延迟、强隔离的机制交给硬件;把灵活、复杂、需要策略判断的部分交给软件。undefined这就是嵌入式系统中软硬件协同设计的核心价值。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。