http://www.cnblogs.com/jiayy/p/3429725.html
一 前言
http://www.dpdk.org/ dpdk 是 intel 开发的x86芯片上用于高性能网络处理的基础库,业内比较常用的模式是linux-app模式,即
利用该基础库,在用户层空间做数据包处理,有了这个基础库,可以方便地在写应用层的网络包处理高性能程序,目前该库已经开源。
Main libraries
- multicore framework 多核框架,dpdk库面向intel i3/i5/i7/ep 等多核架构芯片,内置了对多核的支持
- huge page memory 内存管理,dpdk库基于linux hugepage实现了一套内存管理基础库,为应用层包处理做了很多优化
- ring buffers 共享队列,dpdk库提供的无锁多生产者-多消费者队列,是应用层包处理程序的基础组件
- poll-mode drivers 轮询驱动,dpdk库基于linux uio实现的用户态网卡驱动
下面分几个模块说明上述组件的功能和实现。
二 内存管理
1 hugepage技术
hugepage(2M/1G..)相对于普通的page(4K)来说有几个特点,a hugepage 这种页面不受虚拟内存管理影响,不会被替换出内存,而
普通的4kpage 如果物理内存不够可能会被虚拟内存管理模块替换到交换区。 b 同样的内存大小,hugepage产生的页表项数目远少于4kpage.举一个例子,用户进程需要使用 4M 大小的内存,如果采用4Kpage, 需要1K的页表项存放虚拟地址到物理地址的映射关系,而
采用hugepage 2M 只需要产生2条页表项,这样会带来两个后果,一是使用hugepage的内存产生的页表比较少,这对于数据库系统等
动不动就需要映射非常大的数据到进程的应用来说,页表的开销是很可观的,所以很多数据库系统都采用hugepage技术。二是tlb冲突率
大大减少,tlb 驻留在cpu的1级cache里,是芯片访问最快的缓存,一般只能容纳100多条页表项,如果采用hugepage,则可以极大减少
tlb miss 导致的开销:tlb命中,立即就获取到物理地址,如果不命中,需要查 rc3->进程页目录表pgd->进程页中间表pmd->进程页框->物理内存,如果这中间pmd或者页框被虚拟内存系统替换到交互区,则还需要交互区load回内存。。总之,tlb miss是性能大杀手,而采用
hugepage可以有效降低tlb miss
linux 使用hugepage的方式比较简单,
/sys/kernel/mm/hugepages/hugepages-2048kB/ 通过修改这个目录下的文件可以修改hugepage页面的大小和总数目
mount -t hugetlbfs nodev /mnt/huge linux将hugepage实现为一种文件系统hugetlbfs,需要将该文件系统mount到某个文件
mmap /mnt/huge 在用户进程里通过mmap 映射hugetlbfs mount 的目标文件,这个mmap返回的地址就是大页面的了
2 多进程共享
mmap 系统调用可以设置为共享的映射,dpdk的内存共享就依赖于此,在这多个进程中,分为两种角色,第一种是主进程(RTE_PROC_PRIMARY),第二种是从进程(RTE_PROC_SECONDARY)。主进程只有一个,必须在从进程之前启动,
负责执行DPDK库环境的初始化,从进程attach到主进程初始化的DPDK上,主进程先mmap hugetlbfs 文件,构建内存管理相关结构
将这些结构存入hugetlbfs 上的配置文件rte_config,然后其他进程mmap rte_config文件,获取内存管理结构,dpdk采用了一定的
技巧,使得最终同样的共享物理内存在不同进程内部对应的虚拟地址是完全一样的,意味着一个进程内部的基于dpdk的共享数据和指向
这些共享数据的指针,可以在不同进程间通用。
内存全局配置结构rte_config
rte_config 是每个程序私有的数据结构,这些东西都是每个程序的私有配置。
lcore_role:这个DPDK程序使用-c参数设置的它同时跑在哪几个核上。
master_lcore:DPDK的架构上,每个程序分配的lcore_role 有一个主核,对使用者来说影响不大。
lcore_count:这个程序可以使用的核数。
process_type:DPDK多进程:一个程序是主程序,否则初始化DPDK内存表,其他从程序使用这个表。RTE_PROC_PRIMARY/RTE_PROC_SECONDARY
mem_config:指向设备各个DPDK程序共享的内存配置结构,这个结构被mmap到文件/var/run/.rte_config,通过这个方式多进程实现对mem_config结构的共享。
Hugepage页表数组
这个是struct hugepage数组,每个struct hugepage 都代表一个hugepage 页面,存储的每个页面的物理地址和程序的虚拟地址的映射关系。然后,把整个数组映射到文件/var/run /. rte_hugepage_info,同样这个文件也是设备共享的,主/从进程都能访问它。
file_id: 每个文件在hugepage 文件系统中都有一个编号,就是数组1-N
filepath:%s/%smap_%file_id mount 的hugepage文件系统中的文件路径名
size; 这个hugepage页面的size,2M还是1G
socket_id:这个页面属于那个CPU socket 。
Physaddr:这个hugepage 页面的物理地址
orig_va:它和final_va一样都是指这个huagepage页面的虚拟地址。这个地址是主程序初始化huagepage用的,后来就没用了。
final_va:这个最终这个页面映射到主/从程序中的虚拟地址。
首先因为整个数组都映射到文件里面,所有的程序之间都是共享的。主程序负责初始化这个数组,首先在它内部通过mmap把所有的hugepage物理页面都映射到虚存空间里面,然后把这种映射关系保存到这个文件里面。从程序启动的时候,读取这个文件,然后在它内存也创建和它一模一样的映射,这样的话,整个DPDK管理的内存在所有的程序里面都是可见,而且地址都一样。
在对各个页面的物理地址份配虚拟地址时,DPDK尽可能把物理地址连续的页面分配连续的虚存地址上,这个东西还是比较有用的,因为CPU/cache/内存控制器的等等看到的都是物理内存,我们在访问内存时,如果物理地址连续的话,性能会高一些。
至于到底哪些地址是连续的,那些不是连续的,DPDK在这个结构之上又有一个新的结构rte_mem_config. Memseg来管理。因为rte_mem_config也映射到文件里面,所有的程序都可见rte_mem_config. Memseg结构。
rte_mem_config
这个数据结构mmap 到文件/var/run /.rte_config中,主/从进程通过这个文件访问实现对这个数据结构的共享。
在每个程序内,使用rte_config .mem_config 访问这个结构。
memseg
memseg 数组是维护物理地址的,在上面讲到struct hugepage结构对每个hugepage物理页面都存储了它在程序里面的虚存地址。memseg 数组的作用是将物理地址、虚拟地址都连续的hugepage,并且都在同一个socket,pagesize 也相同的hugepage页面集合,把它们都划在一个memseg结构里面,这样做的好处就是优化内存。
Rte_memseg这个结构也很简单:
phys_addr:这个memseg的包含的所有的hugepage页面的起始地址
addr:这些hugepage页面的起始的虚存地址
len:这个memseg的包含的空间size
hugepage_sz; 这些页面的size 2M /1G?
这些信息都是从hugepage页表数组里面获得的。
free_memseg
上面memseg是存储一个整体的这种映射的映射,最终这些地址是要被分配出去的。free_memseg记录了当前整个DPDK内存空闲的memseg段,注意,这是对所有进程而言的。初始化等于memseg表示所有的内存都是free的,后面随着分配内存,它越来越小。
必须得说明,rte_mem_config结构在各个程序里面都是共享的,所以对任何成员的修改都必须加锁。
mlock :读写锁,用来保护memseg结构。
rte_memzone_reserve
一般情况下,各个功能分配内存都使用这个函数分配一个memzone
const struct rte_memzone *
rte_memzone_reserve(const char *name, size_t len, int socket_id,
unsigned flags)
{
1、这里就在free_memseg数组里面找满足socket_id的要求,最小的memseg,从这里面划出来一块内存,当然这时候free_memseg需要更新把这部分内存刨出去。
2、把这块内存记录到memzone里面。
}
上面提到rte_memzone_reserve分配内存后,同时在rte_mem_config.memzone数组里面分配一个元素保存它。对于从memseg中分配内存,以memzone为单位来分配,对于所有的分配情况,都记录在memzone数组里面,当然这个数组是多进程共享,大家都能看到。
在rte_mem_config结构里面memzone_idx 变量记录当前分配的memzone,每申请一次这个变量+1。
这个rte_memzone结构如下:
Name:给这片内存起个名字。
phys_addr:这个memzone 分配的内存区的物理地址
addr:这个memzone 分配的内存区的虚拟地址
len:这个zone的空间长度
hugepage_sz:这个zone的页面size
socket_id:页面的socket号
rte_memzone 是dpdk内存管理最终向客户程序提供的基础接口,通过 rte_memzone_reverse 可以获取基于
dpdk hugepage 的属于同一个物理cpu的物理内存连续的虚拟内存也连续的一块地址。rte_ring/rte_malloc/rte_mempool等
组件都是依赖于rte_memzone 组件实现的。
3 dpdk 内存初始化源码解析
入口:
rte_eal_init(int argc,char ** argv) dpdk 运行环境初始化入口函数
—— eal_hugepage_info_init 这4个是内存相关的初始化函数
——rte_config_init
——rte_eal_memory_init
——rte_eal_memzone_init
3.1 eal_hugepage_info_init
这个函数比较简单,主要是从 /sys/kernel/mm/hugepages 目录下面读取目录名和文件名,从而获取系统的hugetlbfs文件系统数,
以及每个 hugetlbfs 的大页面数目和每个页面大小,并保存在一个文件里,这个函数,只有主进程会调用。存放在internal_config结构里
3.2 rte_config_init
构造 rte_config 结构
rte_config_init
——rte_eal_config_create 主进程执行
——rte_eal_config_attach 从进程执行
rte_eal_config_create 和 rte_eal_config_attach 做的事情比较简单,就是将 /var/run/.config 文件shared 型
mmap 到自己的进程空间的 rte_config.mem_config结构上,这样主进程和从进程都可以访问这块内存,
rte_eal_config_attach
3.3 rte_eal_memory_init
rte_eal_memory_init
——rte_eal_hugepage_init 主进程执行,dpdk 内存初始化核心函数
——rte_eal_hugepage_attach 从进程执行
rte_eal_hugepage_init 函数分几个步骤:
/*
* Prepare physical memory mapping: fill configuration structure with
* these infos, return 0 on success.
* 1. map N huge pages in separate files in hugetlbfs
* 2. find associated physical addr
* 3. find associated NUMA socket ID
* 4. sort all huge pages by physical address
* 5. remap these N huge pages in the correct order
* 6. unmap the first mapping
* 7. fill memsegs in configuration with contiguous zones
*/
函数一开始,将rte_config_init函数获取的配置结构放到本地变量 mcfg 上,然后检查系统是否开启hugetlbfs,如果
不开启,则直接通过系统的malloc函数申请配置需要的内存,然后跳出这个函数。
接下来主要是构建 hugepage 结构的数组 tmp_hp(上图)
下面就是重点了。。
构建hugepage 结构数组分下面几步
首先,循环遍历系统所有的hugetlbfs 文件系统,一般来说,一个系统只会使用一种hugetlbfs ,所以这一层的循环可以认为
没有作用,一种 hugetlbfs 文件系统对应的基础数据包括:页面大小,比如2M,页面数目,比如2K个页面
其次,将特定的hugetlbfs的全部页面映射到本进程,放到本进程的 hugepage 数组管理,这个过程主要由 map_all_hugepages函数完成,
第一次映射的虚拟地址存放在 hugepage结构的 orig_va变量
第三,遍历hugepage数组,找到每个虚拟地址对应的物理地址和所属的物理cpu,将这些信息也记入 hugepage数组,物理地址
记录在hugepage结构的phyaddr变量,物理cpu号记录在 hugepage结构的socket_id变量
第四,跟据物理地址大小对hugepage数组做排序
第五,根据排序结果重新映射,这个也是由函数 map_all_hugepages完成,重新映射后的虚拟地址存放在hugepage结构的final_va变量
第六,将第一次映射关系解除,即将orig_va 变量对应的虚拟地址空间返回给内核
下面看 map_all_hugepages的实现过程
这个函数是复用的,共有两次调用。
对于第一次调用,就是根据hugetlbfs 文件系统的页面数m,构造
m个文件名称并创建文件,每个文件对应一个大页面,然后通过mmap系统调用映射到进程的一块虚拟地址
空间,并将虚拟地址存放在hugepage结构的orig_va地址上。如果该hugetlbfs有1K个页面,最终会在
hugetlbfs 挂载的目录上生成 1K 个文件,这1K 个文件mmap到进程的虚拟地址由进程内部的hugepage数组维护
对于第二次调用,由于hugepage数组已经基于物理地址排序,这些有序的物理地址可能有2种情况,一种是连续的,
另一种是不连续的,这时候的调用会遍历这个hugepage数组,然后统计连续物理地址的最大内存,这个统计有什么好处?
因为第二次的映射需要保证物理内存连续的其虚拟内存也是连续的,在获取了最大连续物理内存大小后,比如是100个页面大小,
会调用 get_virtual_area 函数向内涵申请100个页面大小的虚拟空间,如果成功,说明虚拟地址可以满足,然后循环100次,
每次映射mmap的首个参数就是get_virtual_area函数返回的虚拟地址+i*页面大小,这样,这100个页面的虚拟地址和物理地址
都是连续的,虚拟地址存放到final_va 变量上。
下面看 find_physaddr的实现过程
这个函数的作用就是找到hugepage数组里每个虚拟地址对应的物理地址,并存放到 phyaddr变量上,最终实现由函数
rte_mem_virt2phy(const void * virt)函数实现,其原理相当于页表查找,主要是通过linux的页表文件 /proc/self/pagemap 实现
/proc/self/pagemap 页表文件记录了本进程的页表,即本进程虚拟地址到物理地址的映射关系,主要是通过虚拟地址的前面若干位
定位到物理页框,然后物理页框+虚拟地址偏移构成物理地址,其实现如下
下面看 find_numasocket的实现过程
这个函数的作用是找到hugepage数组里每个虚拟地址对应的物理cpu号,基本原理是通过linux提供的 /proc/self/numa_maps 文件,
/proc/self/numa_maps 文件记录了本 进程的虚拟地址与物理cpu号(多核系统)的对应关系,在遍历的时候将非huge page的虚拟地址
过滤掉,剩下的虚拟地址与hugepage数组里的orig_va 比较,实现如下
sort_by_physaddr 根据hugepage结构的phyaddr 排序,比较简单
unmap_all_hugepages_orig 调用 mumap 系统调用将 hugepage结构的orig_va 虚拟地址返回给内核
上面几步就完成了hugepage数组的构造,现在这个数组对应了某个hugetlbfs系统的大页面,数组的每一个节点是一个
hugepage结构,该结构的phyaddr存放着该页面的物理内存地址,final_va存放着phyaddr映射到进程空间的虚拟地址,
socket_id存放着物理cpu号,如果多个hugepage结构的final_va虚拟地址是连续的,则其 phyaddr物理地址也是连续的。
下面是rte_eal_hugepage_init函数的余下部分,主要分两个方面,一是将hugepage数组里 属于同一个物理cpu,物理内存连续
的多个hugepage 用一层 memseg 结构管理起来。 一个memseg 结构维护的内存必然是同一个物理cpu上的,虚拟地址和物理
地址都连续的内存,最终的memzone 接口是通过操作memseg实现的;2是将 hugepage数组和memseg数组的信息记录到共享文件里,
方便从进程获取;
遍历hugepage数组,将物理地址连续的hugepage放到一个memseg结构上,同时将该memseg id 放到 hugepage结构
的 memseg_id 变量上
下面是创建文件 hugepage_info 到共享内存上,然后hugepage数组的信息拷贝到这块共享内存上,并释放hugepage数组,
其他进程通过映射 hugepage_info 文件就可以获取 hugepage数组,从而管理hugepage共享内存
谈谈dpdk应用层包处理程序的多进程和多线程模型选择时的若干考虑
看到知乎上有个关于linux多进程、多线程的讨论:http://www.zhihu.com/question/19903801/answer/14842584
自己项目里也对这个问题有过很多探讨和测试,所以正好开贴整理一下,题目有点长,其实就2点:
1. 多进程模型和多线程模型,这两种模型在linux上有什么区别,各有何优缺点?
这里仅限于linux平台,因为linux平台跟win平台关于线程的实现差异很大。
2. 采用intel dpdk做包处理程序,是采用多进程模型好,还是多线程模型好?
这里仅限于包处理程序(ips,waf,其他网络设备引擎),因为不同应用场景区别也很大。
首先知乎里边的评论,有个miao网友说的跟我的经验比较相符,先将其说法贴一下:
"linux使用的1:1的线程模型,在内核中是不区分线程和进程的,都是可运行的任务而已。fork调用clone(最少的共享),pthread_create也是调用clone(最大共享).fork创建会比pthread_create多消耗一点点,因为要拷贝tables和cow mapping.但是其实差别真的很细微,这些在内核开发者的努力下已经变的很小了。
再来说说contex switch的cost吧。线程的context switch是要比process小一些,因为线程共享了大部分的memory和tables,当switch的时候这些东西已经在缓存中了。
但是其实差别也很细微。但是在multiprocessor的系统中不共享memory其实是会比共享memory要有一点优势的,因为当任务在不同的processor中运行的时候,同步memory带来的损耗是不可忽视的。"
他这里说了两点有价值的信息,1 linux里的线程实现决定,创建、调度、切换线程的开销跟进程相比,好不了多少。
2 多核CPU下由于缓存命中率的问题,进程这种天生不共享内存的做法,实际上比线程这种天生共享内存
的做法,从性能上是有好处的。
这两点见解跟我们项目实际测试和研究结果是相符合的。下面从几个方面探讨这些问题:
1 linux 线程创建方式
linux提供的线程实际上是核外线程,即主要的线程机制是通过应用层面的库pthread提供的(线程的id分配、线程创建和管理,据说基本实现是pthread库为每一个进程维护一个管理线程,单调用 pthread_create等posix API时,调用者与该管理线程通过管道传递命令),
核内层面,线程几乎可以等同于进程。 这里贴一段从引用1 拷贝的内容:
Linux的线程实现是在核外进行的,核内提供的是创建进程的接口do_fork()。内核提供了两个系统调用__clone()和fork(),最终都用不同的参数调用do_fork()核内API。 do_fork() 提供了很多参数,包括CLONE_VM(共享内存空间)、CLONE_FS(共享文件系统信息)、CLONE_FILES(共享文件描述符表)、CLONE_SIGHAND(共享信号句柄表)和CLONE_PID(共享进程ID,仅对核内进程,即0号进程有效)。当使用fork系统调用产生多进程时,内核调用do_fork()不使用任何共享属性,进程拥有独立的运行环境。当使用pthread_create()来创建线程时,则最终设置了所有这些属性来调用__clone(),而这些参数又全部传给核内的do_fork(),从而创建的”进程”拥有共享的运行环境,只有栈是独立的,由 __clone()传入。
即:Linux下不管是多线程编程还是多进程编程,最终都是用do_fork实现的多进程编程,只是进程创建时的参数不同,从而导致有不同的共享环境。Linux线程在核内是以轻量级进程的形式存在的,拥有独立的进程表项,而所有的创建、同步、删除等操作都在核外pthread库中进行。pthread 库使用一个管理线程(__pthread_manager() ,每个进程独立且唯一)来管理线程的创建和终止,为线程分配线程ID,发送线程相关的信号,而主线程pthread_create()) 的调用者则通过管道将请求信息传给管理线程。
上述内容基本可以这么表示:
创建进程= fork ——> do_fork(不使用共享属性)
创建线程= pthread_create——>__clone ——> do_fork(共享地址空间(代码区、数据区)、页表、文件描述符、信号。。)
这里其实另外一种多进程创建方式,就是脚本直接启动多个进程
下面再贴一段:
“对于一个进程来说必须有的数据段、代码段、堆栈段是不是全盘复制呢?对于多进程来说,代码段是肯定不用复制的,因为父进程和各子进程的代码段是相同的,数据段和堆栈段呢?也不一定,因为在Linux里广泛使用的一个技术叫copy-on-write,即写时拷贝。copy-on-write意味着什么呢?意味着资源节省,假设有一个变量x在父进程里存在,当这个父进程创建一个子进程或多个子进程时这个变量x是否复制到了子进程的内存空间呢?不会的,子进程和父进程使用同一个内存空间的变量,但当子进程或父进程要改变变量x的值时就会复制该变量,从而导致父子进程里的变量值不同。”
这里我的理解是,刚fork完,子进程和父进程代码段、页表等还是共享的,接下去有两种可能发展方向,1是子进程修改了数据,这时候,代码段:仍然是共享的,不需要拷贝;堆和静态数据区: 根据copy-on-wirte机制,不改变值的地方仍然共享,改变值的地方需要重新申请物理页面并修改值,修改页表(可能还要拷贝页表);栈: 不管进程还是线程,都不能共享,都需要创建的时候分配栈区。2是fork之后马上调用 exec 用新的进程替换,这时候会载入新的代码段、数据段,构建新的页表。
对于我们的包处理系统而已,无论怎么启动,创建时的性能开销其实是无所谓的,因为都是在系统初始化的时候创建。
2 调度和切换
由于核内的线程本质就是进程,其调度过程跟进程一样。切换,不论是进程切换还是线程切换,都需要替换运行环境(内核堆栈,运行时寄存器等),对于内存的切换,内核部分内存是一样的,用户空间部分:如果是进程,需要替换页目录基址寄存器,如果是线程,不需要替换;总体而言,linux进程和线程的切换,从内存寄存器、内核堆栈寄存器、其他寄存器等的换值开销应该是差不多的。具体切换代码参考引用2
但是由于多线程共享地址空间,从一个线程切换到同一个进程上另一个线程运行,页表,数据区等很多都已经在内存甚至缓存里,而从一个进程切换到另一个进程,可能由于刚切换进来的进程的页面被虚拟内存管理模块替换出去导致的页面替换开销,另外还有缓存tlb失效导致的缓存更新开销,这里性能有所差别。
对于我们的包处理系统而已,采用多核架构,主体进程/线程是绑定到不同的物理CPU core上并独占的,所以发生调度和切换的情况不多,因而这种影响不是很重要。
3. 地址空间共享相关问题
进程地址空间是独立的,这意味着,不同进程的内存天生就是不共享的,如果要共享,则需要开发者自己构建共享机制,比如使用IPC。
线程地址空间是共享的,这意味着,同一进程不同线程的内存天生是共享的,如果想要不共享,需要开发者自己实施,比如使用线程本地变量。
进程模型和线程模型,地址空间不共享和共享,会引发以下系列问题:
3.1 进程模型更安全、更健壮、更容易开发
由于一般公司成熟产品不是从无到有一个项目就开发完毕,必然有很多历史代码、多项目组合作的代码,这时候采用多进程模型,
可以有效隔离历史代码和当下代码、不同项目组的代码,当然,这需要产品本身是可以这么做的。比如,项目组A开发包处理进程,
项目组B开发包安全检测功能,两个功能是两个进程,这种模型无疑更容易开发和维护
另外,由于天生所以变量都不共享,对开发者要求也比开发多线程要低
3.2 多核下的性能
传统意义上,一般认为多线程比多进程性能要高,这其实是有前提的。比如不同线程之间需要频繁交互大量数据,由于IPC本身的开销,
如果数据交互非常频繁且量大,多线程会比多进程性能要高。
对于基于DPDK的多核数据包处理程序而言,由于3个原因,多进程模型更可预见性能高于多线程:
a DPDK提供了基于hugepage的共享内存机制,使得多进程物理地址相同,其虚拟地址也相同,这事实上就跟多线程之间共享地址空间是
一样的了。即采用DPDK的基础库,多进程之间不需要共享部分使用普通内存(libc malloc,静态区,栈区),相互隔离很安全。需要共享
部分采用dpdk hugepage 内存,通过特殊映射,也能共享虚拟地址。在这片共享内存上交互数据和指针(虚拟地址是一样的),性能
远高于利用内核的IPC机制。
b 多核缓存伪共享问题
这个问题在之前帖子里http://www.cnblogs.com/jiayy/p/3246133.html说过,多核架构一般有3层缓存,缓存命中率是系统整体性能最关键是因素之一。缓存命中率有一个致命杀手就是
伪共享现象,多线程由于天生所有内存全部是共享的,所以更容易发生伪共享现象,其任何变量,只要一个CPU核改了,其余CPU核都产生
一次缓存失效并重新加载。。,而多进程模型,共享部分是有限的且开发者可以精确设计和控制的,其伪共享现象可以得到有效控制。
在项目实际开发中,经常的情况就是多线程性能低于多进程,需要将很大变量改为线程局部变量,才能让性能有所提升。
c 同步互斥
其实,无论是多线程还是多进程,都需要面临同步和互斥,这个不是进程/线程模型决定的,而是业务模型决定的。dpdk 提供了应用层
空间实现的基础互斥同步接口,包括原子操作、自旋锁、读写锁等,主要是配合共享内存的访问,因为从数据包处理系统来说,基本上
没有阻塞的概念,所以这种原子操作和忙等待的锁可以满足大部分需求,对于需要阻塞的系统,比如应用层协议栈,则还是需要使用内核的
机制,比如信号量等
4 最终采用的模型
最终我们采用的模型是:主体框架是多进程,主进程内部有若干线程用于处理诸如命令接收、文件监控、配置同步、统计数据写出、
debug数据写出等功能,包处理的主体流程是多进程的,不同进程之间基础表项、数据包等数据采用dpdk共享内存,在系统启动时
静态映射好,这些关键的基础表项和数据包结构针对缓存做细致优化,比如对齐内存以避免发生伪共享。由于我们的业务同步和互斥方面
的要求不多,所以只使用了有限的忙等待的锁和原子操作函数。这种模型实际上也是intel 推荐的模型。当然,选择多进程模型后,
又有很多需要考虑的东西了,比如是流水线的worker1-worker2-worker3的多进程,还是 master-worker-worker-worker的对称多进程,这里头根据业务逻辑、同步互斥、性能、扩展性、可维护性有很多深入的考虑,这里就不详细说了。