内存映射输入输出(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 指令,分别有 outboutwoutl)。I/O 设备有一个和内存地址空间相互独立的 I/O 地址空间,通过专用 I/O 针脚或者专用的总线和 CPU 相连。因为这个 I/O 地址空间和内存地址空间相互独立,所以有时候称为独立输入输出 (isolated I/O)。I/O 端口也可以通过 ioport_map 映射到虚拟地址空间进行访问。

Implement

用户空间想访问 I/O 端口必须使用 iopermiopl 系统调用来获得进行操作 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_MISCONFIG110 表示该页可读可写但是还未分配或者不存在,是一个错误的 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 而不是虚拟机的物理内存,如:

  1. set the special mask: SPTE_SPECIAL_MASK.
  2. 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 技术分析 - 云+社区 - 腾讯云