有关linux下多进程与多线程的区别总结

谈谈dpdk应用层包处理程序的多进程和多线程模型选择时的若干考虑

看到知乎上有个关于linux多进程、多线程的讨论:链接地址

自己项目里也对这个问题有过很多探讨和测试,所以正好开贴整理一下,题目有点长,其实就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 多核缓存伪共享问题

这个问题在之前帖子里链接地址说过,多核架构一般有3层缓存,缓存命中率是系统整体性能最关键是因素之一。缓存命中率有一个致命杀手就是

伪共享现象,多线程由于天生所有内存全部是共享的,所以更容易发生伪共享现象,其任何变量,只要一个CPU核改了,其余CPU核都产生

一次缓存失效并重新加载。。,而多进程模型,共享部分是有限的且开发者可以精确设计和控制的,其伪共享现象可以得到有效控制。

在项目实际开发中,经常的情况就是多线程性能低于多进程,需要将很大变量改为线程局部变量,才能让性能有所提升。

c 同步互斥

其实,无论是多线程还是多进程,都需要面临同步和互斥,这个不是进程/线程模型决定的,而是业务模型决定的。dpdk 提供了应用层

空间实现的基础互斥同步接口,包括原子操作、自旋锁、读写锁等,主要是配合共享内存的访问,因为从数据包处理系统来说,基本上

没有阻塞的概念,所以这种原子操作和忙等待的锁可以满足大部分需求,对于需要阻塞的系统,比如应用层协议栈,则还是需要使用内核的

机制,比如信号量等

4 最终采用的模型

最终我们采用的模型是:主体框架是多进程,主进程内部有若干线程用于处理诸如命令接收、文件监控、配置同步、统计数据写出、

debug数据写出等功能,包处理的主体流程是多进程的,不同进程之间基础表项、数据包等数据采用dpdk共享内存,在系统启动时

静态映射好,这些关键的基础表项和数据包结构针对缓存做细致优化,比如对齐内存以避免发生伪共享。由于我们的业务同步和互斥方面

的要求不多,所以只使用了有限的忙等待的锁和原子操作函数。这种模型实际上也是intel 推荐的模型。当然,选择多进程模型后,

又有很多需要考虑的东西了,比如是流水线的worker1-worker2-worker3的多进程,还是 master-worker-worker-worker的对称多进程,这里头根据业务逻辑、同步互斥、性能、扩展性、可维护性有很多深入的考虑,这里就不详细说了。


鱼还是熊掌:浅谈多进程多线程的选择

关于多进程和多线程,教科书上最经典的一句话是“进程是资源分配的最小单位,线程是CPU调度的最小单位”,这句话应付考试基本上够了,但如果在工作中遇到类似的选择问题,那就没有这么简单了,选的不好,会让你深受其害。

 

经常在网络上看到有的XDJM问“多进程好还是多线程好?”、“Linux下用多进程还是多线程?”等等期望一劳永逸的问题,我只能说:没有最好,只有更好。根据实际情况来判断,哪个更加合适就是哪个好。

 

我们按照多个不同的维度,来看看多线程和多进程的对比(注:因为是感性的比较,因此都是相对的,不是说一个好得不得了,另外一个差的无法忍受)。

对比维度

多进程

多线程

总结

数据共享、同步

数据共享复杂,需要用IPC;数据是分开的,同步简单

因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂

各有优势

内存、CPU

占用内存多,切换复杂,CPU利用率低

占用内存少,切换简单,CPU利用率高

线程占优

创建销毁、切换

创建销毁、切换复杂,速度慢

创建销毁、切换简单,速度很快

线程占优

编程、调试

编程简单,调试简单

编程复杂,调试复杂

进程占优

可靠性

进程间不会互相影响

一个线程挂掉将导致整个进程挂掉

进程占优

分布式

适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单

适应于多核分布式

进程占优

 

看起来比较简单,优势对比上是“线程 3.5 v 2.5 进程”,我们只管选线程就是了?

 

呵呵,有这么简单我就不用在这里浪费口舌了,还是那句话,没有绝对的好与坏,只有哪个更加合适的问题。我们来看实际应用中究竟如何判断更加合适。

1)需要频繁创建销毁的优先用线程

原因请看上面的对比。

这种原则最常见的应用就是Web服务器了,来一个连接建立一个线程,断了就销毁线程,要是用进程,创建和销毁的代价是很难承受的

2)需要进行大量计算的优先使用线程

所谓大量计算,当然就是要耗费很多CPU,切换频繁了,这种情况下线程是最合适的。

这种原则最常见的是图像处理、算法处理。

3)强相关的处理用线程,弱相关的处理用进程

什么叫强相关、弱相关?理论上很难定义,给个简单的例子就明白了。

一般的Server需要完成如下任务:消息收发、消息处理。“消息收发”和“消息处理”就是弱相关的任务,而“消息处理”里面可能又分为“消息解码”、“业务处理”,这两个任务相对来说相关性就要强多了。因此“消息收发”和“消息处理”可以分进程设计,“消息解码”、“业务处理”可以分线程设计。

当然这种划分方式不是一成不变的,也可以根据实际情况进行调整。

4)可能要扩展到多机分布的用进程,多核分布的用线程

原因请看上面对比。

5)都满足需求的情况下,用你最熟悉、最拿手的方式

至于“数据共享、同步”、“编程、调试”、“可靠性”这几个维度的所谓的“复杂、简单”应该怎么取舍,我只能说:没有明确的选择方法。但我可以告诉你一个选择原则:如果多进程和多线程都能够满足要求,那么选择你最熟悉、最拿手的那个。

 

需要提醒的是:虽然我给了这么多的选择原则,但实际应用中基本上都是“进程+线程”的结合方式,千万不要真的陷入一种非此即彼的误区。


1、进程与线程

进程是程序执行时的一个实例,即它是程序已经执行到课中程度的数据结构的汇集。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位

线程是进程的一个执行流,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。一个进程由几个线程组成(拥有很多相对独立的执行流的用户程序共享应用程序的大部分数据结构),线程与同属一个进程的其他的线程共享进程所拥有的全部资源。

"进程——资源分配的最小单位,线程——程序执行的最小单位"

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。

 

总的来说就是:进程有独立的地址空间,线程没有单独的地址空间(同一进程内的线程共享进程的地址空间)。(下面的内容摘自Linux下的多线程编程

使用多线程的理由之一是和进程相比,它是一种非常"节俭"的多任务操作方式。我们知道,在Linux系统下,启动一个新的进程必须分配给它独立的地址空间,建立众多的数据表来维护它的代码段、堆栈段和数据段,这是一种"昂贵"的多任务工作方式。而运行于一个进程中的多个线程,它们彼此之间使用相同的地址空间,共享大部分数据,启动一个线程所花费的空间远远小于启动一个进程所花费的空间,而且,线程间彼此切换所需的时间也远远小于进程间切换所需要的时间。据统计,总的说来,一个进程的开销大约是一个线程开销的30倍左右,当然,在具体的系统上,这个数据可能会有较大的区别。

使用多线程的理由之二是线程间方便的通信机制。对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。线程则不然,由于同一进程下的线程之间共享数据空间,所以一个线程的数据可以直接为其它线程所用,这不仅快捷,而且方便。当然,数据的共享也带来其他一些问题,有的变量不能同时被两个线程所修改,有的子程序中声明为static的数据更有可能给多线程程序带来灾难性的打击

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值