DPDK驱动UIO框架及网卡初始化分析

本文基于DPDK-17.05.2分析总结

一、DPDK的可执行程序运行步骤:

1、分配大页内存:

mkdir -p /mnt/huge-dpdk
mount -t hugetlbfs nodev /mnt/huge-dpdk
echo 640 > /sys/devices/system/node/node0/hugepages/hugepages-2048kB/nr_hugepages

2、解压编译好的python解释器:

cp python.tar.gz /usr/local/
cd /usr/local/
tar zxvf python.tar.gz
ln -s /usr/local/bin/python2.7 /usr/bin/python

3、运行igb_uio.ko:

insmod /fw_dat/igb_uio.ko

4、绑定网卡:

./dpdk-devbind.py --bind=igb_uio 0000:01:00.0
./dpdk-devbind.py --bind=igb_uio 0000:01:00.1

5、运行二层转发程序:

./l2fwd -c f -n 2 – -q 1 -p 0x3

二、DPDK运行步骤与源代码执行关系:

uio代码相关的可以分为三个部分:内核uio框架及内核内部函数,uio用户驱动内核部分,uio用户驱动用户部分。

1、内核启动初始化:
内核uio框架通过配置内核选项CONFIG_UIO=y使能Userspace I/O drivers,在内核初始化时会调用uio_init创建uio_class; -----内核uio.c中完成,纯内核操作;

module_init(uio_init)----uio_init----init_uio_class----uio_major_init----class_register(&uio_class) 
                                                          |                       |------>创建“/sys/class/uio”设备目录或文件系统,此时该目录为空,在insmod igb_uio.ko后且运行python脚本绑定网卡后此目录下才有内容;
                                                          |---->申请字符设备号(alloc_chrdev_region),分配字符设备(cdev_alloc),uio字符设备操作函数挂载(cdev->ops = &uio_fops;)并将字符设备"uio"注册到系统中(cdev_add);
     
uio核心的任务就是管理好调用uio_register_device向它注册的uio设备。
两个重要数据结构:uio_device----uio设备使用的数据结构;
                 uio_info----保存设备属性,比如name, open(), release()等操作都放在了uio_info结构中,注册驱动之前要设置好。

2、insmod igb_uio.ko 加载igb_uio驱动(含igb、ixgbe等):
igb_uio内核驱动通过编译运行igb_uio.ko加载并注册一个pci设备驱动,但是igbuio_pci_driver对应的保存pci设备信息的 -----dpdk igb_uio.c中完成,编译时依赖内核开发包kernel-devel,纯内核操作;
id_table指针为空,这样在内核注册此pci设备驱动时,会找不到匹配的设备,就不会调用igb_uio驱动中的探测probe函数,只会在/sys/bus/pci/drivers/目录下创建Igb_uio相应的目录;在运行dpdk提供的Python脚本dpdk-devbind.py绑定网卡设备后才会执行其probe函数;

static struct pci_driver igbuio_pci_driver = {
	.name = "igb_uio",
	.id_table = NULL,  -----id_table指针为空(即deviceid为空,类似内核igb_pci_tbl,dpdk的pci_id_igb_map),找不到驱动能匹配的设备,不会调用.probe钩子函数初始化;----可以试试增加id_table,去掉Python脚本dpdk-devbind网卡操作;
	.probe = igbuio_pci_probe,
	.remove = igbuio_pci_remove,
};

module_init----igbuio_pci_init_module----pci_register_driver(&igbuio_pci_driver)---->igbuio_pci_probe----uio_register_device(__uio_register_device)|----uio_get_minor
                                         |此之前都是在dpdk的igb_uio.c中调用内核接口函数<-----|---->此之后都是在内核的uio.c中调用内核接口函数                   |---->利用idr机制(idr_alloc)建立了次设备号(整数ID)和uio_device类型指针之间的联系,idev->minor = retval
                                         |---->注册igbuio设备驱动,在/sys/bus/pci/drivers/目录下创建Igb_uio相应的目录                                  |----device_create(&uio_class, parent,MKDEV(uio_major, idev->minor), idev,"uio%d", idev->minor);
                                                                                                                                                            |---->创建设备并关联uio_class,调用完毕后在 /sys/class/uio/下就会出现代表uio设备的uioX文件夹,其中X为uio设备的次设备号idev->minor。
                                                                                                                                                     |----request_irq
                                                                                                                                                            |---->初始化设备中断(uio_interrupt),每个uio设备的硬件中断处理函数都是单独注册的,非收发包使用,好像是设备link状态中断;

注:“/sys/bus/pci/drivers/igb_uio/”下存放的是驱动igb_uio目录文件;“/sys/class/uio/uioX”下存放的是uio设备目录文件,与“/dev/uioX”设备句柄对应;

3、Python脚本dpdk-devbind.py执行bind网卡
1)使用Python脚本dpdk-devbind.py -s查看当前设备状态,能够显示网卡个数、驱动类型,是否被内核驱动或DPDK驱动接管;
获取参数指定的网卡eth0的设备信息。使用lspci –Dvmmn查看。

Slot: 0000:06:00.1
Class: 0200
Vendor: 8086
Device: 1521
SVendor: 15d9
SDevice: 1521 Rev: 01

可以查看到slot槽位信息、厂商号vendor ID、设备号device ID等信息。

2)使用Python脚本dpdk-devbind.py --bind=igb_uio eth0/0000:04:00.1,将eth0网卡绑定到igb_uio模块,已驱动的网卡会先卸载驱动(ifconfig eth0消失了),然后重新bind到igb_uio模块;
**a、unbind已经加载完驱动的igb网卡:**就是将eth对应的slot信息值写入/sys/bus/pci/drivers/igb_uio/unbind文件;
----内核中处理此步的函数为unbind_store,调用device_release_driver–pci_device_remove卸载内核驱动(内核代码就是将igb模块信息和此pci设备Dev去关联。将dev->driver指针置为空);

static int __pci_device_probe(struct pci_driver *drv, struct pci_dev *pci_dev)
{
	const struct pci_device_id *id;
	int error = 0;

	if (!pci_dev->driver && drv->probe) { ----判断该PCI设备是否已有驱动接管,probe初始化函数是否为空;若已被驱动或无初始化函数则返回;
		error = -ENODEV;

		id = pci_match_device(drv, pci_dev); ----遍历pci驱动的id_table与网卡设备信息比较,如果匹配成功则认为该设备可以被当前驱动支持;
		if (id)
			error = pci_call_probe(drv, pci_dev, id);----回调该网卡对应驱动probe初始化函数,比如igb_probe();(local_pci_probe--pci_drv->probe(pci_dev, ddi->id))
		if (error >= 0)
			error = 0;
	}
	return error;
}
static inline const struct pci_device_id *
pci_match_one_device(const struct pci_device_id *id, const struct pci_dev *dev)
{
	if ((id->vendor == PCI_ANY_ID || id->vendor == dev->vendor) &&  -----pci驱动的device_id与网卡设备dev信息比较;
	    (id->device == PCI_ANY_ID || id->device == dev->device) &&
	    (id->subvendor == PCI_ANY_ID || id->subvendor == dev->subsystem_vendor) &&
	    (id->subdevice == PCI_ANY_ID || id->subdevice == dev->subsystem_device) &&
	    !((id->class ^ dev->class) & id->class_mask))
		return id; ----匹配成功则返回设备ID,否则返回空;
	return NULL;
}

**b、bind网卡到新的igb_uio模块:**就是将eth设备的vendor和device ID信息写入/sys/bus/pci/drivers/igb_uio/new_id文件;
----内核中处理此步的函数为store_new_id,此函数中是将写入的vendor和device存入到此uio driver,也就是igb_uio的id_table,然后以此与PCI上的设备进行匹配并成功,然后调用igb_uio模块的probe函数(igbuio_pci_probe)进行初始化动作。

调用:store_new_id----pci_add_dynid----driver_attach----driver_probe_device----really_probe----dev->bus->probe(igbuio_pci_probe);bus、drivers类型与目录/sys/bus/pci/drivers/igb_uio/对应;
                                                                                                           |---->udev = kzalloc(sizeof(struct rte_uio_pci_dev), GFP_KERNEL),分配一个UIO设备数据结构,包括了uio_info;
                                                                                                           |---->pci_enable_device\pci_set_master,使能PCI设备及设置掩码;
                                                                                                           |---->igbuio_setup_bars--igbuio_pci_setup_iomem--pci_resource_start\pci_resource_len--ioremap()
                                                                                                           |     |---->映射UIO设备PCI资源空间(PCI设备的物理地址及大小),填充uio_info结构体的内存信息;
                                                                                                           |---->uio_register_device(__uio_register_device)
                                                                                                                 |----uio_get_minor
                                                                                                                 |       |---->利用idr机制(idr_alloc)建立了次设备号(整数ID)和uio_device类型指针之间的联系,idev->minor = retval                                    |
                                                                                                                 |----device_create(&uio_class, parent,MKDEV(uio_major, idev->minor), idev,"uio%d", idev->minor);
                                                                                                                 |       |---->创建设备并关联uio_class,调用完毕后在 /sys/class/uio/下就会出现代表uio设备的uioX文件夹,其中X为uio设备的次设备号idev->minor。
                                                                                                                 |----request_irq
                                                                                                                 |       |---->初始化设备中断(uio_interrupt),每个uio设备的硬件中断处理函数都是单独注册的,非收发包使用,好像是设备link状态中断;
                                                                                                  

bing操作后,dmesg就会看到igb_uio模块的probe函数执行了(id_table不为空了),也就是意味着扫描到了匹配的pci设备,将uio_info注册到内核中,注册后在/sys/class/uio/uioX,同时生成/dev/uioX设备(X为次设备号),此时/sys/class/uio/目录下已产生于/dev/uioX设备对应的内容。
其中X是我们注册的第几个uio设备,比如uio0,在该文件夹下的map/map0会有我们刚才填充的一些信息,包括addr、name、size、offset,其中addr保存的是设备的物理地址,size保存的是地址的大小,这些在用户态会将其读出,并mmap至用户态进程空间,这样用户态便可直接操作设备的内存空间。

4、运行实例程序(l2fwd):
1)各网卡驱动注册到全局驱动链表:
其使用了一种奇技淫巧的方法,使用GCC attribute扩展属性的constructor属性,使得网卡驱动的注册在程序main函数之前就执行了。

igb_ethdev.c:5432
static struct rte_pci_driver rte_igb_pmd = {                   //igb网卡驱动
	.id_table = pci_id_igb_map,                                  //igb网卡驱动所能支持的设备列表
	.drv_flags = RTE_PCI_DRV_NEED_MAPPING | RTE_PCI_DRV_INTR_LSC,//初始化时需要映射PCI BAR地址空间,并且支持link状态硬件中断
	.probe = eth_igb_pci_probe,                                  //igb网卡驱动的初始化函数
	.remove = eth_igb_pci_remove,
};
RTE_PMD_REGISTER_PCI(net_e1000_igb, rte_igb_pmd);              //注册rte_igb_pmd驱动到&rte_pci_bus.driver_list驱动链表
RTE_PMD_REGISTER_PCI_TABLE(net_e1000_igb, pci_id_igb_map);     //注册igb网卡驱动所能支持的设备列表为静态变量
rte_pci.h:514
#define RTE_PMD_REGISTER_PCI(nm, pci_drv) \                    //所有dpdk支持的网卡驱动以此方式注册到&rte_pci_bus.driver_list驱动链表
RTE_INIT(pciinitfn_ ##nm); \
static void pciinitfn_ ##nm(void) \
{\
	(pci_drv).driver.name = RTE_STR(nm);\
	rte_pci_register(&pci_drv); \
} \
RTE_PMD_EXPORT_NAME(nm, __COUNTER__)
#define RTE_INIT(func) \
static void __attribute__((constructor, used)) func(void)     //使用GCC attribute扩展属性的constructor属性,使得网卡驱动的注册在程序main函数之前就执行,相同的还有RTE_REGISTER_BUS(PCI_BUS_NAME, rte_pci_bus.bus);
eal_common_pci.c:479
/* register a driver */
void rte_pci_register(struct rte_pci_driver *driver)
{
	TAILQ_INSERT_TAIL(&rte_pci_bus.driver_list, driver, next);  //注册rte_igb_pmd驱动到&rte_pci_bus.driver_list驱动链表
	driver->bus = &rte_pci_bus;
}

2)PCI总线注册到全局总线链表:
其使用了和网卡驱动注册相同的方法,使用GCC attribute扩展属性的constructor属性,使得pci_bus总线注册在程序main函数之前就执行

eal_common_pci.c:515
struct rte_pci_bus rte_pci_bus = {  //rte_pci_bus.bus最终插入到rte_bus_list全局链表中;RTE_REGISTER_BUS(PCI_BUS_NAME, rte_pci_bus.bus);
	.bus = {
		.scan = rte_pci_scan,           //此钩子函数在rte_bus_scan中回调:TAILQ_FOREACH(bus, &rte_bus_list, next) {ret = bus->scan();}
		.probe = rte_pci_probe,         //此钩子函数在rte_bus_probe中回调:TAILQ_FOREACH(bus, &rte_bus_list, next) {ret = bus->probe();;}
	},                                //dpdk17.05.2相比于dpdk16.07.2变化很大,设备链表device_list和驱动链表driver_list合并到了一个结构体全局变量中
	.device_list = TAILQ_HEAD_INITIALIZER(rte_pci_bus.device_list),  //各网卡设备注册到全局设备链表
	.driver_list = TAILQ_HEAD_INITIALIZER(rte_pci_bus.driver_list),  //各网卡驱动注册到全局驱动链表:
};

对比:

/** Global list of device drivers. */
static struct rte_driver_list dev_driver_list =  //dpdk16.07.2的驱动链表dev_driver_list是单独的
	TAILQ_HEAD_INITIALIZER(dev_driver_list);

/** Global list of user devices */              //dpdk16.07.2的设备链表devargs_list是单独的
struct rte_devargs_list devargs_list =
	TAILQ_HEAD_INITIALIZER(devargs_list);
rte_bus.h:147
#define RTE_REGISTER_BUS(nm, bus) \
static void __attribute__((constructor(101), used)) businitfn_ ##nm(void) \ //使用GCC attribute扩展属性的constructor属性,使得pci_bus总线注册在程序main函数之前就执行
{\
	(bus).name = RTE_STR(nm);\
	rte_bus_register(&bus); \     //总线注册函数,rte_pci_bus.bus最终插入到rte_bus_list全局链表中,bus总线遍历时使用,目前好像只有PCI一种,新增的,可能为以后扩展考虑
}

eal_common_bus.c:46
void rte_bus_register(struct rte_bus *bus)
{
	RTE_VERIFY(bus);
	RTE_VERIFY(bus->name && strlen(bus->name));
	/* A bus should mandatorily have the scan implemented */
	RTE_VERIFY(bus->scan);                         //挂载rte_pci_bus.bus总线扫描函数,rte_pci_scan
	RTE_VERIFY(bus->probe);                        //挂载rte_pci_bus.bus总线初始化函数,rte_pci_probe

	TAILQ_INSERT_TAIL(&rte_bus_list, bus, next);   //rte_bus_list全局链表入队
	RTE_LOG(DEBUG, EAL, "Registered [%s] bus.\n", bus->name);
}

3)网卡设备注册到全局设备链表:
初始化EAL环境抽象层时才会进行驱动与设备匹配加载。

main----rte_eal_init----rte_bus_scan(钩子回调)----rte_pci_scan----|----pci_get_sysfs_path---->打开/sys/bus/pci/devices/目录
				eal.c:497				eal_common_bus.c:67				 eal_pci.c:239		|----parse_pci_addr_format---->循环读取/sys/bus/pci/devices/目录下的各设备目录信息,扫描当前系统的PCI设备,并初始化struct rte_pci_addr数据结构的slot成员(domain、bus、devid、function)
																														  	    |----pci_scan_one---->分配rte_pci_device
																														  	         |----eal_parse_sysfs_value---->获取并赋值vendor id、device id 、numa_node等信息
																														  	   			|----rte_pci_device_name---->根据rte_pci_addr数据结构的slot成员组装rte_pci_device->name、rte_device->name,此处修改设备名字@
																														  	         |----pci_parse_sysfs_resource---->获取网卡PCI资源空间phys_addr、end_addr、len等信息
																														  	         |----pci_get_kernel_driver_by_path---->获取网卡对应的驱动类型
																														  	         |----rte_pci_add_device---->将网卡设备注册到全局设备链表&rte_pci_bus.device_list

4)网卡设备驱动初始化:
初始化EAL环境抽象层时才会进行网卡设备驱动初始化。

main----rte_eal_init----rte_bus_probe(钩子回调)----rte_pci_probe----|----FOREACH_DEVICE_ON_PCIBUS---->循环遍历设备链表
				eal.c:497				eal_common_bus.c:86			 eal_common_pci.c:408     |----pci_probe_all_drivers----FOREACH_DRIVER_ON_PCIBUS---->循环遍历驱动链表
																																															          |----rte_pci_probe_one_driver
																																															               |----rte_pci_match---->循环判断各网卡驱动与当前设备是否匹配,依据:vendor_id、device_id、class_id等
																																															               |----rte_pci_map_device----pci_uio_map_resource----pci_uio_map_resource_by_index---pci_map_resource---->mmap映射网卡空间地址为虚拟地址,
																																															               |----ret = dr->probe(dr, dev)      |---->将所有UIO设备的resource信息都记录在struct mapped_pci_resource数据结构中,并挂到全局链表pci_res_list上
																																															               |回调具体驱动初始化函数|---->eth_igb_pci_probe

struct rte_eth_dev rte_eth_devices[RTE_MAX_ETHPORTS];
static struct rte_eth_dev_data *rte_eth_dev_data;
单独分析:eth_igb_pci_probe----rte_eth_dev_pci_generic_probe
	                            |----rte_eth_dev_pci_allocate
	                            |    |----name = dev->device.name---->设备名称赋值,此处修改设备名字,可以覆盖之前的@
	                            |    |----eth_dev->data->dev_private = rte_zmalloc_socket---->设备私有数据e1000_adapter内存分配
	                            |		 |----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_get---->将port_id、rte_eth_dev、rte_eth_dev_data三者关联,下标为port_id,rte_eth_dev[port_id]->data=rte_eth_dev_data[port_id]
															|					|----eth_dev->data->name=name、eth_dev->data->port_id = port_id、eth_dev->data->mtu = ETHER_MTU;初始化网口date数据(name、id、mtu)
															|----eth_igb_dev_init		
														  |    |----eth_dev->dev_ops = &eth_igb_ops; //设置网卡设备的操作函数集以及收包、发包函数。@igb_setops,钩子兼容
															|		 |		eth_dev->rx_pkt_burst = &eth_igb_recv_pkts;
															|		 |		eth_dev->tx_pkt_burst = &eth_igb_xmit_pkts;
															|    |----rte_eth_copy_pci_info---->初始化rte_eth_dev->data的dev_flags、numa_node、kdrv、drv_name等变量
															|    |----igb_identify_hardware---->网卡硬件信息存入e1000_hw,包括vendor_id、device_id、mac->type(e1000_i350)
															|    |----e1000_setup_init_funcs---->走了两次,挂载网卡mac、phy、nvm、mbx操作函数,大部分为空操作,应该是预留的接口;
															|    |----hw->mac.autoneg = 1、hw->phy.autoneg_advertised = E1000_ALL_SPEED_DUPLEX;//设置自协商及声明支持的速率、双工模式
															|    |----igb_pf_reset_hw---->重启硬件网口,使能PF/VF Mail Ops,支持SR-IOV
															|    |----e1000_read_mac_addr---->从网卡硬件中读取MAC,此处修改MAC,读取生成文件的@
															|    |----igb_hardware_init---->读寄存器获取rx_buf_size、phy_info、check_for_link,再次重启硬件igb_pf_reset_hw
															|    |----rte_intr_callback_register---->注册中断处理函数eth_igb_interrupt_handler,主要是硬件link变化中断
															|    |----igb_intr_enable---->中断使能,没有设备registe操作了
															|					
															|---->@此处添加,根据eth_igb_dev_init结果参考调用man_net_probe注册net_device、tun假接口,入参是rte_eth_dev,两者信息保持一致(name一致,portid、index不好一致)

5)网卡收发资源分配:

main|----rte_pktmbuf_pool_create---->创建收发包资源池mbuf_pool
    |----rte_eth_dev_configure
    |     |----rte_eth_dev_rx_queue_config---->设置接收队列长度,并存入dev->data->nb_rx_queues
    |     |----rte_eth_dev_tx_queue_config---->设置发送队列长度,并存入dev->data->nb_tx_queues																			
    |----rte_eth_rx_queue_setup----回调eth_igb_rx_queue_setup---->分配接收队列内存,初始化igb_rx_queue结构,主要的queue_id、port_id、rdt_reg_addr尾指针、rdh_reg_addr头指针、rx_ring_phys_addr(dma)、nb_rx_desc、reg_idx等
    |----rte_eth_tx_queue_setup----回调eth_igb_tx_queue_setup---->分配接收队列内存,初始化igb_tx_queue结构,主要的queue_id、port_id、nb_tx_desc、tx_ring_phys_addr(dma)、reg_idx等
    |
    |----rte_eth_dev_start----eth_igb_start|----eth_igb_dev_set_link_up---->上电PHY
    |																			 |----igb_pf_host_configure---->配置PF模式(如果SR-IOV开启)
    																			 |----eth_igb_tx_init---->设置发送DMA寄存器,base、lenth、head、tail;
    																			 |----eth_igb_rx_init|---->设置接收DMA寄存器,base、lenth、head、tail;
    																			 |									 |----igb_alloc_rx_queue_mbufs---->初始化分配收包的rte_mbuf	
    																			 |									 |----igb_dev_mq_rx_configure----igb_rss_configure----igb_hw_rss_hash_set---->设置多队列及RSS	
    																			 |----e1000_setup_link---->设置网卡速率、双工
    																			 |----igb_intr_enable---->使能硬件中断

6)网卡收发报文:

main----l2fwd_launch_one_lcore----l2fwd_main_loop----rte_eth_rx_burst
																								 ----l2fwd_simple_forward----rte_eth_tx_buffer----rte_eth_tx_buffer_flush----rte_eth_tx_burst----eth_igb_xmit_pkts

原创不易,转发请注明出处。

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值