侧边栏壁纸
博主头像
蔚然小站博主等级

未来会有的,不要辜负了梦想

  • 累计撰写 35 篇文章
  • 累计创建 14 个标签
  • 累计收到 9 条评论

目 录CONTENT

文章目录
kvm

设备虚拟化:PCI配置空间及其模拟

皮蛋熊
2023-08-25 / 0 评论 / 0 点赞 / 144 阅读 / 7184 字
温馨提示:
本文最后更新于 2023-08-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

PCI标准约定,每个PCI设备都需要实现一个称为配置空间(Configuration Space)的结构,该结构就是若干寄存器的集合,其大小为256字节,包括预定义头部(predefined header region)和设备相关部分(device dependent region)预定义头部占据64字节,其余192字节为设备相关部分。

预定义头部定义了PCI设备的基本信息以及通用控制相关部分,包括Vendor ID、Device ID等;VenderID是唯一的,由PCI SIG统一负责分配的Member Companies | PCI-SIG (pcisig.com)(什么统一分配,就是我定了标准,你要用就得交钱,我司是1ED5)。

在Linux内核中,PCI设备驱动就是通过Device ID和Vendor ID来匹配设备的。

所有PCI设备的预定义头部的前16字节完全相同,16~63字节的内容则依具体的PCI设备类型而定。位于配置空间中的偏移0x0E处的寄存器Header Type定义了 PCI设备的类型,00h为普通PCI设备,01h为PCI桥,02h为CardBus桥。

picture

系统初始化时,BIOS(或者UEFI)将把PCI设备的配置空间映射到处理器的I/O地址空间,操作系统通过I/O端口访问配置空间中的寄存器。后来的PCI Exepress标准约定配置空间从256字节扩展到了4096字节,处理器需要通过MMIO方式访问配置空间,当然前256字节仍然可以通过I/O端口方式访问。

PCI设备支持的MSI(X)中断机制,就是利用PCI设备配置空间中设备相关部分来存储中断信息的,包括中断目的地址(即目的CPU),以及中断向量。操作系统初始化中断时将为PCI设备分配的中断信息写入PCI配置空间中的设备相关部分。

PCI标准提出了一个聪明的办法,即各PCI设备自己提出需要占据的地址空间的大小,以及板上内存是映射到内存地址空间,还是I/O地址空间,然后将这些诉求记录在配置空间的寄存器BAR中,每个PCI最多可以请求映射6个区域。至于映射到地址空间的什么位置,由BIOS(或者UEFI)在系统初始化时,访问寄存器BAR,查询各PCI设备的诉求,统一为PCI设备划分地址空间。 picture

PCI设备地址空间映射

PCI总线通过PCI Host Bridge和CPU总线相连,PCI Host Bridge和PCI设备之间通过PCI总线通信。PCI Host Bridge内部有两个寄存器用于系统软件访问PCI设备的配置空间,一个是位于CF8h的CONFIG_ADDRESS,另外一个是位于CFCh的CONFIG_DATA。

当系统软件访问PCI设备配置空间中的寄存器时,首先将目标地址写入寄存器CONFIG_ADDRESS中,然后向寄存器CONFIG_DATA发起访问操作,比如向寄存器CONFIG_DATA写入一个值。当PCI HostBridge感知到CPU访问CONFIG_DATA时,其根据地址寄存器CONFIG_ADDRESS中的值,片选目标PCI设备,即有效连接目标PCI设备的管脚IDSEL(Initialization Device Select),然后将寄存器CONFIG_ADDRESS中的功能号和寄存器号发送到PCI总线上。目标PCI设备在收到地址信息后,在接下来的时钟周期内与PCI Host Bridge完成数据传输操作。这个过程如图4-6所示。对于PCIe总线,下图中的PCI Host Bridge对应为Root Complex。 picture 但对于PCIe设备,上面的方法可以访问0-255的空间,但是无法访问剩下的255-4K的空间,需要使用MMIO的方式访问。

上图中的Memory controller只会认领设备内存映射的内存地址,对于内存条的地址,会由内存控制器来认领。

在BIOS(UEFI)为PCI设备分配内存地址空间后,会将其告诉PCI Host bridge,所以PCI Host Bridge知晓哪些地址应该发往PCI设备。

根据PCI的体系结构可见,寻址一个PCI配置空间的寄存器,显然需要总线号(Bus Number)、设备号(Device Number)、功能号(Function Number)以及最后的寄存器地址,也就是我们通常简称的BDF加上偏移地址。

如果是PCIe设备,还需要在总线号前面加上一个RC(Root Complex)号。因此,PCI Host Bridge中的寄存器CONFIG_ADDRESS的格式如下图所示: picture 访问具体的PCI设备时,作为CPU与PCI设备之间的中间人PCI Host Bridge,还需要将系统软件发送过来的地址格式转换为PCI总线地址格式,转换方式如: picture kvmtool演示,如何虚拟PCI设备配置空间。

commit 06f4810348a34acd550ebd39e80162397200fbd9
kvm tools: MSI-X fixes
kvmtool.git/pci.c
01 static struct pci_device_header *pci_devices[PCI_MAX_DEVICES];
02 static struct pci_config_address pci_config_address;
03
04 static void *pci_config_address_ptr(u16 port)
05 {
06     unsigned long offset;
07     void *base;
08
09     offset = port - PCI_CONFIG_ADDRESS;
10     base = &pci_config_address;
11
12     return base + offset;
13 }
14
15 static bool pci_config_address_in(struct ioport *ioport,
16         struct kvm *kvm, u16 port, void *data, int size)
17 {
18     void *p = pci_config_address_ptr(port);
19
20     memcpy(data, p, size);
21
22     return true;
23 }
24
25 static bool pci_config_data_in(struct ioport *ioport,
26         struct kvm *kvm, u16 port, void *data, int size)
27 {
28     unsigned long start;
29     u8 dev_num;
30     …
31     start = port - PCI_CONFIG_DATA;
32
33     dev_num = pci_config_address.device_number;
34
35     if (pci_device_exists(0, dev_num, 0)) {
36         unsigned long offset;
37
38         offset = start + (pci_config_address.register_number << 2);
39         if (offset < sizeof(struct pci_device_header)) {
40             void *p = pci_devices[dev_num];
41
42             memcpy(data, p + offset, size);
43     } else
44             memset(data, 0x00, size);
45     } else
46             memset(data, 0xff, size);
47
48     return true;
49 }

pci_config_address_in就是当Guest向寄存器CONFIG_ADDRESS写入将要访问的目标PCI设备的地址时,触发VM exit陷入VMM后,VMM进行模拟处理的过程。kvmtool将Guest准备访问的目标PCI设备地址记录在变量pci_config_address中。

Guest将通过访问寄存器CONFIG_DATA读写PCI配置空间头的信息,Guest访问寄存器CONFIG_DATA的这个I/O操作将触发VM exit,处理过程进入KVM,代码第25~49行是KVM中对这个写寄存器CONFIG_DATA过程的模拟。

第38行代码中有个变量start,用来处理Guest以非4字节对齐的方式访问PCI设备配置空间。

kvmtool中Virtio设备初始化相关代码:

kvmtool.git/virtio/pci.c
int virtio_pci__init(…)
{
	u8 pin, line, ndev;
	vpci->dev = dev;
	vpci->msix_io_block = pci_get_io_space_block();
	vpci->msix_pba_block = pci_get_io_space_block();
	vpci->base_addr = ioport__register(IOPORT_EMPTY,
		&virtio_pci__io_ops, IOPORT_SIZE, vpci);
	…
	vpci->pci_hdr = (struct pci_device_header) {
		.vendor_id = PCI_VENDOR_ID_REDHAT_QUMRANET,
	…
		.bar[0] = vpci->base_addr | PCI_BASE_ADDRESS_SPACE_IO,
		.bar[1] = vpci->msix_io_block |
		PCI_BASE_ADDRESS_SPACE_MEMORY |
		PCI_BASE_ADDRESS_MEM_TYPE_64,
		.bar[3] = vpci->msix_pba_block |
		PCI_BASE_ADDRESS_SPACE_MEMORY |
		PCI_BASE_ADDRESS_MEM_TYPE_64,
	…
	};
	…
	pci__register(&vpci->pci_hdr, ndev);
	return 0;
}

virtio_pci__init为virtio PCI准备了3块板上内存区间。

寄存器bar[0]中的板上存储区间需要映射到Guest的I/O地址空间, 起始地址为vpci->base_addr;

寄存器bar[1]中的板上存储空间需要映射到Guest的内存地址空间, 起始地址为vpci->msix_io_block;

寄存器bar[3]中的板上存储空间页需要映射到Guest的内存地址空间, 起始地址为vpci->msix_pba_block。

kvmtool中为PCI设备分配内存地址空间的函数为pci_get_io_space_block。

kvmtool.git/pci.c  
static u32 io_space_blocks = KVM_32BIT_GAP_START + 0x1000000;  
u32 pci_get_io_space_block(void)  
{  
	u32 block = io_space_blocks;  
	io_space_blocks += PCI_IO_SIZE;  
	return block;  
}

kvmtool从地址KVM_32BIT_GAP_START+0X1000000开始为PCI设备分配地址空间。每当PCI设备申请地址空间时, 函数pci_get_io_space_block从这个地址处依次叠加. 类似的, kvmtool为PCI设备分配I/O地址空间的函数为ioport__register。

在函数virtio_pci__init的最后, 我们看到其调用pci__register在记录PCI设备的数组pci_devices中注册了设备, 这样Guest就可以枚举这些设备。

kvmtool.git/pci.c  
void pci__register(struct pci_device_header *dev, u8 dev_num)  
{  
… 
	pci_devices[dev_num] = dev;  
}

参考

0
kvm
  1. 支付宝打赏

    qrcode alipay
  2. 微信打赏

    qrcode weixin

评论区