设为首页 收藏本站
查看: 1202|回复: 0

[经验分享] KVM虚拟机代码揭秘——QEMU的PCI总线与设备

[复制链接]

尚未签到

发表于 2015-12-24 14:36:00 | 显示全部楼层 |阅读模式
最近研究了一下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. QEMUPCI总线
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. QEMUPCI-PCI
QEMU中,所有的设备包括总线,桥,一般设备都对应一个设备结构,通过register函数将所有的设备链接起来,就像Linux的模块一样,在QEMU启动的时候会初始化所有的QEMU设备,而对于PCI设备来说,QEMU在初始化以后还会进行一次RESET,将所有的PCI bar上的地址清空,然后进行统一分配。
QEMUx86)里面的PCI的默认PCI设都是挂载主总线上的,貌似没有看到PCI-PCI桥,而桥的作用一般也就是连接两个总线,然后进行终端和IO的映射。我在x86里面找了半天没结果,又为了省事,就把ppc架构里面的DEC桥强偷过来使用一下,嘎嘎,关键就是包含一下头文件,改一改x86下面的配置文件,将DEC强行的配置下去,这种i440FXDEC的组合在真实设备上还真没见过。哈哈
有了现成的桥使用起来就很简单了,代码如下,一看就能看出来参数是之前的主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. QEMUPCI设备
一般的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设备又5bar,对应0-5,这个我们也可以在上面的PCI基本结构中看到这6bar,这个也是后文中提到的6region,我们这里设置第一个也就是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基地址呢?
要给PCI设备分配固定的IO基地址,那么就需要先了解PCI设备是如何刷新和分配IO基地址的。
1. PCI设备的重置与刷新
PCI在需要的时候,如第一次启动,IO重叠等就需要重置PCI设备,并且清空PCI bar上面的地址信息。主要调用函数pci_device_reset

void pci_device_reset(PCIDevice *dev)
{
    int r;

    ... ...
    ... ...
    dev->config[PCI_CACHE_LINE_SIZE] = 0x0;
    dev->config[PCI_INTERRUPT_LINE] = 0x0;
    for (r = 0; r < PCI_NUM_REGIONS; ++r) {    /*遍历所有的region,这个的region就是bar,清空region里面的IO地址*/
        PCIIORegion *region = &dev->io_regions[r];
        if (!region->size) {
            continue;
        }

        if (!(region->type & PCI_BASE_ADDRESS_SPACE_IO) &&
            region->type & PCI_BASE_ADDRESS_MEM_TYPE_64) {
            pci_set_quad(dev->config + pci_bar(dev, r), region->type);
        } else {

            /*用type将bar上所有的数据都覆盖,之前分配的IO基地址也没了*/
            pci_set_long(dev->config + pci_bar(dev, r), region->type);

            /*刷新设备*/
            pci_update_mappings(dev);

        }
    }

    /*刷新IO地址,更新IO读写映射*/
    pci_update_mappings(dev);
}


刷新IO地址函数展开如下:
static void pci_update_mappings(PCIDevice *d)
{
    PCIIORegion *r;
    int i;
    pcibus_t new_addr, filtered_size;

    for(i = 0; i < PCI_NUM_REGIONS; i++) {
        r = &d->io_regions;

        /* 如果没有注册region,那么不进行任何操作*/
        if (!r->size)
            continue;

         /* 得到设备bar上存储的基地址 */
        new_addr = pci_bar_address(d, i, r->type, r->size);
        /* bridge filtering */
        filtered_size = r->size;

         /* 如果分配了bar地址,那么比较设备地址与父桥的地址,看是否匹配*/
        if (new_addr != PCI_BAR_UNMAPPED) {
            pci_bridge_filter(d, &new_addr, &filtered_size, r->type);
        }

        /* 如果得到的新地址没有改变,大小也没变,那么不更新IO重映射,否则将IO读写进行重新映射。*/
        if (new_addr == r->addr && filtered_size == r->filtered_size)
            continue;

        /* 调用IO读写映射函数 */
       ... ...

       ... ...

    }
}

得到设备bar上存储的基地址的函数展开如下:
static pcibus_t pci_bar_address(PCIDevice *d, int reg, uint8_t type, pcibus_t size)
{
    pcibus_t new_addr, last_addr;

    /*获得region里基地址的偏移位置*/
    int bar = pci_bar(d, reg);

    /*检查PCI设备IO是否分配,分配以后command应该置1*/
    uint16_t cmd = pci_get_word(d->config + PCI_COMMAND);

    if (type & PCI_BASE_ADDRESS_SPACE_IO) {
        /*如果没有设置type或者没有分配IO那么直接返回地址未映射,将基地址重新置成-1*/
        if (!(cmd & PCI_COMMAND_IO)) {
            return PCI_BAR_UNMAPPED;         

        }
        /*将地址进行对齐,大小范围内清0,这个不是很好解释,因为前面我们这个size是制定为2的N此方的,所以减1就尾数全为1,取反为清0*/
        new_addr = pci_get_long(d->config + bar) & ~(size - 1);

        /*得到region结束地址*/
        last_addr = new_addr + size - 1;
        /* NOTE: we have only 64K ioports on PC */

        /*检查地址是否合法*/
        if (last_addr  UINT16_MAX) {
            return PCI_BAR_UNMAPPED;
        }

        /*返回新地址*/
        return new_addr;
    }

    ... ...
    ... ...
}
从这里可以看出,要保证地址不被清空,只要保证之前有基地址,而且合法,所以,只要reset不清空地址,那么在这里只要地址合法,就不会清楚映射好的地址。
当刷新得到新地址以后就进行与父桥的地址匹配,函数展开如下:
static void pci_bridge_filter(PCIDevice *d, pcibus_t *addr, pcibus_t *size, uint8_t type)
{

     ... ...
     ... ...
     /*取桥与设备基地址的最大值作为设备基地址,取桥与设备结束的最小值作为设备的结束地址,如果这个地址合法,那么保证设备在桥地址的范围内*/
     base = MAX(base, pci_bridge_get_base(br, type));
     limit = MIN(limit, pci_bridge_get_limit(br, type));
    /*如果取得地址不匹配,说明设备不在桥的范围内,而且无法截断,将设备地址设置成无效,重新匹配*/

    if (base > limit) {
        goto no_map;
    }

     /*匹配成功*/
    *addr = base;
    *size = limit - base + 1;
    return;
no_map:
    *addr = PCI_BAR_UNMAPPED;
    *size = 0;
}

从这个函数可以看出来,设备的地址分配是受桥的地址分配约束的,只要桥的地址分配了,设备的地址只能分配在桥的范围内,否则就会被置为无效,然后重新分配,一直到分配在桥的范围内为止。所以只要固定了桥的地址,自然就固定了设备的地址。

所以只需要初始化桥的地址,并且在reset的时候跳过桥的基地址重置,就能实现设备和桥地址的固定。添加的函数和代码如下:
添加桥的初始地址,因为桥的地址固定写在bar3上,通过写20可以将基地址固定在0x2000上,同时还需要写命令位,置1.
static int dec_21154_initfn(PCIDevice *dev)
{

     ... ...
     ... ...
     pci_set_word(dev->config + PCI_BASE_ADDRESS_3,0x2020);
     pci_set_word(dev->config + PCI_COMMAND,0x1);

     void pci_device_reset(PCIDevice *dev)

     return 1;
}

在重置桥里面过滤我们的桥,通过dev的名字可以识别我们自己定义的设备,如果是我们的设备就不重置,直接进行更新IO映射。
void pci_device_reset(PCIDevice *dev)
{

     if(strcmp(dev->name,"dec_name")==0){
          pci_update_mappings(dev);
          return   

     }
     ... ...
     ... ...
}
通过上面的步骤就能实现一般的IO基地址固定,我们可以在Linux中使用 cat /proc/ioports 命令来查看当前PCI设备的IO映射地址关系。

2. 直接重写config_write函数。
我用这种方法测试过几种操作系统,不同系统的PCI设备初始化可能会有区别,有些不能够自适应分配IO基地址设备的,那么我们就需要强行overide PCI配置读写函数。

在QEMU中,每一个PCI设备都要注册一个读写配置函数,用来提供给操作系统读写PCI设备的内存信息,通过读写这两个函数,就能实现对PCI设备IO基地址进行设置,而我们的IO基地址之所以会动态的变化,也就是因为这个函数将新的IO基地址写到了我们虚拟的PCI设备的bar里面,造成我们自己设置的基地址被覆盖。如果我们不重写它,就使用系统默认的配置函数,不改变重写的数值,如果我们有些特殊的需求,如强行给PCI内存赋值,就可以重写这个函数,虽然有些暴力,但是确实可行。
这样做我们需要修改之前定义的设备结构体。在结构体里面增添.config_write和.config_read。并且在write里面强行的把基地址写成我们想固定的地址。

static PCIDeviceInfo fpga_info={
     .qdev.name = "fpga",
     .qdev.size = sizeof(FPGAState),
     .init      = pci_fpga_init,

     .config_write = fpga_config_write,
     .config_read = fpga_config_read,
};

void fpga_write_config(PCIDevice *d, uint32_t addr, uint32_t val, int l)
{
      /*如果是bar0 则是0x10,这个必须根据我们分配的bar不同而变化*/
      if(addr = 0x10) pci_default_write_config(d,addr,0x20,l);
      else pci_default_write_config(d,addr,val,l);
}

同样的方法我们也可以用在桥里面,将桥的IO基地址固定,然而桥的PCI桥地址的基地址是放在bar3上的,所以判断起来要判断1d,如:
       if(addr==1d)   pci_bridge_write_config(d,addr,0x20,l);
       else   pci_bridge_write_config(d,addr,val,l);

这样我们就强行的将两者的IO基地址固定了,这个我在操作系统上测试通过了,并且KVM IO拦截运行正常。
总结
通过上面两种改写就能够确保模拟出来的PCI总线设备和桥固定在我们想要的IO空间段,不用系统随机的分配。这样做可以满足我们一些特殊化得需求,如某些板子的某些设备是固定IO地址的,而相应的操作系统不是通过class和subclass,vendor,device ID这些来读取设备,而是通过固定IO来访问设备的就能起到作用。对一些固定的操作系统有更强的兼容性。另外也在一定的程度上帮助我们更深入的理解了PCI设备,理解了硬件与操作系统的IO交互。


运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-155795-1-1.html 上篇帖子: QEMU/KVM中磁盘cache关闭(cache=off/none) 下篇帖子: Qemu-KVM Guest OS Time Tick Source
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表