KVM虚拟机代码揭秘——QEMU的PCI总线与设备(上)

来源:互联网 发布:数据分析师官网 编辑:程序博客网 时间:2024/05/16 11:39

最近研究了一下QEMU的虚拟PCI设备,打算虚拟一个PCI-PCI桥和一个PCI设备,设备挂在桥上,桥挂在pci主桥上。并且给设备固定映射一个IO基地址,但是发现还是件头疼的事情,经过几天的辛苦,终于算是有点收获,和大家分享一下,有什么问题希望大家支持,一起讨论,共同提高。 

申明:本文主要针对x86架构进行说明。 

1. PCI 结构简介

为了大家更加容易的理解后文,先来回顾一下PCI总线的基本内存结构。每一个PCI设备都对应一段内存空间,里面按照地址位置放置PCI设备的信息,包括厂家信息,bar信息,中断等等,也可以理解成一个数组,一些设备一出厂,相关的信息已经写在里面,我们这里模拟设备,所有这些所有的信息我们都要进行动态的读和写。在这里只列出了本文相关的数据够。

         0x0                            0x04                             0x08                             0xc

0x00 |vendor ID    dev ID    | command                    |                                   |

0x10 |bar0 addr                  | bar1 addr                    |bar2 addr                     |bar3 addr

0x20 |bar4 addr                  | bar5 addr                    |                                  |

0x30 |                                |                                  | interrupt line                |

另外,我们可以在LInux中使用lspci -x 看到PCI设备的相关内存数据信息。

 

 2. QEMU的PCI总线 

QEMU在初始化硬件的时候,最开始的函数就是pc_init1。在这个函数里面会相继的初始化CPU,中断控制器,ISA总线,然后就要判断是否需要支持PCI,如果支持则调用i440fx_init初始化我们伟大的PCI总线。

i440fx_init函数主要参数就是之前初始化好的ISA总线以及中断控制器,返回值就是PCI总线,之后我们就可以将我们自己喜欢的设备统统挂载在这个上面,下面来简单分析一下这个函数:

 

    dev = qdev_create(NULL, "i440FX-pcihost");             /*创建PCI主总线设备*/
    s = FROM_SYSBUS(I440FXState, sysbus_from_qdev(dev));
    b = pci_bus_new(&s->busdev.qdev, NULL, 0);           /*创建我们真正的PCI总线*/
    s->bus = b;
    qdev_init_nofail(dev);                                                 /*初始化主总线设备*/

    d = pci_create_simple(b, 0, "i440FX");                        /*创建主桥*/
    *pi440fx_state = DO_UPCAST(PCII440FXState, dev, d);   
   
    piix3 = DO_UPCAST(PIIX3State, dev,                           /*创建ISA桥*/
                     pci_create_simple_multifunction(b, -1,true,"PIIX3"));   
    piix3->pic = pic;                                                            /*连接8259中断控制器,IOAPIC貌似也和在一起*/
    pci_bus_irqs(b, piix3_set_irq, pci_slot_get_pirq, piix3, 4);
    (*pi440fx_state)->piix3 = piix3;

 经过上面的初始化我们就得到了系统的主PCI总线b了,接着就可以挂载我们的设备。

 另外,在Linux里面我们可以使用lspci -t来看PCI总线的结构图。 

 

 

3. QEMU的PCI-PCI桥 

在QEMU中,所有的设备包括总线,桥,一般设备都对应一个设备结构,通过register函数将所有的设备链接起来,就像Linux的模块一样,在QEMU启动的时候会初始化所有的QEMU设备,而对于PCI设备来说,QEMU在初始化以后还会进行一次RESET,将所有的PCI bar上的地址清空,然后进行统一分配。

QEMU(x86)里面的PCI的默认PCI设都是挂载主总线上的,貌似没有看到PCI-PCI桥,而桥的作用一般也就是连接两个总线,然后进行终端和IO的映射。我在x86里面找了半天没结果,又为了省事,就把ppc架构里面的DEC桥强偷过来使用一下,嘎嘎,关键就是包含一下头文件,改一改x86下面的配置文件,将DEC强行的配置下去,这种i440FX加DEC的组合在真实设备上还真没见过。哈哈

有了现成的桥使用起来就很简单了,代码如下,一看就能看出来参数是之前的主PCI总线,返回子总线。

sub_bus= pci_dec_21154_init(pci_bus,-1);

 

有人会问如果自己要写一个PCI-PCI桥怎么办,其实也很简单,接来下来简单分析一下DEC桥的初始化过程:

PCIBus *pci_dec_21154_init(PCIBus *parent_bus, int devfn)
{
    PCIDevice *dev;
    PCIBridge *br;
    dev = pci_create_multifunction(parent_bus, devfn, false,    /*在主PCI专线上创建DEC桥设备*/
                                   "dec-21154-p2p-bridge"); 
    br = DO_UPCAST(PCIBridge, dev, dev);                            /*得到桥结构体,Linux的 upcast确实强大*/
    pci_bridge_map_irq(br, "DEC 21154 PCI-PCI bridge", dec_map_irq);
    qdev_init_nofail(&dev->qdev);                                        /*初始化DEC桥设备,在这里就会调用下面这个函数进行进一步的初始化*/                                    
    return pci_bridge_get_sec_bus(br);                                  /*返回桥另外一端的PCI总线指针*/

static int dec_21154_initfn(PCIDevice *dev)
{
    int rc;

/*这个函数是所有PCI-PCI桥的通用函数,就不具体展开描述了,它初始化PCI-PCI桥除了厂家设备名以外的其他的共通属性。比如class,另一端的PCI总线等等。所以我们在写自己的桥的时候也要调用这个函数来初始化桥属性和从桥上引出的另外一根总线。*/
    rc = pci_bridge_initfn(dev); 
    if (rc < 0) {
        return rc;
    }

    /*初始化厂商ID和设备ID,这个是每个设备的标识*/

    pci_config_set_vendor_id(dev->config, PCI_VENDOR_ID_DEC);
    pci_config_set_device_id(dev->config, PCI_DEVICE_ID_DEC_21154);
    return 0;
}

通过上面的几个关键步骤我们就能初始化一个我们自己的PCI桥设备。使用lspci -t 能够看到我们自己初始化桥的结构图。

 

4. QEMU的PCI设备

一般的PCI设备其实和桥很像,甚至更简单,关键区分桥和一般设备的地方就是class属性和bar地址。所谓落尽繁华才是本质,下面看一下一个标准的PCI设备结构是怎么样的。 

 

static PCIDeviceInfo fpga_info={
     .qdev.name = "fpga",
     .qdev.size = sizeof(FPGAState),
     .init      = pci_fpga_init,                  /*PCI 设备注册初始化函数,这个和上面的类似,初始化PCI各种属性*/
};

static void fpga_register_devices(void)
{
      pci_qdev_register(&fpga_info);        /*注册设备结构*/
}
device_init(fpga_register_devices)         /*设备添加到QEMU设备列表*/
在上面的过程中,pci_fpga_init函数在之前的文章中描述过就不展开了,然而其中主要的一条就是给bar分配IO地址,调用函数如下:pci_register_bar(&s->dev,0,0x800,PCI_BASE_ADDRESS_SPACE_IO,fpga_ioport_map);

其中第一个参数是设备;第二个参数是bar的编号,每个PCI设备又5个bar,对应0-5,这个我们也可以在上面的PCI基本结构中看到这6个bar,这个也是后文中提到的6个region,我们这里设置第一个也就是0;第三个参数是分配的IO地址空间范围;第四个参数是表示IO类型是PIO而不是MMIO;最后一个参数是IO读写映射函数。

 

从这里我们会发现一个问题,这里并没有给设备分配IO空间的基地址,只有一个空间长度而已,这也进一步说明PCI设备在QEMU中一般是随机动态分配空间的,通过不断的updatemapping来不断更新IO空间的映射。

当PCI设备结构都构造好以后,就可以通过 pci_create_simple_multifunction(sub_bus, -1,true,"fpga"));  来挂载我们设备了,这里的sub_bus就是我们之前通过创建桥得到的子总线。

 

通过上文我们了解了QEMU基本PCI设备,并且能成功的添加一个PCI桥和一个设备了,但是遗留了一个问题就是如何给一个PCI设备的bar动态分配一个IO基地址呢?我们在接下面的文章进行进一步讨论。

0 0
原创粉丝点击