DPDK 16.07 驱动初始化和收发包函数学习笔记

DPDK 16.07 驱动初始化和收发包函数学习笔记

  1. 文档保留了 markdown 格式。可以转为纯文本格式,方便在其他编辑器中使用语法高亮阅读代码。
  2. 文档行文主要是提纲式的。如果阅读过程发现缺少了方向。请立刻回到章节的开首处,查询总体的函数调用图。
  3. 文档只讨论了千兆(I350 Gigabit)和 万兆(82599ES 10-Gigabit SFI/SFP+)的驱动做为例子。
  4. 分析代码的过程中,各分别绑定了一个网口千兆(04:00.0)和 万兆(08:00.0)做为例子。
  5. 如果你的时间宝贵。请着重阅读以下重要的章节:
    5.1. <#### 2.2.2.1. id_table 的定义> <== DPDK 所支持的驱动列表
    5.2. <### 4.1.4. 创建 收包队列的 mbuf> <== DPDK 零拷贝的实现
  6. 文档的图例基本使用 UML。但也有自定义的图例。详细见 [本文档所使用的图例] 的说明。

本文档所使用的图例:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第1张图片

注意:

聚合和组合都表示整体和部分的关联关系。
如果整体销毁后,部分也随之销毁。会使用组合表示。
如果整体销毁后,部分可以独立存在。会使用聚合表示。

1. rte_driver 驱动 的 注册

rte_driver 驱动 是用宏函数 PMD_REGISTER_DRIVER(),创建一个拼接函数,然后注册驱动的。

函数调用图如下:

	PMD_REGISTER_DRIVER(xxx)
	+-> devinitfn_xxx_drv				/* 拼接生成的注册函数 */
		+-> rte_eal_driver_register()	/* 注册驱动 */

1.1. rte_driver 驱动 的 结构体

dpdkrte_driver 驱动 的 结构体,定义 如下。

	struct rte_driver {
		TAILQ_ENTRY(rte_driver) next;  /**< Next in list. */
		enum pmd_type type;	       /**< PMD Driver type */
		const char *name;          /**< Driver name. */
		rte_dev_init_t *init;      /**< Device init. function. */
		rte_dev_uninit_t *uninit;  /**< Device uninit. function. */
	};

rte_driver 结构体 类图:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第2张图片

其中最重要的是 回调函数 rte_dev_init_t *init。用于 指向不同驱动的 初始化实现函数。
另外 type 则表示驱动的类型:

  1. PMD_PDEV 是物理设备驱动;
  2. PMD_VDEV 是虚拟设备驱动。

例子:

  1. 千兆网卡的驱动为 pmd_igb_drv
    其中 的 回调函数 init 设置 为 rte_igb_pmd_init
	static struct rte_driver pmd_igb_drv = {
		.type = PMD_PDEV,
		.init = rte_igb_pmd_init,	/* <== 驱动的初始化 */
	};
  1. 万兆网卡的驱动为 rte_ixgbe_driver
    其中 的 回调函数 init 设置 为 rte_ixgbe_pmd_init。
	static struct rte_driver rte_ixgbe_driver = {
		.type = PMD_PDEV,
		.init = rte_ixgbe_pmd_init,	/* <== 驱动的初始化 */
	};

1.2. rte_driver 驱动的 注册函数

驱动 的注册,是以 PMD_REGISTER_DRIVER 宏函数 来实现的。

该宏函数 会 拼接生成一个 注册函数,该 注册函数 的属性 __attribute__() 设置为 constructor
所以可以在 main() 之前,自动的运行该注册函数。

注意:
注册函数 里面 调用 了 rte_eal_driver_register 对结构体 struct rte_driver 进行 注册。

	#define PMD_REGISTER_DRIVER(drv, nm)\
	void devinitfn_ ##drv(void);\
	void __attribute__((constructor, used)) devinitfn_ ##drv(void)\
	{\
		(drv).name = RTE_STR(nm);\
		rte_eal_driver_register(&drv);\
	} \
	DRIVER_EXPORT_NAME(nm, __COUNTER__)	

例子:

  1. 千兆网卡的注册:
	PMD_REGISTER_DRIVER(pmd_igb_drv, igb);

	/* 宏展开后,结果如下 */
	void devinitfn_pmd_igb_drv(void);
	void __attribute__((constructor, used)) devinitfn_pmd_igb_drv(void)
	{
		(pmd_igb_drv).name = "igb";
		rte_eal_driver_register(&pmd_igb_drv);	/* <== 注册千兆驱动 */
	}
  1. 万兆网卡的注册:
	PMD_REGISTER_DRIVER(rte_ixgbe_driver, ixgbe);

	/* 宏展开后,结果如下 */
	void devinitfn_rte_ixgbe_driver(void);
	void __attribute__((constructor, used)) devinitfn_rte_ixgbe_driver(void)
	{
		(rte_ixgbe_driver).name = "ixgbe";
		rte_eal_driver_register(&rte_ixgbe_driver);	/* <== 注册万兆驱动 */
	}

1.3. rte_eal_driver_register() 的作用

rte_eal_driver_register 是驱动在运行时的注册函数。
rte_eal_driver_register 将驱动 driver 放置到 名为 dev_driver_listtail_queue 链表中。

	void
	rte_eal_driver_register(struct rte_driver *driver)
	{
		TAILQ_INSERT_TAIL(&dev_driver_list, driver, next);
	}

dev_driver_list 是一个全局变量,登记了所有的 驱动。

	/** Double linked list of device drivers. */
	TAILQ_HEAD(rte_driver_list, rte_driver);

	/** Global list of device drivers. */
	static struct rte_driver_list dev_driver_list =
		TAILQ_HEAD_INITIALIZER(dev_driver_list);

注册了千兆和万兆网卡驱动的 rte_driver_list 链表
DPDK 16.07 驱动初始化和收发包函数学习笔记_第3张图片

2. EAL 的初始化

环境抽象层 EAL 初始化,其中与设备相关的部分主要函数如下:

  1. rte_eal_pci_init 对 所有 dpdk 托管 pci 网卡 的 进行初始化。
  2. rte_eal_dev_init 设备 的初始化。
  3. rte_eal_pci_probe pci 设备的侦测(侦测和初始化)。

rte_eal_init() 总体函数调用图如下:

	main
	|	/* ------------------------ */
	|	/*       EAL 的初始化       */
	+-> rte_eal_init
		|
		+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		|	+-> rte_eal_pci_scan	/* 取得 网口 的 addr 信息 */
		|		+-> opendir(pci_get_sysfs_path()); /* 读取系统设备目录下的pci设备。 */
		|		+-> parse_pci_addr_format /* 提取 pci 地址 信息 */
		|		+-> pci_scan_one	/* 解释 单个 pci 设备的详细信息。 */
		|			+-> eal_parse_sysfs_value	 /* 解释 pci 设备的文件子系统的内容 */
		|			+-> pci_parse_sysfs_resource /* 取得 网口 的 resource 信息 */
		|			|	+-> pci_parse_one_sysfs_resource  /* 解释一条的 resource 信息 */
		|			+-> pci_get_kernel_driver_by_path /* 取得 内核驱动的 名字 */
		|			+-> TAILQ_INSERT_TAIL(&pci_device_list, dev, next) /* 把设备 放到 pci_device_list 链表 */
		|
		+-> rte_eal_dev_init				/* 设备 的初始化 */
		|	+-> rte_eal_vdev_init 			/* 初始化 虚拟设备。暂不深入展开。 */
		|	+-> driver->init(NULL, NULL); 	/* 触发 dev_driver_list 上 所有 driver->init() 的调用。 */
		|		.	/* 千兆: rte_igb_pmd_init */
		|		+~> rte_igb_pmd_init		
		|		.	+-> rte_eth_driver_register(&rte_igb_pmd); /* 不同的驱动会调用 ret_eth_driver_register 来注册。 */
		|		.		+-> eth_drv->pci_drv.devinit = rte_eth_dev_init; /* 设置 网口 设备 初始化 回调函数。 */
		|		.		+-> eth_drv->pci_drv.devuninit = rte_eth_dev_uninit; /* 设置 网口 设备 反初始化 回调函数。 */
		|		.		+-> rte_eal_pci_register(&eth_drv->pci_drv)	/* 注册 pci 驱动。 */
		|		.			+-> TAILQ_INSERT_TAIL(&pci_driver_list, driver, next); /* 将驱动放置到 pci_driver_list 中。 */
		|       .
		|		.	/* 万兆: rte_ixgbe_pmd_init */
		|		+~> rte_ixgbe_pmd_init		
		|			+-> rte_eth_driver_register(&rte_ixgbe_pmd); /* 不同的驱动会调用 ret_eth_driver_register 来注册。 */
		|				+-> eth_drv->pci_drv.devinit = rte_eth_dev_init; /* 设置 网口 设备 初始化 回调函数。 */
		|				+-> eth_drv->pci_drv.devuninit = rte_eth_dev_uninit; /* 设置 网口 设备 反初始化 回调函数。 */
		|				+-> rte_eal_pci_register(&eth_drv->pci_drv)	/* 注册 pci 驱动。 */
		|					+-> TAILQ_INSERT_TAIL(&pci_driver_list, driver, next); /* 将驱动放置到 pci_driver_list 中。 */
		|
		+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
			+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
				+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
					+->	dr->devinit(dr, dev);		/* 触发 驱动 对 设备 初始化。参见 rte_eth_driver_register */
						|	/* 无论 千兆 和 万兆 网卡。都是使用 rte_eth_dev_init。 */
						+~> rte_eth_dev_init		/* 网口 设备 初始化。 */
							+-> rte_eth_dev_create_unique_device_name
							+-> rte_eth_dev_allocate // 
							|	+-> rte_eth_dev_find_free_port	/* 从 rte_eth_devices 中,查找 空闲的 port id */
							|	+-> rte_eth_dev_data_alloc		/* 只有 当 rte_eth_dev_data[] 没有初始化的时候创建数据实体。 */
							|
							+-> eth_dev->data->dev_private = rte_zmalloc() /* 为 以太网设备的私有数据 分配空间 */
							+-> TAILQ_INIT(&(eth_dev->link_intr_cbs)) /* 初始化中断处理回调函数链表 */
							+-> (*eth_drv->eth_dev_init)(eth_dev); /* 调用 驱动 的 eth_dev_init 回调函数 初始化网口设备 */
								.	/* 千兆: rte_igb_pmd.eth_dev_init = eth_igb_dev_init */
								+~> eth_igb_dev_init()				/* 千兆以太网 设备初始化 */
								.	+-> eth_dev->dev_ops = &eth_igb_ops;			/* 关联网口设备操作回调函数 */
								.	+-> eth_dev->rx_pkt_burst = &eth_igb_recv_pkts;	/* 设置默认收包回调函数 */
								.	+-> eth_dev->tx_pkt_burst = &eth_igb_xmit_pkts; /* 设置默认发包回调函数 */
								.	+-> igb_hardware_init() 						/* 初始化 硬件 */ 
								.	+-> rte_intr_callback_register()				/* 初始化中断 */
								.
								.	/* 万兆: rte_ixgbe_pmd.eth_dev_init = eth_ixgbe_dev_init */
								+~> eth_ixgbe_dev_init()			/* 万兆以太网 设备初始化 */
									+-> eth_dev->dev_ops = &ixgbe_eth_dev_ops;		/* 关联网口设备操作回调函数 */
									+-> eth_dev->rx_pkt_burst = &ixgbe_recv_pkts;   /* 设置默认收包回调函数 */
									+-> eth_dev->tx_pkt_burst = &ixgbe_xmit_pkts;   /* 设置默认发包回调函数 */
									+-> ixgbe_init_hw()								/* 初始化 硬件 */ 
									+-> rte_intr_callback_register()				/* 初始化中断 */ 

2.1. rte_eal_pci_init() 函数

rte_eal_pci_init 函数 用于 pci 设备和驱动的初始化:

  1. rte_eal_pci_init 函数 初始化 了 两个全局的 tail queue
    1.1. pci_driver_list,用于组织 pci 驱动
    2.2. pci_device_list,用于组织 pci 设备
  2. 最后通过 rte_eal_pci_scan 函数,来 扫描 网络设备。

链表 pci_driver_listpci_device_list 的定义:

	TAILQ_HEAD(pci_device_list, rte_pci_device); /**< PCI devices in D-linked Q. */
	TAILQ_HEAD(pci_driver_list, rte_pci_driver); /**< PCI drivers in D-linked Q. */

	extern struct pci_driver_list pci_driver_list; /**< Global list of PCI drivers. */
	extern struct pci_device_list pci_device_list; /**< Global list of PCI devices. */

2.1.1. struct rte_pci_device

struct rte_pci_device 用于表示 一个 pci 设备

其中记录了 pci 设备addrid, mem_resource。当中最重要的是 id 字段 和 driver 指针。

  1. id 字段 记录了 pci 设备vendor_iddevice_id。后续用于探测驱动 driver
  2. driver 指针指向 关联的 pci 驱动。后续需要 driver 来初始化 设备。
	/**
	 * A structure describing a PCI device.
	 */
	struct rte_pci_device {
		TAILQ_ENTRY(rte_pci_device) next;       /**< Next probed PCI device. */
		struct rte_pci_addr addr;               /**< PCI location. */
		struct rte_pci_id id;                   /**< <== PCI ID. */
		struct rte_pci_resource mem_resource[PCI_MAX_RESOURCE];   /**< PCI Memory Resource */
		struct rte_intr_handle intr_handle;     /**< Interrupt handle */
		struct rte_pci_driver *driver;          /**< <== Associated driver */
		uint16_t max_vfs;                       /**< sriov enable if not zero */
		int numa_node;                          /**< NUMA node connection */
		struct rte_devargs *devargs;            /**< Device user arguments */
		enum rte_kernel_driver kdrv;            /**< Kernel driver passthrough */
	};

struct rte_pci_device 类图
DPDK 16.07 驱动初始化和收发包函数学习笔记_第4张图片

2.1.2. struct rte_pci_driver

struct rte_pci_driver 表示 pci 驱动

其中比较重要的成员变量有:

  1. 回调函数 devinitdevuninit,用于以 pci 设备的 初始化和 反初始化。
  2. id_table 指向 pci 驱动所支持的 供应商设备类型。用于 rte_eal_pci_probe_one_driver 函数。

注意:
千兆 和 万兆 网卡 id_table 的定义,请参考全局变量 pci_id_igb_map(千兆) 或者 pci_id_ixgbe_map(万兆)。

	/**
	 * A structure describing a PCI driver.
	 */
	struct rte_pci_driver {
		TAILQ_ENTRY(rte_pci_driver) next;       /**< Next in list. */
		const char *name;                       /**< Driver name. */
		pci_devinit_t *devinit;                 /**< <== Device init. function. */
		pci_devuninit_t *devuninit;             /**< <== Device uninit function. */
		const struct rte_pci_id *id_table;		/**< <== ID table, NULL terminated. */
		uint32_t drv_flags;                     /**< Flags contolling handling of device. */
	};

struct rte_pci_driver 类图
DPDK 16.07 驱动初始化和收发包函数学习笔记_第5张图片

2.1.3. rte_eal_pci_scan 函数

rte_eal_pci_scan 函数用于扫描 pci 设备(网口)。

  1. 读取 ‘SYSFS_PCI_DEVICES’ ("/sys/bus/pci/devices") 目录下的 pci设备 的 pci 地址。
  2. 使用 parse_pci_addr_format 函数,提取 的 pci 地址 信息 (domain, bus, devid, function)。
  3. 使用 pci_get_sysfs_path 返回的 SYSFS_PCI_DEVICES ("/sys/bus/pci/devices"),拼接出 pci 设备的文件子系统路径。
  4. pci_scan_one 解释 单个 pci 设备的详细信息。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		+=> rte_eal_pci_scan	/* <== 解释 单个 pci 设备的详细信息 */

以下的是部分的 rte_eal_pci_scan 函数的代码。

	int
	rte_eal_pci_scan(void)
	{
		struct dirent *e;
		DIR *dir;
		char dirname[PATH_MAX];
		uint16_t domain;
		uint8_t bus, devid, function;
	
		/* ------------------------------------*/
		/* 读取 SYSFS_PCI_DEVICES "/sys/bus/pci/devices" 目录下的 pci设备 的 pci 地址。 */
		dir = opendir(pci_get_sysfs_path());
		if (dir == NULL) {
			RTE_LOG(ERR, EAL, "%s(): opendir failed: %s\n",
				__func__, strerror(errno));
			return -1;
		}
	
		while ((e = readdir(dir)) != NULL) {
			if (e->d_name[0] == '.')
				continue;
	
			/* ------------------------------------*/
			/* 提取 的 pci 地址 信息 (domain, bus, devid, function) */
			if (parse_pci_addr_format(e->d_name, sizeof(e->d_name), &domain,
					&bus, &devid, &function) != 0)
				continue;
	
			/* ------------------------------------*/
			/* 拼接出 pci 设备的文件子系统路径 */
			snprintf(dirname, sizeof(dirname), "%s/%s",
					pci_get_sysfs_path(), e->d_name);
			
			/* ------------------------------------*/
			/* 解释 单个 pci 设备的详细信息。*/
			if (pci_scan_one(dirname, domain, bus, devid, function) < 0)
				goto error;
		}
		closedir(dir);
		return 0;
	
	error:
		closedir(dir);
		return -1;
	}

2.1.3.1. 读取 pci设备 文件

rte_eal_pci_scan 函数 会 查看 ‘SYSFS_PCI_DEVICES’ ("/sys/bus/pci/devices") 目录下的 pci设备 文件,绑定pci设备
文件中每一文件代表一个 pci 设备(网口)的地址。

其中 pci_get_sysfs_path 函数,对于 linux 系统,默认返回的是 SYSFS_PCI_DEVICES ("/sys/bus/pci/devices")。

例子:
通过命令行,打印出网络设备的 pci 地址

	ls /sys/bus/pci/devices
	>	0000:04:00.0
	>	0000:04:00.1
	>	0000:04:00.2
	>	0000:04:00.3
	>	0000:05:00.0
	>	0000:05:00.1
	>	0000:05:00.2
	>	0000:05:00.3
	>	0000:08:00.0
	>	0000:08:00.1
	>	0000:09:00.0
	>	0000:09:00.1

2.1.3.2. parse_pci_addr_format 函数

parse_pci_addr_format 函数,从 pci 地址提取,pci 地址 信息

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		+-> rte_eal_pci_scan	/* 解释 单个 pci 设备的详细信息 */
			+=> parse_pci_addr_format /* 提取 pci 地址 信息 */

pci 地址 信息 的组成格式为:

	::.
名称 类型 字符串表示为
domain uint16_t “0000” 4字符,有前导零,十六进制表示
bus uint8_t “00” 2字符,有前导零,十六进制表示
devid uint8_t “00” 2字符,有前导零,十六进制表示
function uint8_t “#” 无前导零,十进制表示

以 “0000:04:00.0” 为例,可解读得出:

名称 字符串值
domain “0000”
bus “04”
devid “00”
function “0”

2.1.3.3. pci 设备的文件子系统路径

为了拼接得到 pci 设备的文件子系统路径,需要先调用 pci_get_sysfs_path 函数,返回 总的 pci 设备的文件子系统路径。
最后在后边补上 pci 设备的地址。

例子:
"0000:04:00.0" 为例,则该 pci 设备的文件子系统路径 为 "/sys/bus/pci/devices/0000:04:00.0"

以下使用 ll 命令,打印出 千兆网口 /sys/bus/pci/devices/0000:04:00.0 的内容(只显示了部分)。
后续的 pci_scan_one 函数,将会解释 pci 设备的 文件子系统的详细数据。

	cd /sys/bus/pci/devices/0000:04:00.0
	ll
	>	total 0
	>	...
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 class
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 device
	>	lrwxrwxrwx 1 root root      0 Dec 12 13:34 driver -> ../../../../../../bus/pci/drivers/igb_uio
	>	-rw-r--r-- 1 root root   4096 Dec 12 13:34 max_vfs
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 numa_node
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 resource
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 subsystem_device
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 subsystem_vendor
	>	-r--r--r-- 1 root root   4096 Dec 12 13:34 vendor
	>	...

注意:

上面的例子当中 软连接 "/sys/bus/pci/devices/0000:04:00.0/driver" 所指向的文件 "../../../../../../bus/pci/drivers/igb_uio",是 内核驱动类型。
后续的 pci_scan_one 函数会解释该信息。

2.1.3.4. pci_scan_one 函数

pci_scan_one 函数,用于解释 单个 pci 设备的详细信息。

  1. pci 设备 分配内存。
  2. 填充 pci 设备pci 地址 信息
  3. 读取 pci 设备 id 信息 和 max_vfs 等信息。
  4. 解释 pci 设备resource
  5. 解释 pci 设备内核驱动类型
  6. pci 设备 插入到 pci_device_list 链表。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		+-> rte_eal_pci_scan	/* 取得 网口 的 addr 信息 */
			+=> pci_scan_one	/* <== 解释 单个 pci 设备的详细信息。 */

以下是 pci_scan_one 的简化后的代码:

	/* Scan one pci sysfs entry, and fill the devices list from it. */
	static int
	pci_scan_one(const char *dirname, uint16_t domain, uint8_t bus,
			 uint8_t devid, uint8_t function)
	{
		char filename[PATH_MAX];
		unsigned long tmp;
		struct rte_pci_device *dev;
		char driver[PATH_MAX];
		int ret;

		/* ------------------------------------*/
		/* 为 pci 设备 分配内存。*/
		dev = malloc(sizeof(*dev));
		if (dev == NULL)
			return -1;

		/* ------------------------------------*/
		/* 填充 pci 设备 的 pci 地址 信息。*/
		memset(dev, 0, sizeof(*dev));
		dev->addr.domain = domain;
		dev->addr.bus = bus;
		dev->addr.devid = devid;
		dev->addr.function = function;

		/* ------------------------------------*/
		/* 读取 pci 设备 id 信息 和 max_vfs 等信息。*/
		/* get vendor id */
		snprintf(filename, sizeof(filename), "%s/vendor", dirname);
		eal_parse_sysfs_value(filename, &tmp);
		dev->id.vendor_id = (uint16_t)tmp;

		/* get device id */
		snprintf(filename, sizeof(filename), "%s/device", dirname);
		eal_parse_sysfs_value(filename, &tmp);
		dev->id.device_id = (uint16_t)tmp;

		/* get subsystem_vendor id */
		snprintf(filename, sizeof(filename), "%s/subsystem_vendor",
			 dirname);
		eal_parse_sysfs_value(filename, &tmp);
		dev->id.subsystem_vendor_id = (uint16_t)tmp;

		/* get subsystem_device id */
		snprintf(filename, sizeof(filename), "%s/subsystem_device",
			 dirname);
		eal_parse_sysfs_value(filename, &tmp);
		dev->id.subsystem_device_id = (uint16_t)tmp;

		/* get class_id */
		snprintf(filename, sizeof(filename), "%s/class",
			 dirname);
		eal_parse_sysfs_value(filename, &tmp);
		/* the least 24 bits are valid: class, subclass, program interface */
		dev->id.class_id = (uint32_t)tmp & RTE_CLASS_ANY_ID;

		/* get max_vfs */
		dev->max_vfs = 0;
		snprintf(filename, sizeof(filename), "%s/max_vfs", dirname);
		if (!access(filename, F_OK) &&
			eal_parse_sysfs_value(filename, &tmp) == 0)
			dev->max_vfs = (uint16_t)tmp;
		else {
			/* for non igb_uio driver, need kernel version >= 3.8 */
			snprintf(filename, sizeof(filename),
				 "%s/sriov_numvfs", dirname);
			if (!access(filename, F_OK) &&
				eal_parse_sysfs_value(filename, &tmp) == 0)
				dev->max_vfs = (uint16_t)tmp;
		}

		/* get numa node */
		snprintf(filename, sizeof(filename), "%s/numa_node",
			 dirname);
		if (access(filename, R_OK) != 0) {
			/* if no NUMA support, set default to 0 */
			dev->numa_node = 0;
		} else {
			eal_parse_sysfs_value(filename, &tmp);
			dev->numa_node = tmp;
		}

		/* ------------------------------------*/
		/* 解释 pci 设备 的 resource。*/
		/* parse resources */
		snprintf(filename, sizeof(filename), "%s/resource", dirname);
		pci_parse_sysfs_resource(filename, dev);

		/* ------------------------------------*/
		/* 解释 pci 设备 的 内核驱动类型。*/
		/* parse driver */
		snprintf(filename, sizeof(filename), "%s/driver", dirname);
		ret = pci_get_kernel_driver_by_path(filename, driver);

		if (!ret) {
			if (!strcmp(driver, "vfio-pci"))
				dev->kdrv = RTE_KDRV_VFIO;
			else if (!strcmp(driver, "igb_uio"))
				dev->kdrv = RTE_KDRV_IGB_UIO;
			else if (!strcmp(driver, "uio_pci_generic"))
				dev->kdrv = RTE_KDRV_UIO_GENERIC;
			else
				dev->kdrv = RTE_KDRV_UNKNOWN;
		} else
			dev->kdrv = RTE_KDRV_NONE;

		/* ------------------------------------*/
		/* 将 pci 设备 插入到 pci_device_list 链表。*/
		/* device is valid, add in list (sorted) */
		if (TAILQ_EMPTY(&pci_device_list)) {
			TAILQ_INSERT_TAIL(&pci_device_list, dev, next);
		} else {
			struct rte_pci_device *dev2;
			int ret;

			TAILQ_FOREACH(dev2, &pci_device_list, next) {
				ret = rte_eal_compare_pci_addr(&dev->addr, &dev2->addr);

				if (ret == 0) { /* already registered */
					dev2->kdrv = dev->kdrv;
					dev2->max_vfs = dev->max_vfs;
					memmove(dev2->mem_resource, dev->mem_resource,
						sizeof(dev->mem_resource));
					free(dev);
					
					return 0;
				} else {
					continue;
				}
			}
			TAILQ_INSERT_TAIL(&pci_device_list, dev, next);
		}

		return 0;
	}
2.1.3.4.1. 填充 pci 设备pci 地址 信息

struct rte_pci_device 中有属于 struct rte_pci_addr 的成员变量 addr。该成员变量 表示的是 pci 地址 信息

struct rte_pci_addr 的定义如下:

	/**
	 * A structure describing the location of a PCI device.
	 */
	struct rte_pci_addr {
		uint16_t domain;                /**< Device domain */
		uint8_t bus;                    /**< Device bus */
		uint8_t devid;                  /**< Device ID */
		uint8_t function;               /**< Device function. */
	};

之前 parse_pci_addr_format 函数 所解释得到的 pci 地址信息,通过传参的方式传入到 pci_scan_one 函数,最后用来填充 pci 设备的成员变量 addr

	static int
	pci_scan_one(const char *dirname, uint16_t domain, uint8_t bus,
			 uint8_t devid, uint8_t function)
	{
		/* ------------------------------------*/
		/* 填充 pci 设备 的 pci 地址 信息。*/
		memset(dev, 0, sizeof(*dev));
		dev->addr.domain = domain;
		dev->addr.bus = bus;
		dev->addr.devid = devid;
		dev->addr.function = function;
		/* ... */
	}
2.1.3.4.2. 读取 pci 设备 id 信息 和 max_vfs 等信息

struct rte_pci_device 中有属于 struct rte_pci_id 的成员变量 id。用于表示 pci id 信息

struct rte_pci_id 的定义:

	/**
	 * A structure describing an ID for a PCI driver. Each driver provides a
	 * table of these IDs for each device that it supports.
	 */
	struct rte_pci_id {
		uint32_t class_id;            /**< Class ID (class, subclass, pi) or RTE_CLASS_ANY_ID. */
		uint16_t vendor_id;           /**< <== Vendor ID or PCI_ANY_ID. */
		uint16_t device_id;           /**< <== Device ID or PCI_ANY_ID. */
		uint16_t subsystem_vendor_id; /**< Subsystem vendor ID or PCI_ANY_ID. */
		uint16_t subsystem_device_id; /**< Subsystem device ID or PCI_ANY_ID. */
	};

pci_scan_one 函数中,则通过 eal_parse_sysfs_value 函数依次解读出 pci id 信息

重点:

pci 设备中的 成员变量 id,其中的 vendor_iddevice_id 是主要用于侦察和匹配 pci 驱动的。

例子:

使用命令行,打印 pci 设备 id 信息max_vfs 等信息。

	cat /sys/bus/pci/devices/0000:04:00.0/vendor
	>	0x8086
	cat /sys/bus/pci/devices/0000:04:00.0/device
	>	0x1521
	cat /sys/bus/pci/devices/0000:04:00.0/subsystem_vendor
	>	0x1304
	cat /sys/bus/pci/devices/0000:04:00.0/subsystem_device
	>	0xffff
	cat /sys/bus/pci/devices/0000:04:00.0/class
	>	0x020000
	cat /sys/bus/pci/devices/0000:04:00.0/max_vfs
	>	0
	cat /sys/bus/pci/devices/0000:04:00.0/numa_node
	>	-1
2.1.3.4.3. 解释 pci 设备resource

struct rte_pci_device 中有属于 struct rte_pci_resource 的成员变量 mem_resource[PCI_MAX_RESOURCE]
mem_resource[PCI_MAX_RESOURCE]用于记录,后续 mmap 要用到的uio 资源的物理地址。

struct rte_pci_resource 的定义:

	/**
	 * A structure describing a PCI resource.
	 */
	struct rte_pci_resource {
		uint64_t phys_addr;   /**< Physical address, 0 if no resource. */
		uint64_t len;         /**< Length of the resource. */
		void *addr;           /**< Virtual address, NULL when not mapped. */
	};

pci_parse_sysfs_resource 函数读取 /sys/bus/pci/devices/xxxx:xx:xx.x/resource 的信息来填充 mem_resource[]
每一行的数据会先由 pci_parse_one_sysfs_resource 解读。
最后只有是 flags 属于 IORESOURCE_MEM 类型填充的数据,才填充到 mem_resource[]

	/** IO resource type: */
	#define IORESOURCE_IO         0x00000100	/* IO资源 */
	#define IORESOURCE_MEM        0x00000200	/* <== 内存资源 */

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		+-> rte_eal_pci_scan	/* 取得 网口 的 addr 信息 */
			+-> pci_scan_one	/* 解释 单个 pci 设备的详细信息。 */
				+=> pci_parse_sysfs_resource /* 取得 网口 的 resource 信息 */

以下是简化的 函数:

	static int
	pci_parse_sysfs_resource(const char *filename, struct rte_pci_device *dev)
	{
		FILE *f;
		char buf[BUFSIZ];
		int i;
		uint64_t phys_addr, end_addr, flags;

		f = fopen(filename, "r");

		for (i = 0; i<PCI_MAX_RESOURCE; i++) {

			fgets(buf, sizeof(buf), f);

			pci_parse_one_sysfs_resource(buf, sizeof(buf), &phys_addr,
					&end_addr, &flags);
					
			/* 只有是 flags 属于 IORESOURCE_MEM 类型填充的数据,才填充 */
			if (flags & IORESOURCE_MEM) {
				dev->mem_resource[i].phys_addr = phys_addr;
				dev->mem_resource[i].len = end_addr - phys_addr + 1;
				/* not mapped for now */
				dev->mem_resource[i].addr = NULL;
			}
		}
		fclose(f);

例子:

  1. 打印 千兆 pci 设备 resource 信息
	cat /sys/bus/pci/devices/0000:04:00.0/resource
	>	# i  # phys_addr        # end_addr         # flags               # resource
	>	00   0x00000000f7b60000 0x00000000f7b7ffff 0x0000000000040200    # IORESOURCE_MEM	<== mmap 有效
	>	01   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	02   0x000000000000e060 0x000000000000e07f 0x0000000000040101    # IORESOURCE_IO
	>	03   0x00000000f7b8c000 0x00000000f7b8ffff 0x0000000000040200    # IORESOURCE_MEM	<== mmap 有效
	>	04   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	05   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	06   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	07   0x00000000f0000000 0x00000000f001ffff 0x000000000014220c    # IORESOURCE_MEM	<== mmap 有效
	>	08   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	09   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	10   0x00000000f0020000 0x00000000f003ffff 0x000000000014220c    # IORESOURCE_MEM	<== mmap 有效
	>	11   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	12   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
  1. 打印 万兆 pci 设备 resource 信息
	cat /sys/bus/pci/devices/0000:08:00.0/resource
	>	# i  # phys_addr        # end_addr         # flags               # resource
	>	00   0x00000000f7920000 0x00000000f793ffff 0x0000000000140204    # IORESOURCE_MEM	<== mmap 有效
	>	01   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	02   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	03   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	04   0x00000000f7944000 0x00000000f7947fff 0x0000000000140204    # IORESOURCE_MEM	<== mmap 有效
	>	05   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	06   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	07   0x00000000f0300000 0x00000000f03fffff 0x000000000014220c    # IORESOURCE_MEM	<== mmap 有效
	>	08   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	09   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	10   0x00000000f0400000 0x00000000f04fffff 0x000000000014220c    # IORESOURCE_MEM	<== mmap 有效
	>	11   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 
	>	12   0x0000000000000000 0x0000000000000000 0x0000000000000000    # 

上面一条的 resource 中。数据的格式如下:

	  
名称 位置 例子
phys_addr 第一列 “0x00000000f7b60000”
end_addr 第二列 “0x00000000f7b7ffff”
flags 第三列 “0x0000000000040200”

注意:

  • 最后只有是 flags 属于 IORESOURCE_MEM 类型填充的数据,才填充到 mem_resource[]
  • 只有 phys_addr 是非零的资源。最后才会被 mmap。
2.1.3.4.4. 解释 pci 设备内核驱动类型

pci_get_kernel_driver_by_path 函数 用来 取得 内核驱动的 名字。

其中使用了 readlink 函数来取得,软连接所指向的 驱动的路径。
最后取得位于末尾分段的驱动的名字。(用 basename 函数,会更容易实现)

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_init		/* 对 所有 dpdk 托管 pci 网口 的 进行初始化 */
		+-> rte_eal_pci_scan	/* 取得 网口 的 addr 信息 */
			+-> pci_scan_one	/* 解释 单个 pci 设备的详细信息。 */
				+=> pci_get_kernel_driver_by_path /* 取得 内核驱动的 名字 */

以下是简化的 pci_get_kernel_driver_by_path 函数:

	static int
	pci_get_kernel_driver_by_path(const char *filename, char *dri_name)
	{
		int count;
		char path[PATH_MAX];
		char *name;

		/* 取得软连接所指向的 驱动的路径 */
		count = readlink(filename, path, PATH_MAX);
		
		/* 取得位于末尾分段的驱动的名字 */
		name = strrchr(path, '/');
		if (name) {
			strncpy(dri_name, name + 1, strlen(name + 1) + 1);
			return 0;
		}

		return -1;
	}

pci_scan_one 函数最后通过字符串比较,就可以得出 内核驱动 类型
以下是 pci_scan_one 函数,调用 pci_get_kernel_driver_by_path 和得出 内核驱动 类型

	static int
	pci_scan_one(const char *dirname, uint16_t domain, uint8_t bus,
			 uint8_t devid, uint8_t function)
	{
		/* ... */
		ret = pci_get_kernel_driver_by_path(filename, driver);

		if (!ret) {
			if (!strcmp(driver, "vfio-pci"))
				dev->kdrv = RTE_KDRV_VFIO;
			else if (!strcmp(driver, "igb_uio"))
				dev->kdrv = RTE_KDRV_IGB_UIO;
			else if (!strcmp(driver, "uio_pci_generic"))
				dev->kdrv = RTE_KDRV_UIO_GENERIC;
			else
				dev->kdrv = RTE_KDRV_UNKNOWN;
		} else
			dev->kdrv = RTE_KDRV_NONE;
		/* ... */
	}

dpdk 中,内核驱动 类型的 枚举类型,定义如下:

	enum rte_kernel_driver {
		RTE_KDRV_UNKNOWN = 0,
		RTE_KDRV_IGB_UIO,
		RTE_KDRV_VFIO,
		RTE_KDRV_UIO_GENERIC,
		RTE_KDRV_NIC_UIO,
		RTE_KDRV_NONE,
	};

例子:

使用 readlink 命令来打印出 网卡的驱动名字。
两种不同类型的网卡所打印出来的 内核驱动 类型,都为 igb_uio

	# 千兆 铜线
	# 04:00.0 Ethernet controller: Intel Corporation I350 Gigabit Network Connection (rev 01)
	readlink /sys/bus/pci/devices/0000:04:00.0/driver
	>	../../../../../../bus/pci/drivers/igb_uio
	
	# 万兆
	# 08:00.0 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)
	readlink /sys/bus/pci/devices/0000:08:00.0/driver
	>	../../../../../../../../bus/pci/drivers/igb_uio
2.1.3.4.5. 将 pci 设备 插入到 pci_device_list 链表

pci_scan_one 函数最后通过 TAILQ_INSERT_TAILpci 设备插入到 pci_device_list
pci_device_list 内部元素,是按照 pci 地址升序排列。

以下的是插入到 pci_device_list 代码片段。

	static int
	pci_scan_one(const char *dirname, uint16_t domain, uint8_t bus,
			 uint8_t devid, uint8_t function)
{
		/* ... */
		/* device is valid, add in list (sorted) */
		if (TAILQ_EMPTY(&pci_device_list)) {
			TAILQ_INSERT_TAIL(&pci_device_list, dev, next);
		} else {
			struct rte_pci_device *dev2;
			int ret;

			TAILQ_FOREACH(dev2, &pci_device_list, next) {
				ret = rte_eal_compare_pci_addr(&dev->addr, &dev2->addr);
				if (ret > 0)
					continue;

				if (ret < 0) {
					TAILQ_INSERT_BEFORE(dev2, dev, next);
				} else { /* already registered */
					dev2->kdrv = dev->kdrv;
					dev2->max_vfs = dev->max_vfs;
					memmove(dev2->mem_resource, dev->mem_resource,
						sizeof(dev->mem_resource));
					free(dev);
				}
				return 0;
			}
			TAILQ_INSERT_TAIL(&pci_device_list, dev, next);
		}
		/* ... */
	}

最后,pci_device_list 会组成一个链表。如下图所示:

pci_device_list 设备链表
DPDK 16.07 驱动初始化和收发包函数学习笔记_第6张图片

2.2. rte_eal_dev_init 函数

rte_eal_dev_init 用于 设备的初始化。其中包括:

  1. 虚拟设备 的初始化。
  2. 物理设备的 PMD 驱动的初始化。

函数调用图如下:

	rte_eal_init
	+=> rte_eal_dev_init				/* <== 设备 的初始化 */
		+-> rte_eal_vdev_init 			/* 虚拟设备 初始化 (暂不讨论) */
		+-> driver->init(NULL, NULL);	/* 物理设备的 PMD 驱动的初始化 */

rte_eal_dev_init 函数会从 dev_driver_list 中,遍历所有 struct rte_driver,并调用各个 driverinit() 回调函数,来初始化 PMD 驱动
一旦PMD 驱动初始化完成后,dev_driver_list 就完成任务,再也没有什么作用了。

rte_eal_dev_init 函数源码:

	int
	rte_eal_dev_init(void)
	{
		struct rte_devargs *devargs;
		struct rte_driver *driver;

		/*
		 * Note that the dev_driver_list is populated here
		 * from calls made to rte_eal_driver_register from constructor functions
		 * embedded into PMD modules via the PMD_REGISTER_DRIVER macro
		 */

		/* 虚拟设备 初始化 */
		/* call the init function for each virtual device */
		TAILQ_FOREACH(devargs, &devargs_list, next) {

			if (devargs->type != RTE_DEVTYPE_VIRTUAL)
				continue;

			if (rte_eal_vdev_init(devargs->virt.drv_name,
						devargs->args)) {
				RTE_LOG(ERR, EAL, "failed to initialize %s device\n",
						devargs->virt.drv_name);
				return -1;
			}
		}

		/* 物理设备的 PMD 驱动的初始化 */
		/* Once the vdevs are initalized, start calling all the pdev drivers */
		TAILQ_FOREACH(driver, &dev_driver_list, next) {
			if (driver->type != PMD_PDEV)
				continue;
			/* PDEV drivers don't get passed any parameters */
			driver->init(NULL, NULL);
		}
		return 0;
	}

例子:

假设 dev_driver_list 上有 千兆网卡驱动(pmd_igb_drv) 和 万兆网卡驱动(rte_ixgbe_driver) 两种。

  1. 千兆网卡驱动 的 init() 回调函数为:rte_igb_pmd_init
  2. 万兆网卡驱动 的 init() 回调函数为:rte_ixgbe_pmd_init

rte_eal_dev_init 逐个触发 dev_driver_list 上所有驱动的 init() 回调函数
DPDK 16.07 驱动初始化和收发包函数学习笔记_第7张图片

2.2.1. struct rte_driverinit() 回调函数的实现

无论 千兆网卡驱动初始化 rte_igb_pmd_init() 还是 万兆网卡驱动初始化 rte_ixgbe_pmd_init(),最后都调用了 rte_eth_driver_register() 来注册 各自的以太网 PMD 驱动

详细的以太网 PMD 驱动 结构,见后续的 struct eth_driver

  1. 千兆网卡驱动初始化 rte_igb_pmd_init 函数调用图

rte_igb_pmd_init 注册了 rte_igb_pmd 驱动。后续 struct eth_driver 一节中有例子说明。

	rte_eal_init
	+-> rte_eal_dev_init
		+-> driver->init(NULL, NULL);
			+=> rte_igb_pmd_init		/* 千兆网卡驱动初始化 */
				+-> rte_eth_driver_register(&rte_igb_pmd); 
  1. 万兆网卡驱动初始化 rte_ixgbe_pmd_init 函数调用图

rte_ixgbe_pmd_init 注册了 rte_ixgbe_pmd 驱动。后续 struct eth_driver 一节中有例子说明。

	rte_eal_init
	+-> rte_eal_dev_init
		+-> driver->init(NULL, NULL);					
			+=> rte_ixgbe_pmd_init		/* 万兆网卡驱动初始化 */
				+-> rte_eth_driver_register(&rte_ixgbe_pmd);

2.2.2. struct eth_driver

struct eth_driver 表示的是一个以太网 PMD 驱动

由于其中的第一个成员变量 pci_drvstruct rte_pci_driver,所以可以强行转换为 struct rte_pci_driver
因此该结构体 为 可以认为是 struct rte_pci_driver 的子类。

所以前面所提及的 pci_driver_list 链表头,可以指向于 struct eth_driver

结构体当中最重要的是 eth_dev_initeth_dev_uninit,用于以太网设备的 初始化和 反初始化。

	struct eth_driver {
		struct rte_pci_driver pci_drv;    /**< The PMD is also a PCI driver. */
		eth_dev_init_t eth_dev_init;      /**< <== Device init function. */
		eth_dev_uninit_t eth_dev_uninit;  /**< <== Device uninit function. */
		unsigned int dev_private_size;    /**< Size of device private data. */
	};

struct eth_driverstruct rte_pci_driver 的关系
DPDK 16.07 驱动初始化和收发包函数学习笔记_第8张图片

注意:

struct eth_driver 继承自 struct rte_pci_driver

例子:

  1. 千兆以太网 PMD 驱动: rte_igb_pmd

注意,其中的 eth_dev_init 设置为 eth_igb_dev_init

	static struct eth_driver rte_igb_pmd = {
		.pci_drv = {
			.name = "rte_igb_pmd",
			.id_table = pci_id_igb_map,		/* <== 千兆 所支持 pci 设备的驱动 id 列表。*/
			.drv_flags = RTE_PCI_DRV_NEED_MAPPING | RTE_PCI_DRV_INTR_LSC |
				RTE_PCI_DRV_DETACHABLE,
		},
		.eth_dev_init = eth_igb_dev_init,	/* <== 千兆以太网设备初始化 */
		.eth_dev_uninit = eth_igb_dev_uninit,
		.dev_private_size = sizeof(struct e1000_adapter),
	};
  1. 万兆以太网 PMD 驱动: rte_ixgbe_pmd

注意,其中的 eth_dev_init 设置为 eth_ixgbe_dev_init

	static struct eth_driver rte_ixgbe_pmd = {
		.pci_drv = {
			.name = "rte_ixgbe_pmd",
			.id_table = pci_id_ixgbe_map,	/* <== 万兆 所支持 pci 设备的驱动 id 列表。*/
			.drv_flags = RTE_PCI_DRV_NEED_MAPPING | RTE_PCI_DRV_INTR_LSC |
				RTE_PCI_DRV_DETACHABLE,
		},
		.eth_dev_init = eth_ixgbe_dev_init,	/* <== 万兆以太网设备初始化 */
		.eth_dev_uninit = eth_ixgbe_dev_uninit,
		.dev_private_size = sizeof(struct ixgbe_adapter),
	};

设置了基础信息 的 以太网 PMD 驱动 rte_igb_pmd(千兆) 和 rte_ixgbe_pmd(万兆)
DPDK 16.07 驱动初始化和收发包函数学习笔记_第9张图片

注意:

  1. 成员变量 pci_drv.id_table 指向了 所支持 pci 设备的驱动 id 列表,用于 pci 设备的侦测。
  2. rte_igb_pmdrte_ixgbe_pmd 都只是 设置了 eth_dev_initeth_dev_uninit。后续 在调用 rte_eth_driver_register 的时候,会进一步的设置 成员变量 pci_drv 的内容。

2.2.2.1. id_table 的定义

全局变量 pci_id_igb_map(千兆) 或者 pci_id_ixgbe_map(万兆) 分别是 千兆 和 万兆 所支持 pci 设备的驱动 id 列表

例子:

  1. 千兆以太网 id_table:

pci_id_igb_map 的代码。

	/*
	 * The set of PCI devices this driver supports
	 */
	static const struct rte_pci_id pci_id_igb_map[] = {
		#define RTE_PCI_DEV_ID_DECL_IGB(vend, dev) {RTE_PCI_DEVICE(vend, dev)},
		#include "rte_pci_dev_ids.h"
		{0},
	};

千兆以太网 id_table#include "rte_pci_dev_ids.h" 后,代码展开后如下:

  1. 千兆 设备
	/* 千兆 设备: 
	 *     如果宏函数没有定义,默认是空的
	 *     所以不会插入任何代码
	 */
	#ifndef RTE_PCI_DEV_ID_DECL_IGB
	#define RTE_PCI_DEV_ID_DECL_IGB(vend, dev)
	#endif

	/******************** Physical IGB devices from e1000_hw.h ********************/
	/* 声明支持 千兆设备 若干*/
	RTE_PCI_DEV_ID_DECL_IGB(PCI_VENDOR_ID_INTEL, E1000_DEV_ID_I350_COPPER)	/* 前面例子的千兆设备 */
	RTE_PCI_DEV_ID_DECL_IGB(PCI_VENDOR_ID_INTEL, E1000_DEV_ID_I350_FIBER)
	RTE_PCI_DEV_ID_DECL_IGB(PCI_VENDOR_ID_INTEL, E1000_DEV_ID_I350_SERDES)
	RTE_PCI_DEV_ID_DECL_IGB(PCI_VENDOR_ID_INTEL, E1000_DEV_ID_I350_SGMII)
	RTE_PCI_DEV_ID_DECL_IGB(PCI_VENDOR_ID_INTEL, E1000_DEV_ID_I350_DA4)
	/* ... */

	/*
	 * Undef all RTE_PCI_DEV_ID_DECL_* here.
	 * 设置宏函数设为未定义
	 */
	#undef RTE_PCI_DEV_ID_DECL_IGB
	#undef RTE_PCI_DEV_ID_DECL_IXGBE
  1. 万兆以太网 id_table:

pci_id_ixgbe_map 的代码。

	/*
	 * The set of PCI devices this driver supports
	 */
	static const struct rte_pci_id pci_id_ixgbe_map[] = {
		#define RTE_PCI_DEV_ID_DECL_IXGBE(vend, dev) {RTE_PCI_DEVICE(vend, dev)},
		#include "rte_pci_dev_ids.h"
		{ .vendor_id = 0, /* sentinel */ },
	};

万兆以太网 id_table#include "rte_pci_dev_ids.h" 后,代码展开后如下:

	/* 万兆 设备:
	 *     如果宏函数没有定义,默认是空的
	 *     所以不会插入任何代码
	 */
	#ifndef RTE_PCI_DEV_ID_DECL_IXGBE
	#define RTE_PCI_DEV_ID_DECL_IXGBE(vend, dev)
	#endif
	
	/****************** Physical IXGBE devices from ixgbe_type.h ******************/
	/* 声明支持 万兆设备 若干*/
	RTE_PCI_DEV_ID_DECL_IXGBE(PCI_VENDOR_ID_INTEL, IXGBE_DEV_ID_82599_SFP) /* 前面例子的万兆设备 */

	/* ... */
	
	/*
	 * Undef all RTE_PCI_DEV_ID_DECL_* here.
	 * 设置宏函数设为未定义
	 */
	#undef RTE_PCI_DEV_ID_DECL_IGB
	#undef RTE_PCI_DEV_ID_DECL_IXGBE

代码分析:

  1. 在头文件 "rte_pci_dev_ids.h" 开始处。如果宏函数 RTE_PCI_DEV_ID_DECL_xxx 没有定义,则默认是空的,所以在默认情况下不会插入任何代码。
  2. pci_id_igb_map[] 等,在包含头文件之前,会正确的定义 RTE_PCI_DEV_ID_DECL_IGB() 等宏函数。所以已经定义过的宏函数可以正确展开,插入需要的驱动类型。
  3. 在头文件 "rte_pci_dev_ids.h" 结束处。所有的宏函数 RTE_PCI_DEV_ID_DECL_xxx 设为未定义。为下一次的使用作准备。

应用:

如果需要添加驱动。则需要先 在头文件中 "rte_pci_dev_ids.h" 添加 id。那么在 pci_probe_all_drivers 函数可以找到合适的驱动。

2.2.2.2. rte_eth_driver_register 函数

rte_eth_driver_register 函数 用于注册 以太网 PMD 驱动 (struct eth_driver)。

  1. rte_eth_driver_register 函数 会设置 eth_drv 内嵌的 pci_drv 结构体中 的 字段 devinitdevuninit。当后续 调用 rte_eth_dev_init 函数,进行 pci 设备侦测 的时候,就会 调用 devinit() 回调函数,来初始化 以太网设备
  2. rte_eth_driver_register 函数 内部 会调用 rte_eal_pci_register 注册 eth_drv

函数调用图如下:

	rte_eal_init
	+-> rte_eal_dev_init				/* 设备 的初始化 */
		+-> driver->init(NULL, NULL); 	/* 触发 dev_driver_list 上 所有 driver->init() 的调用。 */
			.	/* 千兆: rte_igb_pmd_init */
			+~> rte_igb_pmd_init		
			.	+=> rte_eth_driver_register(&rte_igb_pmd); /* <== 不同的驱动会调用 ret_eth_driver_register 来注册。 */
			.		+-> eth_drv->pci_drv.devinit = rte_eth_dev_init; /* 设置 网口 设备 初始化 回调函数。 */
			.		+-> eth_drv->pci_drv.devuninit = rte_eth_dev_uninit; /* 设置 网口 设备 反初始化 回调函数。 */
			.		+-> rte_eal_pci_register(&eth_drv->pci_drv)	/* 注册 pci 驱动。 */
			.			+-> TAILQ_INSERT_TAIL(&pci_driver_list, driver, next); /* 将驱动放置到 pci_driver_list 中。 */
			.
			.	/* 万兆: rte_ixgbe_pmd_init */
			+~> rte_ixgbe_pmd_init		
				+=> rte_eth_driver_register(&rte_ixgbe_pmd); /* <== 不同的驱动会调用 ret_eth_driver_register 来注册。 */
					+-> eth_drv->pci_drv.devinit = rte_eth_dev_init; /* 设置 网口 设备 初始化 回调函数。 */
					+-> eth_drv->pci_drv.devuninit = rte_eth_dev_uninit; /* 设置 网口 设备 反初始化 回调函数。 */
					+-> rte_eal_pci_register(&eth_drv->pci_drv)	/* 注册 pci 驱动。 */
						+-> TAILQ_INSERT_TAIL(&pci_driver_list, driver, next); /* 将驱动放置到 pci_driver_list 中。 */

源码分析如下:

注意:

无论 千兆 还是 万兆设备 其中的:

  1. pci_drv.devinit 都设置为 rte_eth_dev_init
  2. pci_drv.devuninit 都设置为 rte_eth_dev_uninit
	void
	rte_eth_driver_register(struct eth_driver *eth_drv)
	{
		eth_drv->pci_drv.devinit = rte_eth_dev_init;	/* <== devinit 都设置为 rte_eth_dev_init */
		eth_drv->pci_drv.devuninit = rte_eth_dev_uninit; /* <== devuninit 都设置为 rte_eth_dev_uninit */
		rte_eal_pci_register(&eth_drv->pci_drv);
	}

例子:

以下是 填充了 pci_drv.devinitpci_drv.devuninit 字段的以太网 PMD 驱动

设置了 pci_drv 的 以太网 PMD 驱动 rte_igb_pmd(千兆) 和 rte_ixgbe_pmd(万兆)
DPDK 16.07 驱动初始化和收发包函数学习笔记_第10张图片

2.2.2.2.1. rte_eal_pci_register 函数

rte_eal_pci_register 函数 将 驱动 追加到 pci_driver_list 链表 的尾部。

pci_driver_list 链表的表头 则在 rte_eal_pci_init 函数中,就已经初始化好了。

	/* register a driver */
	void
	rte_eal_pci_register(struct rte_pci_driver *driver)
	{
		TAILQ_INSERT_TAIL(&pci_driver_list, driver, next);
	}

例子:
以下是 注册后的 以太网 PMD 驱动 rte_igb_pmd(千兆) 和 rte_ixgbe_pmd(万兆):

DPDK 16.07 驱动初始化和收发包函数学习笔记_第11张图片

2.3. rte_eal_pci_probe 函数

rte_eal_pci_probe 函数 用于 pci 设备的侦测。

  1. 遍历 pci_device_list 中所有的 设备。
    1.1. 使用 pci_devargs_lookup 查询 pci 设备 用于数据。设置 设备 的 devargs
    1.2. 调用 pci_probe_all_drivers() 为 该设备选择合适的 驱动。

函数调用图如下:

	rte_eal_init
	+=> rte_eal_pci_probe		/* <== pci 设备的侦测(侦测和初始化) */

源码分析如下:

  1. rte_eal_devargs_type_count(RTE_DEVTYPE_WHITELISTED_PCI) 用于 查找 "白名单"的 pci 设备
    1.1. 白名单的 pci 设备,可以在命令行中 通过 -w 来指定。
    1.2. 如果命令行中没有设置 “白名单” 的 pci 设备,则返回 0。所以 probe_all 会设置为 1。表示侦测所有的 pci 设备
  2. pci_devargs_lookup 函数,是会查找 命令行中是否有设置 “黑/白名单” 的设备。
    2.1. 如果命令行中没有设置 “黑/白名单” 的 pci 设备,则返回 NULL
	/*
	 * Scan the content of the PCI bus, and call the devinit() function for
	 * all registered drivers that have a matching entry in its id_table
	 * for discovered devices.
	 */
	int
	rte_eal_pci_probe(void)
	{
		struct rte_pci_device *dev = NULL;
		struct rte_devargs *devargs;
		int probe_all = 0;
		int ret = 0;

		/* 没有设置 "白名单",则侦测所有的 pci 设备 */
		if (rte_eal_devargs_type_count(RTE_DEVTYPE_WHITELISTED_PCI) == 0)
			probe_all = 1;

		TAILQ_FOREACH(dev, &pci_device_list, next) {

			/* set devargs in PCI structure */
			devargs = pci_devargs_lookup(dev); /* 一般,devargs 为 NULL */
			if (devargs != NULL)
				dev->devargs = devargs;

			/* probe all or only whitelisted devices */
			if (probe_all)	/* 一般,侦测所有的 pci 设备 */
				ret = pci_probe_all_drivers(dev);
			else if (devargs != NULL &&
				devargs->type == RTE_DEVTYPE_WHITELISTED_PCI)
				ret = pci_probe_all_drivers(dev);
				
			if (ret < 0)
				rte_exit(EXIT_FAILURE, "Requested device " PCI_PRI_FMT
					 " cannot be used\n", dev->addr.domain, dev->addr.bus,
					 dev->addr.devid, dev->addr.function);
		}

		return 0;
	}

2.3.1. pci_probe_all_drivers 函数

pci_probe_all_drivers 函数,会探测所有的 驱动。一旦匹配,则用来初始化设备。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+=> pci_probe_all_drivers	 /* <== pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> TAILQ_FOREACH(dr, &pci_driver_list, next)	/* 遍历所有 驱动 */
			+-> rte_eal_pci_probe_one_driver(dr, dev)	/* 探测 单个驱动 dr 和 设备 dev */

注意:

pci_driver_list 链表 中的元素 虽然是 struct eth_driver 类型。但是可以强转为 struct rte_pci_device 的。

源码分析如下:

  1. 从 pci_driver_list 列表中上 遍历所有的 驱动 dr。
    1.1. 将 驱动 dr 和 设置 dev。通过 rte_eal_pci_probe_one_driver() 进行探测比对。
    1.1.1. 探测比对 过程出错,返回 -1。
    1.1.2. 探测比对 不匹配,继续尝试匹配下一个 驱动。
    1.1.3. 探测比对 匹配,返回 0。
  2. 如果 所有的 驱动都不匹配,返回 1。
	/*
	 * If vendor/device ID match, call the devinit() function of all
	 * registered driver for the given device. Return -1 if initialization
	 * failed, return 1 if no driver is found for this device.
	 */
	static int
	pci_probe_all_drivers(struct rte_pci_device *dev)
	{
		struct rte_pci_driver *dr = NULL;
		int rc = 0;

		if (dev == NULL)
			return -1;

		TAILQ_FOREACH(dr, &pci_driver_list, next) {		/* 遍历所有 驱动 */
			rc = rte_eal_pci_probe_one_driver(dr, dev);	/* 探测 单个驱动 dr 和 设备 dev */
			if (rc < 0) /* 出错 */
				/* negative value is an error */
				return -1;
			if (rc > 0) /* 驱动和设备不匹配,测试下一个 驱动 */
				/* positive value means driver doesn't support it */
				continue;
			return 0;	/* 驱动和设备匹配 */
		}
		return 1;
	}

2.3.1.1. rte_eal_pci_probe_one_driver 函数

rte_eal_pci_probe_one_driver 函数 探测 单个驱动 dr 和 设备 dev 是否适合。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+=> rte_eal_pci_probe_one_driver /* <== 探测 驱动 和 设备 是否适合 */
				+-> rte_eal_pci_map_device(dev); 	/* igb_uio 地址映射 */
				+-> dev->driver = dr;		/* 设备 的驱动字段。指向 探测出来的 驱动 dr。 */
				+-> dr->devinit(dr, dev) 	/* 使用 驱动 的 devinit 来初始化设备。 */

源码分析如下:

  1. 遍历驱动 drid_table[] 数组。
    1.1. 如果设备 dev 上的 id 字段,与 驱动 dr 某个 id_table[] 的元素匹配上。
    1.1.1. 则匹配成功,跳到 1.2 进行后续的初始化动作。
    1.1.2. 否则测试下一个 id_table[] 的元素。
    1.2. 如果 设备 dev 是属于 “黑名单” 中。则不予初始化。
    1.3. 如果 驱动 dr->drv_flagsRTE_PCI_DRV_NEED_MAPPING 标志。则 设备做内存映射。
    1.3.1. 否则如果 驱动 dr->drv_flagsRTE_PCI_DRV_FORCE_UNBIND 标志 并且是 主进程。则 设备解绑。
    1.4. 设备 devdriver 字段指向 探测出来的 驱动 dr
    1.5. 使用 驱动 drdevinit() 来初始化设备。

rte_eal_pci_probe_one_driver 函数 返回值:

  • 出错,返回 -1
  • 驱动和设备不匹配,返回 1
  • 驱动和设备匹配,返回 0
	/*
	 * If vendor/device ID match, call the devinit() function of the
	 * driver.
	 */
	static int
	rte_eal_pci_probe_one_driver(struct rte_pci_driver *dr, struct rte_pci_device *dev)
	{
		int ret;
		const struct rte_pci_id *id_table;

		for (id_table = dr->id_table; id_table->vendor_id != 0; id_table++) {
			/* 测试 驱动 dr 的 id_table 和 设备 dev 的 id 是否匹配 */
			/* check if device's identifiers match the driver's ones */
			if (id_table->vendor_id != dev->id.vendor_id &&
					id_table->vendor_id != PCI_ANY_ID)
				continue;
			if (id_table->device_id != dev->id.device_id &&
					id_table->device_id != PCI_ANY_ID)
				continue;
			if (id_table->subsystem_vendor_id != dev->id.subsystem_vendor_id &&
					id_table->subsystem_vendor_id != PCI_ANY_ID)
				continue;
			if (id_table->subsystem_device_id != dev->id.subsystem_device_id &&
					id_table->subsystem_device_id != PCI_ANY_ID)
				continue;
			if (id_table->class_id != dev->id.class_id &&
					id_table->class_id != RTE_CLASS_ANY_ID)
				continue;

			struct rte_pci_addr *loc = &dev->addr;

			RTE_LOG(INFO, EAL, "PCI device "PCI_PRI_FMT" on NUMA socket %i\n",
					loc->domain, loc->bus, loc->devid, loc->function,
					dev->numa_node);

			/*如果 设备 dev 是属于"黑名单"中。则不予初始化。*/
			/* no initialization when blacklisted, return without error */
			if (dev->devargs != NULL &&
				dev->devargs->type == RTE_DEVTYPE_BLACKLISTED_PCI) {
				RTE_LOG(INFO, EAL, "  Device is blacklisted, not initializing\n");
				return 1;
			}

			RTE_LOG(INFO, EAL, "  probe driver: %x:%x %s\n", dev->id.vendor_id,
					dev->id.device_id, dr->name);

			/* 如果 驱动 dr->drv_flags 有 RTE_PCI_DRV_NEED_MAPPING 标志。则 设备做内存映射。
			 * 否则如果 驱动 dr->drv_flags 有 RTE_PCI_DRV_FORCE_UNBIND 标志 并且是 主进程。则 设备解绑。
			 */
			if (dr->drv_flags & RTE_PCI_DRV_NEED_MAPPING) {
				/* map resources for devices that use igb_uio */
				ret = rte_eal_pci_map_device(dev);
				if (ret != 0)
					return ret;
			} else if (dr->drv_flags & RTE_PCI_DRV_FORCE_UNBIND &&
					rte_eal_process_type() == RTE_PROC_PRIMARY) {
				/* unbind current driver */
				if (pci_unbind_kernel_driver(dev) < 0)
					return -1;
			}

			/* 设备 dev 的 driver 字段指向 探测出来的 驱动 dr */
			/* reference driver structure */
			dev->driver = dr;

			/* 使用 驱动 dr 的 devinit() 来初始化设备。 */
			/* call the driver devinit() function */
			return dr->devinit(dr, dev);
		}
		/* return positive value if driver doesn't support this device */
		return 1;
	}

rte_eal_pci_probe_one_driver 图例:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第12张图片

注意:

无论 千兆以太网 PMD 驱动(rte_igb_pmd) 还是 万兆以太网 PMD 驱动(rte_ixgbe_pmd)都使用了 rte_eth_driver_register 来注册。所以其中的 pci_drv.devinit 都设置为 rte_eth_dev_init()

2.3.1.1.1. rte_eal_pci_map_device 函数

rte_eal_pci_map_device 函数,用于映射pci 设备

  1. 如果内核驱动类型为RTE_KDRV_VFIO (虚拟网卡),使用pci_vfio_map_resource()函数映射。
  2. 如果内核驱动类型为RTE_KDRV_IGB_UIO 或者 RTE_KDRV_UIO_GENERIC(物理网卡),使用pci_uio_map_resource()函数映射。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /*  探测 驱动 和 设备 是否适合 */
				+=> rte_eal_pci_map_device(dev); 	/* <== igb_uio 地址映射 */
					.
					.	/* case: [RTE_KDRV_VFIO] */
					+-> pci_vfio_map_resource(dev);	/*  映射 vfio 驱动(虚拟网卡) */
					.
					.	/* case: [RTE_KDRV_IGB_UIO | RTE_KDRV_UIO_GENERIC] */
					+-> pci_uio_map_resource(dev);	/*  映射 uio 驱动(物理网卡) */

函数源码如下:

	/* Map pci device */
	int
	rte_eal_pci_map_device(struct rte_pci_device *dev)
	{
		int ret = -1;

		/* try mapping the NIC resources using VFIO if it exists */
		switch (dev->kdrv) {
		case RTE_KDRV_VFIO:
	#ifdef VFIO_PRESENT
			if (pci_vfio_is_enabled())
				ret = pci_vfio_map_resource(dev);
	#endif
			break;
		case RTE_KDRV_IGB_UIO:
		case RTE_KDRV_UIO_GENERIC:
			/* map resources for devices that use uio */
			ret = pci_uio_map_resource(dev);
			break;
		default:
			RTE_LOG(DEBUG, EAL,
				"  Not managed by a supported kernel driver, skipped\n");
			ret = 1;
			break;
		}

		return ret;
	}
2.3.1.1.1.1. `pci_uio_map_resource’ 函数

pci_uio_map_resource' 函数,会将pci设备uio 资源` 映射到 虚拟地址。

  1. 调用 pci_uio_alloc_resource 分配 uio 资源,用变量 uio_res 来指向新的uio 资源
  2. 多次调用 pci_uio_map_resource_by_index 映射pci设备资源到大页内存。
	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /*  探测 驱动 和 设备 是否适合 */
				+-> rte_eal_pci_map_device(dev); 	/* igb_uio 地址映射 */
					.
					.	/* case: [RTE_KDRV_VFIO] */
					+-> pci_vfio_map_resource(dev);	/*  映射 vfio 驱动(虚拟网卡) */
					.
					.	/* case: [RTE_KDRV_IGB_UIO | RTE_KDRV_UIO_GENERIC] */
					+=> pci_uio_map_resource(dev);	/* <== 映射 uio 驱动(物理网卡) */
						.
						.	/* case: [secondary processes] */
						+->	pci_uio_map_secondary(dev);
						.
						.	/* case: [primary process] */
						+->	pci_uio_alloc_resource();			/* 分配 uio 资源 内存空间 */
						|	+-> dev->intr_handle.fd = open(devname, O_RDWR);	/* 打开 uio 资源文件 */
						|	+-> dev->intr_handle.uio_cfg_fd = open(cfgname, O_RDWR); /* 打开 uio 资源配置文件 */
						|
						+->	pci_uio_map_resource_by_index();	/* 映射 uio 资源 */
						|	+-> pci_map_addr = pci_find_max_end_va();	/* 在大页内存的尾部找一个地方,作为`pci设备`资源映射出来的首地址 */
						|	+-> mapaddr = pci_map_resource(pci_map_addr,...);	/* 映射`pci设备`资源到大页内存 */
						|	|	+-> mmap(requested_addr, size,...,fd);
						|	+-> pci_map_addr = RTE_PTR_ADD(mapaddr,...);		/* 初始化大页内存的`pci设备`资源 */
						|
						+-> TAILQ_INSERT_TAIL(uio_res_list, uio_res, next);	/* 将 uio 资源追加到uio_res_list 链表 */

`pci_uio_map_resource’ 函数源码如下:

	/* map the PCI resource of a PCI device in virtual memory */
	int
	pci_uio_map_resource(struct rte_pci_device *dev)
	{
		int i, map_idx = 0, ret;
		uint64_t phaddr;
		struct mapped_pci_resource *uio_res = NULL;
		struct mapped_pci_res_list *uio_res_list =
			RTE_TAILQ_CAST(rte_uio_tailq.head, mapped_pci_res_list);

		dev->intr_handle.fd = -1;
		dev->intr_handle.uio_cfg_fd = -1;
		dev->intr_handle.type = RTE_INTR_HANDLE_UNKNOWN;

		/* secondary processes - use already recorded details */
		if (rte_eal_process_type() != RTE_PROC_PRIMARY)
			return pci_uio_map_secondary(dev);

		/* 分配 uio 资源 内存空间 */
		/* allocate uio resource */
		ret = pci_uio_alloc_resource(dev, &uio_res);
		if (ret)
			return ret;

		/* Map all BARs */
		for (i = 0; i != PCI_MAX_RESOURCE; i++) {
			/* 跳过 phys_addr 为 0 的 资源 */
			/* skip empty BAR */
			phaddr = dev->mem_resource[i].phys_addr;
			if (phaddr == 0)
				continue;

			/* 映射 uio 资源 */
			ret = pci_uio_map_resource_by_index(dev, i,
					uio_res, map_idx);
			if (ret)
				goto error;

			map_idx++;
		}

		uio_res->nb_maps = map_idx;

		TAILQ_INSERT_TAIL(uio_res_list, uio_res, next);

		return 0;
	error:
		for (i = 0; i < map_idx; i++) {
			pci_unmap_resource(uio_res->maps[i].addr,
					(size_t)uio_res->maps[i].size);
			rte_free(uio_res->maps[i].path);
		}
		pci_uio_free_resource(dev, uio_res);
		return -1;
	}

例子:

打印 pci设备 资源 映射地址。
完整的资源 映射地址可以参考前面的:[2.1.3.4.3. 解释 pci 设备resource]。

  1. 千兆 以太网 设备:
	# `pci设备` 资源 映射地址
	cat /sys/bus/pci/devices/0000:04:00.0/resource
	>	# 只打印有效的资源映射地址 ((flags & IORESOURCE_MEM) && (phys_addr != 0))。
	>	# i  # phys_addr        # end_addr         # flags           
	>	00   0x00000000f7b60000 0x00000000f7b7ffff 0x0000000000040200
	>	03   0x00000000f7b8c000 0x00000000f7b8ffff 0x0000000000040200
	>	07   0x00000000f0000000 0x00000000f001ffff 0x000000000014220c
	>	10   0x00000000f0020000 0x00000000f003ffff 0x000000000014220c
  1. 万兆 以太网 设备:
	# `pci设备` 资源 映射地址
	cat /sys/bus/pci/devices/0000:04:00.0/resource
	>	# 只打印有效的资源映射地址 ((flags & IORESOURCE_MEM) && (phys_addr != 0))。
	>	# i  # phys_addr        # end_addr         # flags  
	>	00   0x00000000f7920000 0x00000000f793ffff 0x0000000000140204
	>	04   0x00000000f7944000 0x00000000f7947fff 0x0000000000140204
	>	07   0x00000000f0300000 0x00000000f03fffff 0x000000000014220c
	>	10   0x00000000f0400000 0x00000000f04fffff 0x000000000014220c

####### 2.3.1.1.1.1.1. pci_uio_alloc_resource 函数

pci_uio_alloc_resource 函数,用于分配 uio 资源 内存空间。

  1. 查找 uio 资源的文件名称。
  2. 打开 uio 资源文件("/dev/uio${uio_id}"),保存 文件描述符到 dev->intr_handle.fd。
  3. 打开 uio 资源配置文件("sys/class/uio/uio${uio_id}/device/config"),保存 文件描述符到 dev->intr_handle.uio_cfg_fd。
  4. 设置 dev->intr_handle.type 的类型。
  5. 分配 uio 资源 内存空间 ,并且填充成员变量。

在绑定网口后,内核驱动 uio 会生成的系统文件:

  • uio 资源文件:("/dev/uio${uio_id}")
  • uio 设备文件目录:("sys/class/uio/uio${uio_id}")
  • uio 资源配置文件:("sys/class/uio/uio${uio_id}/device/config")
    其中 ${uio_id}uio 设备id。和 绑定的网口号 ${port_id} 一一对应。

这些的 uio 的系统文件。是内核态和用户态沟通的桥梁。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
				+-> rte_eal_pci_map_device(dev); 	/* igb_uio 地址映射 */
					.	/* case: [RTE_KDRV_IGB_UIO | RTE_KDRV_UIO_GENERIC] */
					+-> pci_uio_map_resource(dev);	/*  映射 uio 驱动(物理网卡) */
						.	/* case: [primary process] */
						+=>	pci_uio_alloc_resource();			/* <== 分配 uio 资源 内存空间 */
						|	+-> dev->intr_handle.fd = open(devname, O_RDWR);	/* 打开 uio 资源文件 */
						|	+-> dev->intr_handle.uio_cfg_fd = open(cfgname, O_RDWR); /* 打开 uio 资源配置文件 */
						|
						+->	pci_uio_map_resource_by_index();	/* 映射 uio 资源 */
						+-> TAILQ_INSERT_TAIL(uio_res_list, uio_res, next);	/* 将 uio 资源追加到uio_res_list 链表 */

函数源码如下:

	int
	pci_uio_alloc_resource(struct rte_pci_device *dev,
			struct mapped_pci_resource **uio_res)
	{
		char dirname[PATH_MAX];
		char cfgname[PATH_MAX];
		char devname[PATH_MAX]; /* contains the /dev/uioX */
		int uio_num;
		struct rte_pci_addr *loc;
		
		loc = &dev->addr;

		/* 查找 uio 资源的文件名称 */
		/* find uio resource */
		uio_num = pci_get_uio_dev(dev, dirname, sizeof(dirname), 1);
		if (uio_num < 0) {
			RTE_LOG(WARNING, EAL, "  "PCI_PRI_FMT" not managed by UIO driver, "
					"skipping\n", loc->domain, loc->bus, loc->devid, loc->function);
			return 1;
		}
		snprintf(devname, sizeof(devname), "/dev/uio%u", uio_num);

		/* 打开 uio 资源文件,保存 文件描述符到 dev->intr_handle.fd */
		/* save fd if in primary process */
		dev->intr_handle.fd = open(devname, O_RDWR);
		if (dev->intr_handle.fd < 0) {
			RTE_LOG(ERR, EAL, "Cannot open %s: %s\n",
				devname, strerror(errno));
			goto error;
		}

		/* 打开 uio 资源配置文件,保存 文件描述符到 dev->intr_handle.uio_cfg_fd */
		snprintf(cfgname, sizeof(cfgname),
				"/sys/class/uio/uio%u/device/config", uio_num);
		dev->intr_handle.uio_cfg_fd = open(cfgname, O_RDWR);
		if (dev->intr_handle.uio_cfg_fd < 0) {
			RTE_LOG(ERR, EAL, "Cannot open %s: %s\n",
				cfgname, strerror(errno));
			goto error;
		}

		/* 设置 dev->intr_handle.type 的类型 */
		if (dev->kdrv == RTE_KDRV_IGB_UIO)
			dev->intr_handle.type = RTE_INTR_HANDLE_UIO;
		else {
			dev->intr_handle.type = RTE_INTR_HANDLE_UIO_INTX;

			/* set bus master that is not done by uio_pci_generic */
			if (pci_uio_set_bus_master(dev->intr_handle.uio_cfg_fd)) {
				RTE_LOG(ERR, EAL, "Cannot set up bus mastering!\n");
				goto error;
			}
		}

		/* 分配 uio 资源 内存空间 ,并且填充成员变量 */
		/* allocate the mapping details for secondary processes*/
		*uio_res = rte_zmalloc("UIO_RES", sizeof(**uio_res), 0);
		if (*uio_res == NULL) {
			RTE_LOG(ERR, EAL,
				"%s(): cannot store uio mmap details\n", __func__);
			goto error;
		}

		snprintf((*uio_res)->path, sizeof((*uio_res)->path), "%s", devname);
		memcpy(&(*uio_res)->pci_addr, &dev->addr, sizeof((*uio_res)->pci_addr));

		return 0;

	error:
		pci_uio_free_resource(dev, *uio_res);
		return -1;
	}

例子:

使用命令行,打印 uio 资源。

  1. 千兆 以太网 设备:
	# `uio 资源文件` (`"/dev/uio${uio_id}"`)
	ll /dev/uio0
	>	crw------- 1 root root 247, 0 Feb 11 11:13 /dev/uio0

	# `uio 设备`文件目录(`"sys/class/uio/uio${uio_id}"`)
	ll /sys/class/uio/uio0
	>	lrwxrwxrwx 1 root root 0 Mar  4 11:33 /sys/class/uio/uio0 -> ../../devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:05.0/0000:04:00.0/uio/uio0

	# `uio 资源配置文件`(`"sys/class/uio/uio${uio_id}/device/config"`) 
	ll /sys/class/uio/uio0/device/config
	>	-rw-r--r-- 1 root root 4096 Feb 11 08:56 /sys/class/uio/uio0/device/config
  1. 万兆 以太网 设备:
	# `uio 资源文件` (`"/dev/uio${uio_id}"`)
	ll /dev/uio1
	>	crw------- 1 root root 247, 1 Feb 11 11:13 /dev/uio1
	
	# `uio 设备`文件目录(`"sys/class/uio/uio${uio_id}"`)
	ll /sys/class/uio/uio1
	>	lrwxrwxrwx 1 root root 0 Feb 11 11:14 /sys/class/uio/uio1 -> ../../devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:09.0/0000:06:00.0/0000:07:02.0/0000:08:00.0/uio/uio1
	
	# `uio 资源配置文件`(`"sys/class/uio/uio${uio_id}/device/config"`) 
	ll /sys/class/uio/uio1/device/config
	>	-rw-r--r-- 1 root root 4096 Feb 11 08:56 /sys/class/uio/uio1/device/config

####### 2.3.1.1.1.1.2. pci_uio_map_resource_by_index 函数

pci_uio_map_resource_by_index 函数,用于映射pci设备资源到大页内存。

  1. 取得 pci设备 资源路径 的名称。
  2. pci设备 资源文件 的名称分配空间。
  3. 打开 pci设备 资源 文件 ("/sys/bus/pci/devices/ p c i a d d r e s s / r e s o u r c e / r e s o u r c e {pci_address}/resource/resource pciaddress/resource/resource{resource_id}")。
  4. 第一次的时候,在大页内存的尾部找一个地方,作为pci设备资源映射出来的首地址。
  5. 映射pci设备资源到大页内存。
  6. 指向下一个映射地址。
  7. 初始化大页内存的pci设备资源。

在绑定网口后,内核驱动 uio 会生成 pci设备 资源文件:

  • ``pci设备资源文件:("/sys/bus/pci/devices/${pci_address}/resource/resource${resource_id}")
    其中
  • ${pci_address} 是 pci 地址。如 “0000:04:00.0” 等。
  • ${resource_id} 是 资源文件的号码。

注意:

  • pci设备资源的映射。是实现网卡收发包的零拷贝的关键。

函数调用图如下:

	rte_eal_init
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /*  探测 驱动 和 设备 是否适合 */
				+-> rte_eal_pci_map_device(dev); 	/* igb_uio 地址映射 */
					.	/* case: [RTE_KDRV_IGB_UIO | RTE_KDRV_UIO_GENERIC] */
					+-> pci_uio_map_resource(dev);	/* 映射 uio 驱动(物理网卡) */
						.	/* case: [primary process] */
						+->	pci_uio_alloc_resource();			/* 分配 uio 资源 内存空间 */
						|
						+=>	pci_uio_map_resource_by_index();	/* <== 映射 uio 资源 */
						|	+-> pci_map_addr = pci_find_max_end_va();	/* 在大页内存的尾部找一个地方,作为`pci设备`资源映射出来的首地址 */
						|	+-> mapaddr = pci_map_resource(pci_map_addr,...);	/* 映射`pci设备`资源到大页内存 */
						|	|	+-> mmap(requested_addr, size,...,fd);
						|	+-> pci_map_addr = RTE_PTR_ADD(mapaddr,...);		/* 初始化大页内存的`pci设备`资源 */
						|
						+-> TAILQ_INSERT_TAIL(uio_res_list, uio_res, next);	/* 将 uio 资源追加到uio_res_list 链表 */

函数源码如下:

	int
	pci_uio_map_resource_by_index(struct rte_pci_device *dev, int res_idx,
			struct mapped_pci_resource *uio_res, int map_idx)
	{
		int fd;
		char devname[PATH_MAX];
		void *mapaddr;
		struct rte_pci_addr *loc;
		struct pci_map *maps;

		loc = &dev->addr;
		maps = uio_res->maps;

		/* 取得 pci设备 资源路径 的名称 */
		/* update devname for mmap  */
		snprintf(devname, sizeof(devname),
				"%s/" PCI_PRI_FMT "/resource%d",
				pci_get_sysfs_path(),
				loc->domain, loc->bus, loc->devid,
				loc->function, res_idx);

		/* 为 pci设备 资源路径 的名称分配空间 */
		/* allocate memory to keep path */
		maps[map_idx].path = rte_malloc(NULL, strlen(devname) + 1, 0);
		if (maps[map_idx].path == NULL) {
			RTE_LOG(ERR, EAL, "Cannot allocate memory for path: %s\n",
					strerror(errno));
			return -1;
		}
		
		/* 打开 pci设备 资源路径 文件 */
		/*
		 * open resource file, to mmap it
		 */
		fd = open(devname, O_RDWR);
		if (fd < 0) {
			RTE_LOG(ERR, EAL, "Cannot open %s: %s\n",
					devname, strerror(errno));
			goto error;
		}

		/* 第一次的时候,在大页内存的尾部找一个地方,
		 * 作为pci设备资源映射出来的首地址 */
		/* try mapping somewhere close to the end of hugepages */
		if (pci_map_addr == NULL)
			pci_map_addr = pci_find_max_end_va();

		/* 映射pci设备资源到大页内存 */
		mapaddr = pci_map_resource(pci_map_addr, fd, 0,
				(size_t)dev->mem_resource[res_idx].len, 0);
		close(fd);
		if (mapaddr == MAP_FAILED)
			goto error;

		/* 指向下一个映射地址 */
		pci_map_addr = RTE_PTR_ADD(mapaddr,
				(size_t)dev->mem_resource[res_idx].len);

		/* 初始化大页内存的pci设备资源 */
		maps[map_idx].phaddr = dev->mem_resource[res_idx].phys_addr;
		maps[map_idx].size = dev->mem_resource[res_idx].len;
		maps[map_idx].addr = mapaddr;
		maps[map_idx].offset = 0;
		strcpy(maps[map_idx].path, devname);
		dev->mem_resource[res_idx].addr = mapaddr;

		return 0;

	error:
		rte_free(maps[map_idx].path);
		return -1;
	}

例子:

通过命令行打印出`pci设备 资源路径文件。

  1. 千兆 以太网 设备:
	# `pci设备` 的名称
	ll  /sys/bus/pci/devices/0000:04:00.0
	>	lrwxrwxrwx 1 root root 0 Mar  4 11:24 /sys/bus/pci/devices/0000:04:00.0 -> ../../../devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:05.0/0000:04:00.0

	# `pci设备` 资源 映射地址
	cat /sys/bus/pci/devices/0000:04:00.0/resource
	>	# 只打印有效的资源映射地址。
	>	# i  # phys_addr        # end_addr         # flags           
	>	00   0x00000000f7b60000 0x00000000f7b7ffff 0x0000000000040200	<== 有对应 资源文件 resource0 ---------+
	>	03   0x00000000f7b8c000 0x00000000f7b8ffff 0x0000000000040200	<== 有对应 资源文件 resource3 ---------|--+
	>	07   0x00000000f0000000 0x00000000f001ffff 0x000000000014220c	<== 无对应 资源文件                    |  |
	>	10   0x00000000f0020000 0x00000000f003ffff 0x000000000014220c	<== 无对应 资源文件                    |  |
	                                                                                                           |  |
	# `pci设备` 资源 文件                                                                                      |  |
	ll /sys/bus/pci/devices/0000:04:00.0/resource                                                              |  |
	>	-rw------- 1 root root 131072 Mar  4 11:32 resource0	# 千兆 `pci设备` 资源文件  <== 可以成功 mmap --+  |
	>	-rw------- 1 root root     32 Mar  4 11:32 resource2	# 千兆 `pci设备` 资源文件                         |
	>	-rw------- 1 root root  16384 Mar  4 11:32 resource3	# 千兆 `pci设备` 资源文件  <== 可以成功 mmap -----+
  1. 万兆 以太网 设备:
	# `pci设备` 的名称
	ll  /sys/bus/pci/devices/0000:08:00.0
	>	lrwxrwxrwx 1 root root 0 Mar  4 11:24 /sys/bus/pci/devices/0000:08:00.0 -> ../../../devices/pci0000:00/0000:00:01.0/0000:01:00.0/0000:02:09.0/0000:06:00.0/0000:07:02.0/0000:08:00.0

	# `pci设备` 资源 映射地址
	cat /sys/bus/pci/devices/0000:04:00.0/resource
	>	# 只打印有效的资源映射地址。
	>	# i  # phys_addr        # end_addr         # flags  
	>	00   0x00000000f7920000 0x00000000f793ffff 0x0000000000140204	<== 有对应 资源文件 resource0 ---------+
	>	04   0x00000000f7944000 0x00000000f7947fff 0x0000000000140204	<== 有对应 资源文件 resource4 ---------|--+
	>	07   0x00000000f0300000 0x00000000f03fffff 0x000000000014220c	<== 无对应 资源文件                    |  |
	>	10   0x00000000f0400000 0x00000000f04fffff 0x000000000014220c	<== 无对应 资源文件                    |  |
	                                                                                                           |  |
	# `pci设备` 资源 文件                                                                                      |  |
	ll  /sys/bus/pci/devices/0000:08:00.0/resource                                                             |  |
	>	-rw------- 1 root root 131072 Mar  4 11:32 resource0	# 万兆 `pci设备` 资源文件  <== 可以成功 mmap --+  |
	>	-rw------- 1 root root  16384 Mar  4 11:32 resource4	# 万兆 `pci设备` 资源文件  <== 可以成功 mmap -----+

2.3.1.1.2. rte_eth_dev_init 函数

rte_eth_dev_init 用于 以太网设备的初始化。

函数调用图如下:

	rte_eal_init						
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
				+->	dr->devinit(dr, dev);		/* 触发 驱动 对 设备 初始化。参见 rte_eth_driver_register */
					+=> rte_eth_dev_init		/* <== 网口 设备 初始化。 */
						+-> rte_eth_dev_create_unique_device_name	/* 创建 唯一的 设备名称 */
						+-> eth_dev = rte_eth_dev_allocate /* 为 以太网设备 分配空间 */
						+-> eth_dev->data->dev_private = rte_zmalloc() /* 为 以太网设备的私有数据 分配空间 */
						+-> TAILQ_INIT(&(eth_dev->link_intr_cbs)) /* 初始化中断处理回调函数链表 */
						+-> (*eth_drv->eth_dev_init)(eth_dev); /* 调用 PMD 设备的初始化 回调函数 */

源码分析如下:

  1. 创建 唯一的 设备名称。
  2. 为 以太网设备 分配空间。
  3. 为 以太网设备的私有数据 分配空间。
  4. 初始化中断处理回调函数链表。
  5. 调用 PMD 设备的初始化 回调函数。这个类似于在基类的初始化后,再执行派生类的初始化。
  6. 初始化失败后的 处理。
	static int
	rte_eth_dev_init(struct rte_pci_driver *pci_drv,
			 struct rte_pci_device *pci_dev)
	{
		struct eth_driver    *eth_drv;
		struct rte_eth_dev *eth_dev;
		char ethdev_name[RTE_ETH_NAME_MAX_LEN];

		int diag;

		eth_drv = (struct eth_driver *)pci_drv;

		/* ------------------------------------*/
		/* 创建 唯一的 设备名称 */
		/* Create unique Ethernet device name using PCI address */
		rte_eth_dev_create_unique_device_name(ethdev_name,
				sizeof(ethdev_name), pci_dev);

		/* ------------------------------------*/
		/* 为 以太网设备 分配空间 */
		eth_dev = rte_eth_dev_allocate(ethdev_name, RTE_ETH_DEV_PCI);
		if (eth_dev == NULL)
			return -ENOMEM;

		/* ------------------------------------*/
		/* 为 以太网设备的私有数据 分配空间 */
		if (rte_eal_process_type() == RTE_PROC_PRIMARY) {
			eth_dev->data->dev_private = rte_zmalloc("ethdev private structure",
					  eth_drv->dev_private_size,
					  RTE_CACHE_LINE_SIZE);
		}
		eth_dev->pci_dev = pci_dev;
		eth_dev->driver = eth_drv;
		eth_dev->data->rx_mbuf_alloc_failed = 0;

		/* ------------------------------------*/
		/* 初始化中断处理回调函数链表 */
		/* init user callbacks */
		TAILQ_INIT(&(eth_dev->link_intr_cbs));

		/* ------------------------------------*/
		/* 设置默认 MTU */
		/*
		 * Set the default MTU.
		 */
		eth_dev->data->mtu = ETHER_MTU;

		/* ------------------------------------*/
		/* 调用 PMD 设备的初始化 回调函数 */
		/* Invoke PMD device initialization function */
		diag = (*eth_drv->eth_dev_init)(eth_dev);
		if (diag == 0)
			return 0;

		/* ------------------------------------*/
		/* 初始化失败后的 处理 */
		if (rte_eal_process_type() == RTE_PROC_PRIMARY)
			rte_free(eth_dev->data->dev_private);
		rte_eth_dev_release_port(eth_dev);
		return diag;
	}

后续的 六个小节会详细的分析 以太网设备的初始化 的过程。

2.3.1.1.2.1. 以太网设备的结构体:struct rte_eth_dev

struct rte_eth_dev 表示一个以太网设备

	struct rte_eth_dev {
		eth_rx_burst_t rx_pkt_burst; /**< Pointer to PMD receive function. */
		eth_tx_burst_t tx_pkt_burst; /**< Pointer to PMD transmit function. */
		struct rte_eth_dev_data *data;  /**< Pointer to device data */
		const struct eth_driver *driver;/**< Driver for this device */
		const struct eth_dev_ops *dev_ops; /**< Functions exported by PMD */
		struct rte_pci_device *pci_dev; /**< PCI info. supplied by probing */
		/** User application callbacks for NIC interrupts */
		struct rte_eth_dev_cb_list link_intr_cbs;
		/**
		 * User-supplied functions called from rx_burst to post-process
		 * received packets before passing them to the user
		 */
		struct rte_eth_rxtx_callback *post_rx_burst_cbs[RTE_MAX_QUEUES_PER_PORT];
		/**
		 * User-supplied functions called from tx_burst to pre-process
		 * received packets before passing them to the driver for transmission.
		 */
		struct rte_eth_rxtx_callback *pre_tx_burst_cbs[RTE_MAX_QUEUES_PER_PORT];
		uint8_t attached; /**< Flag indicating the port is attached */
		enum rte_eth_dev_type dev_type; /**< Flag indicating the device type */
	} __rte_cache_aligned;

重点:

  1. driver 字段,指向 以太网设备 的 驱动。
  2. pci_dev 字段,指向 以太网设备 的 所属的 pci 设备。由于 以太网设备 是通过 pci 总线连接到 CPU 的,所以也是属于 pci 设备。
  3. data 字段,指向 以太网设备 的数据。
  4. dev_ops 字段,指向 以太网设备 的 操作。

struct rte_eth_dev 类图:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第13张图片

2.3.1.1.2.2. 创建 唯一的 设备名称

rte_eth_dev_init 中,通过 rte_eth_dev_create_unique_device_name 使用 pci 地址 拼接出 唯一的 设备名称。
该 唯一的 设备名称,用于后续的 rte_eth_dev_allocate 函数。

	static int
	rte_eth_dev_create_unique_device_name(char *name, size_t size,
			struct rte_pci_device *pci_dev)
	{
		int ret;

		ret = snprintf(name, size, "%d:%d.%d",
				pci_dev->addr.bus, pci_dev->addr.devid,
				pci_dev->addr.function);
		if (ret < 0)
			return ret;
		return 0;
	}

例子:

可以使用 lspci 命令来观察 网卡的 pci 地址

  1. 千兆 以太网 设备:
	04:00.0 Ethernet controller: Intel Corporation I350 Gigabit Network Connection (rev 01)

rte_eth_dev_create_unique_device_name 函数 返回 设备名称为: “4:0.0”

  1. 万兆 以太网 设备:
	08:00.0 Ethernet controller: Intel Corporation 82599ES 10-Gigabit SFI/SFP+ Network Connection (rev 01)

rte_eth_dev_create_unique_device_name 函数 返回 设备名称为: “8:0.0”

2.3.1.1.2.3. 为 以太网设备 分配空间

rte_eth_dev_allocate 遍历全局变量 rte_eth_devices[] 数组,找到 空闲的位置。来放置 以太网设备。

函数调用图如下:

	rte_eal_init						
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
				+->	dr->devinit(dr, dev);		/* 触发 驱动 对 设备 初始化。参见 rte_eth_driver_register */
					+~> rte_eth_dev_init		/* 网口 设备 初始化。 */
						+-> rte_eth_dev_create_unique_device_name	/* 创建 唯一的 设备名称 */
						+=> rte_eth_dev_allocate /* <== 为 以太网设备 分配空间 */
							+-> port_id = rte_eth_dev_find_free_port()		/* 遍历 rte_eth_devices 数组,找到 空闲的 port_id。 */
							+-> rte_eth_dev_data_alloc						/* 分配 设备数据 的内存空间 */
							+-> if (rte_eth_dev_allocated(name) != NULL) 	/* 检测 设备 是否已经被绑定上。 */
							+-> eth_dev = &rte_eth_devices[port_id]; 		/* eth_dev 指向可用的 rte_eth_devices[port_id] */
							+-> eth_dev->data = &rte_eth_dev_data[port_id]; /* eth_dev->data 指向可用的 rte_eth_dev_data[port_id] */
							+-> eth_dev->attached = DEV_ATTACHED; 			/* 打上 DEV_ATTACHED,表示已经绑定上设备。 */
							+-> nb_ports++									/* 使用的 网口数 加一 */

源码分析如下:

  1. 使用 rte_eth_dev_find_free_port 函数,遍历 rte_eth_devices[] 数组,找到 空闲的 port_id
  2. 如果 全局变量 rte_eth_dev_data 初始化。则使用 rte_eth_dev_data_alloc 函数,分配 设备数据 的内存空间。
  3. 使用 rte_eth_dev_allocated 函数,检测 设备 是否已经被绑定上。如果 设备已经 绑定上,函数返回 NULL,否则跳到第 4 步。
  4. 初始化 eth_deveth_dev->data 内部的 成员变量。
    4.1. eth_dev 指向可用的 rte_eth_devices[port_id]
    4.2. eth_dev->data 指向可用的 rte_eth_dev_data[port_id]
    4.3. eth_dev->attached 赋值为 DEV_ATTACHED。表示已经绑定上设备。
	rte_eth_dev_allocate(const char *name, enum rte_eth_dev_type type)
	{
		uint8_t port_id;
		struct rte_eth_dev *eth_dev;

		/* 遍历 rte_eth_devices 数组,找到 空闲的 port_id。 */
		port_id = rte_eth_dev_find_free_port();
		if (port_id == RTE_MAX_ETHPORTS) {
			RTE_PMD_DEBUG_TRACE("Reached maximum number of Ethernet ports\n");
			return NULL;
		}

		/* 分配 设备数据 的内存空间 */
		if (rte_eth_dev_data == NULL)
			rte_eth_dev_data_alloc();

		/* 检测 设备 是否已经被绑定上。*/
		if (rte_eth_dev_allocated(name) != NULL) {
			RTE_PMD_DEBUG_TRACE("Ethernet Device with name %s already allocated!\n",
					name);
			return NULL;
		}

		/* eth_dev 指向可用的 rte_eth_devices[port_id]
		 * eth_dev->data 指向可用的 rte_eth_dev_data[port_id]
		 */
		eth_dev = &rte_eth_devices[port_id];
		eth_dev->data = &rte_eth_dev_data[port_id];
		snprintf(eth_dev->data->name, sizeof(eth_dev->data->name), "%s", name);
		eth_dev->data->port_id = port_id;
		eth_dev->attached = DEV_ATTACHED;	/* 表示已经绑定上设备 */
		eth_dev->dev_type = type;
		nb_ports++;							/* 使用的 网口数 加一 */
		return eth_dev;
	}

以太网设备 分配空间:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第14张图片

2.3.1.1.2.4. 为 以太网设备的私有数据 分配空间

源码分析如下:

  1. rte_eth_dev_init 使用 rte_zmalloc以太网设备私有数据 分配空间。
  2. eth_dev->pci_dev 指向 pci_dev。关联 以太网设备pci设备
  3. eth_dev->driver 指向 eth_drv。关联 以太网设备以太网驱动
	static int
	rte_eth_dev_init(struct rte_pci_driver *pci_drv,
			 struct rte_pci_device *pci_dev)
	{
		/* ... */
		
		/* 为 以太网设备的私有数据 分配空间 */
		if (rte_eal_process_type() == RTE_PROC_PRIMARY) {
			eth_dev->data->dev_private = rte_zmalloc("ethdev private structure",
					  eth_drv->dev_private_size,
					  RTE_CACHE_LINE_SIZE);
		}
		eth_dev->pci_dev = pci_dev;	/* 关联 以太网设备 和 pci设备 */
		eth_dev->driver = eth_drv;	/* 关联 以太网设备 和 以太网驱动 */
		eth_dev->data->rx_mbuf_alloc_failed = 0;
	
		/* ... */
	}

例子:

  1. 千兆 以太网 设备:

rte_igb_pmd 在定义的时候,设置 dev_private_sizesizeof(struct e1000_adapter)
rte_zmalloc() 就为千兆以太网设备分配了 struct e1000_adapter 类型大小的内存空间。
所以最后,千兆以太网设备的 eth_dev->data->dev_private 指向的是 struct e1000_adapter 类型数据。

  1. 万兆 以太网 设备:

rte_ixgbe_pmd 在定义的时候,设置 dev_private_sizesizeof(struct ixgbe_adapter)
rte_zmalloc() 就为万兆以太网设备分配了 struct ixgbe_adapter 类型大小的内存空间。
所以最后,万兆以太网设备的 eth_dev->data->dev_private 指向的是 struct ixgbe_adapter 类型数据。

注意:

struct e1000_adapterstruct ixgbe_adapter 表示的是一个物理网口的私有信息。里面存储了硬件信息等。详细可以查看 相关芯片的 datasheet,这里不详细展开。
[为 以太网设备的私有数据 分配空间]

2.3.1.1.2.5. 初始化中断处理回调函数链表

rte_eth_dev_init 函数中,初始化 设备eth_dev 上的 link_intr_cbs 中断处理回调函数链表。

	static int
	rte_eth_dev_init(struct rte_pci_driver *pci_drv,
			 struct rte_pci_device *pci_dev)
	{
		/* ... */
		/* ------------------------------------*/
		/* 初始化中断处理回调函数链表 */
		/* init user callbacks */
		TAILQ_INIT(&(eth_dev->link_intr_cbs));
		/* ... */
	}
2.3.1.1.2.6. 调用 PMD 设备的初始化 回调函数

eth_drv->eth_dev_init() 是一个回调函数。不同的驱动将会初始化各自的 以太网设备。

	static int
	rte_eth_dev_init(struct rte_pci_driver *pci_drv,
			 struct rte_pci_device *pci_dev)
	{
		/* ... */
		/* ------------------------------------*/
		/* 调用 PMD 设备的初始化 回调函数 */
		/* Invoke PMD device initialization function */
		diag = (*eth_drv->eth_dev_init)(eth_dev);
		if (diag == 0)
			return 0;
		/* ... */
	}

例子:

  1. 千兆以太网 设备初始化:

因为 rte_igb_pmd.eth_dev_init 设置为 eth_igb_dev_init
所以最后以 eth_igb_dev_init 来初始化 千兆以太网设备。

eth_igb_dev_init() 的函数调用图。

	rte_eal_init						
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
				+->	dr->devinit(dr, dev);		/* 触发 驱动 对 设备 初始化。参见 rte_eth_driver_register */
					+~> rte_eth_dev_init		/* 网口 设备 初始化。 */
						+-> (*eth_drv->eth_dev_init)(eth_dev); /* 调用 驱动 的 eth_dev_init 回调函数 初始化网口设备 */
							+=> eth_igb_dev_init()				/* <== 千兆以太网 设备初始化 */
								+-> eth_dev->dev_ops = &eth_igb_ops;			/* 关联网口设备操作回调函数 */
								+-> eth_dev->rx_pkt_burst = &eth_igb_recv_pkts;	/* 设置默认收包回调函数 */
								+-> eth_dev->tx_pkt_burst = &eth_igb_xmit_pkts; /* 设置默认发包回调函数 */
								+-> igb_hardware_init() 						/* 初始化 硬件 */ 
								+-> rte_intr_callback_register()				/* 初始化中断 */
  1. 万兆以太网 设备初始化:

因为 rte_ixgbe_pmd.eth_dev_init 设置为 eth_ixgbe_dev_init
所以最后以 eth_ixgbe_dev_init 来初始化 万兆以太网设备。

eth_ixgbe_dev_init() 的函数调用图。

	rte_eal_init						
	+-> rte_eal_pci_probe		/* pci 设备的侦测(侦测和初始化) */
		+-> pci_probe_all_drivers	 /* pci 设备 遍历所有 驱动,找到合适的驱动 */
			+-> rte_eal_pci_probe_one_driver /* 探测 驱动 和 设备 是否适合 */
				+->	dr->devinit(dr, dev);		/* 触发 驱动 对 设备 初始化。参见 rte_eth_driver_register */
					+~> rte_eth_dev_init		/* 网口 设备 初始化。 */
						+-> (*eth_drv->eth_dev_init)(eth_dev); /* 调用 驱动 的 eth_dev_init 回调函数 初始化网口设备 */
							+=> eth_ixgbe_dev_init()			/* <== 万兆以太网 设备初始化 */
								+-> eth_dev->dev_ops = &ixgbe_eth_dev_ops;		/* 关联网口设备操作回调函数 */
								+-> eth_dev->rx_pkt_burst = &ixgbe_recv_pkts;   /* 设置默认收包回调函数 */
								+-> eth_dev->tx_pkt_burst = &ixgbe_xmit_pkts;   /* 设置默认发包回调函数 */
								+-> ixgbe_init_hw()								/* 初始化 硬件 */ 
								+-> rte_intr_callback_register()				/* 初始化中断 */ 

重点:

千兆和万兆以太网设备的初始化函数 (eth_igb_dev_initeth_ixgbe_dev_init) 其中内部的实现。由于硬件的缘故,是大相径庭的。
但是通过上面千兆和万兆的 “函数调用图”,粗略地提取代码间的相同点。可以让我们了解代码的总体思想:

  1. 关联网口设备操作回调函数 eth_dev->dev_op。千兆设置为 eth_igb_ops,万兆设置为 ixgbe_eth_dev_ops
  2. 设置默认收包回调函数 eth_dev->rx_pkt_burst。千兆设置为 eth_igb_recv_pkts,万兆设置为 ixgbe_recv_pkts
  3. 设置默认发包回调函数 eth_dev->tx_pkt_burst。千兆设置为 eth_igb_xmit_pkts,万兆设置为 ixgbe_xmit_pkts
  4. 初始化硬件。千兆调用函数 igb_hardware_init,万兆调用函数 ixgbe_init_hw
  5. 初始化中断。千兆调用函数 rte_intr_callback_register,万兆调用函数 rte_intr_callback_register

千兆和万兆以太网口的初始化函数的图例:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第15张图片

3. 以太网设备的配置

设备与驱动关联之后。仍然需要配置。主要的函数有:

  1. rte_eth_dev_configure,用户配置 以太网设备
  2. rte_eth_rx_queue_setup,设置 以太网设备 的收包队列
  3. rte_eth_tx_queue_setup,设置 以太网设备 的发包队列

总体函数调用图如下:

	main
	|	/* ------------------------ */
	|	/*    以太网设备的配置      */
	+-> rte_eth_dev_configure							/* 用户配置 以太网设备 */
	|	+-> memcpy(&dev->data->dev_conf, dev_conf, ...)	/* 拷贝用户设置到 设备的数据结构中 */
	|	+-> rte_eth_dev_rx_queue_config					/* 设置 收包队列 */
	|	|	+-> dev->data->rx_queues = rte_zmalloc()	/* 分配收包队列空间 */
	|	+-> rte_eth_dev_tx_queue_config 				/* 设置 发包队列 */
	|	|	+-> dev->data->tx_queues = rte_zmalloc()	/* 分配发包队列空间 */
	|	+-> (*dev->dev_ops->dev_configure)(dev)			/* 用户设置 以太网设备 的回调函数 */
	|		.	/* 千兆:eth_igb_ops.dev_configure = eth_igb_configure */
	|		+-> eth_igb_configure		/* 千兆:用户配置 以太网设备 */
	|		.	+-> igb_check_mq_mode	/* 千兆:检查用户配置的收发包模式 */
	|		.
	|		.	/* 万兆:ixgbe_eth_dev_ops.dev_configure = ixgbe_dev_configure */
	|		+-> ixgbe_dev_configure		/* 万兆:用户配置 以太网设备 */
	|			+-> ixgbe_check_mq_mode	/* 万兆:检查用户配置的收发包模式 */
	|
	+-> rte_eth_rx_queue_setup					/* 设置 以太网设备 的收包队列 */
	|	+-> (*dev->dev_ops->rx_queue_setup)()	/* 网口的收包队列 的初始化 */
	|		.	/* 千兆:eth_igb_ops.rx_queue_setup = eth_igb_rx_queue_setup */
	|		+~> eth_igb_rx_queue_setup
	|		.
	|		.	/* 万兆:ixgbe_eth_dev_ops.rx_queue_setup = ixgbe_dev_rx_queue_setup */
	|		+~> ixgbe_dev_rx_queue_setup		
	|			
	+-> rte_eth_tx_queue_setup					/* 设置 以太网设备 的发包队列 */
		+-> (*dev->dev_ops->tx_queue_setup)()	/* 网口的发包队列 的初始化 */
			.	/* 千兆:eth_igb_ops.tx_queue_setup = eth_igb_tx_queue_setup */
			+~> eth_igb_tx_queue_setup
			.
			.	/* 万兆:ixgbe_eth_dev_ops.tx_queue_setup = eth_igb_tx_queue_setup */
			+~> ixgbe_dev_tx_queue_setup

有关设备配置的结构体是 struct rte_eth_dev_data

struct rte_eth_dev_data 相关的类图:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第16张图片

注意:

由于不同的以太网设备对应的收发包队列的数据结构都有不同。
所以以下的类图中,以 “xxx_” 为首的结构体,是以斜体表示的,表示虚构的。
在后续的物件图中,将会同时以 千兆和 万兆以太网设备为例来描述。

rte_eth_dev_configure 函数

rte_eth_dev_configure 用于 用户配置 以太网设备。主要的步骤有:

  1. 拷贝用户设置到 设备的数据结构中。
  2. 使用 rte_eth_dev_rx_queue_config,分配收包队列空间。
    2.1. 如果是第一次配置,那么就为每个收包队列分配一个指针。
    2.2. 如果是重新配置,而且新的队列数量不为0,那么就释放之前的队列,再重新分配空间。
    2.3. 如果是重新配置,而且新的队列数量为0,那么就释放所有的队列。
  3. 使用 rte_eth_dev_tx_queue_config, 分配发包队列空间。
    3.1. 如果是第一次配置,那么就为每个发包队列分配一个指针。
    3.2. 如果是重新配置,而且新的队列数量不为0,那么就释放之前的队列,再重新分配空间。
    3.3. 如果是重新配置,而且新的队列数量为0,那么就释放所有的队列。
  4. 触发 dev_ops->dev_configure() 回调函数。
    4.1. 按照不同的以太网设备类型,配置用户设置。这里不详细描述。

函数调用图如下:

	rte_eth_dev_configure						/* <== 用户配置 以太网设备 */
	+-> memcpy(&dev->data->dev_conf, dev_conf, ...)	/* 拷贝用户设置到 设备的数据结构中 */
	+-> rte_eth_dev_rx_queue_config					/* 设置 收包队列 */
	|	+-> dev->data->rx_queues = rte_zmalloc()	/* 分配收包队列空间 */
	+-> rte_eth_dev_tx_queue_config 				/* 设置 发包队列 */
	|	+-> dev->data->tx_queues = rte_zmalloc()	/* 分配发包队列空间 */
	+-> (*dev->dev_ops->dev_configure)(dev)			/* 用户设置 以太网设备 的回调函数 */
		.	/* 千兆:eth_igb_ops.dev_configure = eth_igb_configure */
		+-> eth_igb_configure		/* 千兆:用户配置 以太网设备 */
		.	+-> igb_check_mq_mode	/* 千兆:检查用户配置的收发包模式 */
		.
		.	/* 万兆:ixgbe_eth_dev_ops.dev_configure = ixgbe_dev_configure */
		+-> ixgbe_dev_configure		/* 万兆:用户配置 以太网设备 */
			+-> ixgbe_check_mq_mode	/* 万兆:检查用户配置的收发包模式 */

以下是 rte_eth_dev_rx_queue_configrte_eth_dev_tx_queue_config 简化后的代码。
只是摘录了第一次为收发包队列分配内存的情况。

	static int
	rte_eth_dev_rx_queue_config(struct rte_eth_dev *dev, uint16_t nb_queues)
	{
		uint16_t old_nb_queues = dev->data->nb_rx_queues;
		void **rxq;
		unsigned i;
		/* 第一次配置,为每个收包队列分配一个指针 */
		if (dev->data->rx_queues == NULL && nb_queues != 0) { /* first time configuration */
			dev->data->rx_queues = rte_zmalloc("ethdev->rx_queues",
					sizeof(dev->data->rx_queues[0]) * nb_queues,
					RTE_CACHE_LINE_SIZE);
			if (dev->data->rx_queues == NULL) {
				dev->data->nb_rx_queues = 0;
				return -(ENOMEM);
			}
		} 
		
		/* ... */
	}

	static int
	rte_eth_dev_tx_queue_config(struct rte_eth_dev *dev, uint16_t nb_queues)
	{
		uint16_t old_nb_queues = dev->data->nb_tx_queues;
		void **txq;
		unsigned i;
		/* 第一次配置,为每个发包队列分配一个指针 */
		if (dev->data->tx_queues == NULL && nb_queues != 0) { /* first time configuration */
			dev->data->tx_queues = rte_zmalloc("ethdev->tx_queues",
							   sizeof(dev->data->tx_queues[0]) * nb_queues,
							   RTE_CACHE_LINE_SIZE);
			if (dev->data->tx_queues == NULL) {
				dev->data->nb_tx_queues = 0;
				return -(ENOMEM);
			}
		}
		
		/* ... */
	}

注意:

  1. struct rte_eth_dev_data 中,成员变量 rx_queuestx_queues 都为 void ** 类型。
  2. dev->data->rx_queues[0]dev->data->tx_queues[0] 所以都为 void * 类型。
  3. dev->data->rx_queuesdev->data->tx_queues 指向的是 void * 类型的数组,数组元素个数为 nb_queues

千兆以太网设备 & 万兆以太网设备 都会调用 rte_eth_dev_rx_queue_configrte_eth_dev_tx_queue_config 分配收发包队列空间。图例如下:

分配收发包队列空间后:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第17张图片

rte_eth_rx_queue_setup 函数

rte_eth_rx_queue_setup 用于 设置 以太网设备 的收包队列。函数最后会调用 dev->dev_ops->rx_queue_setup 的回调函数。

函数调用图如下:

	rte_eth_rx_queue_setup		/* <== 设置 以太网设备 的收包队列 */
	+-> (*dev->dev_ops->rx_queue_setup)()	/* 网口的收包队列 的初始化 */
		.	/* 千兆:eth_igb_ops.rx_queue_setup = eth_igb_rx_queue_setup */
		+~> eth_igb_rx_queue_setup
		.
		.	/* 万兆:ixgbe_eth_dev_ops.rx_queue_setup = ixgbe_dev_rx_queue_setup */
		+~> ixgbe_dev_rx_queue_setup		

例子:

  1. 千兆以太网设备:

因为 eth_igb_ops.rx_queue_setup 设置为 eth_igb_rx_queue_setup
所以使用 eth_igb_rx_queue_setup 设置 以太网设备 的收包队列。

	rte_eth_rx_queue_setup		/* 设置 以太网设备 的收包队列 */
	+-> (*dev->dev_ops->rx_queue_setup)()	/* 网口的收包队列 的初始化 */
		+=> eth_igb_rx_queue_setup			/* <== 千兆: 设置 以太网设备 的收包队列。 */
			+-> rxq = rte_zmalloc("ethdev RX queue", sizeof(struct igb_rx_queue),...); 		/* 分配 收包队列 空间。 */
			+-> rz = rte_eth_dma_zone_reserve(dev, "rx_ring", queue_idx,...); 				/* 分配 硬件收包队列 空间。 */
			+-> rxq->rx_ring_phys_addr = rte_mem_phy2mch(..., rz->phys_addr); 				/* 关联 硬件收包队列的物理地址。 */
			+-> rxq->rx_ring = (union e1000_adv_rx_desc *) rz->addr;						/* 关联 硬件收包队列的虚拟地址。 */
			+-> rxq->sw_ring = rte_zmalloc("rxq->sw_ring", sizeof(struct igb_rx_entry)...);	/* 分配 软件收包队列 空间 */
			+-> dev->data->rx_queues[queue_idx] = rxq;										/* 关联 收包队列 */
  1. 万兆以太网设备:
    因为 ixgbe_eth_dev_ops.rx_queue_setup 设置为 ixgbe_dev_rx_queue_setup
    所以使用 ixgbe_dev_rx_queue_setup 设置 以太网设备 的收包队列。
	rte_eth_rx_queue_setup		/* 设置 以太网设备 的收包队列 */
	+-> (*dev->dev_ops->rx_queue_setup)()	/* 网口的收包队列 的初始化 */
		+=> ixgbe_dev_rx_queue_setup		/* <== 万兆: 设置 以太网设备 的收包队列。 */
			+-> rxq = rte_zmalloc_socket("ethdev RX queue", sizeof(struct ixgbe_rx_queue),...);		/* 分配 收包队列 空间。 */
			+-> rz = rte_eth_dma_zone_reserve(dev, "rx_ring", queue_idx,...);						/* 分配 硬件收包队列 空间。 */
			+-> rxq->rx_ring_phys_addr = rte_mem_phy2mch(..., rz->phys_addr);						/* 关联 硬件收包队列的物理地址。 */
			+-> rxq->rx_ring = (union ixgbe_adv_rx_desc *) rz->addr;								/* 关联 硬件收包队列的虚拟地址。 */
			+-> rxq->sw_ring = rte_zmalloc_socket("rxq->sw_ring", sizeof(struct ixgbe_rx_entry)...);/* 分配 软件收包队列 空间 */
			+-> dev->data->rx_queues[queue_idx] = rxq;												/* 关联 收包队列 */

分析上面 千兆和万兆以太网设备 的 eth_igb_rx_queue_setupixgbe_dev_rx_queue_setup 函数调用图。得到主要的步骤如下:

  1. 分配 收包队列 rxq
  2. 分配 硬件收包队列 rx_wing
  3. 分配 软件收包队列 sw_ring

收包队列完成初始化后:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第18张图片

rte_eth_tx_queue_setup 函数

rte_eth_tx_queue_setup 用于 设置 以太网设备 的发包队列。函数最后会调用 dev->dev_ops->tx_queue_setup 的回调函数。

函数调用图如下:

	rte_eth_tx_queue_setup		/* <== 设置 以太网设备 的发包队列 */
	+-> (*dev->dev_ops->tx_queue_setup)()	/* 网口的发包队列 的初始化 */
		*	/* 千兆:eth_igb_ops.tx_queue_setup = eth_igb_tx_queue_setup */
		+~> eth_igb_tx_queue_setup
		*
		*	/* 万兆:ixgbe_eth_dev_ops.tx_queue_setup = eth_igb_tx_queue_setup */
		+~> ixgbe_dev_tx_queue_setup	

例子:

  1. 千兆以太网设备:
    因为 eth_igb_ops.tx_queue_setup 设置为 eth_igb_tx_queue_setup
    所以使用 eth_igb_tx_queue_setup 设置 以太网设备 的发包队列。
	rte_eth_tx_queue_setup		/* 设置 以太网设备 的发包队列 */
	+-> (*dev->dev_ops->tx_queue_setup)()	/* 网口的发包队列 的初始化 */
		+=> eth_igb_tx_queue_setup			/* <== 千兆: 设置 以太网设备 的发包队列。 */
			+-> txq = rte_zmalloc("ethdev TX queue", sizeof(struct igb_tx_queue),...);		/* 分配 发包队列 空间。 */
			+-> tz = rte_eth_dma_zone_reserve(dev, "tx_ring", queue_idx,...);				/* 分配 硬件发包队列 空间。 */
			+-> txq->tx_ring_phys_addr = rte_mem_phy2mch(..., tz->phys_addr);				/* 关联 硬件发包队列的物理地址。 */
			+-> txq->tx_ring = (union e1000_adv_tx_desc *) tz->addr;						/* 关联 硬件发包队列的虚拟地址。 */
			+-> txq->sw_ring = rte_zmalloc("txq->sw_ring", sizeof(struct igb_tx_entry)...);	/* 分配 软件发包队列 空间 */
			+-> dev->tx_pkt_burst = eth_igb_xmit_pkts;
			+-> dev->data->tx_queues[queue_idx] = txq;										/* 关联 发包队列 */
  1. 万兆以太网设备:
    因为 ixgbe_eth_dev_ops.tx_queue_setup 设置为 ixgbe_dev_tx_queue_setup
    所以使用 ixgbe_dev_tx_queue_setup 设置 以太网设备 的发包队列。
	rte_eth_tx_queue_setup		/* 设置 以太网设备 的发包队列 */
	+-> (*dev->dev_ops->tx_queue_setup)()	/* 网口的发包队列 的初始化 */
		+=> ixgbe_dev_tx_queue_setup		/* <== 万兆: 设置 以太网设备 的发包队列。 */
			+-> txq = rte_zmalloc_socket("ethdev TX queue", sizeof(struct ixgbe_tx_queue),...);		/* 分配 发包队列 空间。 */
			+-> tz = rte_eth_dma_zone_reserve(dev, "tx_ring", queue_idx,...);						/* 分配 硬件发包队列 空间。 */
			+-> txq->tx_ring_phys_addr = rte_mem_phy2mch(..., tz->phys_addr);						/* 关联 硬件发包队列的物理地址。 */
			+-> txq->tx_ring = (union ixgbe_adv_tx_desc *) tz->addr;								/* 关联 硬件发包队列的虚拟地址。 */
			+-> txq->sw_ring = rte_zmalloc_socket("txq->sw_ring", sizeof(struct ixgbe_tx_entry)...);/* 分配 软件发包队列 空间 */
			+-> ixgbe_set_tx_function(dev, txq);
			+-> dev->data->tx_queues[queue_idx] = txq;												/* 关联 发包队列 */

分析上面 千兆和万兆以太网设备 的 eth_igb_tx_queue_setupixgbe_dev_tx_queue_setup 函数调用图。得到主要的步骤如下:

  1. 分配 发包队列 txq
  2. 分配 硬件发包队列 rx_ring
  3. 分配 软件发包队列 sw_ring

发包队列完成初始化后:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第19张图片

4. 以太网设备的启动

启动设备 的过程中,涉及到中断,硬件等等的设置。但在这里主要讨论 收发包的回调函数 的设置。

以下是 以太网设备的启动 总体的函数调用图。

	main
	|	/* ------------------------ */	
	|	/*    以太网设备的启动      */
	+-> rte_eth_dev_start					/* 以太网设备启动 */
		+-> (*dev->dev_ops->dev_start)(dev)
			.	/* 千兆:eth_igb_ops.dev_start = eth_igb_start */
			+~> eth_igb_start				/* 千兆:以太网设备启动 */
			.	+-> eth_igb_tx_init()		/* 千兆:初始化发送单元 */
			.	+-> eth_igb_rx_init()		/* 千兆:初始化接收单元 */
			.		+-> igb_alloc_rx_queue_mbufs()	/* <== 创建 收包队列的 mbuf ==> */
			.
			.	/* 万兆:ixgbe_eth_dev_ops.dev_start = ixgbe_dev_start */
			+~> ixgbe_dev_start				/* 万兆:以太网设备启动 */
				+-> ixgbe_dev_tx_init()		/* 万兆:初始化发送单元 */
				+-> ixgbe_dev_rx_init()		/* 万兆:初始化接收单元 */
				+-> ixgbe_dev_rxtx_start()	/* 启动收发包单元 */
					+-> ixgbe_dev_rx_queue_start()	/* 启动收包单元 */
						+-> ixgbe_alloc_rx_queue_mbufs()	/* <== 创建 收包队列的 mbuf ==> */						

4.1. rte_eth_dev_start 函数

rte_eth_dev_start() 函数用于启动一个 以太网设备。

  1. 当 以太网设备 启动的时候,会触发 dev_ops->dev_start() 回调函数来启动。
  2. 最后不同类型的 以太网设备 会调用对应的 实现函数 来启动设备。

重点:

xxx_alloc_rx_queue_mbufs 函数,用于 创建 收包队列的 mbuf

例子:

  1. 千兆以太网设备 的 设备启动函数 eth_igb_start

eth_igb_start 的函数主要流程如下:

  1. eth_igb_tx_init 会修改 千兆以太网设备 发包的回调函数 tx_pkt_burst。这是最后修改 的地方。
  2. eth_igb_rx_init 会修改 千兆以太网设备 收包的回调函数 rx_pkt_burst。这是最后修改 的地方。
    2.1. 在 eth_igb_rx_init 中,会调用 igb_alloc_rx_queue_mbufs 函数 用于 创建 收包队列的 mbuf

eth_igb_start 函数调用图如下:

	rte_eth_dev_start()
	+-> (*dev->dev_ops->dev_start)(dev)
		+=> eth_igb_start()					/* <== 千兆:以太网设备启动 */
			+-> rte_intr_disable()			/* disable uio/vfio intr/eventfd mapping */
			+-> eth_igb_dev_set_link_up()	/* Power up the phy. Needed to make the link go Up */
			+-> e1000_rar_set()			/* Put the address into the Receive Address Array */
			+-> igb_hardware_init()		/* Initialize the hardware */
			+-> igb_pf_host_configure()	/* configure PF module if SRIOV enabled */
			+-> rte_intr_cap_multiple()	/* check and configure queue intr-vector mapping */
			+-> rte_intr_efd_enable()
			+-> eth_igb_configure_msix_intr()	/* confiugre msix for rx interrupt */
			+-> igb_init_manageability()		/* Configure for OS presence */
			+-> eth_igb_tx_init()		/* 1. 初始化发送单元 */
			+-> eth_igb_rx_init()		/* 2. 初始化接收单元 */
			|	+-> igb_alloc_rx_queue_mbufs()	/* <== 2.1. 创建 收包队列的 mbuf ==> */ 
			+-> e1000_clear_hw_cntrs_base_generic()
			+-> eth_igb_vlan_offload_set()
			+-> e1000_setup_link()
			+-> eth_igb_lsc_interrupt_setup()	/* check if lsc interrupt is enabled */
			+-> eth_igb_rxq_interrupt_setup()	/* check if rxq interrupt is enabled */
			+-> rte_intr_enable()		/* enable uio/vfio intr/eventfd mapping */
			+-> igb_intr_enable() 		/* resume enabled intr since hw reset */
  1. 万兆以太网设备 设备启动函数 ixgbe_dev_start

ixgbe_dev_start 的函数主要流程如下:

  1. ixgbe_dev_tx_init 会修改 万兆以太网设备 发包的回调函数 tx_pkt_burst。这是最后修改 的地方。
  2. ixgbe_dev_rx_init 会修改 万兆以太网设备 收包的回调函数 rx_pkt_burst。这是最后修改 的地方。
  3. ixgbe_dev_rxtx_start 函数 用于启动收发包单元。
    3.1. ixgbe_dev_tx_queue_start 函数 用于启动发包单元。
    3.2. ixgbe_dev_rx_queue_start 函数 用于启动收包单元。
    3.2.1. ixgbe_alloc_rx_queue_mbufs 函数 用于 创建 收包队列的 mbuf。

ixgbe_dev_start 函数调用图如下:

	rte_eth_dev_start()
	+-> (*dev->dev_ops->dev_start)(dev)
		+=> ixgbe_dev_start()				/* <== 万兆:以太网设备启动 */
			+-> rte_intr_disable() 			/* disable uio/vfio intr/eventfd mapping */
			+-> ixgbe_stop_adapter() 		/* 停掉适配器,停止发送和接收单元 */
			+-> ixgbe_pf_reset_hw() 		/* 重启适配器 */
			+-> ixgbe_pf_host_configure() 	/* configure PF module if SRIOV enabled */
			+-> ixgbe_dev_phy_intr_setup()
			+-> rte_intr_cap_multiple()	/* check and configure queue intr-vector mapping */
			+-> rte_intr_efd_enable()
			+-> ixgbe_configure_msix()	/* confiugre msix for sleep until rx interrupt */
			+-> ixgbe_dev_tx_init()		/* 1. 初始化发送单元 */
			+-> ixgbe_dev_rx_init()		/* 2. 初始化接收单元 */
			+-> ixgbe_dev_rxtx_start()	/* 3. 启动收发包单元 */
			|	+-> ixgbe_dev_tx_queue_start()	/* 3.1. 启动发包单元 */
			|	+-> ixgbe_dev_rx_queue_start()	/* 3.2. 启动收包单元 */
			|		+-> ixgbe_alloc_rx_queue_mbufs()	/* <== 3.2.1. 创建 收包队列的 mbuf ==> */
			+-> hw->mac.ops.setup_sfp()
			+-> ixgbe_set_phy_power()	/* Turn on the copper */
			+-> ixgbe_enable_tx_laser()	/* Turn on the laser */
			+-> ixgbe_check_link()
			+-> ixgbe_setup_link()
			+-> ixgbe_dev_lsc_interrupt_setup() /* check if lsc interrupt is enabled */
			+-> ixgbe_dev_rxq_interrupt_setup() /* check if rxq interrupt is enabled */
			+-> rte_intr_enable()				/* enable uio/vfio intr/eventfd mapping */
			+-> ixgbe_enable_intr()				/* resume enabled intr since hw reset */
			+-> ixgbe_vlan_offload_set()
			+-> ixgbe_vmdq_vlan_hw_filter_enable()	/* Enable vlan filtering for VMDq */
			+-> ixgbe_configure_dcb()				/* Configure DCB hw */
			+-> ixgbe_fdir_configure()
			+-> ixgbe_set_vf_rate_limit()			/* Restore vf rate limit */
			+-> ixgbe_restore_statistics_mapping()

4.1.1. rte_eth_dev_start 函数中,对 rx_pkt_burst 收包实现函数的选择

rte_eth_dev_init 函数中,不同的驱动会触发 eth_dev_init 回调函数,为 struct rte_eth_dev 中的 rx_pkt_burst 的函数指针,设置了默认的收包实现函数。

但是到了 rte_eth_dev_start 函数中,会 对 rx_pkt_burst 进一步选择不同的 收包实现函数。

4.1.1.1. 千兆以太网设备 rx_pkt_burst 收包实现函数的选择

千兆以太网设备,有 2 种的 rx_pkt_burst 收包实现函数。

函数名 描述
eth_igb_recv_pkts 千兆 默认收包函数 (报文最多由 1个 segment 组成)
eth_igb_recv_scattered_pkts 千兆 Rx Scattered (报文可以由 多个 segment 组成)

千兆以太网设备,rx_pkt_burst 收包实现函数的选择,会通过 eth_igb_rx_init 函数来实现。

eth_igb_rx_init 的函数调用图如下:

	rte_eth_dev_start()
	+-> (*dev->dev_ops->dev_start)(dev)
		+~> eth_igb_start()
			+=> eth_igb_rx_init()		/* <== initialize receiving unit */

eth_igb_rx_init 中,有关 rx_pkt_burst 设置的代码,简化如下:

	int
	eth_igb_rx_init(struct rte_eth_dev *dev)
	{
		/* ... */
		/* default */
		dev->rx_pkt_burst = eth_igb_recv_pkts;
		
		/* scattered */
		if (dev->data->dev_conf.rxmode.enable_scatter) {
			dev->rx_pkt_burst = eth_igb_recv_scattered_pkts;
		}
		/* ... */
}

4.1.1.2. 万兆以太网设备 rx_pkt_burst 收包实现函数的选择

万兆以太网设备,有 6 种的 rx_pkt_burst 收包实现函数。

函数名 描述
ixgbe_recv_pkts_lro_bulk_alloc 万兆 Rx LRO bulk allocation
ixgbe_recv_pkts_lro_single_alloc 万兆 Rx LRO single allocation
ixgbe_recv_scattered_pkts_vec 万兆 Rx Vector Scattered
ixgbe_recv_pkts_vec 万兆 Rx Vector enabled
ixgbe_recv_pkts_bulk_alloc 万兆 Rx Burst Bulk allocation
ixgbe_recv_pkts 万兆 默认收包函数

万兆以太网设备,rx_pkt_burst 收包实现函数的选择,会通过 ixgbe_set_rx_function 函数来实现。

ixgbe_set_rx_function 的函数调用图如下:

	rte_eth_dev_start
	+-> (*dev->dev_ops->dev_start)(dev)
		+~> ixgbe_dev_start
			+-> ixgbe_dev_rx_init()		/* initialize receiving unit */
				+=> ixgbe_set_rx_function /* <== set receiving function */

ixgbe_set_rx_function 中,有关 rx_pkt_burst 设置的代码,简化如下:

	void ixgbe_set_rx_function(struct rte_eth_dev *dev)
	{
		/* ... */
		if (dev->data->lro) {
			if (adapter->rx_bulk_alloc_allowed) {
				/* LRO is requested. Using a bulk allocation version */
				dev->rx_pkt_burst = ixgbe_recv_pkts_lro_bulk_alloc;
			} else {
				/* LRO is requested. Using a single allocation version */
				dev->rx_pkt_burst = ixgbe_recv_pkts_lro_single_alloc;
			}
		} else if (dev->data->scattered_rx) {
			if (adapter->rx_vec_allowed) {
				/* Using Vector Scattered Rx */
				dev->rx_pkt_burst = ixgbe_recv_scattered_pkts_vec;
			} else if (adapter->rx_bulk_alloc_allowed) {
				/* Using a Scattered with bulk allocation */
				dev->rx_pkt_burst = ixgbe_recv_pkts_lro_bulk_alloc;
			} else {
				/* Using Regualr (non-vector, single allocation) Scattered Rx */
				dev->rx_pkt_burst = ixgbe_recv_pkts_lro_single_alloc;
			}
		} else if (adapter->rx_vec_allowed) {
			/* Vector rx enabled */
			dev->rx_pkt_burst = ixgbe_recv_pkts_vec;
		} else if (adapter->rx_bulk_alloc_allowed) {
			/* Rx Burst Bulk Alloc Preconditions are satisfied. Rx Burst Bulk Alloc */
			dev->rx_pkt_burst = ixgbe_recv_pkts_bulk_alloc;
		} else {
			/* Rx Burst Bulk Alloc Preconditions are not satisfied, or Scattered Rx is requested */
			dev->rx_pkt_burst = ixgbe_recv_pkts;
		}
		/* ... */
}

4.1.2. rte_eth_dev_start 函数中,tx_pkt_burst 发包实现函数的选择

rte_eth_dev_init 函数中,不同的驱动会触发 eth_dev_init 回调函数,为 struct rte_eth_dev 中的 tx_pkt_burst 的函数指针,设置了默认的发包实现函数。

但是到了 rte_eth_dev_start 函数中,会进一步选择不同的 发包实现函数。

4.1.2.1. 千兆以太网设备 tx_pkt_burst 发包实现函数的选择

千兆以太网设备,有 1 种的 tx_pkt_burst 发包实现函数。

函数名 描述
eth_igb_recv_pkts 千兆 默认发包函数

千兆以太网设备,tx_pkt_burst 发包实现函数的选择,在 eth_igb_dev_init 函数里设置。采用默认的 发包函数 eth_igb_recv_pkts

4.1.2.2. 万兆以太网设备 tx_pkt_burst 发包实现函数的选择

万兆以太网设备,有 3 种的 tx_pkt_burst 发包实现函数。

函数名 描述
ixgbe_xmit_pkts_vec 万兆 Rx Vector enabled
ixgbe_xmit_pkts_simple 万兆 Tx simple
ixgbe_xmit_pkts 万兆 默认发包函数

万兆以太网设备,tx_pkt_burst 发包实现函数的选择,会通过 ixgbe_set_tx_function 函数来实现。

ixgbe_set_tx_function 的函数调用图如下:

	rte_eth_dev_start
	+-> (*dev->dev_ops->dev_start)(dev)
		+~> ixgbe_dev_start
			+-> ixgbe_dev_tx_init()		/* initialize transmission unit */
				+=> ixgbe_set_tx_function /* <== set transmission function */

ixgbe_set_tx_function 中,有关 tx_pkt_burst 设置的代码,简化如下:

	ixgbe_set_tx_function(struct rte_eth_dev *dev, struct ixgbe_tx_queue *txq)
	{
		/* ... */
		/* Use a simple Tx queue (no offloads, no multi segs) if possible */
		if (((txq->txq_flags & IXGBE_SIMPLE_FLAGS) == IXGBE_SIMPLE_FLAGS)
				&& (txq->tx_rs_thresh >= RTE_PMD_IXGBE_TX_MAX_BURST)) {
			/* Using simple tx code path */
	#ifdef RTE_IXGBE_INC_VECTOR
			if (txq->tx_rs_thresh <= RTE_IXGBE_TX_MAX_FREE_BUF_SZ &&
					(rte_eal_process_type() != RTE_PROC_PRIMARY ||
						ixgbe_txq_vec_setup(txq) == 0)) {
				/* "Vector tx enabled." */
				dev->tx_pkt_burst = ixgbe_xmit_pkts_vec;
			} else
	#endif
			dev->tx_pkt_burst = ixgbe_xmit_pkts_simple;
		} else {
			/* Using full-featured tx code path */
			dev->tx_pkt_burst = ixgbe_xmit_pkts;
		}
	}

4.1.3. 网卡的收包的描述符(receive descriptor

千兆和万兆以太网设备分别定义了 union e1000_adv_rx_descunion ixgbe_adv_rx_desc 作为网卡的收包的描述符(receive descriptor)。

receive descriptor 的结构是由 以太网设备 芯片所决定的。具体可以查询 芯片的 datasheet。
以 Intel® I350 为例,其中的 receive descriptor 有两种的格式,分别是 readwrite-back,分别定义如下:

Descriptor Read Format
在这里插入图片描述

Descriptor Write-Back Format
在这里插入图片描述

union e1000_adv_rx_descunion ixgbe_adv_rx_desc 是按照以上两种的格式来定义的。

重点:

  1. receive descriptorread.pkt_addr 指向网卡接收数据的 DMA 地址。网卡收包时,会将报文数据直接写入其所指向的地方。
  2. receive descriptorread.hdr_addr 的 最低位为 DD (Descriptor Done) 标志,当接收到报文后,DD 标志会由网卡硬件置位。
    后续的报文接收函数就可以通过 DD 标志,确定是否完成收包。
  3. receive descriptorwb 结构体的数据,是由网卡硬件填充的。可以用于收包后 mbuf 数据的初始化。

例子:

千兆以太网设备 的 receive descriptor 源码:

	/* Receive Descriptor - Advanced */
	union e1000_adv_rx_desc {
		struct {
			__le64 pkt_addr; /* Packet buffer address */
			__le64 hdr_addr; /* Header buffer address */
		} read;
		struct {
			struct {
				union {
					__le32 data;
					struct {
						__le16 pkt_info; /*RSS type, Pkt type*/
						/* Split Header, header buffer len */
						__le16 hdr_info;
					} hs_rss;
				} lo_dword;
				union {
					__le32 rss; /* RSS Hash */
					struct {
						__le16 ip_id; /* IP id */
						__le16 csum; /* Packet Checksum */
					} csum_ip;
				} hi_dword;
			} lower;
			struct {
				__le32 status_error; /* ext status/error */
				__le16 length; /* Packet length */
				__le16 vlan; /* VLAN tag */
			} upper;
		} wb;  /* writeback */
	};

万兆以太网设备 的 receive descriptor 源码:

/* Receive Descriptor - Advanced */
union ixgbe_adv_rx_desc {
	struct {
		__le64 pkt_addr; /* Packet buffer address */
		__le64 hdr_addr; /* Header buffer address */
	} read;
	struct {
		struct {
			union {
				__le32 data;
				struct {
					__le16 pkt_info; /* RSS, Pkt type */
					__le16 hdr_info; /* Splithdr, hdrlen */
				} hs_rss;
			} lo_dword;
			union {
				__le32 rss; /* RSS Hash */
				struct {
					__le16 ip_id; /* IP id */
					__le16 csum; /* Packet Checksum */
				} csum_ip;
			} hi_dword;
		} lower;
		struct {
			__le32 status_error; /* ext status/error */
			__le16 length; /* Packet length */
			__le16 vlan; /* VLAN tag */
		} upper;
	} wb;  /* writeback */
};

参考:

receive descriptor,可以参考 Intel® I350 的 datasheet 中的 “Advanced Receive Descriptors” 一节。

4.1.4. 创建 收包队列的 mbuf

无论千兆还是万兆的设备,都需要 创建 收包队列的 mbuf

  1. 千兆:调用 igb_alloc_rx_queue_mbufs 函数。
  2. 万兆:调用 ixgbe_alloc_rx_queue_mbufs 函数。

函数的调用图如下:

	rte_eth_dev_start					/* 以太网设备启动 */
	+-> (*dev->dev_ops->dev_start)(dev)
		.	/* 千兆:eth_igb_ops.dev_start = eth_igb_start */
		+~> eth_igb_start				/* 千兆:以太网设备启动 */
		.	+-> eth_igb_rx_init()		/* 千兆:初始化接收单元 */
		.		+=> igb_alloc_rx_queue_mbufs()	/* <== 创建 收包队列的 mbuf ==> */
		.
		.	/* 万兆:ixgbe_eth_dev_ops.dev_start = ixgbe_dev_start */
		+~> ixgbe_dev_start				/* 万兆:以太网设备启动 */
			+-> ixgbe_dev_rxtx_start()	/* 启动收发包单元 */
				+-> ixgbe_dev_rx_queue_start()	/* 启动收包单元 */
					+=> ixgbe_alloc_rx_queue_mbufs()	/* <== 创建 收包队列的 mbuf ==> */

为了可以更好的对比不同设备的代码。将代码简化如下。

  1. 千兆设备代码:
	static int
	igb_alloc_rx_queue_mbufs(struct igb_rx_queue *rxq)
	{
		struct igb_rx_entry *rxe = rxq->sw_ring;
		uint64_t dma_addr;
		unsigned int i;

		/* Initialize software ring entries */
		for (i = 0; i < rxq->nb_rx_desc; i++) {
			volatile union e1000_adv_rx_desc *rxd;
			struct rte_mbuf *mbuf = 
				rte_mbuf_raw_alloc(rxq->mb_pool);

			dma_addr = rte_cpu_to_le_64(
				rte_mbuf_data_dma_addr_default(mbuf));
			rxd = &rxq->rx_ring[i];
			rxd->read.hdr_addr = 0;
			rxd->read.pkt_addr = dma_addr;
			rxe[i].mbuf = mbuf;
		}

		return 0;
	}
  1. 万兆设备代码:
	static int
	ixgbe_alloc_rx_queue_mbufs(struct ixgbe_rx_queue *rxq)
	{
		struct ixgbe_rx_entry *rxe = rxq->sw_ring;
		uint64_t dma_addr;
		unsigned int i;

		/* Initialize software ring entries */
		for (i = 0; i < rxq->nb_rx_desc; i++) {
			volatile union ixgbe_adv_rx_desc *rxd;
			struct rte_mbuf *mbuf = 
				rte_mbuf_raw_alloc(rxq->mb_pool);

			dma_addr = rte_cpu_to_le_64(
				rte_mbuf_data_dma_addr_default(mbuf));
			rxd = &rxq->rx_ring[i];
			rxd->read.hdr_addr = 0;
			rxd->read.pkt_addr = dma_addr;
			rxe[i].mbuf = mbuf;
		}

		return 0;
	}

简化后的代码,基本结构是相同的,除了 receive descriptor 类型有所不同。

代码分析如下:

  1. 使用 rte_mbuf_raw_alloc 从 收包队列(rxq)所属的 mbuf 内存池中(mb_pool)中取出一个 mbuf
	struct rte_mbuf *mbuf = rte_mbuf_raw_alloc(rxq->mb_pool);
  1. 使用 rte_mbuf_data_dma_addr_default 计算得出 mbuf 的报文数据所在的物理地址(最后使用变量 dma_addr 暂存)。
	/*  rte_mbuf_data_dma_addr_default 的定义:
	 *		static inline phys_addr_t
	 *		rte_mbuf_data_dma_addr_default(const struct rte_mbuf *mb)
	 *		{
	 *			return mb->buf_physaddr + RTE_PKTMBUF_HEADROOM;
	 *		}
	 *
	 *  在 x86 架构下 rte_cpu_to_le_64 的定义:
	 *  	#define rte_cpu_to_le_64(x) (x)
	 */
	dma_addr = rte_cpu_to_le_64(rte_mbuf_data_dma_addr_default(mbuf));
  1. 清除 DD (Descriptor Done) 标志。等待报文数据的接收。
	rxd->read.hdr_addr = 0;
  1. dma_addr 赋值到 rx_ring[i]read.pkt_addr 字段中。网卡收包时,就会直接对 mbuf 的报文数据所在的物理地址写入数据,从而免除了从内核态到用户态之间的拷贝。
	rxd->read.pkt_addr = dma_addr;
  1. 最后将 mbuf 的虚拟地址赋值到 sw_ring[i]mbuf 字段中,从而记录下入列的 mbuf。当收包完成后,就可以通过 sw_ring[] 找到 mbuf,从而取得报文。
	rxe[i].mbuf = mbuf;
  1. 重复以上步骤,直到 sw_ring[]rx_ring[] 都通过 mbuf 一一关联起来。

以下为千兆的 igb_alloc_rx_queue_mbufs 函数的完成创建 收包队列的 mbuf 后的图例:
DPDK 16.07 驱动初始化和收发包函数学习笔记_第20张图片

5. 以太网设备的收发包

以上的以太网设备都准备好后。就可以使用 rte_eth_rx_burstrte_eth_tx_burst 来收发报文了。总体的函数调用图如下所示:

	main
	|	/* ------------------------ */
	|	/*    以太网设备的收包   */
	+-> rte_eth_rx_burst
	|	+-> (*dev->rx_pkt_burst)(dev->data->rx_queues[queue_id], ...)
	|		.	/* 千兆:rx_pkt_burst 默认收包函数为 eth_igb_recv_pkts */
	|		+~> eth_igb_recv_pkts
	|		. 
	|		.	/* 万兆:rx_pkt_burst 默认收包函数为 ixgbe_recv_pkts */
	|		+~> ixgbe_recv_pkts
	|
	|	/* ------------------------ */
	|	/*    以太网设备的发包   */
	+-> rte_eth_tx_burst
		+-> (*dev->tx_pkt_burst)(dev->data->tx_queues[queue_id], ...)
			.	/* 千兆:tx_pkt_burst 默认发包函数为 eth_igb_xmit_pkts */
			+~> eth_igb_xmit_pkts
			. 
			.	/* 万兆:tx_pkt_burst 默认发包函数为 ixgbe_xmit_pkts */
			+~> ixgbe_xmit_pkts

5.1. rte_eth_rx_burst 函数

rte_eth_rx_burst 函数 用于收包。在 最后会采用对应以太网设备的收包函数来实现。收包函数的主要流程如下:

  1. 通过 DD (Descriptor Done) 标志,判断 是否接收到报文。接收到报文后,DD 标志会由网卡硬件置位。
  2. 如果有 已经接收到的报文 则进行如下处理。
    2.1. 新建一个 mbuf,用来替换掉 已经接收到的报文 mbufrx_ringsw_ring 则都指向新的 mbuf,为下一次收包做准备。
    2.2. 使用 receive descriptor 中的数据,初始化 已经接收到报文的 mbuf
  3. 重复以上的步骤,直到达到需要读取的报文个数,或者遇到无报文接收而退出。
  4. 收包队列尾部 指向下一个 receive descriptor
  5. 如果空闲的 receive descriptor 大于 阈值。则将 RDT 设置为 最后的 receive descriptor -1。从而避免了 RDT 等于 RDH,造成丢包或者阻塞的情况。

函数调用图如下:

注意下图只使用了默认收包函数表示。

	rte_eth_rx_burst
	+-> (*dev->rx_pkt_burst)(dev->data->rx_queues[queue_id],...);
		.	/* 千兆:rx_pkt_burst 默认收包函数为 eth_igb_recv_pkts */
		+~> eth_igb_recv_pkts
		. 
		.	/* 万兆:rx_pkt_burst 默认收包函数为 ixgbe_recv_pkts */
		+~> ixgbe_recv_pkts

例子:

注意,为了方便描述,代码删除了 prefetch 的代码片段和调试打印,并增加了注释。

  1. 千兆默认收包函数 eth_igb_recv_pkts 简化后的代码。
	uint16_t
	eth_igb_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts,
			   uint16_t nb_pkts)
	{
		struct igb_rx_queue *rxq;
		volatile union e1000_adv_rx_desc *rx_ring;
		volatile union e1000_adv_rx_desc *rxdp;
		struct igb_rx_entry *sw_ring;
		struct igb_rx_entry *rxe;
		struct rte_mbuf *rxm;
		struct rte_mbuf *nmb;
		union e1000_adv_rx_desc rxd;
		uint64_t dma_addr;
		uint32_t staterr;
		uint32_t hlen_type_rss;
		uint16_t pkt_len;
		uint16_t rx_id;
		uint16_t nb_rx;
		uint16_t nb_hold;
		uint64_t pkt_flags;

		nb_rx = 0;
		nb_hold = 0;
		rxq = rx_queue;
		rx_id = rxq->rx_tail;
		rx_ring = rxq->rx_ring;
		sw_ring = rxq->sw_ring;
		while (nb_rx < nb_pkts) {
			/* ----------------------------- *
			 *       判断 是否接收到报文     *
			 * ----------------------------- */
			/* DD (Descriptor Done) 表示 Descriptor 已经收到报文,并填充到 mbuf 中。
			 * 如果 Descriptor Done 没有设置,就跳出循环。
			 */
			rxdp = &rx_ring[rx_id];
			staterr = rxdp->wb.upper.status_error;
			if (! (staterr & rte_cpu_to_le_32(E1000_RXD_STAT_DD)))
				break;
			rxd = *rxdp;
		
			/* ----------------------------- *
			 *     处理 已经接收到的报文     *
			 * ----------------------------- */
			/* 从 内存池中取一个新的 mbuf,用 nmb 指向它 
			 * 如果失败就用 rx_mbuf_alloc_failed 统计一下,然后跳出循环。
			 */
			nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
			if (nmb == NULL) {
				rte_eth_devices[rxq->port_id].data->rx_mbuf_alloc_failed++;
				break;
			}

			/* 用 rx_id 记录下一个 Descriptor 的下标
			 */
			nb_hold++;
			rxe = &sw_ring[rx_id];
			rx_id++;
			if (rx_id == rxq->nb_rx_desc)
				rx_id = 0;

			/* 用 rxm 指向 已经接收到报文的 mbuf。
			 * rx_ring 和 sw_ring 则都指向新的 mbuf,nmb。
			 */
			rxm = rxe->mbuf;
			rxe->mbuf = nmb;
			dma_addr =
				rte_cpu_to_le_64(rte_mbuf_data_dma_addr_default(nmb));
			rxdp->read.hdr_addr = 0;
			rxdp->read.pkt_addr = dma_addr;

			/* 初始化 已经接收到报文的 mbuf
			 * Initialize the returned mbuf.
			 * 1) setup generic mbuf fields:
			 *    - number of segments,
			 *    - next segment,
			 *    - packet length,
			 *    - RX port identifier.
			 * 2) integrate hardware offload data, if any:
			 *    - RSS flag & hash,
			 *    - IP checksum flag,
			 *    - VLAN TCI, if any,
			 *    - error flags.
			 */
			pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) -
						  rxq->crc_len);			/* 报文长度 */
			rxm->data_off = RTE_PKTMBUF_HEADROOM;	/* 报文数据的偏移 */ 
			rxm->nb_segs = 1;						/* segment 个数 */
			rxm->next = NULL;
			rxm->pkt_len = pkt_len;					/* 报文长度 */
			rxm->data_len = pkt_len;				/* 数据长度 */
			rxm->port = rxq->port_id;				/* 收包的 port_id */

			rxm->hash.rss = rxd.wb.lower.hi_dword.rss;	/* 硬件 rss 分流 hash key */
			hlen_type_rss = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data);
			/* Only valid if PKT_RX_VLAN_PKT set in pkt_flags */
			rxm->vlan_tci = rte_le_to_cpu_16(rxd.wb.upper.vlan);	/* vlan tag */

			pkt_flags = rx_desc_hlen_type_rss_to_pkt_flags(rxq, hlen_type_rss);
			pkt_flags = pkt_flags | rx_desc_status_to_pkt_flags(staterr);	/* status flags */
			pkt_flags = pkt_flags | rx_desc_error_to_pkt_flags(staterr);	/* error flags */
			rxm->ol_flags = pkt_flags;				/* offload features */
			rxm->packet_type = igb_rxd_pkt_info_to_pkt_type(rxd.wb.lower.
							lo_dword.hs_rss.pkt_info); /* 报文类型 */

			/* 将 mbuf 的地址,放置到出参的收包数组中。
			 */
			rx_pkts[nb_rx++] = rxm;
		}
		/* 收包队列尾部 指向下一个 Descriptor */
		rxq->rx_tail = rx_id;

		/* 更新 RDT,避免出现收包队列溢出,造成丢包或者阻塞的情况
		 * If the number of free RX descriptors is greater than the RX free
		 * threshold of the queue, advance the Receive Descriptor Tail (RDT)
		 * register.
		 * Update the RDT with the value of the last processed RX descriptor
		 * minus 1, to guarantee that the RDT register is never equal to the
		 * RDH register, which creates a "full" ring situtation from the
		 * hardware point of view...
		 */
		nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
		if (nb_hold > rxq->rx_free_thresh) {
			rx_id = (uint16_t) ((rx_id == 0) ?
						 (rxq->nb_rx_desc - 1) : (rx_id - 1));
			E1000_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
			nb_hold = 0;
		}
		rxq->nb_rx_hold = nb_hold;
		return nb_rx;
	}
  1. 万兆默认收包函数 ixgbe_recv_pkts 简化后的代码。
	uint16_t
	ixgbe_recv_pkts(void *rx_queue, struct rte_mbuf **rx_pkts,
			uint16_t nb_pkts)
	{
		struct ixgbe_rx_queue *rxq;
		volatile union ixgbe_adv_rx_desc *rx_ring;
		volatile union ixgbe_adv_rx_desc *rxdp;
		struct ixgbe_rx_entry *sw_ring;
		struct ixgbe_rx_entry *rxe;
		struct rte_mbuf *rxm;
		struct rte_mbuf *nmb;
		union ixgbe_adv_rx_desc rxd;
		uint64_t dma_addr;
		uint32_t staterr;
		uint32_t pkt_info;
		uint16_t pkt_len;
		uint16_t rx_id;
		uint16_t nb_rx;
		uint16_t nb_hold;
		uint64_t pkt_flags;
		uint64_t vlan_flags;

		nb_rx = 0;
		nb_hold = 0;
		rxq = rx_queue;
		rx_id = rxq->rx_tail;
		rx_ring = rxq->rx_ring;
		sw_ring = rxq->sw_ring;
		vlan_flags = rxq->vlan_flags;
		while (nb_rx < nb_pkts) {
			/* ----------------------------- *
			 *       判断 是否接收到报文     *
			 * ----------------------------- */
			/* DD (Descriptor Done) 表示 Descriptor 已经收到报文,并填充到 mbuf 中。
			 * 如果 Descriptor Done 没有设置,就跳出循环。
			 */
			rxdp = &rx_ring[rx_id];
			staterr = rxdp->wb.upper.status_error;
			if (!(staterr & rte_cpu_to_le_32(IXGBE_RXDADV_STAT_DD)))
				break;
			rxd = *rxdp;
		
			/* ----------------------------- *
			 *     处理 已经接收到的报文     *
			 * ----------------------------- */
			/* 从 内存池中取一个新的 mbuf,用 nmb 指向它 
			 * 如果失败就用 rx_mbuf_alloc_failed 统计一下,然后跳出循环。
			 */
			nmb = rte_mbuf_raw_alloc(rxq->mb_pool);
			if (nmb == NULL) {
				rte_eth_devices[rxq->port_id].data->rx_mbuf_alloc_failed++;
				break;
			}

			/* 用 rx_id 记录下一个 Descriptor 的下标
			 */
			nb_hold++;
			rxe = &sw_ring[rx_id];
			rx_id++;
			if (rx_id == rxq->nb_rx_desc)
				rx_id = 0;

			/* 用 rxm 指向 已经接收到报文的 mbuf。
			 * rx_ring 和 sw_ring 则都指向新的 mbuf,nmb。
			 */
			rxm = rxe->mbuf;
			rxe->mbuf = nmb;
			dma_addr =
				rte_cpu_to_le_64(rte_mbuf_data_dma_addr_default(nmb));
			rxdp->read.hdr_addr = 0;
			rxdp->read.pkt_addr = dma_addr;

			/* 初始化 已经接收到报文的 mbuf
			 * Initialize the returned mbuf.
			 * 1) setup generic mbuf fields:
			 *    - number of segments,
			 *    - next segment,
			 *    - packet length,
			 *    - RX port identifier.
			 * 2) integrate hardware offload data, if any:
			 *    - RSS flag & hash,
			 *    - IP checksum flag,
			 *    - VLAN TCI, if any,
			 *    - error flags.
			 */
			pkt_len = (uint16_t) (rte_le_to_cpu_16(rxd.wb.upper.length) -
						  rxq->crc_len);			/* 报文长度 */
			rxm->data_off = RTE_PKTMBUF_HEADROOM;	/* 报文数据的偏移 */ 
			rxm->nb_segs = 1;						/* segment 个数 */
			rxm->next = NULL;
			rxm->pkt_len = pkt_len;					/* 报文长度 */
			rxm->data_len = pkt_len;				/* 数据长度 */
			rxm->port = rxq->port_id;				/* 收包的 port_id */

			pkt_info = rte_le_to_cpu_32(rxd.wb.lower.lo_dword.data);
			/* Only valid if PKT_RX_VLAN_PKT set in pkt_flags */
			rxm->vlan_tci = rte_le_to_cpu_16(rxd.wb.upper.vlan);	/* vlan tag */

			pkt_flags = rx_desc_status_to_pkt_flags(staterr, vlan_flags);	/* vlan flags */
			pkt_flags = pkt_flags | rx_desc_error_to_pkt_flags(staterr);	/* error flags */
			pkt_flags = pkt_flags |
				ixgbe_rxd_pkt_info_to_pkt_flags((uint16_t)pkt_info);
			rxm->ol_flags = pkt_flags;
			rxm->packet_type =
				ixgbe_rxd_pkt_info_to_pkt_type(pkt_info,
								   rxq->pkt_type_mask); /* 报文类型 */

			if (likely(pkt_flags & PKT_RX_RSS_HASH))
				rxm->hash.rss = rte_le_to_cpu_32(
							rxd.wb.lower.hi_dword.rss);	/* rss hash key */
			else if (pkt_flags & PKT_RX_FDIR) {
				rxm->hash.fdir.hash = rte_le_to_cpu_16(
						rxd.wb.lower.hi_dword.csum_ip.csum) &
						IXGBE_ATR_HASH_MASK;
				rxm->hash.fdir.id = rte_le_to_cpu_16(
						rxd.wb.lower.hi_dword.csum_ip.ip_id);
			}
			/* 将 mbuf 的地址,放置到出参的收包数组中。
			 */
			rx_pkts[nb_rx++] = rxm;
		}
		/* 收包队列尾部 指向下一个 Descriptor */
		rxq->rx_tail = rx_id;

		/* 更新 RDT,避免出现收包队列溢出,造成丢包或者阻塞的情况
		 * If the number of free RX descriptors is greater than the RX free
		 * threshold of the queue, advance the Receive Descriptor Tail (RDT)
		 * register.
		 * Update the RDT with the value of the last processed RX descriptor
		 * minus 1, to guarantee that the RDT register is never equal to the
		 * RDH register, which creates a "full" ring situtation from the
		 * hardware point of view...
		 */
		nb_hold = (uint16_t) (nb_hold + rxq->nb_rx_hold);
		if (nb_hold > rxq->rx_free_thresh) {
			rx_id = (uint16_t) ((rx_id == 0) ?
						 (rxq->nb_rx_desc - 1) : (rx_id - 1));
			IXGBE_PCI_REG_WRITE(rxq->rdt_reg_addr, rx_id);
			nb_hold = 0;
		}
		rxq->nb_rx_hold = nb_hold;
		return nb_rx;
	}

参考:

有关 RDTRDH 的关系以及网卡硬件收包队列,可以参考 Intel® I350 的 datasheet 中的 “Receive Descriptor Ring Structure” 一节。

5.2. rte_eth_tx_burst 函数

rte_eth_rx_burst 函数 用于发包。

函数调用图如下:

注意只使用了默认发包函数表示。

	rte_eth_tx_burst
	+-> (*dev->tx_pkt_burst)(dev->data->tx_queues[queue_id], ...)
		.	/* 千兆:tx_pkt_burst 默认发包函数为 eth_igb_xmit_pkts */
		+~> eth_igb_xmit_pkts
		. 
		.	/* 万兆:tx_pkt_burst 默认发包函数为 ixgbe_xmit_pkts */
		+~> ixgbe_xmit_pkts

6. 应用

6. 1. 通过全局变量 rte_eth_devices[],查看网卡。

只要网卡初始化后,都可以在 gdb 中打印出 全局变量 rte_eth_devices[] 的内容。
最常用的是查看,收发包的回调函数 rx_pkt_bursttx_pkt_burst 的具体调用。

例子:

	" 千兆网卡
	(gdb) p rte_eth_devices[0]
	$32 = {
		rx_pkt_burst = 0x7ffff22f0940	, 
		tx_pkt_burst = 0x7ffff22f1150	, 
		data = 0x7ffd685cec40, 
		driver = 0x7ffff25071e0 , 
		dev_ops = 0x7ffff2506860 , 
		pci_dev = 0x60b390, 
		link_intr_cbs	= {
			tqh_first =	0x7fffe9400fc0,	
			tqh_last = 0x7fffe9400fc0
		}, 
		post_rx_burst_cbs = {[0] = 0x0 },	
		pre_tx_burst_cbs = {[0] = 0x0 }, 
		attached = 1 '\001', 
		dev_type = RTE_ETH_DEV_PCI
	}

	" 万兆网卡
	(gdb) p rte_eth_devices[1]
	$33 = {
		rx_pkt_burst = 0x7ffff35be7b0	, 
		tx_pkt_burst = 0x7ffff35d4ac0	, 
		data = 0x7ffd685d0448, 
		driver = 0x7ffff37e8240 , 
		dev_ops = 0x7ffff37e7ac0 ,	
		pci_dev = 0x60bb10, 
		link_intr_cbs	= {
			tqh_first =	0x7ffd68400880,	
			tqh_last = 0x7ffd68400880
		}, 
		post_rx_burst_cbs = {[0] = 0x0 },	
		pre_tx_burst_cbs = {[0] = 0x0 }, 
		attached = 1 '\001', 
		dev_type = RTE_ETH_DEV_PCI
	}

参考:

eth_igb_xmit_pktsixgbe_xmit_pkts 由于要支持多个 segment 报文。所以除了 data descriptor 还需要 context descriptor
详细可以参考 Intel® I350 的 datasheet 中的 “Transmit Functionality” 一节。

eth_igb_xmit_pktsixgbe_xmit_pkts 详细的代码分析这里就留给读者。

你可能感兴趣的:(c,dpdk,network,linux)