【DPDK】【Multiprocess】一个dpdk多进程场景的坑

【前言】html

  这是一个隐藏了近3年的问题,理论上只要用到DPDK multiprocess场景的都会遇到这个问题,具体出不出问题只能说是看运气,即便不出问题也仍然是一个风险。node

  patch地址:https://patches.dpdk.org/patch/64819/linux

  讨论的patch地址:https://patches.dpdk.org/patch/64526/编程

【场景】数组

  我先描述一下这个问题我是怎么撞到的吧。安全

  我司不一样的产品线都不一样程度的使用了DPDK做为网络IO加速的手段,我相信这也是全部使用DPDK人的初衷,而且我司不一样的产品线在设计上有使用DPDK multiprocess场景实现业务逻辑。网络

  在我这边的状况是这样,用过DPDK的话都知道,DPDK会利用本身的igb_uio/vfio驱动来接管传统内核驱动,这样每每会致使一些问题,就是咱们一些传统的类unix工具,诸如ifconfig、ip、ethtool等工具没法再查看被DPDK驱动接管的网卡状态。数据结构

  举个例子:app

  在传统linux场景下,我向看一下网卡丢包缘由、网卡寄存器状态、网卡的feature,经过一个ethtool就能够搞定,可是到了DPDK这里就行不通了,由于上述传统工具实际上都是去内核拿数据,ethtool底层就是用ioctl去读的内核数据,可是如今网卡驱动已经被DPDK驱动接管了,用ethtool再也拿不到信息了。socket

  所以我从新写了一个ethtool-dpdk,用来专门解决在dpdk场景下的查看网卡驱动状态。这个工具是以secondary进程实现的,每次运行,都会attach到primary进程中,去获取primary进程和secondary进程之间的share memory。其中就包括struct rte_eth_dev_data这个处在share memory的数据结构,经过获取这个结构中的pci bar,我就能够经过“基地址 + 寄存器偏移量”的手段去拿到寄存器状态。

/**
 * @internal
 * The data part, with no function pointers, associated with each ethernet device.
 *
 * This structure is safe to place in shared memory to be common among different (这个结构处于共享内存中)
 * processes in a multi-process configuration.
 */
struct rte_eth_dev_data {
    char name[RTE_ETH_NAME_MAX_LEN]; /**< Unique identifier name */

    void **rx_queues; /**< Array of pointers to RX queues. */
    void **tx_queues; /**< Array of pointers to TX queues. */ uint16_t nb_rx_queues; /**< Number of RX queues. */ uint16_t nb_tx_queues; /**< Number of TX queues. */ struct rte_eth_dev_sriov sriov; /**< SRIOV data */ void *dev_private; /**< PMD-specific private data */ //这个里面存着pci bar struct rte_eth_link dev_link; /**< Link-level information & status */ struct rte_eth_conf dev_conf; /**< Configuration applied to device. */ uint16_t mtu; /**< Maximum Transmission Unit. */ uint32_t min_rx_buf_size; /**< Common rx buffer size handled by all queues */ uint64_t rx_mbuf_alloc_failed; /**< RX ring mbuf allocation failures. */ struct ether_addr* mac_addrs;/**< Device Ethernet Link address. */ uint64_t mac_pool_sel[ETH_NUM_RECEIVE_MAC_ADDR]; /** bitmap array of associating Ethernet MAC addresses to pools */ struct ether_addr* hash_mac_addrs; /** Device Ethernet MAC addresses of hash filtering. */ uint8_t port_id; /**< Device [external] port identifier. */ __extension__ uint8_t promiscuous : 1, /**< RX promiscuous mode ON(1) / OFF(0). */ scattered_rx : 1, /**< RX of scattered packets is ON(1) / OFF(0) */ all_multicast : 1, /**< RX all multicast mode ON(1) / OFF(0). */ dev_started : 1, /**< Device state: STARTED(1) / STOPPED(0). */ lro : 1; /**< RX LRO is ON(1) / OFF(0) */ uint8_t rx_queue_state[RTE_MAX_QUEUES_PER_PORT]; /** Queues state: STARTED(1) / STOPPED(0) */ uint8_t tx_queue_state[RTE_MAX_QUEUES_PER_PORT]; /** Queues state: STARTED(1) / STOPPED(0) */ uint32_t dev_flags; /**< Capabilities */ //请注意这个标记 enum rte_kernel_driver kdrv; /**< Kernel driver passthrough */ int numa_node; /**< NUMA node connection */ struct rte_vlan_filter_conf vlan_filter_conf; /**< VLAN filter configuration. */ };

代码版本:

  代码来源于DPDK 17.08版本,可是此问题不局限于17.08版本,一直到19.11版本都存在,只是我在这个版本的dpdk代码踩到了这个坑,或者换句话说,这版本比较容易踩到这个坑。下列介绍凡是不特别说起,都为dpdk-17.08版本。

代码位置:

  DPDK 根目录/lib/librte_ether/rte_ethdev.h

【问题描述】

  可是偶然一次测试发现了问题。咱们的设备本来是支持网卡热插拔的,可是在启动这个ethtool-dpdk工具后发现网卡的热插拔居然失效了,primary去检查网卡热插拔的标记时,发现标记“消失了””

  标记所在的代码位置:

  DPDK 根目录/lib/librte_ether/rte_ethdev.h

/** Device supports hotplug detach */
#define RTE_ETH_DEV_DETACHABLE   0x0001 //网卡热插拔标记
/** Device supports link state interrupt */
#define RTE_ETH_DEV_INTR_LSC     0x0002 //网卡LSC中断标记
/** Device is a bonded slave */
#define RTE_ETH_DEV_BONDED_SLAVE 0x0004
/** Device supports device removal interrupt */
#define RTE_ETH_DEV_INTR_RMV     0x0008

 

  这些标记时用于给struct rte_eth_dev_data->dev_flags准备的,刚才咱们说过,rte_eth_dev_data这个数据结构处于共享内存中,由primary进程掌控。

  本来struct rte_eth_dev_data->dev_flags的值应该是 RTE_ETH_DEV_DETACHABLE | RTE_ETH_DEV_INTR_LSC,也就是0x0001 | 0x0002 = 0x0003。

  可是在使用ethtool-dpdk工具后,这个值变为了0x0002,也就是说,网卡热插拔标记RTE_ETH_DEV_DETACHABLE消失了...根据我刚才所说rte_eth_dev_data处于共享内存中,所以必定是secondary进程,也就是ethtool-dpdk工具改变了共享内存中的内容致使的。

  注意:若是已经知晓struct rte_eth_dev_data数据处于共享内存中,如下的分析应该扫一眼就知道是怎么回事了

【分析】

  在primary/secondary进程初始化过程当中,也就是调用rte_eal_init()函数进行初始化的过程当中,会去扫描pci设备,获取pci设备的状态信息。这里不了解的话,能够见我另一篇文章《DPDK初始化之PCI》,而且实际上不耽误了解此篇文章中的内容。

  在初始化的过程当中,primary进程和secondary进程都会进入rte_eth_dev_pci_allocate函数去获取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. */ eth_tx_prep_t tx_pkt_prepare; /**< Pointer to PMD transmit prepare function. */ struct rte_eth_dev_data *data; /**< Pointer to device data */ //注意这个指针 const struct eth_dev_ops *dev_ops; /**< Functions exported by PMD */ struct rte_device *device; /**< Backing device */ struct rte_intr_handle *intr_handle; /**< Device interrupt handle */ /** 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]; enum rte_eth_dev_state state; /**< Flag indicating the port state */ } __rte_cache_aligned;

  不了解这里的实现的话,我在这里就直接告诉你们, 这个struct rte_eth_dev数据结构描述的是“设备”,在咱们的场景下能够理解为描述某一个网卡设备,说白了就是一个管理性质的数据结构,网卡设备的抽象。

  先上数据结构:

  (这张图若是看一眼就知道什么意思的基本接下来的分析大概看一看就能明白究竟是什么问题)

  rte_eth_dev_pci_allocate函数的做用实际上就是去得到这个struct rte_eth_dev数据,这里为何视角从关键的rte_eth_dev_data结构转到rte_eth_dev_pci_allocate函数,我先按下不表,跟着思路走便可,由于这里我更倾向于还原整个问题现场与顺序,若是直接从问题出现的上下文出发,反而很差分析。

static inline struct rte_eth_dev *
rte_eth_dev_pci_allocate(struct rte_pci_device *dev, size_t private_data_size) { struct rte_eth_dev *eth_dev; const char *name; if (!dev) return NULL; //step 1.先获取设备名 name = dev->device.name; //step 2.若是是primary进程就去调用rte_eth_dev_allocate函数去“申请”rte_eth_dev结构 //反之若是是secondary进程,就去调用rte_eth_dev_attach_secondary函数去“获取”rte_eth_dev结构 if (rte_eal_process_type() == RTE_PROC_PRIMARY) { eth_dev = rte_eth_dev_allocate(name); if (!eth_dev) return NULL; if (private_data_size) { eth_dev->data->dev_private = rte_zmalloc_socket(name, private_data_size, RTE_CACHE_LINE_SIZE, dev->device.numa_node); if (!eth_dev->data->dev_private) { rte_eth_dev_release_port(eth_dev); return NULL; } } } else { eth_dev = rte_eth_dev_attach_secondary(name); if (!eth_dev) return NULL; } eth_dev->device = &dev->device; //step 3.调用rte_eth_copy_pci_info去根据pci设备数据结构拷贝pci信息  rte_eth_copy_pci_info(eth_dev, dev); return eth_dev; }

  对应的流程图为:

  根据rte_eth_dev_pci_allocate函数的逻辑咱们能够看到有两处关键的地方,即:

  1. 要获取rte_eth_dev数据结构,只不过primary和secondary获取的方式不一样。
  2. 调用rte_eth_copy_pci_info函数,去从描述pci设备的数据结构中拷贝信息至rte_eth_dev这个描述设备的结构。

  先将视角聚焦在第一处关键位置,即获取rte_eth_dev数据结构,咱们这里的场景是secondary进程,所以primary进程执行的代码就不作分析,有兴趣的能够本身了解。

  接下来以secondary进程的视角进入rte_eth_dev_attach_secondary函数,观察secondary是怎么获取的struct rte_eth_dev结构,随之作了什么。

struct rte_eth_dev *
rte_eth_dev_attach_secondary(const char *name) { uint8_t i; struct rte_eth_dev *eth_dev; //step 1.判断全局数据指针rte_eth_dev_data是否为NULL,若是为NULL,则申请。 if (rte_eth_dev_data == NULL) rte_eth_dev_data_alloc(); //step 2.找到与设备名字对应的rte_eth_dev_data结构所在的下标id for (i = 0; i < RTE_MAX_ETHPORTS; i++) { if (strcmp(rte_eth_dev_data[i].name, name) == 0) break; } if (i == RTE_MAX_ETHPORTS) { RTE_PMD_DEBUG_TRACE( "device %s is not driven by the primary process\n", name); return NULL; } //step 3.根据上一步获取的下标id来调用eth_dev_get函数来获取struct rte_eth_dev数据结构 eth_dev = eth_dev_get(i); RTE_ASSERT(eth_dev->data->port_id == i); return eth_dev; }

  对应的流程图为:

 

  这个函数中一样有两个重要的点,即:

  1. 要调用rte_eth_dev_data_alloc()函数去“得到”rte_eth_dev_data这个数据结构
  2. 调用eth_dev_get函数拿到对应的struct rte_eth_dev结构

  咱们暂且跳过rte_eth_dev_data_alloc()函数,回头再来看,先看rte_dev_get函数是怎么拿到的这个struct rte_eth_dev结构。

static struct rte_eth_dev *
eth_dev_get(uint8_t port_id)
{
    struct rte_eth_dev *eth_dev = &rte_eth_devices[port_id]; eth_dev->data = &rte_eth_dev_data[port_id]; //rte_eth_dev中的data指针来自于rte_eth_dev_data结构 eth_dev->state = RTE_ETH_DEV_ATTACHED; TAILQ_INIT(&(eth_dev->link_intr_cbs)); eth_dev_last_created_port = port_id; return eth_dev; }

  对应的流程图为:

  rte_eth_dev结构来自于全局数组rte_eth_devices,说明rte_eth_dev数据为local数据,并非shared memory中的数据,可是。关键在于上述代码注释的那一行,rte_eth_dev中的data指针指向了rte_eth_dev_data数据。而rte_eth_dev_data咱们刚才也说过是在rte_eth_dev_attach_secondary中调用rte_eth_dev_data_alloc函数“得到的”,怎么得到的呢,让咱们接下来回过头来看rte_eth_dev_data_alloc函数是怎么得到的rte_eth_dev_data数据。

static void
rte_eth_dev_data_alloc(void) { const unsigned flags = 0; const struct rte_memzone *mz; //step 1.若是是primary进程则向memzone中申请一块空间做为rte_eth_dev_data数据所在 //若是是secondary进程,则直接lookup收共享内存中的rte_eth_dev_data数据 if (rte_eal_process_type() == RTE_PROC_PRIMARY) { mz = rte_memzone_reserve(MZ_RTE_ETH_DEV_DATA, RTE_MAX_ETHPORTS * sizeof(*rte_eth_dev_data), rte_socket_id(), flags); } else mz = rte_memzone_lookup(MZ_RTE_ETH_DEV_DATA); if (mz == NULL) rte_panic("Cannot allocate memzone for ethernet port data\n"); rte_eth_dev_data = mz->addr; if (rte_eal_process_type() == RTE_PROC_PRIMARY) memset(rte_eth_dev_data, 0, RTE_MAX_ETHPORTS * sizeof(*rte_eth_dev_data)); }

  对应是流程图:

  经过这段代码,咱们能够了解到一个信息,struct rte_eth_dev_data数据是处于共享内存中的,实际secondary进程去读网卡寄存器就是经过这个数据结构索引拿到pci bar,在根据基地址 + 寄存器偏移,拿到的具体的某一个寄存器地址,因此secondary进程才能够去读网卡的寄存器信息。

  通过上述的分析咱们起码知道如下几个线索,能够梳理一下:

  1. dpdk的primary/secondary进程初始化过程当中都会调用rte_eth_dev_pci_allocate函数去拿到struct rte_eth_dev结构。
  2. 在初始化过程当中,struct rte_eth_dev结构来自于全局数组struct rte_eth_devices,也就意味着rte_eth_dev结构为进程的local变量。
  3. 在初始化过程当中,还会给struct rte_eth_dev结构中的data指针初始化指向struct rte_eth_dev_data结构。
  4. struct rte_eth_dev_data结构在secondary进程中,其初始化时经过获取共享内存中的地址获得的,觉得这struct rte_eth_dev_data结构在共享内存中。

  其实上述4点梳理的线索只是为了让你们明白:secondary中握着和primary进程的共享内存结构,这个结果是struct rte_eth_dev_data结构,既然握着共享内存,就容易犯错。

  而犯错的代码就位于rte_eth_dev_pci_allocate函数中第二处关键的位置,即rte_eth_dev_pci_copy_info函数中。

static inline void
rte_eth_copy_pci_info(struct rte_eth_dev *eth_dev, struct rte_pci_device *pci_dev) { if ((eth_dev == NULL) || (pci_dev == NULL)) { RTE_PMD_DEBUG_TRACE("NULL pointer eth_dev=%p pci_dev=%p\n", eth_dev, pci_dev); return; } eth_dev->intr_handle = &pci_dev->intr_handle; //问题代码:将data指针的dev_flags进行reset操做 eth_dev->data->dev_flags = 0; if (pci_dev->driver->drv_flags & RTE_PCI_DRV_INTR_LSC) eth_dev->data->dev_flags |= RTE_ETH_DEV_INTR_LSC; if (pci_dev->driver->drv_flags & RTE_PCI_DRV_INTR_RMV) eth_dev->data->dev_flags |= RTE_ETH_DEV_INTR_RMV; eth_dev->data->kdrv = pci_dev->kdrv; eth_dev->data->numa_node = pci_dev->device.numa_node; }

对应的流程图为:

  能够看到,在rte_eth_dev_pci_copy_info函数中,对struct rte_eth_dev中的data指针中的数据进行了写操做,而这个数据正式来自于shared memory中的struct rte_eth_dev_data结构,而且通过前面对rte_eth_dev_pci_allocate函数的分析咱们知道不管是secondary进程仍是primary进程,都会进入rte_eth_dev_pci_copy_info函数中,那么就会出现这种状况:

  secondary进程在得到struct rte_eth_dev结构后大摇大摆的进入rte_eth_dev_pci_copy_info中去拷贝pci信息,而后顺手就将struct rte_eth_dev中的data指针中的数据重置了,这个数据就是rte_eth_dev_data.dev_flags,而重置时的条件判断却不充分,致使重置后的dev_flags只有两种可能,要么为0x0000,就是什么都没有,要么为0x0002,RTE_ETH_DEV_INTR_LS,或者是RTE_ETH_DEV_INTR_RMV,要么就是RTE_ETH_DEV_INTR_LSC | RTE_ETH_DEV_INTR_RMV,可是除了二者之外的其余值永远回不来了....

  那么回到咱们的场景,在dpdk 17.08版本,struct rte_eth_dev_data.dev_flags本来为RTE_ETH_DEV_DETACHABLE | RTE_ETH_DEV_INTR_LSC,值为0x0003,通过rte_eth_dev_pci_copy_info函数中的逻辑重置后,就只剩下RTE_ETH_DEV_INTR_LSC了,就是由好好的0x0003变为了0x0002,从而致使primary中的网卡热插拔特性被莫名其妙的重置掉了。

【dpdk 19.11版本】

  在dpdk 19.11版本,此问题仍然存在,函数名都没有变,只不过就是函数所在的文件位置发生了变化,dev_flags的值发生了变化,RTE_ETH_DEV_DETACHABLE已经被废弃,可是问题真的只是RTE_ETH_DEV_DETACHABLE标志消失致使网卡热插拔出问题么?相信通过上述的分析你们内心天然有答案。

【结论】

   这个问题的本质其实是在secondary函数初始化时进入rte_eth_pci_copy_info函数私自改变了共享内存struct rte_eth_dev_data中的值。关于这个问题我我的有两种角度来看:

  1. 从语义编程的角度讲,secondary进程的确须要进入rte_eth_pci_copy_info函数去重置(若是真的是这样的话,我我的表示不理解),只不过在重置时没有考虑全全部的状况,致使重置后的状态和充值前的状态出现了差别。
  2. 从我我的的想法来看,我我的坚持secondary进程不须要进入rte_eth_dev_pci_copy_info函数去从新设置rte_eth_dev_data->dev_flags,我以为这个问题本质上与标志位无关,与是否网卡支持热插拔无关,与标志位的值,与标志位是否被废弃也无关,由于我我的认为dpdk的secondary进程就不该该有权利去触碰共享内存的数据,只能读不能写,更况且是与驱动相关的struct rte_eth_dev_data中的数据。secondary进程就不该该就如rte_eth_dev_pci_copy_info函数。而且为何须要从新设置rte_eth_dev_data->dev_flags呢?数据来自于共享内存,primary已经把值设置完成了,举个例子就是primary进程已经把菜作好了端在面前了,secondary进程为何还要讲菜倒掉从新作一份呢?而且从最终的结果来看,secondary进程初始化后的驱动状态和primary进程是统一的,既然但愿统一,对secondary进程来说,不设置struct rte_eth_dev_data中的数据岂不是最安全的。

【后续】

  关于这个问题有两种改动方法:

方法一: 最方便改法

static inline void
rte_eth_copy_pci_info(struct rte_eth_dev *eth_dev, struct rte_pci_device *pci_dev) { if ((eth_dev == NULL) || (pci_dev == NULL)) { RTE_PMD_DEBUG_TRACE("NULL pointer eth_dev=%p pci_dev=%p\n", eth_dev, pci_dev); return; } eth_dev->intr_handle = &pci_dev->intr_handle; //加一层if判断,只有primary进程有权利对struct rte_eth_dev_data中的数据进行写操做 if (rte_eal_process_type() == RTE_PROC_PRIMARY) { eth_dev->data->dev_flags = 0; if (pci_dev->driver->drv_flags & RTE_PCI_DRV_INTR_LSC) eth_dev->data->dev_flags |= RTE_ETH_DEV_INTR_LSC; if (pci_dev->driver->drv_flags & RTE_PCI_DRV_INTR_RMV) eth_dev->data->dev_flags |= RTE_ETH_DEV_INTR_RMV; eth_dev->data->kdrv = pci_dev->kdrv; eth_dev->data->numa_node = pci_dev->device.numa_node; } }

方法二:

  坚持我我的的想法,不该该让secondary进程进入rte_eth_dev_pci_copy_info函数,可是这种改法改动巨大,风险也大,由于在dpdk的逻辑中不仅有在初始化时会调用rte_eth_dev_pci_copy_info函数,有兴趣的能够自行研究,这里很少赘述。

 

  最后,这个问题已经提交了patch到dpdk社区,目前已经被采纳。

  P.S.给dpdk提patch还挺费劲的....比在我公司内部提一个patch麻烦的多...dpdk有一个专门提patch的引导,https://doc.dpdk.org/guides/contributing/patches.html,第一次看的时候脑壳都有点大...

 

这个框我也不知道是啥东西,写博客的时候冒出来的... 
相关文章
相关标签/搜索