前言:DPDK使用了UIO(用户空间I/O)的机制,跳过内核态的网卡驱动,转而使用用户态的收发包驱动,从驱动到内存和数据包,继而到数据包的处理,这是一个完整的收发包流程。这篇主要介绍设备驱动的初始化,和收发包的处理。所选代码以DPDK-17.02版本为依据。
数据包的驱动初始化是在rte_eal_init()进行的
,总体上分为2个阶段进行。
rte_eal_pci_init()
,主要是获取系统中的设备PCI。rte_eal_pci_probe()
,这个阶段做的事比较多,匹配对应的设备驱动,分配设备,并对设备进行初始化。我们就按照这个顺序进行介绍。
<1>.先看rte_eal_init()
这个函数,了解这一阶段的处理过程。
在函数中,调用了rte_eal_pci_scan(),来扫描系统目录里的PCI设备。默认的扫描目录是#define SYSFS_PCI_DEVICES "/sys/bus/pci/devices"
,就是依次读取目录下的每一个文件名,解析PCI的地址信息,填充地址信息。
然后调用了pci_scan_one()
来进行设备信息的填充,挂接设备。先分配了一个PCI设备结构,注意:此处分配的是PCI设备结构,并不是rte_eth_dev设备。前者标识一个PCI设备,后者标识一个网卡设备。然后依次读取每个PCI设备目录下的vendor,device等文件,填充刚分配出来的PCI设备结构。接下来使用pci_get_kernel_driver_by_path()
获取设备驱动的类型。比如使用82599的网卡,就会看到类型为igb_uio,设置对应的驱动类型。
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
中:如果设备队列是空的,则直接挂上,如果不是空的,则按照PCI地址排序后挂接在队列中。
<2>.rte_eal_pci_probe()
函数进入了第二阶段的初始化。
先进行了一个设备参数类型的检查,rte_eal_devargs_type_count()
,在这里又涉及到另一个变量---devargs_list
,这个全局变量记录着哪些设备的PCI是在白或者黑名单里面,如果是在黑名单里,后面就不进行初始化。这个devargs_list
的添加注册是在参数解析部分,-w,-b参数指定的名单。
然后依次遍历队列中的每个PCI设备,和devargs_list比较,查看是否有设备在列表中,如果在黑名单中,就不进行初始化。
之后调用pci_probe_all_drivers()
对每个允许的设备进行初始化。c TAILQ_FOREACH(dr, &pci_driver_list, next) { rc = rte_eal_pci_probe_one_driver(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; }
和每个注册的驱动进行比较,注册的驱动都挂接在pci_driver_list
中,驱动的注册是通过下面的一段代码实现的c #define RTE_PMD_REGISTER_PCI(nm, pci_drv) \ RTE_INIT(pciinitfn_ ##nm); \ static void pciinitfn_ ##nm(void) \ {\ (pci_drv).driver.name = RTE_STR(nm);\ rte_eal_pci_register(&pci_drv); \ } \ RTE_PMD_EXPORT_NAME(nm, __COUNTER__)
这里注意注册函数的类型为析构函数,gcc的补充。它是在main函数之前就执行的,所以,在main之前,驱动就已经注册好了。c #define RTE_INIT(func) \ static void __attribute__((constructor, used)) func(void)
进一步查看的话,发现系统注册了这么几种类型的驱动:
(1).rte_igb_pmd
(2).rte_igbvf_pmd
(3).rte_ixgbe_pmd
.....
如rte_ixgbe_pmd驱动c static struct eth_driver rte_ixgbe_pmd = { .pci_drv = { .id_table = pci_id_ixgbe_map, .drv_flags = RTE_PCI_DRV_NEED_MAPPING | RTE_PCI_DRV_INTR_LSC, .probe = rte_eth_dev_pci_probe, .remove = rte_eth_dev_pci_remove, }, .eth_dev_init = eth_ixgbe_dev_init, .eth_dev_uninit = eth_ixgbe_dev_uninit, .dev_private_size = sizeof(struct ixgbe_adapter), };
其中的id_table表中就存放了各种支持的ixgbe设备的vendor号等详细信息。
接下来,自然的,如果匹配上了,就调用对应的驱动probe函数。进入rte_eal_pci_probe_one_driver()
函数进行匹配。
当匹配成功后,对PCI资源进行映射--rte_eal_pci_map_device()
,这个函数就不进行细细分析了。
最重要的地方到了,匹配成功后,就调用了dr->probe
函数,对于ixgbe驱动,就是rte_eth_dev_pci_probe()
函数,我们跳进去看看这个probe函数。
首先检查进程如果为RTE_PROC_PRIMARY类型的,那么就分配一个rte_eth_dev设备,调用rte_eth_dev_allocate()
,分配可用的port_id,然后如果rte_eth_dev_data没有分 配,则一下子分配RTE_MAX_ETHPORTS个这个结构,这个结构描述了每个网卡的数据信息,并把对应port_id的rte_eth_dev_data[port_id]关联到新分配的设备上。
设备创建好了以后,就给设备的私有数据分配空间,c eth_dev->data->dev_private = rte_zmalloc("ethdev private structure", eth_drv->dev_private_size, RTE_CACHE_LINE_SIZE);
然后填充设备的device,driver信息等。最后调用设备的初始化函数--eth_drv->eth_dev_init
,这在ixgbe驱动中,是eth_ixgbe_dev_init()
。
从这个初始化函数,进入最后的初始化环节。
要知道的一点是:在这个函数中,很多的工作肯定还是填充分配的设备结构体。先填充了设备的操作函数,以及非常重要的收发包函数c eth_dev->dev_ops = &ixgbe_eth_dev_ops; eth_dev->rx_pkt_burst = &ixgbe_recv_pkts; eth_dev->tx_pkt_burst = &ixgbe_xmit_pkts; eth_dev->tx_pkt_prepare = &ixgbe_prep_pkts;
再检查如果不是RTE_PROC_PRIMARY进程,则只要检查一下收发函数,并不进一步设置。
然后拷贝一下pci设备的相关信息
```c
rte_eth_copy_pci_info(eth_dev, pci_dev);
eth_dev->data->dev_flags |= RTE_ETH_DEV_DETACHABLE;
/* Vendor and Device ID need to be set before init of shared code /
hw->device_id = pci_dev->id.device_id;
hw->vendor_id = pci_dev->id.vendor_id;
hw->hw_addr = (void )pci_dev->mem_resource[0].addr;
hw->allow_unsupported_sfp = 1;
``接下来针对对应的设备,调用
ixgbe_init_shared_code()根据
hw->device_id`来初始化特定的设备的MAC层操作函数集,ixgbe_mac_operations,如82599设备。
上面的操作都完成后,就可以调用ixgbe_init_hw()
对硬件进行初始化了,初始化的函数在上一步MAC层函数操作集已经初始化。
然后重置设备的硬件统计,分配MAC地址,最后初始化一下各种过滤条件。
Done!!整个PCI驱动的匹配和初始化过程就完成了。
======================================
前言:DPDK提供了内存池机制,使得内存的管理的使用更加简单安全。在设计大的数据结构时,都可以使用mempool分配内存,同时,mempool也提供了内存的获取和释放等操作接口。对于数据包mempool甚至提供了更加详细的接口-rte_pktmbuf_pool_create()
,接下来重点分析通用的内存池相关内容。使用DPDK-17.02版本。
内存池的创建使用的接口是rte_mempool_create()
。在仔细分析代码之前,先说明一下mempool的设计思路:在DPDK-17.02版本中(和2.1等先前版本在初始化略有差异),总体来说,mempool的组织是通过3个部分实现的
struct rte_tailq_elem rte_mempool_tailq
全局队列中,可以根据mempool的名字进行查找,使用rte_mempool_lookup()
接口即可。这只是个mempool的指示结构,mempool分配的内存区并不在这里面,只是通过物理和虚拟地址指向实际的内存地址。接下来,就来具体看看mempool的创建和初始化过程。
先注意一下rte_mempool_create
的参数中的两个-mp_init
和obj_init
,前者负责初始化mempool中配置的私有参数,如在数据包中加入的我们自己的私有结构;后者负责初始化每个mempool对象。我们然后按照mempool的3个关键部分展开说明。
struct rte_mempool
,cache和mempool private。创建是在rte_mempool_create_empty()
中完成的,看这个函数,先进行了对齐的检查RTE_BUILD_BUG_ON((sizeof(struct rte_mempool) & RTE_CACHE_LINE_MASK) != 0);
然后从mempool队列中取出头节点,我们创建的mempool结构填充好,就挂接在这个节点上。接下来做一些检查工作和创建flag的设置。
rte_mempool_calc_obj_size()
计算了每个obj的大小,这个obj又是由三个部分组成的,objhdr,elt_size,objtlr,即头,数据区,尾。在没有开启RTE_LIBRTE_MEMPOOL_DEBUG
调试时,没有尾部分;头部分的结构为:struct rte_mempool_objhdr
,通过这个头部,mempool中的obj都是链接到队列中的,所以,提供了遍历obj的方式(尽管很少这么用)。函数返回最后计算对齐后的obj的大小,为后面分配空间提供依据。
然后分配了一个mempool队列条目,为后面挂接在队列做准备。
te = rte_zmalloc("MEMPOOL_TAILQ_ENTRY", sizeof(*te), 0);
if (te == NULL) {
RTE_LOG(ERR, MEMPOOL, "Cannot allocate tailq entry!\n");
goto exit_unlock;
}
接下来,就是计算整个mempool头结构多大,吐槽这里的命名!
mempool_size = MEMPOOL_HEADER_SIZE(mp, cache_size);
mempool_size += private_data_size;
mempool_size = RTE_ALIGN_CEIL(mempool_size, RTE_MEMPOOL_ALIGN);
mempool_size
这个名字太有误导性,这里指的是计算mempool的头结构的大小。而不是内存池实际的大小。在这里可以清晰的看出这个mempool头结构是由三部分组成的。cache计算的是所有核上的cache之和。
然后,分配这个mempool头结构大小的空间,填充mempool结构体,并把mempool头结构中的cache地址分配给mempool。初始化这部分cache.
最后就是挂接mempool结构。TAILQ_INSERT_TAIL(mempool_list, te, next);
这部分的创建是在函数rte_mempool_populate_default()
中完成的。
首先计算了每个elt的总共的大小
total_elt_sz = mp->header_size + mp->elt_size + mp->trailer_size;
然后计算为这些元素需要分配多大的空间,rte_mempool_xmem_size(n, total_elt_sz, pg_shift);
接着rte_memzone_reserve_aligned()
分配空间。
终于到关键的一步了,rte_mempool_populate_phys()
把元素添加到mempool,实际上就是把申请的空间分给每个元素。
先看到的是这么一段代码:
if ((mp->flags & MEMPOOL_F_POOL_CREATED) == 0) {
ret = rte_mempool_ops_alloc(mp);
if (ret != 0)
return ret;
mp->flags |= MEMPOOL_F_POOL_CREATED;
}
这就是创建ring的过程咯,其中的函数rte_mempool_ops_alloc()就是实现。那么,对应的ops->alloc()在哪注册的呢?
if ((flags & MEMPOOL_F_SP_PUT) && (flags & MEMPOOL_F_SC_GET))
rte_mempool_set_ops_byname(mp, "ring_sp_sc", NULL);
else if (flags & MEMPOOL_F_SP_PUT)
rte_mempool_set_ops_byname(mp, "ring_sp_mc", NULL);
else if (flags & MEMPOOL_F_SC_GET)
rte_mempool_set_ops_byname(mp, "ring_mp_sc", NULL);
else
rte_mempool_set_ops_byname(mp, "ring_mp_mc", NULL);
就是根据ring的类型,来注册对应的操作函数,如默认的就是ring_mp_mc,多生产者多消费者模型,其操作函数不难找到:
static const struct rte_mempool_ops ops_mp_mc = {
.name = "ring_mp_mc",
.alloc = common_ring_alloc,
.free = common_ring_free,
.enqueue = common_ring_mp_enqueue,
.dequeue = common_ring_mc_dequeue,
.get_count = common_ring_get_count,
};
接下来,又分配了一个struct rte_mempool_memhdr *memhdr;
结构的变量,就是这个变量管理着mempool的实际内存区,它记录着mempool实际地址区的物理地址,虚拟地址,长度等信息。
再然后,就是把每个元素对应到mempool池中了:mempool_add_elem()
。在其中,把每个元素都挂在了elt_list中,可以遍历每个元素。最后rte_mempool_ops_enqueue_bulk(mp, &obj, 1);
,最终,把元素对应的地址入队,这样,mempool中的每个元素都放入了ring中。
创建完成!!!
mempool的常见使用是获取元素空间和释放空间。
rte_mempool_get
可以获得池中的元素,其实就是从ring取出可用元素的地址。rte_mempool_put
可以释放元素到池中。rte_mempool_in_use_count
查看池中已经使用的元素个数rte_mempool_avail_count
查看池中可以使用的元素个数mempool是DPDK内存管理的重要组件,这篇重点介绍了 mempool创建使用的过程,对于系统如何做大页映射,统一地址并没有涉及,希望在后面的篇幅中,关注一下大页的映射和共享内存等。再往后,会介绍驱动与收发包等联系较大的内容