从进程/线程/协程看懂WEB服务器

1 概念

1.1 定义

操作系统中的经典定义:

  • 进程:资源分配单位
  • 线程:调度单位
  • 协程:用户态实现的调度单位

进程调度
image.png

注意:进程的切换其实是对线程进行调度

1.2 关系

一个进程可以包含多个线程,一个线程也可以包含多个协程。但是有一点,必须明确,一个线程内的多个协程的运行是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行(同步|依次运行)的,无论有多少个CPU(核)。一个线程内可以运行多个协程,但是这些协程都是串行运行的。当一个协程运行时,其他协程必须挂起。

比较

虽然说,协程与进程、线程相比不是一个维度的概念,但是有时候,我们仍然需要将它们做一番比较,具体如下:

  1. 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
  2. 一个进程可以包含多个线程,一个线程可以包含多个协程。
  3. 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
  4. 协程与进程一样,它们的切换都存在上下文切换问题。

三者上下文

进程 线程 协程
切换者 操作系统 操作系统 用户(编程者/应用程序)
切换时机 根据操作系统自己的切换策略,用户无感知 根据操作系统自己的切换策略,用户无感知 用户自己(的程序)决定
切换内容 页全局目录
内核栈
硬件上下文
内核栈
硬件上下文
硬件上下文
切换内容的保存 保存于内核栈中 保存于内核栈中 保存于用户自己的变量(用户栈或者堆)
切换过程 用户态-内核态-用户态 用户态-内核态-用户态 用户态(没有陷入内核态)
切换效率

1.3 进程和线程

image.png

  1. 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;因此线程可以读写同样的数据结构和变量,便于线程之间的通信。相反,进程间通信(IPC)很困难且消耗更多资源。
  2. 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
  3. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
  4. 二者均可并发执行.
  5. 进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
  6. 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志

1.4 协程

协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程可以在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行。

协程可以被认为是一种用户空间线程,与传统的线程相比,有2个主要的优点:

  • 与线程不同,协程是自己 主动让出(可控) CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制。
  • 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常小

2 Web服务器举例

2.1 多进程单线程模型

这类访问模型, 每一个请求都对应开辟一个新的进程, 在进程内是单线程的。
优点:

  • 编程相对容易;通常不需要考虑锁和同步资源的问题。
  • 更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
  • 有内核保证的隔离:数据和错误隔离。
  • 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)

缺点:
进程切换开销大, 密集访问带会来的并发问题。

2.1.1 Nginx进程模型

Nginx采用的是多进程单线程& 多路IO复用模型,是以多进程(每个进程仅有一个线程)的方式来工作的(当然nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式),但是每个线程可以处理多个客户端的访问,这也成就了它的并发性能。Nginx采用多进程的方式有诸多好处。
image.png
Nginx在启动后,会有一个Master进程和多个Worker进程。

master进程主要用来管理worker进程,包含:接收来自外界的信号,向各worker进程发送信号,监控 worker进程的运行状态,当worker进程退出后(异常情况下),会自动重新启动新的worker进程。

而基本的网络事件,则是放在worker进程中来处理了。多个worker进程之间是对等的,他们同等竞争来自客户端的请求,各进程互相之间是独立的。一个请求,只可能在一个worker进程中处理,一个worker进程,不可能处理其它进程的请求。worker进程的个数是可以设置的,一般我们会设置与机器CPU核数一致,这里面的原因与Nginx的进程模型以及事件处理模型是分不开的。

2.1.2 php-fpm进程模型

PHP-FPM采用的是Master/Worker进程模型。当PHP-FPM启动时,会读取配置文件,然后创建一个Master进程和若干个Worker进程(具体是几个Worker进程是由php-fpm.conf中配置的个数决定)。Worker进程是由Master进程fork出来的。
Master进程和Worker进程的作用:

  • Master进程:负责管理Worker进程、监听端口
  • Worker进程:处理业务逻辑(每个Worker进程同一时间只能处理一个请求)

image.png

FastCGI协议可以理解为将Nginx转发的请求重新组装为php程序可解析识别的上下文环境的标准。

PHP-FPM进程管理方式有动态(Dynamic)、静态(Static)、按需(Ondemand)三种,下面将一一介绍。

  • 动态(Dynamic) 在这种方式下,PHP-FPM启动时会创建一定数量的Worker进程。当请求数逐渐增大时,会动态增加Worker进程的数量;当请求数降下来时,会销毁刚才动态创建出来的Worker进程。在这种方式下,如果配置的最大进程数过大,当请求量增加时会出现大量Worker进程,进程之间会频繁切换,浪费大量CPU资源。
  • 静态(Static) 这种方式下,PHP-FPM启动时会创建配置文件中指定数量的Worker进程,不会根据请求数量的多少而增加减少。因为PHP-FPM开启的每个Worker进程同一时间只能处理一个请求,所以在这种方式下当请求增大的时候,将会出现等待的情形。
  • 按需(Ondemand) 在这种方式下,PHP-FPM启动时,不会创建Worker进程,当请求到达的时候Master进程才会fork出子进程。在这种模式下,如果请求量比较大,Master进程会非常繁忙,会占用大量CPU时间。所以这种模式不适合大流量的环境。

因为PHP-FPM开启的每个Worker进程同一时间只能处理一个请求,所以Nginx+php-fpm的模式一直被诟病,是并发问题的瓶颈。

2.2 单进程多线程模型

这类访问模型, 服务器启动时一个进程, 每个WEB项目是部署一个线程池, 每一个请求针对该项目的请求都对应开辟一个新的线程存于线程池, 在线程内又可以开辟子线程。
优点:

  • 创建速度快,方便高效的数据共享
  • 共享数据:多线程间可以共享同一虚拟地址空间;多进程间的数据共享就需要用到共享内存、信号量等IPC技术;
  • 较轻的上下文切换开销 – 不用切换地址空间,不用更改寄存器,不用刷新TLB。
  • 提供非均质的服务 , 如果全都是计算任务,但每个任务的耗时不都为1s,而是1ms-1s之间波动;这样,多线程相比多进程的优势就体现出来,它能有效降低“简单任务被复杂任务压住”的概率;

缺点:
只有一个进程,一旦其中出现一个错误,整个进程都有可能挂掉。你当然可以为ta编写一个“守护程序”来重启,但是重启期间,你的服务器是真的“挂掉了”。

2.2.1 tomcat的NIO模式

tomcat有三种工作模式, 在NIO模式下启动一个tomcat就是一个进程, 每个针对项目的访问都对应一个线程,如果闲置线程占用满了就会开辟新的线程,直到达到设定的最大线程。

image.png
如图所示,是一个典型的请求处理过程。其中绿色代表线程,蓝色代表数据。

  1. Acceptor线程接受请求,从socketCache里面拿出socket对象(没有的话会创建,缓存的目的是避免对象创建的开销),
  2. Acceptor线程标记好Poller对象,组装成PollerEvent,放入该Poller对象的PollerEvent队列
  3. Poller线程从事件队列里面拿出PollerEvent,将其中的socket注册到自身的selector上,
  4. Poller线程等到有读写事件发生时,分发给SocketProcessor线程去实际处理请求
  5. SocketProcessor线程处理完请求,socket对象被回收,放入socketCache

2.3 进程线程+协程模式

Swoole的协程HTTP服务器为例,下图为进程模型

image.png

image.png

  • Master 进程:Master 进程是一个多线程进程
  • Reactor 线程:
    • Reactor 线程是在 Master 进程中创建的线程
    • 负责维护客户端 TCP 连接、处理网络 IO、处理协议、收发数据
    • 不执行任何 PHP 代码
    • 将 TCP 客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包
  • Worker 进程:
    • 接受由 Reactor 线程投递的请求数据包,并执行 PHP 回调函数处理数据
    • 生成响应数据并发给 Reactor 线程,由 Reactor 线程发送给 TCP 客户端
    • 可以是异步非阻塞模式,也可以是同步阻塞模式
    • Worker 以多进程的方式运行
  • TaskWorker 进程:
    • 接受由 Worker 进程投递的任务
    • 处理任务,并将结果数据返回给 Worker 进程
    • 完全是同步阻塞模式
    • TaskWorker 以多进程的方式运行
  • Manager 进程:负责创建 / 回收 worker/task 进程

而针对各线程中对于请求的处理,则是已协程模式工作。
首先简单认识一下调度器:一个线程运行一个调度器,可以在一个调度器上创建若干个协程。调度器负责调度这些协程。并且调度器在其内部维护了一个多路复用器(epoll/select/poll)。

现在假设我们有3个协程A,B,C分别要进行数次IO操作。这3个协程运行在同一个调度器(线程)的上下文中,并依次使用CPU。

协程A首先运行,当它执行到一个IO操作,但该IO操作并没有立即就绪时,A将该IO事件注册到调度器中,并主动放弃 CPU。这时调度器将B切换到CPU上开始执行,同样,当它碰到一个IO操作的时候将IO事件注册到调度器中,并主动放弃 CPU。调度器将C切换到cpu上开始执行。当所有协程都被“阻塞”后,调度器检查注册的IO事件是否发生或就绪。假设此时协程B注册的IO事件已经就绪,调度器将恢复B的执行,B将从上次放弃CPU的地方接着向下运行。A和C同理。

这样,对于某个协程而言,我们采用的是同步的模型;但是对于整个调度器(线程)而言,实际上却是异步的模型。


本文主要从进程/线程/协程角度出发,分析WEB服务器的工作原理,如有错误,欢迎指正。


参考

www.cnblogs.com/leisure_chn…
blog.csdn.net/shixin_0125…
blog.csdn.net/Dream_Weave…
www.jianshu.com/p/a253d21e4…
www.jianshu.com/p/61b634a8a…

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享