[转载]KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)

在上文中,我们在QEMU中已经成功的虚拟了一个PCI桥和一个PCI设备,接下来我们就来给他们分配固定的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[i];

/* 如果没有注册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 <= new_addr || new_addr == 0 || 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交互。

文章出处:http://blog.csdn.net/yearn520/article/details/6577988

时间: 2024-10-17 20:29:34

[转载]KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)的相关文章

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

最近研究了一下QEMU的虚拟PCI设备,打算虚拟一个PCI-PCI桥和一个PCI设备,设备挂在桥上,桥挂在pci主桥上.并且给设备固定映射一个IO基地址,但是发现还是件头疼的事情,经过几天的辛苦,终于算是有点收获,和大家分享一下,有什么问题希望大家支持,一起讨论,共同提高. 申明:本文主要针对x86架构进行说明. 1. PCI 结构简介 为了大家更加容易的理解后文,先来回顾一下PCI总线的基本内存结构.每一个PCI设备都对应一段内存空间,里面按照地址位置放置PCI设备的信息,包括厂家信息,bar

CentOS 6.3系统安装配置KVM虚拟机

作业环境 服务器端 操作系统:CentOS 6.3 final x86_64 IP: 133.133.10.50 Hostname:myKVM KVM:qemu-kvm-0.12.1.2-2.295.el6_3.2.x86_64 客户端 Ubuntu和Win7,先在服务器端装好VNC,通过VNC连接服务器CentOS 一.安装KVM及相关软件 1.KVM 需要有 CPU 的支持(Intel vmx 或 AMD svm),在安装 KVM 之前检查一下 CPU 是否提供了虚拟技术的支持: [[ema

KVM虚拟机IO处理过程(一) ----Guest VM I/O 处理过程

虚拟化技术主要包含三部分内容:CPU虚拟化,内存虚拟化,设备虚拟化.本系列文章主要描述磁盘设备的虚拟化过程,包含了一个读操作的I/O请求如何从Guest Vm到其最终被处理的整个过程.本系列文章中引用到的linux内核代码版本为3.7.10,使用的虚拟化平台是KVM,qemu的版本是1.6.1. 用户程序想要访问IO设备需要调用操作系统提供的接口,即系统调用.当在用户程序中调用一个read操作时,系统先保存好read操作的参数,然后调用int 80命令(也可能是sysenter)进入内核空间,在

CentOS6.3下安装kvm和创建kvm虚拟机

一.安装kvm 1 在安装CentOS6.3时可以选择安装好kvm 2 如果未安装好kvm,请按照下列方式安装 [创建本地yum源] 挂载iso文件mount -o loop -t iso9660 CentOS-6.3-x86_64-bin-DVD1.iso /mnt 设置本地yum源在/etc/yum.repos.d/创建   "任意文件名.repo"    文件  vi 刚才新建的文件名编辑[localyum]任意名称 ,不能有空格.name= local yum  任意名称bas

KVM之初体验——QEMU安装虚拟机

QEMU简介 QEMU是一款开源的模拟器及虚拟机监管器(Virtual Machine Monitor,  VMM).QEMU主要提供两种功能给用户使用.一是作为用户态模拟器,利用动态代码翻译机制来执行不同于主机架构的代码.二是作为虚拟机监管器,模拟全 系统,利用其他VMM(Xen, KVM, etc)来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机. 是x86架构且硬件支持虚拟化技术(如 intel VT 或 AMD-V)的Linux全虚拟化解决方案. 它包含一个为处理器提供底层虚拟化 

KVM虚拟机IO处理过程(二) ----QEMU/KVM I/O 处理过程

接着KVM虚拟机IO处理过程中Guest Vm IO处理过程(http://blog.csdn.net/dashulu/article/details/16820281),本篇文章主要描述IO从guest vm跳转到kvm和qemu后的处理过程. 首先回顾一下kvm的启动过程(http://blog.csdn.net/dashulu/article/details/17074675).qemu通过调用kvm提供的一系列接口来启动kvm. qemu的入口为vl.c中的main函数,main函数通过

[转载]kvm libvirt 虚拟机管理

http://www.2cto.com/os/201203/123128.html kvm虚拟机管理 一.环境 role         hostname    ip                  OS kvm_server   target      192.168.32.40/24    rhel6.0_x64 vir_guest1   node4       192.168.32.34/24    rhel5.5_i386 vir_guest2   node5       192.16

烂泥: KVM虚拟机Linux系统增加硬盘

本文首发于烂泥行天下. Linux虚拟机在使用过程中,硬盘空间不够使用.由于前期没有做LVM,所以只能手动添加新的硬盘. 给虚拟机添加硬盘有两种方法: 1.通过virsh attach-disk命令添加一块硬盘到系统中,即时生效,但系统重启后新硬盘会消失. 2.通过修改虚拟机配置文件进行添加,永久生效. 现在我来一一介绍方法一和方法二. 方法一.通过virsh attach-disk命令添加硬盘 首先看看在未添加新硬盘系统的分区情况. fdisk –l df –h 从上图我们可以看到目前系统中只

KVM虚拟化笔记(六)------kvm虚拟机的克隆

kvm虚拟机的克隆分为两种情况:kvm主机本机虚拟机直接克隆和通过复制配置文件与磁盘文件的虚拟机复制克隆.接下来我们一一进行测试: (一)kvm主机虚拟机的直接克隆 1,查看虚拟机的配置文件和磁盘文件: [[email protected] qemu]# cat /etc/libvirt/qemu/hadoop1.xml  <!-- WARNING: THIS IS AN AUTO-GENERATED FILE. CHANGES TO IT ARE LIKELY TO BE  OVERWRITT