谈谈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的对称多进程,这里头根据业务逻辑、同步互斥、性能、扩展性、可维护性有很多深入的考虑,这里就不详细说了。

http://www.soft-bin.com/html/2010/07/09/%E5%A4%9A%E8%BF%9B%E7%A8%8Bvs%E5%A4%9A%E7%BA%BF%E7%A8%8B%EF%BC%8C%E4%B8%80%E4%B8%AA%E9%95%BF%E6%9C%9F%E7%9A%84%E4%BA%89%E8%AE%BA.html

http://blog.sina.com.cn/s/blog_d9889c5b0101e7x6.html

posted @ 2013-11-19 10:56  cgj  阅读(6345)  评论(1编辑  收藏  举报