virtio设备可以基于不同总线来实现,本文介绍基于pci实现的virtio-pci设备。以virtio-blk为例,首先介绍PCI配置空间内容,virtio-pci实现的硬件基础——capability,最后分析PIC设备的初始化以及virtio-pci设备的初始化。
PCI配置空间
- virtio设备作为pci设备,必须实现pci local bus spec规定的配置空间(最大256字节),前64字节是spec中定义好的,称预定义空间,其中前16字节对所有类型的pci设备都相同,之后的空间格式因类型而不同,对前16字节空间,我称它为通用配置空间
通用配置空间
- 前16字节中有4个地方用来识别virtio设备
- vendor id:厂商ID,用来标识pci设备出自哪个厂商,这里是0x1af4,来自Red Hat。
- device id:厂商下的产品ID,传统virtio-blk设备,这里是0x1001
- revision id:厂商决定是否使用,设备版本ID,这里未使用
- header type:pci设备类型,0x00(普通设备),0x01(pci bridge),0x02(CardBus bridge)。virtio是普通设备,这里是0x00
- command字段用来控制pci设备,打开某些功能的开关,virtio-blk设备是(0x0507 = 0b1010111),command的各字段含义如下图,低三位的含义如下:
- I/O Space:如果PCI设备实现了IO空间,该字段用来控制是否接收总线上对IO空间的访问。如果PCI设备没有IO空间,该字段不可写。
- Memory Space:如果PCI设备实现了内存空间,该字段用来控制是否接收总线上对内存空间的访问。如果PCI设备没有内存空间,该字段不可写。
- Bus Master:控制pci设备是否具有作为Master角色的权限。
- status字段用来记录pci设备的状态信息,virtio-blk是(0x10 = 0x10000),status各字段含义如下图:
其中有一位是Capabilities List,它是pci规范定义的附加空间标志位,Capabilities List的意义是允许在pci设备配置空间之后加上额外的寄存器,这些寄存器由Capability List组织起来,用来实现特定的功能,附加空间在64字节配置空间之后,最大不能超过256字节。以virtio-blk为例,它标记了这个位,因此在virtio-blk设备配置空间之后,还有一段空间用来实现virtio-blk的一些特有功能。1表示capabilities pointer
字段(0x34)存放了附加寄存器组的起始地址。这里的地址表示附加空间在pci设备空间内的偏移
virtio-blk配置空间的内容可以通过lspci命令查看到,如下
virtio配置空间
virtio-pci设备实现pci spec规定的通用配置空间后,设计了自己的配置空间,用来实现virtio-pci的功能。pci通过status
字段的capabilities list
bit标记自己在64字节预定义配置空间之后有附加的寄存器组,capabilities pointer
会存放寄存器组链表的头部指针,这里的指针代表寄存器在配置空间内的偏移
pci spec中描述的capabilities list
格式如下,第1个字节存放capability ID,标识后面配置空间实现的是哪种capability,第2个字节存放下一个capability的地址。
capability ID查阅参见pci spec3.0 附录H。virtio-blk实现的capability有两种,一种是MSI-X( Message Signaled Interrupts - Extension),ID为0x11,一种是Vendor Specific,ID为0x9,后面一种capability设计目的就是让厂商实现自己的功能。virtio-blk的实现以此为基础
virtio-pci根据自己的功能需求,设计了如下的capabilities布局,右侧是6个capability,其中5个用做描述virtio-pci的capability,1个用作描述MSI-X的capability,这里我们只介绍用作virtio-pci的capability。再看下图,右边描述了virtio-blk的capability
布局,左边是每个capability
指向的物理地址空间布局。virtio-pci设备的初始化,前后端通知,数据传递等核心功能,就在这5个capability中实现
根据virtio spec的规范,要实现virtio-pci的capabilty,其布局应该如下
vndr表示capability类型,next表示下一个capability在pci配置空间的位置,len表示capability这个数据结构的长度,type有如下取值,将virtio-pci的capability又细分成几类
virtio通用配置空间
capability中最核心的内容是virtio_pci_common_cfg
,它是virtio前后端沟通的主要桥梁,common config分两部分,第一部分用于设备配置,第二部分用于virtqueue使用。virtio驱动初始化利用第一部分来和后端进行沟通协商,比如支持的特性(guest_feature),初始化时设备的状态(device_status),设备的virtqueue个数(num_queues)。第二部分用来实现前后段数据传输。后面会详细提到两部分在virtio初始化和数据传输中的作用。virtio_pci_common_cfg
数据结构如下
virtio磁盘配置空间
- TODO
VirtIO-PCI初始化
- virtio-blk基于virtio-pci,virtio-pci基于pci,所以virtio-blk初始化要从pci设备初始化说起
PCI初始化
- PCI驱动框架初始化在内核中有两个入口,分别如下:
- arch_initcall(pci_arch_init),体系结构的初始化,包括初始化IO地址0XCF8
- subsys_initcall(pci_subsys_init),PCI子系统初始化,这个过程会完成PIC总线树上设备的枚举,Host bridge会为PCI设备分配地址空间并将其写入BAR寄存器。体系结构初始化不做介绍,这张主要介绍PCI总线树的枚举和配置
枚举
枚举的前提该PCI设备可以访问,PCI规范规定,设备在还没有配置地址前,CPU往两个IO端口分别写入地址和数据,实现对PCI设备的读写。如下图所示,这两个IO端口对应的是两个Host桥上的寄存器,它们可以直接通过io指令访问
这两个寄存器位于Host桥上,翻看Host桥(Intel 5000X MCH 3.5章节)的手册可以找到寄存器各字段具体含义,如下图所示。当cpu要访问某个pci设备时,先往0XCF8写入4字节的bus/slot/function
地址,然后通过0XCFC的IO空间读取或者写入数据。地址空间0XCF8的初始化发生在pci_arch_init
里面。
CFGE:配置启用
除非设置该位,否则对 CFGDAT 寄存器的访问将不会产生配置访问,而是将被视为其他 I/O 访问。 该位严格来说是 CFC/CF8 访问机制的启用,不会转发到 ESI 或 PCI Express。保留
总线号码
如果为 0,MCH 将检查设备以确定路由到的位置。 如果非零,则根据 PBUSN 和 SBUSN 寄存器进行路由。设备编号
该字段用于选择每个总线 32 个可能的设备之一。功能编号
该字段用于选择本地寻址寄存器的功能。寄存器偏移量
如果该寄存器指定对 MCH 寄存器的访问,则该字段指定要寻址的一组四个字节。 访问的字节由 CFGDAT 寄存器访问的字节使能定义写入这些位没有效果,读取返回 0
配置数据窗口
写入或读取CFGADR指定的配置寄存器(如果有)的数据
有了读写pci设备寄存器的方法,cpu可以读取任意pci总线上任意设备配置空间的任意寄存器。
- 读取slot设备配置空间的vendor id,如果返回全1表示没有设备,如果返回具体值表示slot上存在function,继续判断是否为multifunction。
通过这样的方式逐总线搜索下去,枚举每一个slot上存在的pci设备,直到遍历完总线树。其中两处涉及到配置空间寄存器的读写,一是识别设备的时候需要读取vendor id和device id,一是读取bar空间大小时需要读写bar寄存器。枚举发生在pci_subsys_init,如下:
pci_subsys_init x86_init.pci.init => x86_default_pci_init pci_legacy_init pcibios_scan_root x86_pci_root_bus_resources // 为Host bridge分配资源,通常情况下就是64K IO空间地址和内存空间地址就在这里划分 pci_scan_root_bus // 枚举总线树上的设备 pci_create_root_bus // 创建Host bridge pci_scan_child_bus // 扫描总线树上所有设备,如果有pci桥,递归扫描下去 pci_scan_slot pci_scan_single_device // 扫描设备,读取vendor id和device id pci_scan_device pci_setup_device pci_read_bases __pci_read_base // 读取bar空间大小
读取到PCI设备BAR空间大小后,就可以向Host bridge申请物理地址区间了,如果成功,PCI设备就得到了一段PCI空间的,大于等于BAR空间大小的物理地址。注意,Host bridge掌握着PCI总线上所有设备可以使用的IO资源和存储资源,这里说的资源,就是物理地址空间。下面是两个关键的数据结构
/* * Resources are tree-like, allowing * nesting etc..
* 资源是树状的,允许嵌套等。 */ struct resource { resource_size_t start; resource_size_t end; const char *name; unsigned long flags; unsigned long desc; struct resource *parent, *sibling, *child; };
resource代表一个资源,可以是一段IO地址区间,或者Mem地址区间,总线树上每枚举一个设备,Host bridge就根据设备的BAR空间大小分配合适的资源给这个PCI设备用,这里的资源就是IO或者内存空间的物理地址。PCI设备BAR寄存器的值就是从这里申请得来的。申请的流程如下
pci_read_bases /* 遍历每个BAR寄存器,读取其内容,并为其申请物理地址空间 */ for (pos = 0; pos < howmany; pos++) { struct resource *res = &dev->resource[pos]; // 申请的地址空间放在这里面 reg = PCI_BASE_ADDRESS_0 + (pos << 2); pos += __pci_read_base(dev, pci_bar_unknown, res, reg); } region.start = l64; region.end = l64 + sz64; /* 申请资源,将申请到的资源放在res中, region存放PCI设备BAR空间区间 */ pcibios_bus_to_resource(dev->bus, res, ®ion);
分析资源申请函数,它首先取出PCI设备所在的Host bridge,pci_host_bridge.windows链表维护了Host bridge管理的所有资源,遍历其windows成员链表,找到合适的区间,然后分给PCI设备。
至此,PCI设备有了PCI域的物理地址,当扫描结束后,内核会逐一为这些PCI设备配置这个物理地址
- 枚举过程中识别设备的代码如下
枚举过程中获取bar占用的空间大小步骤如下,代码如下
- 读取BAR空间32bit的原始内容,保存
- BAR空间所有bit写1
- 再次读取BAR空间内容,右起第一个非0位所在的bit位,它的值就是BAR空间大小
假设是右起第12bit为1,那么BAR空间的大小就是2^12 = 4KB - 将原始内容写入BAR空间,恢复其原始状态,留待下一次读取
枚举之后,内核记录PCI总线树上所有PCI设备的基本硬件信息,比如vendor id,device id,同时也建立了一张所有设备的内存拓扑图,填充了大部分pci_dev数据结构,余下部分比如PCI设备的驱动(pci_dev.driver)在这个时候还没有找到
/* * The pci_dev structure is used to describe PCI devices. */ struct pci_dev { struct list_head bus_list; /* node in per-bus list */ struct pci_bus *bus; /* bus this device is on */ struct pci_bus *subordinate; /* bus this device bridges to */ void *sysdata; /* hook for sys-specific extension */ struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */ struct pci_slot *slot; /* Physical slot this device is in */ unsigned int devfn; /* encoded device & function index */ unsigned short vendor; unsigned short device; unsigned short subsystem_vendor; unsigned short subsystem_device; unsigned int class; /* 3 bytes: (base,sub,prog-if) */ u8 revision; /* PCI revision, low byte of class word */ u8 hdr_type; /* PCI header type (`multi' flag masked out) */ #ifdef CONFIG_PCIEAER u16 aer_cap; /* AER capability offset */ #endif u8 pcie_cap; /* PCIe capability offset */ u8 msi_cap; /* MSI capability offset */ u8 msix_cap; /* MSI-X capability offset */ u8 pcie_mpss:3; /* PCIe Max Payload Size Supported */ u8 rom_base_reg; /* which config register controls the ROM */ u8 pin; /* which interrupt pin this device uses */ u16 pcie_flags_reg; /* cached PCIe Capabilities Register */ unsigned long *dma_alias_mask;/* mask of enabled devfn aliases */ struct pci_driver *driver; /* which driver has allocated this device */ u64 dma_mask; /* Mask of the bits of bus address this device implements. Normally this is 0xffffffff. You only need to change this if your device has broken DMA or supports 64-bit transfers. */ ...... }
配置
枚举完成之后,内核得到pci总线树的拓扑图,然后开始配置总线树上的所有设备。Host bridge按照枚举过程中读取的pci设备的bar空间大小,将这段地址空间分成大小不同的地址空间。简单说就是为每个pci设备分配一个能够满足它使用的pci总线域地址段。确立了每个pci设备拥有的地址空间后,Host bridge将其首地址写到bar寄存器中,这就是配置,注意,bar空间写入的起始地址是pci总线域的,不是cpu域的。配置过程在pci_subsys_init中完成,首先遍历总线树上所有设备,统一检查在枚举阶段设备申请的资源,判断多个设备之前是否有资源冲突,内核要保证资源统一并且正确的分配给每一个PCI设备。检查完成后分配资源。最后向每个PCI设备的BAR
寄存器写入分配到的地址空间起始值,完成配置。流程如下:
pci_subsys_init pcibios_resource_survey pcibios_allocate_bus_resources(&pci_root_buses); // 首先将整个资源按照总线再分成一段段空间 pcibios_allocate_resources(0); // 检查资源是否统一并且不冲突 pcibios_allocate_resources(1); pcibios_assign_resources(); // 写入地址到BAR寄存器 pci_assign_resource _pci_assign_resource __pci_assign_resource pci_bus_alloc_resource pci_update_resource pci_std_update_resource pci_write_config_dword(dev, reg, new) // 往BAR寄存器写入起始地址
BAR
寄存器中写入的地址,乍一看就是系统的物理地址,但实际上,它与CPU域的物理地址有所不同,它是PCI域的物理地址。两个域的地址需要通过Host bridge的转换。只不过,X86上Host bridge偷懒了,直接采用了一一映射的方式。因此两个域的地址空间看起来一样。在别的结构上(PowerPC)这个地址不一样。
下面是virtio-blk设备配置空间的格式,截图自pci3.0 spec 6.2.5,bar寄存器配置之后,它的内容是bar空间的pci总线域起始地址,其中低4bit描述了这段空间的属性,在取地址的时候要作与0操作。最低位表示这个pci设备的bar空间,映射到的是cpu总线域的哪类空间,为1是IO空间,为0是内存空间。当映射到内存空间时,还用[2-1]这两位区分cpu域地址总线的宽度,为00表示映射到32位宽总线的cpu域内存空间,为10时表示映射到64位宽总线的cpu域内存空间。
- 下面是一个典型的virtio-blk设备配置空间bar寄存器内容
- BAR0:0XC041,将低4位与0,BAR0空间的起始地址0XC040,从最低位可以看出来这映射的是IO空间
- BAR1:0XFEBD2000,BAR1空间的起始地址0XFEBD2000,这是个内存空间
- BAR4:0XFE00800C,将低4位与0,BAR2空间的起始地址0XFE008000,这是个内存空间,64-bit,支持prefetch
在系统的IO地址空间中,可以看到BAR0占用的IO地址,cat /proc/ioports
在系统的内存空间中,可以看到BAR1和BAR4占用的内存空间,cat /proc/iomem
从主机上可以看到qemu统计的virtio-blk设备BAR空间的使用情况,virsh qemu-monitor-command vm --hmp info pci
pci设备的配置有几个地方容易混淆,特此说明自己的理解,如有不对,请留言指出:
- 枚举过程中访问pci配置空间寄存器,软件接口是往特定寄存器写东西,真正发起读写请求的,是host bridge,它在pci总线上传输的是command type为configuration wirte/read的transaction。配置完成后,软件对pci bar空间关联的内存进行读写,可以直接使用mov内存访问指令,这时候host bridge在pci总线上传输的是command type为IO Read/Write或者Memory Read/Write的transaction。
- pci设备被配置之后,bar寄存器中存放了关联的内存起始地址,这个地址是pci总线域的物理地址,不是cpu域的物理地址,虽然这两个值一样,但这只是x86这种架构的特殊情况,因为host bridge转换的时候是一一映射的,才造成这种假象。在别的架构上,比如power pc,cpu域的地址想要访问pci总线域的地址,需要通过host bridge进行地址转换才能进行,两种地址并不相同。
- pci设备被配置之后,虽然看上去软件可以像访问内存一样访问pci设备的bar的空间,但cpu真正访问的时候,是要把地址交给host bridge,让它去访问才可以。可以说,访问pci设备bar空间的不是cpu,而是host bridge。至始至终,cpu都不能直接管理pci设备的bar空间,它只能通过host bridge来间接管理。
标签:virtio,VirtIO,pci,寄存器,空间,原理,设备,PCI From: https://www.cnblogs.com/imreW/p/17815316.html