MMIO & PMIO
内存映射输入输出(Memory-Mapped I/O,MMI/O,简称为内存映射 I/O),以及端口映射输入输出(Port-Mapped I/O, PMI/O),是 PC 机在中央处理器 (CPU) 和外部设备之间执行输入输出操作中互为补充的两种方法。除此之外,执行输入输出操作也可以使用专用输入输出处理器 (dedicated I/O processors) ——这通常是指大型机上的通道输入输出 (channel I/O),这些专用处理器执行自有的指令集。
MMIO
内存映射 I/O(不要和内存映射文件的输入输出混淆)使用相同的地址总线(共享同一个地址空间)来寻址内存和输入输出设备(I/O 设备)。当 CPU 访问某个地址的时候,可能是要访问某一部分物理内存,也可能是要访问 I/O 设备上的内存。因此,设备内存也可以通过内存访问指令来完成读写。每个 I/O 设备监测 CPU 的地址总线,并且在发现 CPU 访问被分配到本设备的地址区域的时候做出响应,创建数据总线和相应设备寄存器之间的连接。为了实现 CPU 对 MMIO 设备的访问,相应的地址空间必须给这些设备保留(可以是永久保留,也可以是暂时性的保留),并且不能再分配给系统物理内存。
Implement
内核使用 ioremap()
将 I/O 设备的物理内存地址映射到内核空间的虚拟地址上;用户空间程序使用 mmap
将 I/O 设备的物理内存地址映射到用户空间的虚拟内存地址上,使得用户空间的一段内存与 I/O 设备的内存关联起来,当用户访问用户空间的这段内存地址范围时,会转化为对 I/O 设备的访问。
通过 cat /proc/iomem
查看系统 MMIO 虚拟地址映射情况:
$ cat /proc/iomem
00000000-00000fff : Reserved
00001000-0009fbff : System RAM
0009fc00-0009ffff : Reserved
000a0000-000bffff : PCI Bus 0000:00
000c0000-000c91ff : Video ROM
000c9800-000ca1ff : Adapter ROM
000ca800-000ccbff : Adapter ROM
000f0000-000fffff : Reserved
000f0000-000fffff : System ROM
00100000-7ffdffff : System RAM
09600000-0a400eb0 : Kernel code
0a400eb1-0ae5787f : Kernel data
0b121000-0b5fffff : Kernel bss
7ffe0000-7fffffff : Reserved
80000000-febfffff : PCI Bus 0000:00
fc000000-fdffffff : 0000:00:02.0
fc000000-fdffffff : cirrus
feb80000-febbffff : 0000:00:03.0
febd0000-febd0fff : 0000:00:02.0
febd0000-febd0fff : cirrus
febd1000-febd1fff : 0000:00:03.0
febd2000-febd2fff : 0000:00:04.0
febd3000-febd3fff : 0000:00:05.0
fec00000-fec003ff : IOAPIC 0
fee00000-fee00fff : Local APIC
feffc000-feffffff : Reserved
fffc0000-ffffffff : Reserved
PMIO
PMI/O 通常使用一组专门为 I/O 设计的 CPU 指令来执行 I/O 操作,在 Intel 的微处理器中使用 in
/out
指令。这两条指令有一些不同的形式,分别用来在 CPU 的 EAX 寄存器和 I/O 设备的某个端口之间完成对单/双/四字节数据的操作(比如对 out
指令,分别有 outb
、outw
和 outl
)。I/O 设备有一个和内存地址空间相互独立的 I/O 地址空间,通过专用 I/O 针脚或者专用的总线和 CPU 相连。因为这个 I/O 地址空间和内存地址空间相互独立,所以有时候称为独立输入输出 (isolated I/O)。I/O 端口也可以通过 ioport_map
映射到虚拟地址空间进行访问。
Implement
用户空间想访问 I/O 端口必须使用 ioperm
和 iopl
系统调用来获得进行操作 I/O 端口的权限。ioperm
为获取单个端口的操作许可,iopl
为获取整个 I/O 空间许可(这 2 个函数都是 x86 特有的)。x86 CPU 的 I/O 地址空间就只有 64KB (0~0xffff)。
通过 cat /proc/ioports
查看系统 PMIO 端口号映射情况:
$ cat /proc/ioports
0000-0cf7 : PCI Bus 0000:00
0000-001f : dma1
0020-0021 : pic1
0040-0043 : timer0
0050-0053 : timer1
0060-0060 : keyboard
0064-0064 : keyboard
0070-0071 : rtc0
0080-008f : dma page reg
00a0-00a1 : pic2
00c0-00df : dma2
00f0-00ff : fpu
0170-0177 : 0000:00:01.1
0170-0177 : ata_piix
01f0-01f7 : 0000:00:01.1
01f0-01f7 : ata_piix
0376-0376 : 0000:00:01.1
0376-0376 : ata_piix
03f2-03f2 : floppy
03f4-03f5 : floppy
03f6-03f6 : 0000:00:01.1
03f6-03f6 : ata_piix
03f7-03f7 : floppy
03f8-03ff : serial
0505-0505 : QEMU0001:00
0510-051b : QEMU0002:00
0510-051b : fw_cfg_io
0600-063f : 0000:00:01.3
0600-0603 : ACPI PM1a_EVT_BLK
0604-0605 : ACPI PM1a_CNT_BLK
0608-060b : ACPI PM_TMR
0700-070f : 0000:00:01.3
0700-0708 : piix4_smbus
0cf8-0cff : PCI conf1
0d00-adff : PCI Bus 0000:00
ae0f-aeff : PCI Bus 0000:00
af20-afdf : PCI Bus 0000:00
afe0-afe3 : ACPI GPE0_BLK
afe4-ffff : PCI Bus 0000:00
c000-c03f : 0000:00:05.0
c000-c03f : virtio-pci-legacy
c040-c05f : 0000:00:01.2
c040-c05f : uhci_hcd
c060-c07f : 0000:00:03.0
c060-c07f : virtio-pci-legacy
c080-c09f : 0000:00:04.0
c080-c09f : virtio-pci-legacy
c0a0-c0bf : 0000:00:06.0
c0a0-c0bf : virtio-pci-legacy
c0c0-c0cf : 0000:00:01.1
c0c0-c0cf : ata_piix
Difference between MMIO & PMIO
MMIO | PMIO |
---|---|
I/O 设备和内存共享同一地址总线 | I/O 设备和内存地址空间隔离 |
使用读写内存的指令访问 I/O 设备和内存 | 使用特殊的指令访问 I/O 设备 |
申请 ->映射 ->访问 ->释放 |
申请 ->访问 ->释放 |
PS:内存映射 I/O(MMIO 和 PMIO)作为一种 CPU 对 I/O 设备 (CPU-to-device) 的通信方法,并不影响 DMA(直接内存访问),因为 DMA 是一种绕过 CPU 的内存对设备 (memory-to-device) 的通信方法。
QEMU/KVM Emulation
MMIO Emulation
虚拟机物理内存的虚拟化通过 EPT 机制来完成,那么模拟设备的 MMIO 实现也需要利用 EPT 机制。虚拟机的 EPT 页表在 EPT_VIOLATION
异常处理时建立起来,而对于模拟设备而言,通过 EPT_MISCONFIG
来标志 MMIO 访问异常,并触发 VM_EXIT
并交给 Hypervisor 去处理。
EPT_VIOLATION
表示的是对应的物理页不存在,而EPT_MISCONFIG
表示 EPT 页表中有非法的域。EXIT_REASON_EPT_VIOLATION is similar to a "page not present" pagefault. EXIT_REASON_EPT_MISCONFIG is similar to a "reserved bit set" pagefault.
KVM
如果虚拟机开启了 EPT 支持就会调用 ept_set_mmio_spte_mask
初始化 shadow_mmio_mask
,设置 EPT 页表项最低 3 位为 110
就会触发 EPT_MISCONFIG
(110
表示该页可读可写但是还未分配或者不存在,是一个错误的 EPT 页表项)。
static void ept_set_mmio_spte_mask(void)
{
/*
* EPT Misconfigurations can be generated if the value of bits 2:0
* of an EPT paging-structure entry is 110b (write/execute).
*/
kvm_mmu_set_mmio_spte_mask(VMX_EPT_RWX_MASK,
VMX_EPT_MISCONFIG_WX_VALUE);
}
同时还要对 EPT 的一些特殊位进行标记来标志该 spte
表示 MMIO 而不是虚拟机的物理内存,如:
- set the special mask: SPTE_SPECIAL_MASK.
- reserved physical address bits: the setting of a bit in the range
51:12
that is beyond the logical processor’s physical-address width.
Copy
void kvm_mmu_set_mmio_spte_mask(u64 mmio_mask, u64 mmio_value)
{
BUG_ON((mmio_mask & mmio_value) != mmio_value);
shadow_mmio_value = mmio_value | SPTE_SPECIAL_MASK;
shadow_mmio_mask = mmio_mask | SPTE_SPECIAL_MASK;
}
EXPORT_SYMBOL_GPL(kvm_mmu_set_mmio_spte_mask);
static void kvm_set_mmio_spte_mask(void)
{
u64 mask;
int maxphyaddr = boot_cpu_data.x86_phys_bits;
/*
* Set the reserved bits and the present bit of an paging-structure
* entry to generate page fault with PFER.RSV = 1.
*/
/* Mask the reserved physical address bits. */
mask = rsvd_bits(maxphyaddr, 51);
/* Set the present bit. */
mask |= 1ull;
#ifdef CONFIG_X86_64
/*
* If reserved bit is not supported, clear the present bit to disable
* mmio page fault.
*/
if (maxphyaddr == 52)
mask &= ~1ull;
#endif
kvm_mmu_set_mmio_spte_mask(mask, mask);
}
KVM 在建立 EPT 页表项后,设置这些标志位再访问对应页就会触发 EPT_MISCONFIG
退出 VMX Root Mode,然后调用 handle_ept_misconfig
来完成 MMIO 处理操作:
handle_ept_misconfig
->kvm_emulate_instruction
->x86_emulate_instruction
->x86_emulate_insn
->writeback
->segmented_write
->emulator_write_emulated
->emulator_read_write
->emulator_read_write_onepage
->ops->read_write_mmio
(write_mmio
) ->vcpu_mmio_write
->kvm_io_bus_write
->__kvm_io_bus_write
->kvm_iodevice_write
->dev->ops->write
(ioeventfd_write
)
最后会调用到 ioeventfd_write
,写 eventfd
给 QEMU 发送通知事件:
/* MMIO/PIO writes trigger an event if the addr/val match */
static int
ioeventfd_write(struct kvm_vcpu *vcpu, struct kvm_io_device *this, gpa_t addr,
int len, const void *val)
{
struct _ioeventfd *p = to_ioeventfd(this);
if (!ioeventfd_in_range(p, addr, len, val))
return -EOPNOTSUPP;
eventfd_signal(p->eventfd, 1);
return 0;
}
QEMU
以 QEMU 中 e1000 网卡的模拟为例,设备初始化 MMIO 时候时候注册的 MemoryRegion
为 I/O 类型(不是 RAM 类型):
static void
e1000_mmio_setup(E1000State *d)
{
int i;
const uint32_t excluded_regs[] = {
E1000_MDIC, E1000_ICR, E1000_ICS, E1000_IMS,
E1000_IMC, E1000_TCTL, E1000_TDT, PNPMMIO_SIZE
};
memory_region_init_io(&d->mmio, OBJECT(d), &e1000_mmio_ops, d,
"e1000-mmio", PNPMMIO_SIZE);
memory_region_add_coalescing(&d->mmio, 0, excluded_regs[0]);
for (i = 0; excluded_regs[i] != PNPMMIO_SIZE; i++)
memory_region_add_coalescing(&d->mmio, excluded_regs[i] + 4,
excluded_regs[i+1] - excluded_regs[i] - 4);
memory_region_init_io(&d->io, OBJECT(d), &e1000_io_ops, d, "e1000-io", IOPORT_SIZE);
}
QEMU 调用 kvm_set_phys_mem
注册虚拟机的物理内存到 KVM 相关的数据结构中,会调用 memory_region_is_ram
来判断该段物理地址空间是否是 RAM 设备,如果不是 RAM 设备直接返回:
static void kvm_set_phys_mem(KVMMemoryListener *kml,
MemoryRegionSection *section, bool add)
{
......
if (!memory_region_is_ram(mr)) {
if (writeable || !kvm_readonly_mem_allowed) {
return;
} else if (!mr->romd_mode) {
/* If the memory device is not in romd_mode, then we actually want
* to remove the kvm memory slot so all accesses will trap. */
add = false;
}
}
......
}
对于 MMIO 类型的内存,QEMU 不会调用 kvm_set_user_memory_region
对其进行注册,所以 KVM 会认为这段内存的 pfn
(物理页帧号)类型为 KVM_PFN_NOSLOT
,进而调用 set_mmio_spte
来设置这段地址对应到 spte
。函数中会判断 pfn
是否为 NOSLOT
标记进而确认这段地址空间为 MMIO:
static bool set_mmio_spte(struct kvm_vcpu *vcpu, u64 *sptep, gfn_t gfn,
kvm_pfn_t pfn, unsigned access)
{
if (unlikely(is_noslot_pfn(pfn))) {
mark_mmio_spte(vcpu, sptep, gfn, access);
return true;
}
return false;
}
PMIO Emulation
CPU 只要截获虚拟机中的 in
/out
指令,再用软件来模拟硬件的行为。
References
存储器映射输入输出 - 维基百科,自由的百科全书
MMIO 和 PIO - 前进的 code - 博客园
What is the difference between an I/O mapped I/O, and a memory mapped I/O in the interfacing of the microprocessor? - Quora
MMIO Emulation - kernelgo
MMIO 与 PIO 区别 - arun_yh - 博客园
[kvm][virt]MMIO 技术分析 - 云+社区 - 腾讯云