进程
什么是进程
进程Process
是计算机中的程序关于某数据集合上的一次运行活动,是系统分配资源和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体。在当代面向线程设计的计算机结构中,进程是线程的容器。简单来说,程序是指令、数据以及其组织形式的描述,而进程则是程序的实体。
在操作系统中,进程表示正在运行的程序,例如在终端中使用PHP命令运行PHP脚本,此时就相当于创建了一个进程,这个进程会在系统中驻存,申请属于它自己的内存空间和系统资源,并且运行相应的程序。
$ php build.php
<?php //获取当前进程的PID echo posix_getpid(); //修改所在进程的名称 swoole_set_process_name("swoole process master"); //模拟持续运行100秒的程序 sleep(100);//持续运行100秒的目的是为了在进程中可以查看而不至于很快结束
运行程序
$ php build.php 71
查看进程
$ ps aux | grep 71 root 1 0.0 0.1 18188 1712 pts/0 Ss+ 11:07 0:00 /bin/bash root 71 0.0 3.0 340468 30788 pts/2 S+ 13:41 0:00 swoole process master root 76 0.0 0.0 11112 940 pts/1 S+ 13:42 0:00 grep 71
对于一个进程来说,最核心的内容可分为两部分:一部分是它的内存,这个内存是在创建初始时从系统中分配的,进程中所有创建的变量都会存储在内存环境中。另一部分是上下文环境, 进程是运行在操作系统中的,对于程序而言,它的运行依赖于操作系统分配的资源、操作系统的状态以及程序自身的状态,这些就构成了进程的上下文环境。
父子进程
- 子进程会复制父进程的内存空间和上下文环境
- 子进程会复制父进程的IO句柄即
fd
描述符 - 子进程的内存空间与父进程的内存空间是独立,是互不影响的。
- 修改子进程的内存空间并不会修改父进程或其他子进程的内存空间
例如:父进程通过fopen
打开文件后得到一个IO句柄fd
,子进程复制父进程后同样会得到这个fd
。如果父进程和子进程同时对一个文件进行操作,会造成文件混乱,因此需要加互斥锁。
例如:父进程中的变量x=1
,父进程派生子进程后,子进程也会存在变量x=1
,但是修改父进程中的变量x
并不会影响子进程的变量x
的值。
多进程
PHP是单进程执行的,在处理高并发时主要依赖于Web服务器或PHP-FPM的多进程管理以及进程的复用,但在PHP实现多进程尤其是后台PHP-CLI模式下处理大量数据或运行后台Deamon守护进程时,多进程的优势自然是最好的。
PHP的多线程也曾被人提及,但进程内多线程资源共享和分配问题难以解决,PHP有一个多线程过的扩展pthreads
,它要求PHP环境必须是线程安全的。
多进程简单来说就是多个进程同时执行多个任务,可以将耗时但又必须执行的查询分成多个子进程进行操作。
- PHP多进程不支持PHP-FPM和CGI模式,只能通过PHP-CLI模式。
- PHP多进程适用于定时任务执行,互斥且耗时的任务。
开发使用PHP多进程的场景也就是使用PHP-FPM,PHP-FPM作为PHP的多进程管理器,当使用Nginx作为WebServer时,来自客户端的请求会根据Nginx的路由配置,将以PHP为后缀的文件转发给PHP-FPM。当多个用户同时请求API时,PHP-FPM会开启多个PHP的处理进程进行处理。
检查PHP是否支持多进程扩展
$ php -m | grep pcntl
多进程的优势
PHP相比C、C++、Java少了多线程,PHP中只有多进程的方案,所以PHP中的全局变量和对象不是共享的,数据结构也不能跨进程操作,另外Socket文件描述符也不能共享...
多线程看似比多进程强大的多,多线程的缺陷也同样明显:
- 数据同步时,要么牺牲性能到处加锁,要么使用地狱难度的无锁并发编程。
- 当程序逻辑复杂后,锁会越来越难以控制。一旦死锁,程序基本上就完了。
- 某个线程挂掉后所有的线程都会退出
相比较多线程,多进程拥有的优势是
- 配合进程间通信,基本可以实现任意数据共享。
- 多进程不需要锁
- 多进程可以共享内存的数据结构实现一些多线程的功能
对于并发服务器核心是IO,并非大规模密集运算,高并发的服务器单机能维持10W连接,每秒可以处理3~5W笔消息收发。
普通的Web应用都是IO密集型的程序,瓶颈在MySQL上,所以体现不出PHP的性能优势。但在密集计算方面比C/C++、Java等静态编译语言相差几十倍甚至上百倍。
例如:使用多进程方式同时访问Web地址
$ vim multi.php
<?php echo "process begin: ".date("Y-m-d H:i:s").PHP_EOL; //初始化地址数组 $urls = [ "http://www.baidu.com", "http://www.360.com", "http://www.qq.com", "http://www.sina.com" ]; //初始化数组用于回收线程管道内容 $workers = []; //按照任务分配线程 for($i=0; $i<count($urls); $i++){ $url = $urls[$i]; //创建进程 $process = new swoole_process(function(swoole_process $worker) use($url){ //模拟执行耗时任务 file_get_contents($url); //sleep(1);//模拟耗时1秒 echo $url.PHP_EOL; }, true); //开启进程 $pid = $process->start(); $workers[$pid] = $process; } //打印管道内容 foreach($workers as $worker){ echo "pid : ".$worker->read(); } echo "process end: ".date("Y-m-d H:i:s").PHP_EOL;
运行代码
$ php multi.php process begin: 2019-06-22 16:19:48 pid : http://www.baidu.com pid : http://www.360.com pid : http://www.qq.com pid : http://www.sina.com process end: 2019-06-22 16:19:49
内存共享
进程之间是相互独立的,那么如何实现进程之间的通信呢? 这里可以使用共享内存的方式来实现。
共享内存ShareMemory
是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC
方式,是针对其他进程之间通信效率低下而专门设计的,它往往与其它通信机制,如信号量配置使用以实现进程之间的同步和通信。
共享内存是操作系统中比较特殊的内存,它并不依赖于任何进程, 也不属于任何进程。通过调用系统函数创建共享内存,并指定它的索引,也就是它的IDshmid
,通过索引任何进程都可以在共享内存中申请内存空间并存储对应的值。
- 共享内存并不属于任何一个进程
- 在共享内存中分配的内存空间可以被任何进程访问
- 即使进程关闭,共享内存仍然可以继续保存在操作系统中。
查看操作系统中共享内存的分片
$ ipcs -m ------------ 共享内存段 -------------- 键 shmid 拥有者 权限 字节 连接数 状态 0x00000000 131072 jc 777 16384 1 目标 0x00000000 327681 jc 600 67108864 2 目标 0x00000000 262146 jc 777 8077312 2 目标
Swoole没有采用多线程模型而使用了多线程模型,在一定程度上减少了访问数据时加锁解锁的开销,但同时也引入了新的需求 共享内存。Swoole中为了更好的进行内存管理,减少频繁分配释放内存空间造成的损耗和内存碎片,Rango实际并实现了三种不同功能的内存池分别时FixedPool
、RingBuffer
、MemoryGlobal
。
Swoole开发模式
对于传统PHP的Web开发而言,最常用的是LNMP架构。在LNMP架构中,当请求进入时,WebServer会将请求转交给PHP-FPM,PHP-FPM是一个进程池架构的FastCGI服务,内置了PHP解释器。PHP-FPM负责解释执行PHP文件并生成响应,最终返回给WebServer展现至前端。由于PHP-FPM本身是同步阻塞进程模型,在请求结束后会释放掉所有资源,包括框架初始化创建的一些列对象,从而导致PHP进程进入“空转”消耗大量CPU资源,最终导致单机的吞吐能力有限。
另外,在每次请求处理的过程都意味着一次PHP文件解析、环境设置等不必要的耗时操作,当PHP进程处理完后就会销毁,无法在PHP程序中使用连接池等技术实现性能优化。
针对传统架构的问题,Swoole从PHP扩展下手,解决了上述问题。相比较传统的Web架构,Swoole进程模型最大的特点在于多线程Reactor模式处理网络请求,使其能轻松应对大量连接。
除此之外,Swoole是全异步非阻塞,因此占用资源少,程序执行效率高。在Swoole中程序运行只解析加载一次PHP文件,避免每次请求的重复加载。再者,Swoole进程常驻,使得连接池和请求之间的信息传递的实现成为可能。
使用Swoole开发时,需要开发人员对多进程的运行模式有着清晰的认识。另外,Swoole很容易造成内存泄露。在处理全局变量、静态变量的时候要小心,这种不会被GC清理的变量会存在整个生命周期中。如果没有正确的处理,很容易消耗完内存。而在PHP-FPM下,PHP代码执行完毕内存就会被完全释放掉。
Swoole进程结构
LNMP
架构中PHP
是需要依赖Nginx
这样的Web
服务器以及PHP-FPM
这样的多进程的PHP
解析器。当一个请求到来时PHP-FPM
会去创建一个新的进程去处理这个请求,在这种情况下,系统的开销很大程序上都用在创建和销毁进程上,导致了程序的响应效率并不是非常高。
Swoole的强大之处在于进程模型的设计,即解决了异步问题,又解决了并发问题。
Swoole的进程可分为四种角色
- Master进程
保证Swoole机制运行,同时利用它创建Master主线程(负责接收连接、定时器等)和Reactor线程(处理连接并将请求分发给各个Worker进程)。 - Manager进程
Worker进程和Task进程均由Manager进程派生,Manager管理进程负责结束时回收子进程,避免僵尸进程的存在。 - Worker进程
用PHP回调函数处理由Reactor分发过来的请求数据,并生成响应数据发送给Reactor,由Reactor发送给TCP客户端。 - Task进程
接收由Worker进程分发给它的任务,以多进程方式运行,处理好后将结果返回给它的Worker进程。
在Swoole
中采用了和PHP-FPM
完全不同的架构,整个Swoole
扩展可以分为三层:
第1层:Master主进程
Master进程是Swoole的主进程,主要用于处理Swoole的核心事件驱动。Master主进程是一个多线程模型,拥有多个独立的Reactor线程。
Master主进程包含Master线程、Reactor线程、心跳检测线程、UDP收包线程。每个Reactor子线程中都运行着一个epoll
函数的实例,Swoole对于事件的监听都会在Reactor线程中实现,比如来自客户端的连接、本地通信使用的管道、异步操作使用的文件以及文件描述符都会注册在epoll
函数中。
Master主进程使用select/poll
进行IO
事件循环,Master主进程中的文件描述符只有几个,Reactor线程使用epoll
,因为Reactor线程中会监听大量连接的可读事件,使用epoll
可以支持大量的文件描述符。
以HTTP
服务器为例,Master
主进程负责监听端口,然后接收新的连接,并将这个连接分配给一个Reactor
线程,由这个Reactor
线程监听此连接,一旦此连接可读时,它会读取数据并解析协议,然后将请求投递到Worker
工作进程中去执行。
Master主进程内的回调函数
onStart
服务器启动时主进程的主线程回调此函数onShutdown
服务器正常结束时发生
Master 线程
Swoole启动后Master主线程会负责监听服务器的socket
,如果有新的连接accept
,Master主线程会评估每个Reactor线程的连接数量,并将此连接分配给连接最少的Reactor线程。这样做的好处是:
- 每个Reactor线程持有的连接数非常均衡,没有单个线程负载过高的问题。
- 解决了惊群问题,尤其是拥有多个
listen socket
时,节约了线程唤醒和切换的开销。 - 主线程接管了所有信号
signal
的处理,使Reactor线程运行中可以不被信号打断。
主线程Master在accept
新的连接后,会将这个连接分配给一个固定的Reactor线程,并由这个线程负责监听此socket
,在socket
可读时读取数据,并进行协议解析,最后将请求投递到Worker进程。
Reactor线程
- 负责维护客户端
TCP
连接、处理网络IO、处理协议、收发数据。 - 完全是异步非阻塞的模式
- 全部都是C代码,除了
Start/Shutdown
事件回调外,不执行任何PHP
代码。 - 将
TCP
客户端发送来的数据缓冲、拼接、拆分为完整的请求数据包。 Reactor
以多线程的方式运行
Swoole拥有多线程Reactor,所以可以充分利用多核,开启CPU亲和设置后,Reactor线程可以绑定单独的核,节省CPU Cache开销。
Reactor线程负责处理TCP
连接,是收发数据的线程。Swoole的Master主线程在accept
新的连接后,会将这个连接分配给一个固定的Reactor线程,并由这个线程负责监听此socket
。在socket
可读时读取数据,并进行协议解析,将请求投递到Worker工作进程。在socket
可写时,将数据发送给TCP
客户端。
Reactor线程负责维护客户端TCP连接、处理网络IO、处理协议、收发数据,它完全是异步非阻塞的模式。
Reactor线程是全异步非阻塞的,即使Worker进程采用了同步模式,依然不响应Reactor线程的性能。在Worker进程组很繁忙的状态下,Reactor线程完全不受影响,依然可以收发处理数据。
由于TCP是流式的没有边界,所以处理起来很麻烦。Reactor线程可以使用EOF或者包头长度,自动缓存数据、组装数据包,等一个请求完全收到后,再次递交给Worker。
Reactor全部是C代码,除了Start/Shutdown事件回调外,不执行任何PHP代码。它将TCP客户端发来的数据缓冲、拼接、拆分成完整的一个请求数据包。
综上所述,Master主进程中包含两个关键线程:Master主线程和Reactor线程,Master主线程用来处理accept()
事件,创建新的socket fd
,当它接收到新连接后会将新的socket
连接放到Reactor线程的事件监听循环中,Reactor线程负责接收从客户端发送过来的数据,并按协议解析后通过管道pipe
传递给Worker工作进程进行处理。Worker工作进程处理完毕后,会将结果通过管道pipe
回传给Reactor线程,Reactor线程再按照协议将结果通过socket
发送给客户端。可以看到Reactor线程负责数据的IO和传输,在Linux系统下这些IO事件都是通过epoll
机制来处理的。
心跳包检测线程HeartbeatCheck
Swoole配置了心跳检测后心跳包线程会在固定事件内对所有之前在线的连接发送检测数据包。
UDP收包线程UdpRecv
接收并处理客户端UDP数据包
第2层:Manager管理进程
Swoole运行中会创建一个单独的管理进程,所有的Worker进程和Task进程都是从管理进程fork
创建出来的。
Manager管理进程会监听所有子进程的退出事件,当Worker进程发生致命错误或运行生命周期结束时,Manager管理进程会回收此进程并创建新的进程。
Manager管理进程还可以平滑地重启所有工作进程Worker,以实现程序代码的重新加载。
Manager管理进程管理着Worker工作进程或Task任务进程,Worker工作进程或Task任务进程都被Manager管理进程fork
创建并管理着。
Manager进程负责创建和管理下层的Worker进程组和Task进程组,Manager进程中不会运行任何用户层面的业务逻辑,仅仅只做进程的管理和分配。
Manager进程会fork
创建出指定数量的Worker进程和Task进程。
Manager进程的工作职责
- Worker工作进程和Task任务进程都是由Manager管理进程
fork
创建并管理的 - 子进程结束运行时,Manager管理进程负责回收子进程,以避免成为僵尸进程,并创建新的子进程。
- 服务器关闭时,Manager管理进程发送信号给所有子进程,并通知子进程关闭服务。
- 服务器重启时,Manager管理进程会逐个关闭或重启子进程。
Manager进程内的回调函数
onManagerStart
当管理进程启动时调用onManagerStop
当管理进程结束时调用onWorkerError
当Worker进程或Task进程发生异常后会在Manager进程会回调此函数
第3层:工作进程
- 工作进程主要用于处理客户端请求
- 工作进程接收由Reactor线程投递的请求数据包,并执行PHP回调函数处理数据。
- 工作进程生成响应数据并发送给Reactor线程,由Reactor线程发送给TCP客户端。
- 工作进程可以是异步非阻塞模式也可以是同步阻塞模式。
- 工作进程以多进程的方式运行
与传统的半同步半异步服务器不同是,Swoole的工作进程可以同步的也可以异步的。这样带来了工作进程类似于PHP-FPM进程,它接收由Reactor线程投递的请求数据包,并执行PHP回调函数处理数据。工作线程生成响应数据并发送给Reactor线程,由Reactor线程发送给TCP客户端。工作线程可以是异步模式,也可以是同步模式。另外,工作线程以多进程的方式运行。
Swoole想要实现最好的性能就必须创建出多个工作进程帮助处理任务,但是工作进程必须fork
操作,而fork
操作又是不安全的。如果没有管理将会出现很多僵尸进程,进而影响服务器性能。同时工作进程被误杀或由于程序原因会引起异常退出,为了保证服务的稳定性,需要重新创建工作进程。
工作进程可分为两类:Worker进程和Task进程
-
Worker进程是Swoole的主逻辑进程,用于处理来自客户端的请求。
-
Task进程是Swoole提供的异步工作进程,用于处理耗时较长的同步任务。
Worker工作进程
Worker工作进程接收Reactor线程投递过来的数据,执行PHP代码,然后生成数据并交给Reactor线程,由Reactor线程通过TCP
将数据返回给客户端。如果是UDP
,Worker工作进程会直接将数据发送给客户端。
Worker进程中执行的PHP
代码,它等同于PHP-FPM
。PHP-FPM
在处理异步操作时是很无力的,但Swoole提供的Task进程可以很好的解决这个问题。Worker进程可以将一些异步任务投递给Task进程,然后直接返回,处理其他由Reactor线程投递过来的事件。
Worker进程内的回调函数
onWorkerStart
当Worker工作进程或Task任务进程启动时触发onWorkerStop
当Worker进程终止时触发onConnect
当有新的连接进入时触发onClose
当TCP客户端连接关闭后触发onReceive
当接收到数据时触发onPacket
当接收到UDP数据包是时触发onFinish
当Worker工作进程投递的任务在task_worker
中完成时,Task进程会通过finish()
方法将任务处理的结果发送给Worker进程。onWorkerExit
当开启reload_async
特性后有效,即异步重启特性。onPipeMessage
当工作进程收到由sendMessage
发送的管道消息时触发
Task任务进程
- 异步工作进程
- 接收由Worker工作进程通过
swoole_server->task
或swoole_server->taskwait
方法投递的任务。 - 处理任务,并将结果数据返回给Worker工作进程
swoole_server->finish
。 - 同步阻塞模式
- 以多进程的方式运行
Swoolen除了Reactor线程,Task任务工作进程是以异步的方式处理其它任务的进程,使用方式类似于Gearman。它接收由Worker进程通过swoole_server->task/taskwait
方法投递的任务,然后处理任务,并将结果数据使用swoole_server->finish
返回给Worker进程。Task以多进程的方式进行运行。
简单来说,可以将Reactor理解为Nginx,将Worker理解为PHP-FPM。Reactor线程异步并行地处理网络请求,然后再转发给Worker工作进程中去处理(在回调函数中处理)。Reactor和Worker之间通过UnixSocket进行通信。
Swoole除了Reactor线程,Worker工作进程还提供了Task进程池。目的是为了解决业务代码中,有些逻辑部分不需要马上执行。利用Task进程池,可以方便的投递一个异步任务区执行。
Task进程以完全同步阻塞的方式运行,一个Task进程在执行任务期间是不接受从Worker进程投递的任务的,当Task进程执行完任务后,会异步的通知Worker进程并告诉它任务已经完成。
Task进程内的回调函数
onTask
在Task线程内被调用,Worker进程可使用swoole_server_task
函数向Task进程投递新的任务。onWorkerStart
在Worker或Task进程启动时触发onPipeMessage
当工作进程收到由sendMessage
发送的管道消息时触发
Swoole进程协作
- 当客户端主动连入服务器的时候,客户端实际上是与Master主进程中的某个Reactor线程发生了连接。
- 当TCP三次握手成功后,由这个Reactor线程将连接成功的消息告知Manager管理进程,再由Manager管理进程转交给Worker工作进程,最终在Worker工作进程中触发
onConnect
事件对应的方法。 - 当客户端向服务器发送一个数据包的时候,首先接收到数据包的是Reactor线程,同时Reactor线程会完成组包,再将组装好的包交给Manager管理进程,由Manager管理进程转交给Worker工作进程,此时Worker工作进程触发
onReceive
事件。 - 如果Worker工作进程中做了处理操作后再使用
Send
方法将数据发回给客户端时,数据会沿着这个路径逆流而上。 - Task任务进程用来处理一些占用时间较长的业务,主要处理Worker工作进程中占用时间较长的任务。
形象来说
- Master主进程 = 业务窗口
- Reactor线程 = 前台接待员
- Manager管理进程 = 项目经理
- Worker工作进程 = 工人
当在业务窗口办理业务时,如果用户很多,后边的用户需要排队等待服务,Reactor负责与客户直接沟通,对客户的请求进行初步的整理(传输层级别的整理,组包),然后Manager负责将业务分配给合适的Worker,如空闲的Worker,最终Worker负责实现具体的业务。
Swoole进程关系
Reactor和Worker与Task的关系,简单理解可认为:Reactor = Nginx、Worker = PHP-FPM
Reactor线程异步并行地处理网络请求,然后再转发给Worker工作进程中去处理。
Reactor线程和Worker工作进程之间通过socket
进行通信。
在PHP-FPM的应用中,经常会将一个任务异步投递到Redis等队列中,并在后台启动一些PHP进程异步地处理这些任务。
Swoole提供的Worker工作进程是一套更加完整的方案,它将任务投递、队列、PHP任务进程管理融为一体。通过底层的API实现异步任务的处理。另外,Task任务进程可以在任务执行完毕后,再返回一个结果反馈到Worker工作进程。
Swoole的Reactor、Worker、Task之间可以紧密的结合起来,提供更加高级的使用方式。
假设Server是一个工厂
- Reactor:销售,接收客户订单。
- Worker:工人,当销售接单后,Worker去工作生产出客户需要的东西。
- Task:行政人员,帮助Worker干些杂事儿,让Worker专心工作。
底层会为Worker工作进程、Task任务进程分配一个唯一的ID,不同的Worker和Task任务进程之间可以通过sendMessage
接口进行通信。
Swoole执行流程
- 当客户端请求进入Master主进程后会被Master主线程接收到
- 将读写操作的监听注册到对应的Reactor线程中,并通知Worker工作进程处理
onConnect
,也就是接收到连接的回调。 - 客户端的数据会通知对应的Reactor线程并发送给Worker工作进程进行处理。
- 如果Worker工作进程投递任务,将数据通过管道发送给Task任务进程,Task任务进程处理完后会发送给Worker工作进程。
- Worker工作进程会通知Reactor线程发送数据给客户端。
- 当Worker工作进程出现异常时关闭,Manager管理进程会重新创建一个Worker工作进程,保证Worker工作进程的数量是固定的。