QEMU如何虛擬PCI設備

引子

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值。

參考引用

  1. https://wiki.osdev.org/PCI#IRQ_Handling
  2. https://github.com/GiantVM/doc/blob/master/pci.md
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章