一、pci设备背景知识
传统的sata,是一种 半双工设备, 同一时间只能有一个方向在传输数据,传输速率就比较慢了。pci设备是一种全双工设备, 同一时间可以发送数据到其他pci设备,也可以接收来自其他pci设备的数据。
1、pci总线
在系统加载的时候,会将所有的pci设备给挂载到pci总线上,并在/sys/bus/pci/devices目录下创建所有的pci设备文件。从上图可以看出,pci总线与pci设备之间是一颗树形结构。可以通过pci桥来扩展系统总线,从而可以支持更多的pci设备,这个pci桥和usb转接器是同一个概念,目的都是为了拓展设备。系统一共支持255个pci总线;每个pci总线上支持挂载32个pci设备;每个pci设备最大支持8个功能。这里所说的功能号,例如一个pci设备既可以当做硬盘来使用也可以当做内存来使用,则说明这个pci设备有2个功能号。
可以执行lspci命令,就可以看出系统挂载了哪些pci设备,或者在/sys/bus/pci/devices目录下查看。网卡是其中的一种pci设备。每个pci设备,都由厂商id, 设备id, 类型id(属于内存还是网卡等), 总线号,功能号等属性组成。其中总线号,设备id, 功能号三者唯一标识一个pci设备。
2、pci配置空间
每个pci设备都有相应的寄存器,通过读取或者写入数据到寄存器,进而来操作pci设备。 pci设备在内存中的配置空间中由一系列寄存器组成。每个pci配置空间大小为4K, 其中头部的64字节是每个pci设备都遵从的标准协议格式。device id设备id, vendor id厂商id, class code设备类型,bar等都是寄存器。除去头部64字节,剩余的空间是每个pci设备的地址映射空间,里面也是一些寄存器,提供了pci设备的一堆功能,操作这些寄存器就可以操作pci设备。重点来看下bar这个寄存器。
每个pci设备最多由6个BAR寄存器组成(BAR0 — BAR5)。每个pci设备在出厂时,已经硬件上定死了这个pci设备的地址大小以及偏移。 在系统引导时,操作系统会在内存中开辟空间, 并将物理地址存放到相应BAR寄存器中。例如pci网卡提供BAR0和BAR1两个地址空间让cpu访问,则操作系统在内存中申请BAR0指定的大小空间,并将地址保存到BAR0寄存器, 在内存中申请BAR1指定的大小空间, 并将地址保存到BAR1寄存器。这些寄存器的值存放的是pci设备在内存中的地址映射,是真实的物理地址而不是虚拟地址。这样cpu就可以访问这个物理地址从而间接访问pci设备,因为cpu是无法直接访问pci设备的,cpu只能访问内存。
第0位表示寄存器对应的地址空间是io映射还是内存映射,0代表内存映射,1代表io映射。 第1-2位表示是64位的地址还是32位的地址, 00说明是32位地址,10说明是64位的地址。
上面已经提到过,系统一共支持255个pci总线; 每个pci总线上支持挂载32个pci设备;每个pci设备最大支持8个功能。每一个功能都有一个4K的配置空间,则要维护pci设备的配置一共需要花费: 255 * 32 * 8 * 4 * k = 261120K = 255M。 这对于动不动就是几个G的物理内存来说,并不算什么。
可以在/sys/bus/pci/devices下的每个pci设备目录,查看resource文件,这个文件记录的内容就是在系统引导时保存的pci设备BAR寄存器信息。每一行的格式分别是开始地址,结束地址,以及标记信息(是内存映射还是io映射等)
二、pci设备的加载
所谓的pci设备的加载,其实就是为了维护一个pci设备链表结构。dpdk扫描/sys/bus/pci/devices目录,获取每一个pci设备的信息(例如pci地址,pci设备id, pci的bar寄存器映射的信息等),将系统支持的所有pci设备都加载到pci设备链表pci_device_list中。
dpdk有关pci的初始化操作,是从rte_eal_pci_init接口开始的,里面会调用pci_scan接口,开始扫描/sys/bus/pci/devices目录,获取每个pci设备的信息,然后插入到链表
//扫描pci设备
static int pci_scan(void)
{
//扫苗/sys/bus/pci/devices目录下的每个pci设备
while ((e = readdir(dir)) != NULL)
{
//提取pci格式的信息(域,总线,设备,功能号)
parse_pci_addr_format(e->d_name, sizeof(e->d_name), &domain, &bus, &devid, &function);
//获取某个pci设备信息,将这个个pci设备插入到链表中
pci_scan_one(dirname, domain, bus, devid, function);
}
}
每一个pci设备,在/sys/bus/pci/devices目录下都对应一个目录。 例如0000:02:06.0这个pci设备,0000代表的是域, 所谓的域是用来扩展系统总线的,由于系统一共就支持255个系统总线,通常是够用的,在极端场景下系统总线不够用时,可以按照域来划分,使得每个域下都支持255个系统总线,这跟行政区的划分是一个意思。 02代表的是bus总线2; 06代表的是设备id; 最后一个0表示这个设备对于的功能号为0
pci_scan_one就是读取每一个pci设备目录下的文件,例如读取vendor文件获取厂商id; 读取device文件获取设备id; 读取resource文件获取这个pci设备的bar寄存器映射后的物理空间。然后将pci设备按照pci设备地址从小到大的顺序插入到链表中。
static int pci_scan_one(const char *dirname, uint16_t domain, uint8_t bus,
uint8_t devid, uint8_t function)
{
//解析pci资源文件/sys/bus/pci/devices/0000:02:06.0/resource,获取pci设备映射到内存中的地址
snprintf(filename, sizeof(filename), "%s/resource", dirname);
pci_parse_sysfs_resource(filename, dev);
//插入pci设备链表,按pci地址从小到大排序
TAILQ_INSERT_TAIL(&pci_device_list, dev, next);
}
这里要强调的是这个resource文件,例如/sys/bus/pci/devices/0000:02:06.0/resouce, 每个pci设备都有一个这样的文件,记录这个pci设备bar寄存器的地址映射信息。在系统引导时,会将pci设备映射到内存中的物理地址保存到这个bar寄存器中,同时记录到resource文件中。这样应用层读取这个resource文件里面的内容,就可以来访问pci设备了。例如在后续讲解uio时,uio设备就是读取这个resource文件的物理内存地址,然后通过mmap进行地址映射,通过这种方式在应用层就可以操作uio设备, 而操作uio设备就相当于操作网卡设备。相当于在应用层就可以通过访问uio文件来操作网卡设备,对网卡寄存器读写数据进而来操作网卡。
参考文献:
转载自:作者 ApeLife
本文链接:https://blog.csdn.net/ApeLife/article/details/100369843