《ARM SMMU原理与IOMMU技术(“VT-d” DMA、I/O虚拟化、内存虚拟化)》
《提升KVM异构虚拟机启动效率:透传(pass-through)、DMA映射(VFIO、PCI、IOMMU)、virtio-balloon、异步DMA映射、预处理》
《内核引导参数IOMMU与INTEL_IOMMU有何不同?》
在分析Linux kernel dump的时候经常会看到一个叫做dmar的东西,查看中断信息的时候也时常见到一个名称为dmar0的设备,到底什么是dmar呢?
$ cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3
0: 127 0 0 0 IR-IO-APIC-edge timer
1: 2 0 0 0 IR-IO-APIC-edge i8042
...
48: 0 0 0 0 DMAR_MSI-edge dmar0
...
大家知道,I/O设备可以直接存取内存,称为DMA(Direct Memory Access);DMA要存取的内存地址称为DMA地址(也可称为BUS address)。在DMA技术刚出现的时候,DMA地址都是物理内存地址,简单直接,但缺点是不灵活,比如要求物理内存必须是连续的一整块而且不能是高位地址等等,也不能充分满足虚拟机的需要。后来dmar就出现了。 dmar意为DMA remapping,是Intel为支持虚拟机而设计的I/O虚拟化技术,I/O设备访问的DMA地址不再是物理内存地址,而要通过DMA remapping硬件进行转译,DMA remapping硬件会把DMA地址翻译成物理内存地址,并检查访问权限等等。负责DMA remapping操作的硬件称为IOMMU。做个类比:大家都知道MMU是支持内存地址虚拟化的硬件,MMU是为CPU服务的;而IOMMU是为I/O设备服务的,是将DMA地址进行虚拟化的硬件。
IOMMA不仅将DMA地址虚拟化,还起到隔离、保护等作用,如下图所示意,详细请参阅Intel Virtualization Technology for Directed I/O
现在我们知道了dmar的概念,那么Linux中断信息中出现的dmar0又是什么呢? 还是用MMU作类比吧,便于理解:当CPU访问一个在地址翻译表中不存在的地址时,就会触发一个fault,Linux kernel的fault处理例程会判断这是合法地址还是非法地址,如果是合法地址,就分配相应的物理内存页面并建立从物理地址到虚拟地址的翻译表项,如果是非法地址,就给进程发个signal,产生core dump。IOMMU也类似,当I/O设备进行DMA访问也可能触发fault,有些fault是recoverable的,有些是non-recoverable的,这些fault都需要Linux kernel进行处理,所以IOMMU就利用中断(interrupt)的方式呼唤内核,这就是我们在/proc/interrupts中看到的dmar0那一行的意思。 我们看到的中断号48,据此还可以进一步发掘更多的信息:
crash64> irq 48
IRQ: 48
STATUS: 100 (IRQ_INPROGRESS)
HANDLER: ffffffff81a96a40 <dmar_msi_type>
typename: ffffffff81791acb "DMAR_MSI"
startup: ffffffff810e3960 <default_startup>
shutdown: ffffffff810e3920 <default_shutdown>
enable: ffffffff810e3990 <default_enable>
disable: ffffffff810e3860 <default_disable>
ack: ffffffff81031a00 <ack_apic_edge>
mask: ffffffff812add60 <dmar_msi_mask>
mask_ack: 0
unmask: ffffffff812addc0 <dmar_msi_unmask>
eoi: 0
end: ffffffff810e14f0 <noop>
set_affinity: ffffffff81033130 <dmar_msi_set_affinity>
retrigger: ffffffff81031260 <ioapic_retrigger_irq>
set_type: 0
set_wake: 0
ACTION: ffff880439deb8c0
handler: ffffffff812ad9b0 <dmar_fault>
flags: 0
name: ffff880439ca9180 "dmar0"
dev_id: ffff880439ca9140
next: 0
DEPTH: 0
上面最有意思的信息是ACTION的handler,表示IOMMU发生fault之后的中断处理例程,我们看到的例程名是dmar_fault,源代码如下:
1296 irqreturn_t dmar_fault(int irq, void *dev_id)
1297 {
1298 struct intel_iommu *iommu = dev_id;
1299 int reg, fault_index;
1300 u32 fault_status;
1301 unsigned long flag;
1302
1303 spin_lock_irqsave(&iommu->register_lock, flag);
1304 fault_status = readl(iommu->reg + DMAR_FSTS_REG);
1305 if (fault_status)
1306 pr_err("DRHD: handling fault status reg %x\n", fault_status);
1307
1308 /* TBD: ignore advanced fault log currently */
1309 if (!(fault_status & DMA_FSTS_PPF))
1310 goto clear_rest;
1311
1312 fault_index = dma_fsts_fault_record_index(fault_status);
1313 reg = cap_fault_reg_offset(iommu->cap);
1314 while (1) {
1315 u8 fault_reason;
1316 u16 source_id;
1317 u64 guest_addr;
1318 int type;
1319 u32 data;
1320
1321 /* highest 32 bits */
1322 data = readl(iommu->reg + reg +
1323 fault_index * PRIMARY_FAULT_REG_LEN + 12);
1324 if (!(data & DMA_FRCD_F))
1325 break;
1326
1327 fault_reason = dma_frcd_fault_reason(data);
1328 type = dma_frcd_type(data);
1329
1330 data = readl(iommu->reg + reg +
1331 fault_index * PRIMARY_FAULT_REG_LEN + 8);
1332 source_id = dma_frcd_source_id(data);
1333
1334 guest_addr = dmar_readq(iommu->reg + reg +
1335 fault_index * PRIMARY_FAULT_REG_LEN);
1336 guest_addr = dma_frcd_page_addr(guest_addr);
1337 /* clear the fault */
1338 writel(DMA_FRCD_F, iommu->reg + reg +
1339 fault_index * PRIMARY_FAULT_REG_LEN + 12);
1340
1341 spin_unlock_irqrestore(&iommu->register_lock, flag);
1342
1343 dmar_fault_do_one(iommu, type, fault_reason,
1344 source_id, guest_addr);
1345
1346 fault_index++;
1347 if (fault_index >= cap_num_fault_regs(iommu->cap))
1348 fault_index = 0;
1349 spin_lock_irqsave(&iommu->register_lock, flag);
1350 }
1351 clear_rest:
1352 /* clear all the other faults */
1353 fault_status = readl(iommu->reg + DMAR_FSTS_REG);
1354 writel(fault_status, iommu->reg + DMAR_FSTS_REG);
1355
1356 spin_unlock_irqrestore(&iommu->register_lock, flag);
1357 return IRQ_HANDLED;
1358 }
请注意行号1343,dmar_fault_do_one()会报告fault的具体信息,包括对应设备的物理位置。由于一个dmar对应着很多个I/O设备,这条信息可以帮助定位到具体哪一个设备。源代码如下:
1270 static int dmar_fault_do_one(struct intel_iommu *iommu, int type,
1271 u8 fault_reason, u16 source_id, unsigned long long addr)
1272 {
1273 const char *reason;
1274 int fault_type;
1275
1276 reason = dmar_get_fault_reason(fault_reason, &fault_type);
1277
1278 if (fault_type == INTR_REMAP)
1279 pr_err("INTR-REMAP: Request device [[%02x:%02x.%d] "
1280 "fault index %llx\n"
1281 "INTR-REMAP:[fault reason %02d] %s\n",
1282 (source_id >> 8), PCI_SLOT(source_id & 0xFF),
1283 PCI_FUNC(source_id & 0xFF), addr >> 48,
1284 fault_reason, reason);
1285 else
1286 pr_err("DMAR:[%s] Request device [%02x:%02x.%d] "
1287 "fault addr %llx \n"
1288 "DMAR:[fault reason %02d] %s\n",
1289 (type ? "DMA Read" : "DMA Write"),
1290 (source_id >> 8), PCI_SLOT(source_id & 0xFF),
1291 PCI_FUNC(source_id & 0xFF), addr, fault_reason, reason);
1292 return 0;
1293 }
dmar的初始化是kernel根据ACPI中的dmar table进行的,每一个表项对应一个dmar设备,名称从dmar0开始依次递增,涉及取名的代码如下:
0751 int alloc_iommu(struct dmar_drhd_unit *drhd)
0752 {
0753 struct intel_iommu *iommu;
0754 u32 ver;
0755 static int iommu_allocated = 0;
0756 int agaw = 0;
0757 int msagaw = 0;
0758 int err;
0759
0760 if (!drhd->reg_base_addr) {
0761 warn_invalid_dmar(0, "");
0762 return -EINVAL;
0763 }
0764
0765 iommu = kzalloc(sizeof(*iommu), GFP_KERNEL);
0766 if (!iommu)
0767 return -ENOMEM;
0768
0769 iommu->seq_id = iommu_allocated++;
0770 sprintf (iommu->name, "dmar%d", iommu->seq_id);
...
0797 pr_info("IOMMU %d: reg_base_addr %llx ver %d:%d cap %llx ecap %llx\n",
0798 iommu->seq_id,
0799 (unsigned long long)drhd->reg_base_addr,
0800 DMAR_VER_MAJOR(ver), DMAR_VER_MINOR(ver),
0801 (unsigned long long)iommu->cap,
0802 (unsigned long long)iommu->ecap);
...
上面也揭示了boot过程留在dmesg中信息的来历:
dmar: IOMMU 0: reg_base_addr f8ffe000 ver 1:0 cap d2078c106f0462 ecap f020fe
附注: 过去的AMD64芯片也提供一个功能有限的地址转译模块——GART (Graphics Address Remapping Table),有时候它也可以充当IOMMU,这导致了人们对GART和新的IOMMU的混淆。最初设计GART是为了方便图形芯片直接读取内存:使用地址转译功能将收集到内存中的数据映射到一个图形芯片可以“看”到的地址。后来GART被Linux kernel用来帮助传统的32位PCI设备访问可寻址范围之外的内存区域。这件事新的IOMMU当然也可以做到,而且没有GART的局限性(它仅限于显存的范围之内),IOMMU可以将I/O设备的任何DMA地址转换为物理内存地址。
参考资料:
- Linux Kernel的Intel-IOMMU.txt
- Intel’s Virtualization for Directed I/O (a.k.a IOMMU)
- Wikipedia IOMMU
- 理解IOMMU、北桥、MMIO和ioremap
- Intel Virtualization Technology for Directed I/O
- DMAR 与 IOMMU