Abstract
设备之间通过总线互联通信,如果将所有设备都挂在到sysbus(即所谓的内部总线:连接cpu、内存、pic总线控制器等设备的总线)将会增加主板的布线难度,而且cpu并不需要实时与外围设备通信。因此没必要每个设备都直接与cpu连接。
现代cpu通过sysbus直接连接一个控制芯片,这个控制芯片再管理外围设备。控制芯片将数据汇总后再与cpu通信。
这个过程对应到QEMU中的就是:
- cpu通过sysbus找到pcihost
- pcihost通过pcibus找到pci设备完成cpu的指令
本文主要说明QEMU的怎么模拟实现这个过程的
Preface
PCI设备有如下三种不同内存:
- MMIO
- PCI IO space
- PCI configuration space
其中pci configuration space 是用来配置pci设备的,其中也包含了关于pci设备的特定信息。而config空间中的BAR: Base address register可以用来确定设备需要使用的内存或I/O空间的大小,也可以用来存放设备寄存器的地址。
与pci设备通信,先会通过pcihost找到对应的pci设备,修改其config空间、传递指令信息。之后会调用pci_update_mappings
更新pci设备的内存映射,因为可能接下来就要通过mmio/pio与pci设备通信了。如: gdb追踪pci_host_config_write
、pci_host_data_write
、pci_update_mappings
的执行。
会发现pci_host_config_write
传递指令信息保存到config_reg
,pci_host_data_write
根据config_reg
修改config空间,之后会调用pci_update_mappings
。pci_update_mappings
会根据修改的config空间的信息更新内存映射。
下文将对其进行详细说明
模拟方法
pcihost模拟是主要功能是:根据cpu的指令找到pci设备,然后”转发”指令给pci设备
pcibus模拟的主要功能是:维护pci设备信息,为pcihost索引目标设备提供依据
pci设备模拟的主要功能是:维护自己的config空间和操作对应的回调函数,设备的内存空间映射将会根据config空间的内容完成
要了解pci总线模拟应该搞清楚以下问题:
- 如何给设备编号:初始化程序扫描总线上每个插槽并编号
- 对应规范文档:
- 设备编号:pci-pci-bridge specifiation: 13.2. Device Number and Slot Number Assignment Rules,约138页
- 对应qemu:
qemu/hw/pci/pci.c:do_pci_register_device
,当指定devfn=-1
时自动扫描可用设备号
- 对应规范文档:
- cpu如何知道设备编号:初始化时初始化程序从
IRQ Routing Table
获取设备号信息- 对应规范文档:
- 设备号信息初始化:pci-pci-bridge specifiation: 13.6. Run-Time Algorithm for Determining Chassis and Slot Number。约145页
- 对应qemu: TODO,没有找到,可能是操作系统中完成的?
- 对应规范文档:
- pcihost如何找到pci设备
- 对应规范文档:
- 3.2.2.3.2. Software Generation of Configuration Transactions。约33页
- 对应本文章节:
- 对应qemu:
qemu/hw/pci/pci_host.c:pci_dev_find_by_addr()
- 对应规范文档:
- pci总线规范中几个比较重要的协议内容
- 对应规范文档:
- config space的编码规范:Figure 6-1,约191页
- 设备控制:6.2.2. Device Control,约193页
- 设备状态控制:6.2.3. Device Status,约196页
- BAR寄存器:6.2.5. Base Addresses,约201页
- 对应本文章节:
- 设备控制(对应Figure 6-1的command register)和状态控制(对应Figure 6-1的status register)等:bus transaction中举例的接口根据addr参数读写各个寄存器
- BAR寄存器功能:设备IO空间
- 对应qemu:
- 设备控制和状态控制等:
qemu/pci/pci.c:pci_default_write_config
,根据addr参数修改对应寄存器内容
- 设备控制和状态控制等:
- 对应规范文档:
- 如何给设备分配IO空间
- 对应规范文档:
- BAR寄存器:6.2.5. Base Addresses,约201页
- 对应本文章节:
- 对应qemu:
- 设备BAR:
qemu/pci/pci.c:pci_register_bar
- 根据BAR进程内存空间映射:
qemu/pci/pci.c:pci_update_mappings
- 设备BAR:
- 对应规范文档:
- cpu与pci设备通信的流程:中途经过什么设备,做了什么操作,如何读写设备等
- 对应规范文档:
- 整体流程概述:3.2.2.3.2. Software Generation of Configuration Transactions。约33页
- 对应本文章节:
- 中途做了什么操作:控制流程
- 如何读写设备:bus transaction
- 对应qemu:
- 修改config空间的接口:
qemu/pci/pci.c:pci_default_write_config
CONFIG_ADDRESS
阶段:qemu/pci/pci_host.c:pci_host_config_read
,qemu/pci/pci_host.c:pci_host_config_write
CONFIG_DATA
阶段:qemu/pci/pci_host.c:pci_host_data_read
,qemu/pci/pci_host.c:pci_host_data_write
- 修改config空间的接口:
- 对应规范文档:
pcihost
控制流程
pcihost对pci设备控制流程可以分为两阶段
CONFIG_ADDRESS
- cpu访问
CONFIG_ADDRESS
对应的端口时触发,将命令保存到pcihost的config寄存器中
- cpu访问
CONFIG_DATA
- cpu访问
CONFIG_DATA
对应的端口时触发,根据config寄存器中内容进行相应的操作
- cpu访问
CONFIG_ADDRESS
以i440fx北桥芯片这个pcihost为例,他使用两个pio来接收,cpu的指令信息。CF8h处的地址空间称为CONFIG_ADDRESS,CFCh处的地址空间称为CONFIG_DATA。
参考i440fx规范文档,其pci相关的核心内容如下:
- i440fx设备的IO端口:3.1. I/O Mapped Registers,约17页
- 主要内容:
CONFIG_ADDRESS
寄存器在CF8h端口,CONFIG_DATA
寄存器在CFCh端口 - 对应qemu的模拟:
qemu/hw/pci-host/i440fx.c:i440fx_pcihost_realize
- 主要内容:
- pci config空间与各寄存器的功能:3.2. PCI Configuration Space Mapped Registers
- 主要内容:说明了config空格各个字段的内容,访问config空间的格式(TYPE0和TYPE1)
- 对应qemu的模拟:
qemu/hw/pci-host/i440fx.c:pci_host_conf_le_ops
和qemu/hw/pci-host/i440fx.c:pci_host_data_le_ops
1 | static void i440fx_pcihost_realize(DeviceState *dev, Error **errp) |
当对CH8h端口写时,pcihost会将数据用锁存器(latch)保存起来。当对CH8h端口读时,pcihost就会返回CONFIG_ADDRESS*中的数据。pci规范文档中说明如下(约在32页, *3.2.2.3.2. Software Generation of Configuration Transactions):
qemu中的模拟如下,qemu中用s->config_reg
做锁存器,向对CONFIG_ADDRESS
写入的数据。读时就直接返回s->config_reg
1 | static void pci_host_config_write(void *opaque, hwaddr addr, |
CONFIG_DATA
从上图中也可以看到各个字节的含义,CONFIG_DATA相关的操作会围绕CONFIG_ADDRESS中的内容进行。
当对CONFIG_DATA写时,首先会检查CONFIG_ADDRESS的第31bit来使能CONFIG_DATA。如果使能了CONFIG_DATA相关的操作才会继续执行。
1 | static void pci_host_data_write(void *opaque, hwaddr addr, |
pcihost首先会根据总线号来判断目标设备的挂载在当前总线上(Type 0)还是子总线上(Type 1)。为简单起见这里只讨论Type 0。
找到总线后pcihost会解析出设备号找到pci设备然后执行configuration transaction(后文会说明configuration transaction)。(约在pci规范文档的33页, 3.2.2.3.2. Software Generation of Configuration Transactions)
对应到qemu中代码如下:pci_dev_find_by_addr
先根据总线号找到设备,pci_host_config_write_common
发起configuration transactin。
1 | void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, unsigned len) |
编码说明
在pci规范3.2.2 Addressing章节中(约28页)定义了各个字段的编码与含义,如做pio时用会根据pio的编码规范解码,做mmio是用mmio的解码方式。这里仅对一部分进行说明:
Configuration Space Decoding
对于CONFIG_ADDRESS中的字段:
- funtion number: 选择多功能设备中的功能
- register number: 选择目标设备config空间中的寄存器
- 我用寄存器(register)代表config空间中各个字段的内容
configuration transaction
在pci规范结尾的Glossary中,说明了configuration transaction是一种用来初始化系统和配置系统的bus transaction。
configuration transaction: Bus transaction used for system initialization and configuration via the configuration address space.
bus transaction
在实际pci总线硬件电路中,为了完成一次读写(read/write transaction),一次transaction包含一个地址解析过程和多个数据解析过程,如先给出地址信号再给出数据信号,等待ready信号,然后再数据传输写回。具体细节可以看pci规范3.3 Bus Transactions。
但在虚拟机看来pcibus一次transaction就是一次访存。在下面的例子函数会证明这点。
??以下多为猜测:??
在configuration transaction章节中我们介绍了configuration transaction是用来配置和初始化系统的。所以我们对pci通信的流程做如下抽象:
- cpu指令先传递到pcihost解析
- pcihost根据CONFIG_ADDRESS,找到目标设备,并配置config空间,发起
configuration transactions
执行修改- 配置config空间可以包括:(1)控制设备、(2)为设备准备io空间等
- bus transaction再根据
configuration transactions
后config空间的内容进行操作
下面我们通过一个函数深刻理解这点,并将给出(1)、(2)等
内容的对应关系
1 | void pci_default_write_config(PCIDevice *d, uint32_t addr, uint32_t val_in, int l) |
可见应函数中的d->config[addr + i] =
和msi_write_config
等对应:(1)控制设备。函数中的pci_update_mappings
对应:(2)为设备准备io空间。准备了io空间后就可以直接通过qemu总线模拟的地址翻译找到目标设备做模拟了(见最后一章:实例说明BAR的作用)。
在下面的调用栈中可以看到,pcihost调用执行了pci_data_write
,即触发CONFIG_DATA
相关事件。然后在pci_data_write -> pci_host_config_write_common
中调用回调函数发起configuration transaction
: pci_dev->config_write() -> pci_default_write_config
1 | void pci_host_config_write_common(PCIDevice *pci_dev, uint32_t addr, |
pcibus
为了pcihost能完成根据bus number查找总线、根据device number查找设备、根据function number调用设备功能等操作,qemu中通过PCIBus
的层级结构模拟了多条总线的情况,通过PCIBus
的PCIDevice *devices[PCI_SLOT_MAX * PCI_FUNC_MAX]
成员模拟了挂载的设备。通过device number和function number共同索引(一共8bit,对应规范中device number和function number两个register),每个成员对应一个function。
通过pcihost中的pci_dev_find_by_addr
函数可见,解析了CONFIG_ADDRESS
的内容得到总线号和设备功能号devfn
,然后返回对应功能。这是对PCIBus对挂载设备的模拟。
1 | static inline PCIDevice *pci_dev_find_by_addr(PCIBus *bus, uint32_t addr) |
再看pci_find_bus_nr
,看看PCIBus是如何模拟多总线的情况的:
1 | static PCIBus *pci_find_bus_nr(PCIBus *bus, int bus_num) |
PCIBus
结构体用child
链表表示了所有子总线,这样就模拟了总线的层次结构。通过比较总线号,最终返回目标总线。
1 | struct PCIBus { |
pcidevice
每个pci设备都有自己的config空间,config空间可以用来记录设备的信息,也可以用来控制设备执行各种功能,(详见pci规范文档*6.1. Configuration Space Organization
*)。
在qemu中,config空间就是PCIDevice
结构体中的uint8_t *config;
成员模拟的,在初始化是就会申请这段空间(pci设备的config空间为256byte, pcie设备的config空间为4kb)
1 | static PCIDevice *do_pci_register_device(PCIDevice *pci_dev, |
之后对config空间的操作,包括pcihost与pci设备通信和读写设备状态都会通过uint8_t *config
完成。如:
1 | static inline void pci_config_set_vendor_id(uint8_t *pci_config, uint16_t val) |
执行流程
QEMU中对sysbus的模拟就是将设备与MemoryRegion绑定,cpu执行指令,经过翻译后能够找到对应的MemoryRegion从而找到相应的设备。这里就是cpu找到pcihost的过程。
pcihost模拟是主要功能是:根据cpu的指令找到pci设备,然后”转发”指令给pci设备
pcibus模拟的主要功能是:维护pci设备信息,为pcihost索引目标设备提供依据
pci设备模拟的主要功能是:维护自己的config空间和操作所需的回调函数,设备的内存空间映射将会根据config空间的内容完成
QEMU中的总线模拟将有pcibus模拟和pci设备模拟共同完成:pcibus提供设备的连接关系、pci设备提供config空间的信息
读写设备config空间
读写config空间可以分为两个阶段:
CONFIG_ADDRESS
,指令传输CONFIG_DATA
,指令执行
gdb调试的表现为:总是会先调用pci_host_config_write
为s->config_reg
赋值,然后会调用data read/write解析刚才传入的s->config_reg
指令传输
指令传输阶段的任务是将cpu的命令保存到config_reg
中,方便在指令执行阶段根据pci协议规范解析命令。
1 | static void pci_host_config_write(void *opaque, hwaddr addr, |
可见将指令(val
)赋值给了PCIHost的config寄存器config_reg
。
指令执行
指令执行阶段会解析config_reg
中保存的指令。
1 | memory_region_write_accessor -> pci_host_data_write -> pci_data_write |
先是在pci_dev_find_by_addr
中利用config_reg
找到的目标pci设备,根据注释中指出的pci规范进行解析,从而能够找到目标设备。
1 | /* |
找到目标设备后在pci_host_config_write_common
中利用config_reg
找到目标设备config空间:
先是取出要操作的地址(即操作目标设备config空间的那个字段)。然后对该空间进行读写pci_host_config_write_common
,其中会根据PCI设备类型执行相应回调函数完成操作。
1 | uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1); // addr & 0x11 |
设备IO空间
为了能为pci设备分配IO空间,会在设备config空间中记录该设备内存映射相关的信息。这就是config空间中的BAR(Base Address Register)的功能。
如图所示,BAR(Base Address Register)是PCIconfig空间中从0x10到0x24的6个register,用来定义PCI需要的配置空间大小以及配置PCI设备占用的地址空间。
BAR根据mmio和pio有两种不同的布局,其中bit0用于指示是内存是mmio还是pio,详见下图:
每个PCI设备在BAR中描述自己需要占用多少地址空间,bios会通过pci枚举探测pci设备,然后读取其BAR进行合理的地址空间分配。这个过程读应QEMU中虚拟机reset阶段执行的pci_update_mappings
,后文将会说明这点。
pci设备的BAR设置可以使用pci_register_bar
完成,其中io_regions
数组表示设备的BARs,数组中的每一项表示BARs中的一个寄存器,pci_register_bar
完成一项BAR的设置(pci_dev->io_regions[region_num]
)。
1 | void pci_register_bar(PCIDevice *pci_dev, int region_num, |
以e1000网卡设备为例,其实例化函数pci_e1000_realize
中设置了BAR0和BAR1,并在BAR中记录地址与MR绑定,最后虚拟机reset阶段统一将所有设备的所以BAR映射到内存中。
1 | pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &d->mmio); |
pci_register_bar
仅是设置了BAR的的内容,真正根据BAR信息完成pci设备内存映射的是pci_update_mappings
。
虚拟设备创建完成后进入虚拟机reset阶段qemu_system_reset
,这个阶段会遍历所有的pci设备和所有的io_regions
然后调用pci_update_mappings
,根据之前pci_register_bar
中设置的addr
和对应的memory
,为各个pci设备映射内存空间。
1 | static void pci_update_mappings(PCIDevice *d) |
TODO
映射完成后就能通过mmio和pio的方式访问pci设备了??
Q??,映射后就是不是就直接访存了?如cpu直接访存mm,copy数据到VGA的mmio就能显示到屏幕了?
Q?? config space和设备读写的关系如何?该了config空间的某一点后就能能够命令设备读写?那这个过程是怎样的??
基础设施
这里的基础设施指的是PCI相关结构体和API。如pcihost要找到pci设备需要哪些结构,pci设备config空间该如何维护等。
pcibus维护一个pci设备列表PCIDevice bus->devices[256]
记录挂在pci总线上的设备,pcihost可以根据pcibus协议解析cpu传入的addr获得pci设备的索引,从而找到目标pci设备。
1 | struct PCIBus { |
pci设备的config空间由pci设备自己维护(PCIDevice结构的uint8_t *config
成员)。设备实例化时会初始化这段空间(uint8_t *config
,256byte的config空间),并挂载上总线bus->devices[devfn] = pci_dev
。之后的设备内存空间映射和config空间读写都依赖于*config
。
1 | struct PCIDevice { |
pci协议规范规定了config空间中各个bit的含义,在hw/pci/pci.c
中提供了许多API、pci_regs.h
中定义了许多config空间相关的宏,方便我们对config空间进行操作。如pci_config_set_vendor_id
API可以完成config空间vendor id段的填写:
1 | static inline void |
do_pci_register_device
可以方便的配置一个PCI设备。
1 | static PCIDevice *do_pci_register_device(PCIDevice *pci_dev, |
实例说明BAR的作用
BAR主要是用了知道如何为pci设备做内存映射的,如mmio等操作就需要知道那段内存空间是给哪个设备做mmio的。下面通过e1000网卡设备来说明BAR在pci总线模拟流程的作用。
1 | gdb \ |
首先在e1000_mmio_setup
中e1000设备会准备他的mmio和pio空间,此时并没有映射被虚拟机内存空间,仅是初始化了MemoryRegion结构体。
1 | static void |
其mmio和io对应的MemoryRegion为:
接下里在设备具现化的过程中调用pci_register_bar
,会用这些MR配置BAR,为之后的内存映射提供依据:
1 | void pci_register_bar(PCIDevice *pci_dev, int region_num, |
最后,会有几种时机真正的为pci设备映射内存空间,这里列出两种(其他情况有待补充):
- 一种是虚拟设备创建完毕,虚拟机进入reset阶段,遍历所有pci设备调用
pci_update_mappings
为pci设备映射内存 - 另一种是在修改config空间、通知要内存映射后才建立内存映射
这里e1000设备是第二种情况,在e1000_write_config
修改config空间后才建立内存映射
可以看到将e1000_mmio_setup
中创建的(MemoryRegion* 0x555557793250)d->mmio
(xxxx3250)和d->io
(xxxx3340)映射到了内存空间,之后便能通过该MR进行mmio操作。
最后直接就能直接通过mmio访问设备(内存翻译):
值得注意的是并不是所有设备都会通过pci_update_mappings
完成内存映射,如VGA在初始化MemoryRegion的时候就直接add_subregion
完成了映射(也许是因为VGA地址空间固定吧)。然后再调用pci_register_bar
方便后续接收config空间的指令:
1 | static void pci_std_vga_realize(PCIDevice *dev, Error **errp) |
可见在vga_init
和pci_std_vga_mmio_region_init
中初始化MR后就直接做了内存映射,然后再调用pci_register_bar
配置BAR空间。