上一篇文章已经介绍了pci设备的背景知识, 现在我们来分析下pci设备是如何探测到支持的驱动,进而与驱动进行关联;pci与驱动的解除绑定;pci设备与uio设备的关联。
一、pci驱动注册
网卡驱动的注册使用了一种奇技淫巧的方法,使用GCC attribute扩展属性的constructor属性,使得网卡驱动的注册在程序main函数之前就执行了。此时在main函数执行前,就已经把系统支持的驱动通过rte_eal_driver_register注册到驱动链表dev_driver_list中。
struct rte_driver
{
TAILQ_ENTRY(rte_driver) next;
enum pmd_type type; /**< 驱动类型 */
const char *name; /**< 驱动名 */
rte_dev_init_t *init; /**< 设备初始化函数 */
};
static struct rte_driver pmd_igb_drv =
{
.type = PMD_PDEV,
.init = rte_igb_pmd_init,
};
static struct rte_driver rte_ixgbe_driver = {
.type = PMD_PDEV,
.init = rte_ixgbe_pmd_init,
};
类似于c++中的多态,驱动链表节点strcuct rte_driver是一个类, 每一种驱动都具体实现这个类,不同的驱动实现方式不一样,可以自定义init初始化接口。例如igb驱动与ixgbe驱动,这两种驱动都具体实现了这个类。每一种驱动都实现这个类,相当于一个多态模型。最后通过调用PMD_REGISTER_DRIVER注册到驱动链表dev_driver_list。 调用rte_eal_driver_unregister则可以从驱动链表中卸载。
void rte_eal_driver_register(struct rte_driver *driver)
{
TAILQ_INSERT_TAIL(&dev_driver_list, driver, next);
}
需要注意的是,这个dev_driver_list驱动链表是一个全局的配置结构, 是在预加载的时候创建的链表,只是用来说明系统支持哪些pmd驱动而已,是一个过渡链表。真实使用的时候,是不会使用这个链表的。真实使用时,会把这个链表支持的驱动类型注册到另一个链表pci_driver_list中。这个pci_driver_list驱动链表有什么用呢? 可以为每一个pci设备探测对应的驱动,实际上就是遍历这个驱动。将过渡链表转为真实的驱动链表这个操作是在rte_eal_dev_init接口中完成的。
int rte_eal_dev_init(void)
{
//注册驱动链表
TAILQ_FOREACH(driver, &dev_driver_list, next)
{
//设备初始化接口,用于真正注册驱动。例如pmd_igb_drv,接口为rte_igb_pmd_init
driver->init(NULL, NULL);
}
}
以e1000外卡为例, struct rte_driver驱动对象为pmd_igb_drv,他的init接口为rte_igb_pmd_init。由这个接口真正将驱动注册到pci_driver_list链表中。
static struct eth_driver rte_igb_pmd =
{
{
.name = "rte_igb_pmd",
.id_table = pci_id_igb_map,
.drv_flags = RTE_PCI_DRV_NEED_MAPPING | RTE_PCI_DRV_INTR_LSC,
},
.eth_dev_init = eth_igb_dev_init,
.dev_private_size = sizeof(struct e1000_adapter),
};
static int rte_igb_pmd_init(const char *name __rte_unused, const char *params __rte_unused)
{
rte_eth_driver_register(&rte_igb_pmd);
return 0;
}
可以看出struct eth_driver也类似于c++中的类,不同的驱动具体实现这个对象,相当于多态模型。可以看出最终是注册到pci_driver_list链表中。这个链表中的驱动是后面会被使用的,而不再是一个过渡的结构。驱动注册这个过程是不是觉得有点绕,感觉兜了一大圈。觉得绕就对了,这个是dpdk实现方式,你也可以改造代码,只需要一次就注册到最终的驱动链表就好了,没必要通过中间链表来中转。
void rte_eth_driver_register(struct eth_driver *eth_drv)
{
eth_drv->pci_drv.devinit = rte_eth_dev_init;
rte_eal_pci_register(ð_drv->pci_drv);
}
void rte_eal_pci_register(struct rte_pci_driver *driver)
{
TAILQ_INSERT_TAIL(&pci_driver_list, driver, next);
}
二、pci驱动探测
pci设备需要有驱动的支持才能使用, 并不是每一种驱动都适用于所有的pci设备, 例如网卡驱动就不适用于SATA设备。因此需要为每一种pci设备探测支持的驱动。为pci设备探测驱动的入口为rte_eal_pci_probe,来看下这个接口的实现。其实就是遍历上一篇文章提到的pci设备链表, 然后为每一个pci设备查找系统支持的驱动。
int rte_eal_pci_probe(void)
{
//遍历pci设备链表,为每一个pci设备查找驱动
TAILQ_FOREACH(dev, &pci_device_list, next)
{
ret = pci_probe_all_drivers(dev);
}
}
static int pci_probe_all_drivers(struct rte_pci_device *dev)
{
//遍历所有的驱动链表,为某个pci设备查找对应的驱动
TAILQ_FOREACH(dr, &pci_driver_list, next)
{
rc = rte_eal_pci_probe_one_driver(dr, dev);
}
}
每一个驱动都有一个表id_table,表项记录驱动支持哪些pci设备。表项的内容由pci的厂商id, pci设备id, pci子厂商等信息组成。每个pci设备就是根据自己的厂商id, 设备id等信息在每一个驱动提供的id_table表查找当前驱动是否支持自己,如果这些都相等,则说明pci找到了驱动。找到了驱动,就将驱动保存到pci设备结构中,将pci与驱动进行关联,接着进行驱动的初始化流程。驱动初始化会在另一个专题中详细分析,这里就只需要知道整体流程就好了。
int rte_eal_pci_probe_one_driver(struct rte_pci_driver *dr, struct rte_pci_device *dev)
{
for (id_table = dr->id_table ; id_table->vendor_id != 0; id_table++)
{
//检查这个驱动是否支持对应的pci设备。要求厂商id, 设备id等必须匹配
if (id_table->device_id != dev->id.device_id && id_table->device_id != PCI_ANY_ID)
{
continue;
}
//需要将pci物理地址映射到出来,使得通过uio访问pci设备
if (dr->drv_flags & RTE_PCI_DRV_NEED_MAPPING)
{
ret = pci_map_device(dev);
}
//pci关联这个驱动
dev->driver = dr;
//驱动初始化
return dr->devinit(dr, dev);
}
}
这里着重分析下pci设备地址映射的过程。
三、pci地址映射
所谓的pci地址映射,其实就是将pci设备提供给应用层访问的物理地址映射成虚拟地址,这样应用层通过uio就可以访问这个虚拟地址,进而访问pci设备。这里引入了uio的概念,就有必要先简单描述下。当pci设备绑定uio驱动后,uio驱动在探测到有网卡绑定后,会在/dev设备下创建一个uio文件,例如/dev/uio3, 同时也会在pci设备目录下创建相应的uio目录,例如/sys/bus/pci/devices/0000:02:06.0/uio目录,这个uio目录里面记录了pci设备的映射信息,也就是上一篇文章提到的BAR寄存器,其实也是/sys/bus/pci/devices/0000:02:06.0/resource文件中记录的映射信息。这样uio目录下的地址映射信息其实就是pci设备的映射信息,因此当通过mmap地址映射到/dev/uio3后,就将pci物理地址映射到虚拟地址中,通过/dev/uio3就可以访问这个pci设备。
pci地址映射与uio关联是在pci_uio_map_resource接口中完成的, 下面来详细分析下这个接口的实现过程。
1、在pci目录下查找对应的uio目录,这个在pci绑定驱动的时候,就会在pci目录下创建uio目录,以及在/dev目录下创建uio文件。例如: pci设备目录下创建/sys/bus/pci/devices/0000:02:06.0/uio/uio3目录; /dev目录下创建uio文件, /dev/uio3
int pci_uio_map_resource(struct rte_pci_device *dev)
{
//获取某个pci设备对应的uio目录,例如/sys/bus/pci/devices/0000:02:06.0/uio/uio3
uio_num = pci_get_uio_dev(dev, dirname, sizeof(dirname));
//dev/uio3
snprintf(devname, sizeof(devname), "/dev/uio%u", uio_num);
}
pci_get_uio_dev接口负责查找到uio目录文件。需要注意的是,如果应用层指定了需要创建uio文件,则内部会调用pci_mknod_uio_dev接口在/dev目录下重新创建uio文件,例如重新创建/dev/uio3
static int pci_get_uio_dev(struct rte_pci_device *dev, char *dstbuf, unsigned int buflen)
{
//创建dev/uiox文件
if (internal_config.create_uio_dev && pci_mknod_uio_dev(dstbuf, uio_num) < 0)
{
RTE_LOG(WARNING, EAL, "Cannot create /dev/uio%u\n", uio_num);
}
}
//sysfs_uio_path也就是网卡对于uio文件路径,例如/sys/bus/pci/devices/0000:02:06.0/uio/uio3
static int pci_mknod_uio_dev(const char *sysfs_uio_path, unsigned uio_num)
{
例如/sys/bus/pci/devices/0000:02:06.0/uio/uio3
f = fopen(filename, "r");
//读取主从设备号
ret = fscanf(f, "%d:%d", &major, &minor);
//创建/dev/uiox文件,例如/dev/uio3
snprintf(filename, sizeof(filename), "/dev/uio%u", uio_num);
dev = makedev(major, minor);
ret = mknod(filename, S_IFCHR | S_IRUSR | S_IWUSR, dev);
}
2、创建uio中断事件,也就是pci网卡中断事件。这里只负责创建,并没有加入到epoll事件机制中,此时即便网卡有事件也还不会触发,直到在驱动初始化接口eth_igb_dev_init那里会将这个dev/uio3中断源加入到epoll事件机制。注册到epoll后,如果网卡有事件发生,epoll事件模型就会返回,应用层就能处理网卡中断。需要注意的是,uio驱动已经将网卡中断给关闭了,因此dpdk中断是在应用层实现的,以免频繁硬件中断导致上下文切换,占用cpu资源。这里的中断指定是控制中断,也就是给网卡的一些控制操作,例如设置全双工半双工, 协商速率设置等; 而网卡报文的高速转发,应用层还是使用轮询的方式,报文转发就跟中断没有关系了。
int pci_uio_map_resource(struct rte_pci_device *dev)
{
//创建uio中断源/dev/uio3
dev->intr_handle.fd = open(devname, O_RDWR);
dev->intr_handle.type = RTE_INTR_HANDLE_UIO;
}
int eth_igb_dev_init(__attribute__((unused)) struct eth_driver *eth_drv, struct rte_eth_dev *eth_dev)
{
//pci中断源注册到中断事件链表中,内部会将中断源添加到epoll事件机制中
rte_intr_callback_register(&(pci_dev->intr_handle),
eth_igb_interrupt_handler, (void *)eth_dev);
}
3、扫描pci目录下的uio目录,看下uio目录下映射了哪些地址给应用层访问。例如扫描/sys/bus/pci/devices/0000:02:06.0/uio/uio3/maps目录,发现这个目录下有map0, map1两个子目录,这两个子目录记录了两个不同的地址空间,用于提供给应用层访问。pci_uio_get_mappings接口要做的事情就是扫描所有的map,将映射的地址信息保存起来。需要注意的是,这些地址是pci设备在物理内存上的真实地址,而不是虚拟地址。
root@apelife:/sys/bus/pci/devices/0000:02:06.0/uio/uio3/maps# ls
map0 map1
root@apelife:/sys/bus/pci/devices/0000:02:06.0/uio/uio3/maps/map0# ls
addr name offset size
int pci_uio_map_resource(struct rte_pci_device *dev)
{
//获取pci映射的地址范围
nb_maps = pci_uio_get_mappings(dirname, uio_res->maps,RTE_DIM(uio_res->maps));
uio_res->nb_maps = nb_maps;
}
//获取pci映射的地址范围, 也就是扫描/sys/bus/pci/devices/0000:02:06.0/uio/uio3/maps目录,看下
//pci设备映射了哪些地址访问给应用层访问
//devname, pci设备关联的uio路径,例如/sys/bus/pci/devices/0000:02:06.0/uio/uio3
static int pci_uio_get_mappings(const char *devname, struct pci_map maps[], int nb_maps)
{
for (i = 0; i != nb_maps; i++)
{
snprintf(dirname, sizeof(dirname), "%s/maps/map%u", devname, i);
//获取地址偏移
snprintf(filename, sizeof(filename),"%s/offset", dirname);
pci_parse_sysfs_value(filename, &offset);
//获取地址大小
snprintf(filename, sizeof(filename), "%s/size", dirname);
pci_parse_sysfs_value(filename, &size);
//获取物理地址的位置
snprintf(filename, sizeof(filename), "%s/addr", dirname);
pci_parse_sysfs_value(filename, &maps[i].phaddr);
//保存地址大小与偏移
maps[i].offset = offset;
maps[i].size = size;
}
}
4、pci设备目录下的资源文件,例如/sys/bus/pci/devices/0000:02:06.0/resource记录了pci的BAR寄存器的信息,里面的内容为pci设备提供给应用层访问的物理地址空间; 而pci设备目录下的uio文件,例如/sys/bus/pci/devices/0000:02:06.0/uio/uio3/maps也记录了pci设备提供给应用层访问的地址空间,这两者有什么关系呢?
通常在系统引导的时候,系统探测到pci设备后,会创建pci目录,存放pci设备的相关信息,包括resource文件。在pci设备绑定uio驱动时,uio驱动会在pci目录下创建uio子目录,同时会将pci里面的部分文件信息拷贝到uio子目录下。例如会读取resouce文件的内容,将一部分提供给应用层访问的物理地址,在uio子目录下创建map子目录,记录这些物理地址信息。例如resouce文件记录了5个提供给应用层访问的物理地址空间,则uio驱动有可能会在map子目录下创建map0,map1,map2三个目录,用于记录5个提供给应用层访问的物理地址中的三个。那为什么不是5个都提供呢?这个我也还没理清楚。
也就是说uio目录里面的map子目录记录提供给应用层访问的物理地址,和pci设备目录resource文件记录提供给应用层访问的物理地址是相等的。这样对uio文件进行mmap地址映射后,当访问/dev/uiox文件,就相当于访问pci设备提供给应用层访问的物理空间,进而访问这个pci设备。通过这种方式应用层直接访问/dev/uiox文件,就可以访问网卡资源了。
pci_uio_map_resource接口负责地址映射的操作,映射后将保存这个映射后的虚拟地址。需要注意的是,不管一个pci设备提供了多少个给应用层访问的物理地址,都是通过同一个/dev/uiox文件进行映射的。另外还需要注意的是,应用层已经知道了pci提供的物理地址,那为什么还要进行地址映射? 是因为应用层无法直接访问pci设备的物理内存,只能通过虚拟地址进行访问。
int pci_uio_map_resource(struct rte_pci_device *dev)
{
//为pci的每一个提供给应用层访问的物理地址做映射
for (i = 0; i != PCI_MAX_RESOURCE; i++)
{
//查找pci提供给应用层访问的物理地址和/sys/bus/pci/devices/0000:02:06.0/uio/uio3/map的物理地址相等的地址进行映射
for (j = 0; j != nb_maps && (phaddr != maps[j].phaddr || dev->mem_resource[i].len != maps[j].size); j++)
;
//找到则进行映射
if (j != nb_maps)
{
//例如/dev/uiox
fd = open(devname, O_RDWR);
//共享内存映射,通过mmap方式
mapaddr = pci_map_resource(pci_map_addr, fd, (off_t)offset, (size_t)maps[j].size);
//保存映射后的虚拟地址
maps[j].addr = mapaddr;
maps[j].offset = offset;
dev->mem_resource[i].addr = mapaddr;
}
}
}
5、最后就是保存pci设备地址映射后的资源信息,每一个pci设备都有一个这样的资源结构,并插入到pci资源链表pci_res_list。
int pci_uio_map_resource(struct rte_pci_device *dev)
{
uio_res = rte_zmalloc("UIO_RES", sizeof(*uio_res), 0);
//将uio资源插入到链表,此时已经完成了pci设备的地址映射
TAILQ_INSERT_TAIL(pci_res_list, uio_res, next);
}
这个链表有什么作用呢?主要是给dpdk从线程用的, dpdk主线程负责将每个pci设备物理地址映射成虚拟地址,并将映射后的资源信息保存到这个链表中。从线程就没有必要在对每个pci设备进行地址映射了,直接读取这个链表就知道每个pci设备提供给应用层访问的物理地址是多少,映射后的虚拟地址是多少。这个可以从pci_uio_map_secondary接口看出,这个就是从线程调用的接口。
到目前为止,关于pci设备探测驱动以及pci设备与uio的之间的关系已经分析完成了。 至于驱动初始化的逻辑,则在后续会有专门的文章来分析。
原文链接:https://blog.csdn.net/ApeLife/article/details/100529593