一、模型
swoole 是事件驱动的。在使用 swoole 的过程中,我们也体会到,swoole 的使用非常简单,仅仅注册相应的回调处理我们的业务逻辑即可。
但是,在继续学习 swoole 之前,我们有必要再看一看 swoole 的运行流程和进程模型。
前面两篇文章我们已经对 server 和 task 做了简单的介绍,后面再对 server 的创建以及脚本的执行,如无特殊说明均在 CLI 下执行,我就不啰嗦了。
$serv = new swoole_server("127.0.0.1", 9501);
$serv->set([
"worker_num" => 2,
"task_worker_num" => 1,
]);
$serv->on("Connect", function ($serv, $fd) {
});
$serv->on("Receive", function ($serv, $fd, $fromId, $data) {
});
$serv->on("Close", function ($serv, $fd) {
});
$serv->on("Task", function ($serv, $taskId, $fromId, $data) {
});
$serv->on("Finish", function ($serv, $taskId, $data) {
});
$serv->start();
注意这里我们选择了两个 worker 进程个一个 task 进程,那是不是就意味着创建这个 server 就是开启了 3 个进程呢?我们来看下
新开一个终端,我们用 ps 命令看下结果
ps aux | grep server-process
root 21843 xxx... php server-process.php
root 21844 xxx... php server-process.php
root 21846 xxx... php server-process.php
root 21847 xxx... php server-process.php
root 21848 xxx... php server-process.php
root 21854 xxx... grep --color=auto server-process
为了方便阅读,ps 的结果中部分不重要数据已经被稍加处理了。
排除最后一个结果(最后一个是我们运行的 ps 命令)我们发现,竟然有多达 5 个相似的进程在运行,按照我们理解,不应该是 3 个吗,怎么多了两个呢?
还记得我们在进程/线程一文中说过的多进程的实现吗?我们说到多进程的实现一般会被设计 Master-Worker 模式,常见的 nginx 默认的多进程模式也正是如此,当然 swoole 默认的也是多进程模型。
相比 Master-Worker 模式,swoole 的进程模型可以用 Master-Manager-Worker 来形容。即在 Master-Worker 的基础上又增加了一层 Manager 进程。这也就解答了我们开头抛出的问题为什么是 5 个进程而不是 3 个进程了。(1 个 Master 进程+1 个 Manager 进程+2 个 Worker 进程+1 个 Task 进程)
正所谓“存在即合理”,我们来看一下 Master\Manager\Worker 三种进程各自存在的原因。
二、三种进程
Master 进程是一个多线程程序。注解:按照我们之前的理解,多个线程是运行在单一进程的上下文中的,其实对于单一进程中的每一个线程,都有它自己的上下文,但是由于共同存在于同一进程,所以它们也共享这个进程,包括它的代码、数据等等。
再回来继续说 Master 进程,Master 进程就是我们的主进程,掌管生杀大权,它挂了,那底下的都得玩完。Master 进程,包括主线程,多个 Reactor 线程等。
每一个线程都有自己的用途,比如主线程用于 Accept、信号处理等操作,而 Reactor 线程是处理 tcp 连接,处理网络 IO,收发数据的线程。
A、说明两点:
主线程的 Accept 操作,socket 服务端经常用 accept 阻塞,上一节介绍 socket 编程的时候有一张配图,可以看看
信号处理,信号就相当于一条消息,比如我们经常操作的 Ctrl+C 其实就是给 Master 进程的主线程发送一个 SIGINT 的信号,意思就是你可以终止啦,信号有很多种,后面还有介绍
B、Reactor 线程
通常,主线程处理完新的连接后,会将这个连接分配给固定的 Reactor 线程,并且这个 Reactor 线程会一直负责监听此 socket(上文中后面对 socket 更新为 socket 即套接字,是用来与另一个进程进行跨网络通信的文件,文件可读可写),换句话就是说当此 socket 可读时,会读取数据,并将该请求分配给 worker 进程,这也就解释了我们在 swoole 初识讲解 worker 进程内的回调 onReceive 的第三个参数 $fromId 的含义;当此 socket 可写时,会把数据发送给 tcp 客户端。
用一张图清晰的梳理下
那 swoole 为啥不能像 Nginx 一样,是 Master-Worker 进程结构的呢?Manager 进程是干啥的?
这个我正准备说。
C、Manager进程
我们知道,在 Master-Worker 模型中,Master 只有一个,Worker 是由父进程 Master 进程复制出来的,且 Worker 进程可以有多个。
注解:在 linux 中,父进程可以通过调用 fork 函数创建一个新的子进程,子进程是父进程的一个副本,几乎但不完全相同,二者的最大区别就是都拥有自己独立的进程 ID,即 PID。
对于多线程的 Master 进程而言,想要多 Worker 进程就必须 fork 操作,但是 fork 操作是不安全的,所以,在 swoole 中,有一个专职的 Manager 进程,Manager 进程就专门负责 worker/task 进程的 fork 操作和管理。换句话也就是说,对于 worker 进程的创建、回收等操作全权有“保姆”Manager 进程进行管理。
通常,worker 进程被误杀或者由于程序的原因会异常退出,Manager 进程为了保证服务的稳定性,会重新拉起新的 worker 进程,意思就是 Worker 进程你发生意外“死”了,没关系,我自身不“死”,就可以 fork 千千万万个你。
当然,Master 进程和 Manager 进程我们是不怎么关心的,从前面两篇文章我们了解到,真正实现业务逻辑,是在 worker/task 进程内完成的。
再来一张图梳理下 Manager 进程和 Worker/Task 进程的关系。
D、区分进程
再回到我们开篇抛出的的 5 个进程的问题,ps 的结果简直一模一样,有没有办法能区分这 5 个进程哪个是哪个呢?
有同学要说啦,既然各个进程之间存在父子关系,那我们就可以通过 linux 的 pstree 命令查看结果。
pstree | grep server-process
| | \-+= 02548 manks php server-process.php
| | \-+- 02549 manks php server-process.php
| | |--- 02550 manks php server-process.php
| | |--- 02551 manks php server-process.php
| | \--- 02552 manks php server-process.php
| \--- 02572 manks grep server-process
注:centos 下命令可修改为 pstree -ap | grep server-process
从结果中我们可以看出,进程 id 等于 02548 的进程就是 Master 进程,因为从结构上看就它是“父”嘛,02549 是 Manager 进程,Worker 进程和 Task 进程就是 02550、02551和02552了(每个人的电脑上显示的进程 id 可能不同,但顺序是一致的,依照此模型分析即可)。
我们看到 pstree 命令也只能得到大致结果,而且在事先不知道的情况下,根本无法区分 Worker 进程和 Task 进程。
在 swoole 中,我们可以在各个进程启动和关闭的回调中去解决上面这个问题。各个进程的启动和关闭?那岂不是又要记住主进程、Manager 进程、Worker 进程,二三得六,6 个回调函数?
是的,不过这 6 个是最简单也是最好记的,你实际需要了解的可能还要更多。
Master进程:
启动:onStart
关闭:onShutdown
Manager进程:
启动:onManagerStart
关闭:onManagerStop
Worker进程:
启动:onWorkerStart
关闭:onWorkerStop
提醒:task_worker 也会触发 onWorkerStart 回调。
是不是很好记?那我们就在 server-process.php 中通过上面这几种回调来实现对各个进程名的修改。
$serv->on("start", function ($serv){
swoole_set_process_name("server-process: master");
});
// 以下回调发生在Manager进程
$serv->on("ManagerStart", function ($serv){
swoole_set_process_name("server-process: manager");
});
$serv->on("WorkerStart", function ($serv, $workerId){
if($workerId >= $serv->setting["worker_num"]) {
swoole_set_process_name("server-process: task");
} else {
swoole_set_process_name("server-process: worker");
}
});
ps aux | grep server-process
root 27546 xxx... server-process: master
root 27547 xxx... server-process: manager
root 27549 xxx... server-process: task worker
root 27550 xxx... server-process: worker
root 27551 xxx... server-process: worker
root 27570 xxx... grep --color=auto simple
运行结果谁是谁一目了然,简直了!
有同学傻眼了,说在 workerStart 回调中写的看不明白,worker 进程和 task 进程怎么区分的?
我来解释一下:在 onWorkerStart 回调中,$workerId 表示的是一个值,这个值的范围是 0~worker_num,worker_num 是我们的对 worker 进程的配置,其中 0~worker_num 表示 worker 进程的标识,包括 0 但不包括worker_num;worker_num~worker_num+task_worker_num 是 task 进程的标识,包括 worker_num 不包括 worker_num+task_worker_num。
按照高中学的区间的知识可能更好理解,以我们案例的配置,workerId 的值的范围就是 [0,2],[0,2) 表示 worker 进程,[2,3) 就表示 task_worker 进程。
swoole 的进程模型很重要,本节掌握不好,后面的理解可能就会有些问题。
补充:我们在 onWorkerStart 的回调中,用了 serv−>setting 去获取配置的 server 信息,在 swoole 中预留了一些 swooleserver 的属性,我们可以在回调函数中访问。比如说我们可以用 serv->connections 属性获取当前 server的所有的连接,再比如我们可以通过 $serv->master_pid 属性获取当前 server 的 主进程 id 等等。
三、进程
3.1、进程创建
new swoole_process()
参数1:mixed $function 子进程创建成功后执行的函数
参数2:$redirect_stdin_stdout 重定向子进程的标准输入和输出。启用此选项后,在进程内 echo 将不是打印屏幕,而是写入到管道。读取键盘输入将变成从管道钟读取。默认为阻塞读取。
$create_pipe 是否创建管道。启用
$redirect_stdin_stdout 后,此选项将忽略用户参数,强制为 true,如果子进程内没有进程间通信,可以设置为 false
//进程对应的执行函数
function doProcess(swoole_process $worker){
echo "PID",$worker->pid,"\n";
sleep(10);
}
//创建进程
$process = new swoole_process("doProcess");
$pid = $process->start();
$process = new swoole_process("doProcess");
$pid = $process->start();
$process = new swoole_process("doProcess");
$pid = $process->start();
//等待结束
swoole_process::wait();
3.2、进程事件
swoole_event_add()
参数1:
int $sock,int 文件描述符
mixed $read_callback 就是 stream_socket_client/fsockopen 创建的资源
sockets 资源,就是 sockets 扩展钟 socket_create 创建的资源,需要在编译时加入 ./configure --enalbe-sockets
参数2:可读取回调函数
$workers = []; //进程数组
$workerNum = 3; //创建进程的数量
//创建进程
for($i=0; $istart(); //启动进程,并获取进程 ID
$workers[$pid] = $process; //存入进程数组
}
//创建进程执行函数
function doProcess(swoole_process $process){
$process->write("PID:$process->pid"); //子进程写入信息
echo "写入信息:$process->pid $process->callback";
}
//添加进程事件,向每一个子进程添加需要执行的动作
foreach($workers as $process){
swoole_event_add($process->pipe, function($pipe) use($process){
$data = $process->read(); //读取数据
echo "接收到:$data \n";
});
}
3.3、进程队列通信
string swoole_process->pop(int $maxsize = 8192);
bool swoole_process->push(string $data);
array swoole_process::wait(bool $blocking = true);
$workers = []; //进程仓亏
$workerNum = 3; //最多进程数
//批量创建进程
for($i=0; $iuseQueue(); //开启队列,类似于全局函数
$pid = $process->start();
$workers[$pid] = $process;
}
//进程执行函数
function doProcess(swoole_process $process){
$recv = $process->pop(); //默认 8192
echo "从主进程获取到数据:$recv \n";
sleep(5);
$process->exit(0);
}
//主进程向子进程添加数据
foreach($workers as $pid => $process){
$process->push("Hello 子进程 $pid \n");
}
//等待子进程结束,回收资源
for($i=0; $i<$workerNum; $i++){
$ret = swoole_process::wait(); //等待执行完成
$pid = $ret['pid'];
unset($workers[$pid]);
echo "子进程退出 $pid \n";
}
3.4、进程信号触发器
bool swoole_process::signal(int $signo, callable $callback);
function swoole_process::alarm(int $interval_usec, int $type = ITIMER_REAL) : bool
//触发函数,异步执行,达到 10 次停止
swoole_process::signal(SIGALRM, function (){
static $i = 0;
echo "$i \n";
$i++;
if($i>10){
swoole_process::alarm(-1); //清除定时器
}
});
//定时器信号
swoole_process::alarm(100 * 1000);