dpdk pci驱动探测

        上一篇文章已经介绍了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(&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/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的之间的关系已经分析完成了。 至于驱动初始化的逻辑,则在后续会有专门的文章来分析。

  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
DPDK e1000驱动是针对英特尔e1000网卡的一种高性能驱动程序。它使用DPDK框架中的API,通过绕过Linux内核协议栈,直接与网络硬件进行交互,从而提高了网络性能。DPDK e1000驱动的主要工作方式包括初始化、DMA配置和中断处理等,其实现代码主要分布在“lib/librte_e1000_em”和“lib/librte_pmd_e1000_em”两个目录下。 DPDK e1000驱动的初始化过程主要包括初始化硬件设备、动态配置硬件寄存器和设置驱动程序的相关参数等。驱动初始化时,会对网卡进行复位,并设置MAC地址、RSS多队列等参数。此外,驱动还会初始化一些硬件性能参数,如帧大小、Jumbo帧支持等。 在DMA配置方面,DPDK e1000驱动会使用DPDK提供的rte_mempool来管理内存池,对接收和发送的数据包进行缓存和预先分配内存,避免了重复的内存申请和管理操作,从而提升了驱动程序的效率。 在中断处理方面,DPDK e1000驱动会通过rte_intr_enable()和rte_intr_unmask()函数来使能网卡接收中断,并使用rte_eth_rx_burst()和rte_eth_tx_burst()函数提高接收和发送的效率。此外,DPDK e1000驱动还支持RSS多队列技术,可以将接收到的数据包划分到不同的队列中处理,提高网络的处理能力和负载均衡能力。 总之,DPDK e1000驱动在单个处理器上能够达到每秒数百万个数据包的处理速度,是一种高性能的网络驱动程序,广泛应用于云计算、大数据等高性能计算领域。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值