上一篇文章已经介绍了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_initdriver->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(&eth_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/uio3uio_num = pci_get_uio_dev(dev, dirname, sizeof(dirname));//dev/uio3snprintf(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/uio3f = fopen(filename, "r");   //读取主从设备号ret = fscanf(f, "%d:%d", &major, &minor);//创建/dev/uiox文件,例如/dev/uio3snprintf(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/uio3dev->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/uioxfd = 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的之间的关系已经分析完成了。 至于驱动初始化的逻辑,则在后续会有专门的文章来分析。

dpdk pci驱动探测相关推荐

  1. DPDK pci驱动探测(十八)

    上一篇文章已经介绍了pci设备的背景知识, 现在我们来分析下pci设备是如何探测到支持的驱动,进而与驱动进行关联:pci与驱动的解除绑定:pci设备与uio设备的关联. 一.pci驱动注册 网卡驱动的 ...

  2. DPDK uio驱动实现(二十)

    一.dpdk uio驱动框架 在系统加载igb_uio驱动后,每当有网卡和igb_uio驱动进行绑定时, 就会在/dev目录下创建一个uio设备,例如/dev/uio1.uio设备是一个接口层,用于将 ...

  3. DPDK igb_uio驱动分析

    本文整理下之前的学习笔记,基于DPDK17.11版本源码分析.主要分析一下igb_uio驱动源码. 总线-设备-驱动 首先简单介绍一下kernel中的总线-设备-驱动模型,以pci总线为例,pci总线 ...

  4. DPDK网卡驱动流程总结

    本文基于DPDK-16.07.2.Linux 4.4.2分析总结 1 简介 1.1 什么是UIO技术 UIO(Userspace I/O)是运行在用户空间的I/O技术,Linux系统中一般的驱动设备都 ...

  5. linux PCI驱动调用字符设备驱动方式

    上一篇文章写了字符设备驱动的基本结构及访问方式,在实际应用时首先需要绑定自己的硬件设备.本篇主要描述字符设备驱动与PCI接口类型的设备访问方式(内核为2.6.24及以上的方法,测试内核为2.6.32) ...

  6. Linux PCI驱动框架分析:(Peripheral Component Interconnect,外部设备互联)

    <DPDK 20.05 | rte_pci_bus思维导图 | 第一版> <linux系统下:IO端口,内存,PCI总线 的 读写(I/O)操作> <Linux指令:ls ...

  7. dpdk pmd驱动分析

    缘起dpdk_devbind.py 运行dpdk程序之前我们一般都会使用dpdk_devbind.py程序对指定的驱动与设备进行绑定与解绑(如下命令).那么dpdk_devbind.py程序是如何实现 ...

  8. PCI设备简介和PCI驱动使用函数

    一,PCI设备简介 PCI总线的特点: (1)速度上快,时钟频率提高到33M,而且还为进一步把时钟频率提高到66MHZ.总线带宽提高到64位留下了余地.(2)对于地址的分配和设置,系统软件课自动设置, ...

  9. 我的内核学习笔记6:PCI驱动probe的一点认知

    对于PCI的学习,在文章<初识PCI>和<再识PCI:一个PCI驱动实例>中有介绍,文中使用大量代码进行演示.但总觉得有些认知不到位.于是就再写一文. 一.PCI驱动一般框架 ...

  10. 再识PCI:一个PCI驱动实例

    之前写了第一篇关于PCI的文章,当时只是作为入门的接触笔记,后来对PCI又研究了一下,主要包括PCI设备的扫描过程及PCI驱动注册过程. 本文主要给出一个PCI实例,并在内核中做很多的打印以便跟踪其过 ...

最新文章

  1. ajax入门实例代码,AJAX、AJAX实例及AJAX源代码
  2. Java getClass() VS instanceof VS ==
  3. TFS2010迁移后Web工作项访问提示:error HRESULT E_FAIL has been returned from a call to a COM component....
  4. win10笔记本永久删除文件文件怎样恢复
  5. Cover the Tree(2020多校第二场C)
  6. 最常被程序员们谎称读过的计算机书籍
  7. 联想r630服务器开启虚拟化,整合虚拟化 联想万全R630服务器上市
  8. 查找两个字符串中相同字符串_使两个字符串相同的最低成本
  9. 中小学计算机听课记录表,小学数学听课记录范文表格
  10. systemtap notes
  11. 拓端tecdat|R语言用Hessian-free 、Nelder-Mead优化方法对数据进行参数估计
  12. 【Linux】系统移植篇一--linux系统移植导学
  13. html背景毛玻璃,一秒提高格调!帮你快速生成网页毛玻璃背景的酷站(附CSS代码)...
  14. xCodeGhost 事件专题总结
  15. InVEST模型在固碳、生境质量、产水等领域案例分析
  16. c语言对fpga编程,利用C语言对FPGA计算解决方案进行编程方法介绍
  17. C语言入门题库——求数列2/1+3/2+5/3......的和
  18. Flink CheckPoint : Exceeded checkpoint tolerable failure threshold
  19. android 沉浸式_【沉浸式体验】从民俗音乐到绘画雕塑,他们玩了这么多花样!...
  20. 学习方法之08克服拖延症,及时快速地完成任务

热门文章

  1. Python collections模块之Counter()详解
  2. 服务器空岛怎么修改地形,《迷你世界》2021空岛地形码如何输入 空岛地形码介绍...
  3. 理解泰勒中值定理1的证明过程的两个影响理解的简单隐含推导
  4. 代理IP是什么意思?浏览器代理和代理服务器是什么(小白必看,看了必会,不看血亏)
  5. html5 undefined,JS中typeof() !== undefined'是什么意思
  6. 在伦敦生活工作-2(由于本人懒惰的关系,这篇估计是最后一篇了,呵呵)
  7. 中国农大计算机保研,中国农业大学2021届保研情况
  8. 计算机类中文核心期刊简介
  9. JEECMS V8.1常用标签举例
  10. 苹果开发者账号申请 之 公司开发者账号申请2021年