ARM-SMMU技术文档
摘要:介绍ARM-SMMU的开发过程。
背景知识
简单介绍SMMMU的原理与作用。
DMA是什么?为什么需要IOMMU?
运行在hvisor之上的虚拟机,需要和设备进行交互,但如果每次都等待CPU来主持这类工作,会使处理效率降低,那么就出现了DMA机制。DMA 是一种允许设备直接与内存交换数据而不需要 CPU 参与的机制。
那么我们可以大致得出虚拟机通过DMA和设备交互的过程,首先虚拟机发出DMA请求,告诉目标设备把数据写到哪个地方,然后设备根据地址写入内存。
但上述过程需要考虑一些问题:
- hvisor对每个虚拟机都做了内存虚拟化,所以虚拟机发出的DMA请求的目标内存地址是GPA,在这里也叫IOVA,需要将这个地址转为真正的PA,才能写入到物理内存的正确位置。
- 再者,如果不对IOVA的范围加以限制,那么代表着可以通过DMA机制访问任何一个内存地址,从而造成无法预见的严重后果。
所以我们需要一个既能帮我们做地址转换,又能保证操作地址的合法性的机构,就像MMU内存管理单元一样,这个机构就叫IOMMU,在Arm架构中它有另一个名字叫SMMU(后续都称SMMU)。
现在你知道了,SMMU可以将虚拟地址转为物理地址,从而保证设备直接访问内存的合法性。
SMMU具体要做的工作
上面说到SMMU的功能类似MMU,MMU的作用对象是虚拟机或者应用程序,而SMMU的作用对象是每个设备,每个设备以sid作为标识,对应的表叫做stream table。该表以设备的sid作为索引,PCI设备的sid可以从BDF号获取:sid = (B << 5) | (D << 3) | F。
开发工作
目前我们在Qemu中实现了对SMMUv3的stage-2地址转换支持,创建简单的线性表,并且使用PCI设备进行了简单的验证。
IOMMU的工作还未并入主线,可以切换到IOMMU分支查看。
整体思路
我们将PCI HOST直通给zone0,即在提供给zone0的设备树中加上PCI节点,将对应的内存地址在zone0的第二阶段页表中做好映射,并确保中断注入正常。那么zone0就会自己去探测PCI设备并进行配置,而我们在hvisor中只需要做好SMMU的配置工作就好。
Qemu参数
在 machine
中添加 iommu=smmuv3
以开启SMMUv3支持,并在 global
中添加 arm-smmuv3.stage=2
开启第二阶段地址翻译。
注意在Qemu中尚不支持嵌套翻译,如果不指出 stage=2
,则默认只支持第一阶段地址翻译,请使用Qemu-8.1以上版本,低版本中不支持开启第二阶段地址翻译。
添加PCI设备时请注意开启 iommu_platform=on
。
addr可以指定该设备的bdf号。
在Qemu模拟的PCI总线中,除了PCI HOST,还有一个默认的网卡设备,所以其余添加的设备的addr参数必须从2.0开始。
// scripts/qemu-aarch64.mk
QEMU_ARGS := -machine virt,secure=on,gic-version=3,virtualization=on,iommu=smmuv3
QEMU_ARGS += -global arm-smmuv3.stage=2
QEMU_ARGS += -device virtio-blk-pci,drive=Xa003e000,disable-legacy=on,disable-modern=off,iommu_platform=on,addr=2.0
在hvisor的页表中映射SMMU相关的内存
查阅Qemu的源码可知VIRT_SMMU对应的内存区域起始地址为0x09050000,大小为0x20000,我们需要访问这个区域,所以在hvisor的页表中必须进行映射。
// src/arch/aarch64/mm.rs
pub fn init_hv_page_table(fdt: &fdt::Fdt) -> HvResult {
hv_pt.insert(MemoryRegion::new_with_offset_mapper(
smmuv3_base(),
smmuv3_base(),
smmuv3_size(),
MemFlags::READ | MemFlags::WRITE,
))?;
}
SMMUv3数据结构
该结构包含了将会访问的SMMUv3的内存区域的引用,是否支持二级表,sid的最大位数,以及stream table的基地址和分配的页帧。
其中的rp是所定义的 RegisterPage
的引用,RegisterPage
根据SMMUv3手册中第六章的偏移量进行设置,读者可自行查阅。
// src/arch/aarch64/iommu.rs
pub struct Smmuv3{
rp:&'static RegisterPage,
strtab_2lvl:bool,
sid_max_bits:usize,
frames:Vec<Frame>,
// strtab
strtab_base:usize,
// about queues...
}
new()
在完成映射工作后,我们便可以引用对应的这段寄存器区域。
impl Smmuv3{
fn new() -> Self{
let rp = unsafe {
&*(SMMU_BASE_ADDR as *const RegisterPage)
};
let mut r = Self{
...
};
r.check_env();
r.init_structures();
r.device_reset();
r
}
}
check_env()
检查当前环境支持哪个阶段的地址转换、支持什么类型的流表、支持多少位的sid等信息。
以其中检查环境支持哪种表格式为例,支持的表的类型在 IDR0
寄存器中,通过 self.rp.IDR0.get() as usize
获取 IDR0
的数值,通过 extract_bit
进行截取,获取 ST_LEVEL
字段的值,根据手册可知,0b00代表支持线性表,0b01代表支持线性表和二级表,0b1x为保留位,我们可以根据该信息选择创建什么类型的流表。
impl Smmuv3{
fn check_env(&mut self){
let idr0 = self.rp.IDR0.get() as usize;
info!("Smmuv3 IDR0:{:b}",idr0);
// supported types of stream tables.
let stb_support = extract_bits(idr0, IDR0_ST_LEVEL_OFF, IDR0_ST_LEVEL_LEN);
match stb_support{
0 => info!("Smmuv3 Linear Stream Table Supported."),
1 => {info!("Smmuv3 2-level Stream Table Supoorted.");
self.strtab_2lvl = true;
}
_ => info!("Smmuv3 don't support any stream table."),
}
...
}
}
init_linear_strtab()
我们需要支持第二阶段地址转换,且系统中的设备并不多,所以我们选择用线性表。
在申请线性表需要的空间时,我们应该根据当前的sid的最多位数得到表项的个数,乘上每个表项需要的空间 STRTAB_STE_SIZE
,进而知道需要申请多少个页帧。但SMMUv3对stream table的起始地址有着严格的要求,起始地址的低 (5+sid_max_bits) 位必须为0。
由于当前的hvisor中暂不支持这样申请空间,我们在确保安全的情况下,申请一段空间,并在这段空间里面选定一个符合条件的地址作为表基址,虽然这样会造成一些空间的浪费。
申请了空间后,我们可以将这个表的基地址填入 STRTAB_BASE
这个寄存器:
let mut base = extract_bits(self.strtab_base, STRTAB_BASE_OFF, STRTAB_BASE_LEN);
base = base << STRTAB_BASE_OFF;
base |= STRTAB_BASE_RA;
self.rp.STRTAB_BASE.set(base as _);
接着我们还要设置 STRTAB_BASE_CFG
寄存器,来表明我们使用的表的格式是线性表或者二级表,以及表项的个数(使用以LOG2的形式表示,即SID的最大位数):
// format : linear table
cfg |= STRTAB_BASE_CFG_FMT_LINEAR << STRTAB_BASE_CFG_FMT_OFF;
// table size : log2(entries)
// entry_num = 2^(sid_bits)
// log2(size) = sid_bits
cfg |= self.sid_max_bits << STRTAB_BASE_CFG_LOG2SIZE_OFF;
// linear table -> ignore SPLIT field
self.rp.STRTAB_BASE_CFG.set(cfg as _);
init_bypass_ste(sid:usize)
当前我们还未配置任何相关信息,需要先将所有表项置为默认状态。
对于每个sid,根据表基址找到表项的地址,即合法位为0,地址翻译设置为 BYPASS
。
let base = self.strtab_base + sid * STRTAB_STE_SIZE;
let tab = unsafe{&mut *(base as *mut [u64;STRTAB_STE_DWORDS])};
let mut val:usize = 0;
val |= STRTAB_STE_0_V;
val |= STRTAB_STE_0_CFG_BYPASS << STRTAB_STE_0_CFG_OFF;
device_reset()
上面我们做了一些准备工作,但还需要一些额外的配置,比如使能SMMU,否则会导致SMMU处于disabled状态。
let cr0 = CR0_SMMUEN;
self.rp.CR0.set(cr0 as _);
write_ste(sid:usize,vmid:usize,root_pt:usize)
该方法用于配置具体设备的信息。
首先我们同样要根据sid找到对应的表项地址。
let base = self.strtab_base + sid * STRTAB_STE_SIZE;
let tab = unsafe{&mut *(base as *mut [u64;STRTAB_STE_DWORDS])};
第二步我们要指明,这个设备的相关信息,我们是要用来进行第二阶段地址翻译的,且这个表项是合法的了。
let mut val0:usize = 0;
val0 |= STRTAB_STE_0_V;
val0 |= STRTAB_STE_0_CFG_S2_TRANS << STRTAB_STE_0_CFG_OFF;
第三步我们要说明当前这个设备分配给哪个虚拟机来用,并且开启第二阶段页表遍历,S2AA64
代表第二阶段翻译表是基于aarch64的,S2R
代表启用错误记录。
let mut val2:usize = 0;
val2 |= vmid << STRTAB_STE_2_S2VMID_OFF;
val2 |= STRTAB_STE_2_S2PTW;
val2 |= STRTAB_STE_2_S2AA64;
val2 |= STRTAB_STE_2_S2R;
最后一步就是要指出第二阶段翻译的依据,就是在hvisor中的对应虚拟机的页表,只需要将页表基地址填入对应位置即可,即 S2TTB
这个字段。
这里我们也需要说明这个页表的配置信息,这样SMMU才知道这个页表的格式等信息,才能使用这个页表,即 VTCR
这个字段。
let vtcr = 20 + (2<<6) + (1<<8) + (1<<10) + (3<<12) + (0<<14) + (4<<16);
let v = extract_bits(vtcr as _, 0, STRTAB_STE_2_VTCR_LEN);
val2 |= v << STRTAB_STE_2_VTCR_OFF;
let vttbr = extract_bits(root_pt, STRTAB_STE_3_S2TTB_OFF, STRTAB_STE_3_S2TTB_LEN);
初始化以及设备分配
在 src/main.rs
中,进行了hvisor的页表初始化以后(映射了SMMU相关区域),可以进行SMMU的初始化。
fn primary_init_early(dtb: usize) {
...
crate::arch::mm::init_hv_page_table(&host_fdt).unwrap();
info!("Primary CPU init hv page table OK.");
iommu_init();
zone_create(0,ROOT_ENTRY,ROOT_ZONE_DTB_ADDR as _, DTB_IPA).unwrap();
INIT_EARLY_OK.store(1, Ordering::Release);
}
接着需要分配设备,这一步我们在创建虚拟机的时候同步完成,目前我们只将设备分配给zone0使用。
// src/zone.rs
pub fn zone_create(
zone_id: usize,
guest_entry: usize,
dtb_ptr: *const u8,
dtb_ipa: usize,
) -> HvResult<Arc<RwLock<Zone>>> {
...
if zone_id==0{
// add_device(0, 0x8, zone.gpm.root_paddr());
iommu_add_device(zone_id, BLK_PCI_ID, zone.gpm.root_paddr());
}
...
}
简单验证
在qemu启动参数中开启 -trace smmuv3_*
,即可看到相关输出:
smmuv3_config_cache_hit Config cache HIT for sid=0x10 (hits=1469, misses=1, hit rate=99)
smmuv3_translate_success smmuv3-iommu-memory-region-16-2 sid=0x10 iova=0x8e043242 translated=0x8e043242 perm=0x3