前面写的。本系列记录了笔者在项目过程中因好奇了解到的一些DPDK实现细节。更适合有同样好奇心的DPDK初学者。通过本文,您可以了解DPDK的整体工作原理和一些实现细节。你无法学习使用DPDK进行性能调优。如果对DPDK的来历不是很清楚,可以先浏览一下。绝对干货!初学者都能看懂的DPDK分析,重点是Linux+x86的网络IO瓶颈。一句话,Linux内核协议栈太慢了。为了突破这个性能瓶颈,DPDK的解决方案是绕过内核,直接从网卡抓取数据到用户空间。EAL的一些基本概念首先要明白DPDK是以几个lib的形式为应用链接提供的,最终的lib就是EAL。EAL的全称是(EnvironmentAbstractionLayer,环境抽象层),它负责为应用程序间接访问底层资源,比如内存空间、线程、设备、定时器等。如果把我们的应用程序比作拥有者一座豪宅,EAL是豪宅的管家。lcore&socket这两个概念在DPDK代码中随处可见。注意这里的socket不是网络编程中的一套东西,而是CPU相关的东西。具体概念请参考DifferencesbetweenphysicalCPUvslogicalCPUvsCorevsThreadvsSocket或其翻译版physicalCPUvslogicalCPUvsCorevsThreadvsSocket(译)。对于我们来说,知道DPDK可以运行在多个lcores上就足够了。DPDK如何知道有多少个lcore?只是在启动时解析文件系统中的特定文件。引用函数eal_cpu_detectedDPDK的运行形式很大一部分DPDK代码以lib的形式运行在用户应用程序的进程上下文中。为了达到更高的性能。应用程序通常以多个进程或线程的形式运行在不同的lcores上。多线程场景:多进程场景:在多进程场景下,多个应用实例如何保证关键信息(如内存资源)的一致性?答案是不同的进程将公共数据映射到同一个文件中,这样任何进程对数据的任何修改都会影响其他进程。在Primary&Secondary多进程场景下,进程有两个角色,Primary或Secondary。顾名思义,Primary进程可以创建资源,而Secondary进程只能附加已有的资源。一山难容二虎。一个多进程应用只有一个Primary进程,其余都是Secondary进程。应用程序可以通过命令行参数--proc-type指定应用程序类型。DPDK的入口就像是主函数在应用程序中的位置。rte_eal_init函数是DPDK梦开始的地方(其实之前的图已经画好了!),我们来看看它做了什么。/*启动线程,在应用程序init()调用。*/intrte_eal_init(intargc,char**argv){thread_id=pthread_self();rte_eal_cpu_init();eal_parse_args(argc,argv);rte_config_init();rte_mp_channel_init();rte_eal_intr_init();rte_eal_memzone_init();rte_eal_memory_init();rte_eal_malloc_heap_init()eal_thread_init_master(rte_config.master_lcore);RTE_LCORE_FOREACH_SLAVE(i){管道(lcore_config[i].pipe_master2slave);管道(lcore_config[i].pipe_slave2pipe);/*为每个lcore创建一个线程*/ret=pthread_create(&lcore_config[i].thread_id,NULL,eal_thread_loop,NULL);.....}/**在所有从属lcore上启动一个虚拟函数,以便主lcore*在该函数返回时知道它们都已准备就绪。*/rte_eal_mp_remote_launch(sync_func,NULL,SKIP_MASTER);rte_eal_mp_wait_lcore();......}rte_eal_init总结工作是检测哪些lcores可以使用,解析用户的命令行参数,初始化每个子模块,并在所有slavelcores上启动线程。上面提到的概念是slavelcore,对应masterlcore,在一个运行在多个lcore上的DPDK应用中,启动线程运行的lcore就是masterlcore,其余都是slavelcore。masterlcore与所有slavelcore之间通过管道进行通信,拓扑上形成一个星型网络。lcore的状态和配置记录在全局变量lcore_config中,它是一个数组,每个lcore只会访问自己的structlcore_configlcore_config[RTE_MAX_LCORE]副本多进程的情况稍微复杂一点,除了线程之间的通信,还有完成主进程与其他从进程的通信。这是在刚才那一堆子模块的初始化中通过下面的函数完成的(mp的意思是多进程),里面会创建一个单独的线程来接收其他进程的消息。intrte_mp_channel_init(void)内存框架DPDK需要高速处理网络数据包,而数据包需要内存来承载,所以DPDK自然需要频繁的内存申请释放。显然,如果你需要内存的时候malloc,不需要的时候free,那么这个效率就太低了。所以DPDK使用内存池来负责内存申请释放。相关的数据结构主要有rte_memzonerte_ring和rte_mempool。先画出正常情况下三者的关系。rte_memzonerte_memzone在DPDK的内存资源管理中扮演其他资源管家的角色。默认情况下,RTE_MAX_MEMZONErte_memzones会在DPDK初始化时创建,每一个都可以记录一个rte_ring或rte_mempool的内存位置。从上图也可以看出,每个rte_ring或rte_mempool都有一个指针返回其关联的rte_memzonerte_ringrte_ring描述了一个循环队列,它具有以下特点FIFO先进先出队列的容量在创建后是固定的,and必须是2的整数次方。队列中存放的指针(void*)支持单消费者和多消费者模型。它支持单生产者和多生产者模型。支持批量访问。如上图所示,每个rte_ring包含两对游标,用于记录当前rte_ring的存储状态。之所以用两对而不是两对,一是为了支持多消费者模型和多生产者模型,二是为了支持批量访问。这里只说明rte_ring在多个生产者竞争入队场景下的工作原理。上框代表两个核上的本地游标,下框代表记录在rte_ring内部的游标。注意:每个core都适用于多线程和多进程Step1每个core拷贝ring->proc_head到本地proc_head,然后设置proc_next到下一个位置Step2尝试修改ring->proc_head为proc_next的值,这一步使用CompareAndSwap指令来保证原子性,这里,只有当ring->proc_head等于proc_head时,这一步操作才会成功,否则会重复Step1。在此示例中,假设对Core1的操作成功。在Core2上操作时,因为ring->proc_head不等于本地的proc_head,所以不会成功,而是重新copyStep1。在Step3Core2上运行成功,将内容(一个指针)写入rte_ringStep4下一步尝试更新ring->proc_tail,这一步也使用了CompareAndSwap,只有当ring->proc_tail与本地proc_tail,在这种情况下更新到本地proc_head显然只适用于Core1。Step5最后,将ring->proc_tail更新为Core2上的proc_head。其他场景,比如单生产者单消费者多消费者场景,请参考使用rte_ring的应用,了解如何使用可能比了解其内部工作原理更有用。rte_ring主要有两个接口:createrte_ringstructrte_ring*rte_ring_create(constchar*name,unsignedcount,intsocket_id,unsignedflags);根据名字查找创建的rte_ringstructrte_ring*rte_ring_lookup(constchar*name);一般来说,可以在masterlcore或primaryprocess上创建,在slavelcore或secondaryprocess上搜索。存储指向rte_ring(生产者)的指针intrte_ring_enqueue(structrte_ring*r,void*obj);从rte_ring(consumer)中取出一个指针intrte_ring_dequeue(structrte_ring*r,void**obj_p);rte_mempoolrte_ring只能存储一个指针,rte_ring可以存储其他大小的元素的数据,有一定的容量,但是需要注意的是这个元素的大小也必须在创建的时候指定,容量也是指定的.虽然rte_ring和rte_mempool是两个独立的数据结构,但是如上关系图所述,一般的rte_mempool都会内置一个rte_ring来管理rte_mempool中的元素。我想这就是rte_ring存储指针的原因,它指向的内容就是rte_mempool的内容。在LocalCache多核场景下,如果两个线程向同一个rte_mempool申请或释放内存,那么对rte_ring的CAS操作必然会失败。因此,DPDK允许用户在创建rte_mempool时为每个lcore创建一个缓存。缓存中存放着和rte_ring一样的指针。所以对于带缓存的rte_mempool,它在内存中的布局是这样的:当前lcore从cacheReservedmemory中获取,如果有就直接使用(这样不会有竞争),如果没有就从rte_ring中获取。对于使用rte_mempool的应用,rte_mempool提供的主要接口如下。,rte_mempool_obj_cb_t*obj_init,void*obj_init_arg,intsocket_id,unsignedflags);根据名字查找一个rte_mempool.structrte_mempool*rte_mempool_lookup(constchar*name);从内存池中获取一个对象(消费者)intrte_mempool_get(structmp,void**obj_p);返回一个对象到内存池voidrte_mempool_put(structrte_mempool*mpu,void*obj);创建一个空的rte_mempoolstructrte_mempool*rte_mempool_create_empty(constchar*name,unsignedn,unsignedcaunsigned_size,unsignedprivate_data_size,intsocket_id,unsignedflags);一个空的rte_mempool意味着大部分数据结构关系已经建立,但是这个rte_mempool还没有分配池中元素的内存,即用户无法从一个空的rte_mempool中获取内存。如果你用GDB调试的话,可以看到在创建了一个空的rte_mempool之后,在其内置的rte_ring中ring->proc_head=ring->proc_tail,此时我们还是需要使用rte_mempool_populate_*()等函数为内存池实际分配内存(这个过程称为populate)。默认接口如下:intrte_mempool_populate_default(structrte_mempool*mp);所以其实创建一个非空的rte_mempool的大致实现是,先创建一个空的内存池,然后为其元素向系统申请内存structrte_mempool*rte_mempool_create(constchar*name,unsignedn,unsignedelt_size,未签名的cache_size,未签名的private_data_size,rte_mempool_ctor_t*mp_init,void*mp_obinit_arg,_pool_pool_jme_*obj_init_arg,intsocket_id,无符号标志){mp=rte_mempool_create_empty(名称,n,elt_size,cache_size,private_data_size,socket_id,标志);...rte_mempool_populate_default(mp);}
