1 概念
1.1 定义
操作系统中的经典定义:
- 进程:资源分配单位
- 线程:调度单位
- 协程:用户态实现的调度单位
进程调度
注意:进程的切换其实是对线程进行调度
1.2 关系
一个进程可以包含多个线程,一个线程也可以包含多个协程。但是有一点,必须明确,一个线程内的多个协程的运行是串行的。如果有多核CPU的话,多个进程或一个进程内的多个线程是可以并行运行的,但是一个线程内的多个协程却绝对串行(同步|依次运行)的,无论有多少个CPU(核)。一个线程内可以运行多个协程,但是这些协程都是串行运行的。当一个协程运行时,其他协程必须挂起。
比较
虽然说,协程与进程、线程相比不是一个维度的概念,但是有时候,我们仍然需要将它们做一番比较,具体如下:
- 协程既不是进程,也不是线程,协程仅仅是一个特殊的函数,协程跟他们就不是一个维度。
- 一个进程可以包含多个线程,一个线程可以包含多个协程。
- 一个线程内的多个协程虽然可以切换,但是这多个协程是串行执行的,只能在这一个线程内运行,没法利用CPU多核能力。
- 协程与进程一样,它们的切换都存在上下文切换问题。
三者上下文
进程 | 线程 | 协程 | |
---|---|---|---|
切换者 | 操作系统 | 操作系统 | 用户(编程者/应用程序) |
切换时机 | 根据操作系统自己的切换策略,用户无感知 | 根据操作系统自己的切换策略,用户无感知 | 用户自己(的程序)决定 |
切换内容 | 页全局目录 内核栈 硬件上下文 |
内核栈 硬件上下文 |
硬件上下文 |
切换内容的保存 | 保存于内核栈中 | 保存于内核栈中 | 保存于用户自己的变量(用户栈或者堆) |
切换过程 | 用户态-内核态-用户态 | 用户态-内核态-用户态 | 用户态(没有陷入内核态) |
切换效率 | 低 | 中 | 高 |
1.3 进程和线程
- 地址空间:进程内的一个执行单元;进程至少有一个线程;它们共享进程的地址空间;而进程有自己独立的地址空间;因此线程可以读写同样的数据结构和变量,便于线程之间的通信。相反,进程间通信(IPC)很困难且消耗更多资源。
- 资源拥有:进程是资源分配和拥有的单位,同一个进程内的线程共享进程的资源
- 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元
- 二者均可并发执行.
- 进程的创建调用fork或者vfork,而线程的创建调用pthread_create,进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束
- 线程有自己的私有属性TCB,线程id,寄存器、硬件上下文,而进程也有自己的私有属性进程控制块PCB,这些私有属性是不被共享的,用来标示一个进程或一个线程的标志
1.4 协程
协程,即协作式程序,其思想是,一系列互相依赖的协程间依次使用CPU,每次只有一个协程工作,而其他协程处于休眠状态。协程可以在运行期间的某个点上暂停执行,并在恢复运行时从暂停的点上继续执行。
协程可以被认为是一种用户空间线程,与传统的线程相比,有2个主要的优点:
- 与线程不同,协程是自己 主动让出(可控) CPU,并交付他期望的下一个协程运行,而不是在任何时候都有可能被系统调度打断。因此协程的使用更加清晰易懂,并且多数情况下不需要锁机制。
- 与线程相比,协程的切换由程序控制,发生在用户空间而非内核空间,因此切换的代价非常小。
2 Web服务器举例
2.1 多进程单线程模型
这类访问模型, 每一个请求都对应开辟一个新的进程, 在进程内是单线程的。
优点:
- 编程相对容易;通常不需要考虑锁和同步资源的问题。
- 更强的容错性:比起多线程的一个好处是一个进程崩溃了不会影响其他进程。
- 有内核保证的隔离:数据和错误隔离。
- 对于使用如C/C++这些语言编写的本地代码,错误隔离是非常有用的:采用多进程架构的程序一般可以做到一定程度的自恢复;(master守护进程监控所有worker进程,发现进程挂掉后将其重启)
缺点:
进程切换开销大, 密集访问带会来的并发问题。
2.1.1 Nginx进程模型
Nginx采用的是多进程单线程& 多路IO复用模型,是以多进程(每个进程仅有一个线程)的方式来工作的(当然nginx也是支持多线程的方式的,只是我们主流的方式还是多进程的方式),但是每个线程可以处理多个客户端的访问,这也成就了它的并发性能。Nginx采用多进程的方式有诸多好处。
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进程同一时间只能处理一个请求)
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就是一个进程, 每个针对项目的访问都对应一个线程,如果闲置线程占用满了就会开辟新的线程,直到达到设定的最大线程。
如图所示,是一个典型的请求处理过程。其中绿色代表线程,蓝色代表数据。
- Acceptor线程接受请求,从socketCache里面拿出socket对象(没有的话会创建,缓存的目的是避免对象创建的开销),
- Acceptor线程标记好Poller对象,组装成PollerEvent,放入该Poller对象的PollerEvent队列
- Poller线程从事件队列里面拿出PollerEvent,将其中的socket注册到自身的selector上,
- Poller线程等到有读写事件发生时,分发给SocketProcessor线程去实际处理请求
- SocketProcessor线程处理完请求,socket对象被回收,放入socketCache
2.3 进程线程+协程模式
以Swoole的协程HTTP服务器为例,下图为进程模型
- 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…