引子
PCI(PCIE)設備在PC架構中有着舉足輕重的地位,瞭解PCI總線與PCI設備在QEMU中的工作機制有助於我們瞭解CPU和設備之間的溝通機制,會對PC系統有更全面的認知,同時對virtio設備的理解也會更有幫助。
回顧PCI 設備與總線
上圖是比較經典的PC架構圖,從上圖中可以看到CPU之間通過interchip bus連接,然後和I440FX芯片連接,I440FX就是我們熟知的北橋芯片組,用來連接高速外設和南橋芯片組,高速外設包括內存、顯卡和網卡等,南橋芯片組(PIIX4)用來連接各種低速或老舊的外設。當然,最新的PC架構已經沒有這麼嚴格的劃分出南橋和北橋芯片了。
CPU可以直接通過load/store指令來訪問PCI設備,PCI設備有如下三種不同內存:
- MMIO
- PCI IO space
- PCI configuration space
PCI configuration space
pci configuration space 是用來配置pci設備的,其中也包含了關於pci設備的特定信息。其中
BAR: Base address register可以用來確定設備需要使用的內存或I/O空間的大小,也可以
用來存放設備寄存器的地址。有兩種類型的bar,其格式如下:
PCI總線上的流量分爲兩種:
- Command traffic
- Read/Write traffic
TBD
QEMU中PCI總線的初始化流程
首先來看一下,qemu虛擬的pci設備有哪些:
可以看出,PCI設備0:0.0是pci host bridge,PCI設備0:1.0是ISA bridge,PCI設備0:1.1是IDE控制器,PCI設備0:2.0是VGA設備,PCI設備0:3.0是網卡設備。結合下圖,可以看出,pci.0總線是掛在main-system-bus下的,而main-system-bus是根總線,一切所有的設備最終都可以通過各種bus追溯到根總線。
可以查看一下虛擬機中的pci設備信息如下:
00:02.0是VGA設備,由於PCI是小尾端,所以其bar0的值是0xfd000008,和我們從qemu中看到的0xfd000000只有第3位不同,而第三位置1表示該段內存是prefetchable的。
00:03.0是網卡設備,其bar1的值是0xc001,和qemu中的0xc000第0位不同,而第0位置1表示該bar爲i/o。
在pc_init1中,新建了pci root bus,I440FX和PIIX3兩個pci設備。
pc_init1
->i440fx_init
->qdev_create
->pci_root_bus_new #init a root bus
->qbus_create
->pci_root_bus_init
->pci_create_simple(I440FX)
->pci_create_simple_multifunction
->pci_create_multifunction
->qdev_init_nofial
->i440fx_pcihost_realize
->sysbus_add_io(0xcf8, conf_mem...)
->sysbus_add_io(0xcfc, data_mem...)
->pci_create_simple_multifunction(PIIX3)
->piix3_realize
->qemu_register_reset(piix3_reset...)
->pci_bus_irqs
pci_bus_set_route_irq_fn
我們接着來分析pci網卡e1000的初始化流程。在pc_init1調用pc_nic_init來初始化一個網卡設備,默認會新建一個掛在pci總線上的e1000設備,隨着qdev_init_nofail中設置realized爲true,pci_e1000_realize會被調用,該函數會設置該設備的配置空間寫回調函數爲e1000_write_config,在配置空間中設置cache_line_size爲0x10,interrupt pin爲1, 同時調用pci_register_bar來註冊對應的BAR。
pc_init1
->pc_nic_init
->pci_nic_init_nofail
->pci_create
->qdev_init_nofail
->pci_e1000_realize
->pci_register_bar
pci_register_bar是一個重要的函數,分析如下:
void pci_register_bar(PCIDevice *pci_dev, int region_num,
uint8_t type, MemoryRegion *memory)
{
PCIIORegion *r;
uint32_t addr; /* offset in pci config space */
uint64_t wmask;
pcibus_t size = memory_region_size(memory);
r = &pci_dev->io_regions[region_num];
r->addr = PCI_BAR_UNMAPPED;
r->size = size;
r->type = type;
r->memory = memory;
r->address_space = type & PCI_BASE_ADDRESS_SPACE_IO
? pci_get_bus(pci_dev)->address_space_io
: pci_get_bus(pci_dev)->address_space_mem;
...
//if size is 0x1000, then wmask will be 0xffff_ffff_ffff_f000, this
//a mechanism used to determine bar size.
wmask = ~(size - 1);
//addr is bar address, bar0 will be 0x10, bar1 0x14...
addr = pci_bar(pci_dev, region_num);
//type 0 for memory, 1 for I/O
pci_set_long(pci_dev->config + addr, type);
if (!(r->type & PCI_BASE_ADDRESS_SPACE_IO) &&
r->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
pci_set_quad(pci_dev->wmask + addr, wmask);
pci_set_quad(pci_dev->cmask + addr, ~0ULL);
} else {
pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff);
pci_set_long(pci_dev->cmask + addr, 0xffffffff);
}
}
#define PCI_BASE_ADDRESS_0 0x10
int pci_bar(PCIDevice *d, int reg)
{
if (reg != PCI_ROM_SLOT)
return PCI_BASE_ADDRESS_0 + reg * 4;
...
}
此時調用了pci_register_bar之後,假設這個bar是I/O space類型的,且大小爲0x10,則該bar的值爲0xFFFF_FFF1, 此時bar中還沒有真正可用的地址。那地址是信息是什麼是很被寫進bar中的呢,以及bar所表示的內存區域是何時完成映射的?答案如下:
在qemu創建好所有的虛擬設備後,需要調用qemu_system_reset來複位系統,從main-system-bus開始遞歸遍歷調用每個掛在總線上的設備註冊的復位函數,其中pci設備會調用pci_do_device_reset,該函數清空PCI_COMMAND和PCI_STATUAS寄存器,清空cache_line和中斷引腳配置,最後調用pci_update_mapping,從該函數的名字也可判斷,此時是更新內存映射,如下:
static void pci_do_device_reset(PCIDevice *dev)
{
...
/* Clear all writable bits */
pci_word_test_and_clear_mask(dev->config + PCI_COMMAND,
pci_get_word(dev->wmask + PCI_COMMAND) |
pci_get_word(dev->w1cmask + PCI_COMMAND));
pci_word_test_and_clear_mask(dev->config + PCI_STATUS,
pci_get_word(dev->wmask + PCI_STATUS) |
pci_get_word(dev->w1cmask + PCI_STATUS));
dev->config[PCI_CACHE_LINE_SIZE] = 0x0;
dev->config[PCI_INTERRUPT_LINE] = 0x0;
...
pci_update_mappings(dev);
...
}
pci_update_mapping函數會遍歷設備的bar,如果發現bar中已經填寫了不同於r->addr的地址,則說明新的地址已經更新,則首先使用memory_region_del_subregion刪除原先註冊的memory region,隨後更新memory region的offset信息,即r->addr,調用memory_region_add_subregion_overlap將memory region重新註冊。
static void pci_update_mappings(PCIDevice *d)
{
...
for(i = 0; i < PCI_NUM_REGIONS; i++) {
r = &d->io_regions[i];
/* this region isn't registered */
if (!r->size)
continue;
//get the address info stored in specific bar
new_addr = pci_bar_address(d, i, r->type, r->size);
/* This bar isn't changed */
if (new_addr == r->addr)
continue;
/* now do the real mapping */
if (r->addr != PCI_BAR_UNMAPPED) {
memory_region_del_subregion(r->address_space, r->memory);
}
r->addr = new_addr;
if (r->addr != PCI_BAR_UNMAPPED) {
memory_region_add_subregion_overlap(r->address_space,
r->addr, r->memory, 1);
}
}
pci_update_vga(d);
}
當然,此時bar中也有可能是還沒有可用的地址信息的,我們在**if (r->addr != PCI_BAR_UNMAPPED)**這一行打斷點,其調用堆棧如下:
此時由於虛擬機有對3324的I/O口進行了寫操作,所以發生了vm-exit,之後由kvm接管,kvm發現自己不能處理這個I/O操作後,控制權從kvm內核模塊返回到qemu,qemu最終調用該端口註冊的回調函數,即pci_host_config_write_common。那這個端口是幹什麼用的呢,3324端口即0xCFC,是CONFIG_DATA端口,它和0xCF8即CONFIG_ADDRESS端口配合使用,用來讀寫pci設備的configuration space。
例如操作系統會用如下方式來讀pci設備的配置空間:
uint16_t pciConfigReadWord (uint8_t bus, uint8_t slot, uint8_t func, uint8_t offset) {
uint32_t address;
uint32_t lbus = (uint32_t)bus;
uint32_t lslot = (uint32_t)slot;
uint32_t lfunc = (uint32_t)func;
uint16_t tmp = 0;
/* create configuration address as per Figure 1 */
address = (uint32_t)((lbus << 16) | (lslot << 11) |
(lfunc << 8) | (offset & 0xfc) | ((uint32_t)0x80000000));
/* write out the address */
outl(0xCF8, address);
/* read in the data */
/* (offset & 2) * 8) = 0 will choose the first word of the 32 bits register */
tmp = (uint16_t)((inl(0xCFC) >> ((offset & 2) * 8)) & 0xffff);
return (tmp);
}
那是qemu在什麼時候註冊了0xCF8和0xCFC這兩個io的讀寫handler呢?答案是在i440fx也即pci host的class_init和instance_init中註冊:
static void i440fx_pcihost_realize(DeviceState *dev, Error **errp)
{
..
sysbus_add_io(sbd, 0xcf8, &s->conf_mem);
sysbus_init_ioports(sbd, 0xcf8, 4);
sysbus_add_io(sbd, 0xcfc, &s->data_mem);
sysbus_init_ioports(sbd, 0xcfc, 4);
...
}
static void i440fx_pcihost_initfn(Object *obj)
{
...
memory_region_init_io(&s->conf_mem, obj, &pci_host_conf_le_ops, s,
"pci-conf-idx", 4);
memory_region_init_io(&s->data_mem, obj, &pci_host_data_le_ops, s,
"pci-conf-data", 4);
...
}
static void pci_host_config_write(void *opaque, hwaddr addr,
uint64_t val, unsigned len)
{
PCIHostState *s = opaque;
..
s->config_reg = val;
}
static void pci_host_data_write(void *opaque, hwaddr addr,
uint64_t val, unsigned len)
{
PCIHostState *s = opaque;
if (s->config_reg & (1u << 31))
pci_data_write(s->bus, s->config_reg | (addr & 3), val, len);
}
void pci_data_write(PCIBus *s, uint32_t addr, uint32_t val, int len)
{
PCIDevice *pci_dev = pci_dev_find_by_addr(s, addr);
uint32_t config_addr = addr & (PCI_CONFIG_SPACE_SIZE - 1);
pci_host_config_write_common(pci_dev, config_addr, PCI_CONFIG_SPACE_SIZE,
val, len);
}
void pci_host_config_write_common(PCIDevice *pci_dev, uint32_t addr,
uint32_t limit, uint32_t val, uint32_t len)
{
..
pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
}
const MemoryRegionOps pci_host_conf_le_ops = {
.read = pci_host_config_read,
.write = pci_host_config_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
const MemoryRegionOps pci_host_data_le_ops = {
.read = pci_host_data_read,
.write = pci_host_data_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
我們可以從這個memory region得到opaque地址,而這個opaque對應的是PCIHostState數據結構,從而可以得到config_reg值爲0x80000904,這個按如下方式解析:
所以此次訪問的是0:1:1設備的addr爲4的寄存器,從qemu的monitor中我們也可以查到該設備是IDE controller,這也與通過gdb得到的pci設備名字一致。地址爲4的寄存器是command/status寄存器,寫入的值爲259,即0x103,command register解析方式如下:
所以表示使能SERR驅動,響應memory space和I/O space訪問。虛擬機驅動代碼首先設置好所有pci設備的command register爲0x103,接着開始通過這種方式更新各個設備中的BAR值。
參考引用
- https://wiki.osdev.org/PCI#IRQ_Handling
- https://github.com/GiantVM/doc/blob/master/pci.md