[toc] ## 进程管理 ### 官网实例了解进程api 根据官方实例加上注释 * 子进程异常退出时,自动重启 * 主进程异常退出时,子进程会继续执行,完成所有任务后退出 ~~~ class Process { // 主进程的pid public $mpid = 0; // public $works = []; // 创建多少个子进程 public $max_precess = 1; public $new_index = 0; public function __construct() { try { // 给当前主进程命名 swoole_set_process_name(sprintf('php-ps:%s' , 'master')); // 获得当前主进程的PID $this->mpid = posix_getpid(); // 创建子进程并运行 $this->run(); // 运行结束后回收并重启 $this->processWait(); } catch (\Exception $e) { die('ALL ERROR: ' . $e->getMessage()); } } public function run() { for ($i = 0;$i < $this->max_precess;$i ++) { $this->CreateProcess(); } } public function CreateProcess($index = null) { // 创建子进程,用于在检查主进程是否还活着 $process = new \swoole_process( function (\swoole_process $worker) use ($index){ // 设置进程名称 if (is_null($index)) { $index = $this->new_index; $this->new_index ++; } swoole_set_process_name(sprintf('php-ps:%s' , $index)); // 120秒后重启子进程 for ($j = 0;$j < 120;$j ++) { $this->checkMpid($worker); echo "msg: {$j}\n"; sleep(1); } } , false , false ); $pid = $process->start(); $this->works[$index] = $pid; return $pid; } public function checkMpid(&$worker) { // 检查主进程是否存在 if (!\swoole_process::kill($this->mpid , 0)) { // 如果不存在的话就退出子进程 $worker->exit(); // 这句提示,实际是看不到的.需要写到日志中 echo "Master process exited, I [{$worker['pid']}] also quit\n"; } } public function rebootProcess($ret) { $pid = $ret['pid']; $index = array_search($pid , $this->works); if ($index !== false) { $index = intval($index); // 名称设置为一样 $new_pid = $this->CreateProcess($index); echo "rebootProcess: {$index}={$new_pid} Done\n"; return; } throw new \Exception('rebootProcess Error: no pid'); } public function processWait() { while (1) { // 如果子进程存在 if (count($this->works)) { // 等待其结束后回收 $ret = \swoole_process::wait(); if ($ret) { // 回收成功后重启子进程 $this->rebootProcess($ret); } } else { break; } } } } ~~~ 使用`ps aft | grep php`查看进程关系 ![](https://i.loli.net/2019/04/26/5cc29faa3e6aa.png) ### 父子进程通过管道通信 ~~~ // 第二参数设为true,可以使用write和read来通过管道使父子进程通信 $process = new \swoole_process( function (\swoole_process $process){ $process->write('Hello'); } , true ); $process->start(); usleep(100); // 输出 Hello echo $process->read(); ~~~ ### 调用外部程序 ~~~ $process = new \Swoole\Process( function (\Swoole\Process $childProcess){ // 1. 必须写绝对路径 // 2. 参数必须分开放到数组中 $childProcess->exec( '/usr/local/bin/php' , [ '/var/www/project/yii-best-practice/cli/yii' , 't/index' , '-m=123' , 'abc' , 'xyz' ]); // 3. 执行shell命令,略有区别。不过一般使用协程co:exec() $childProcess->exec('/bin/sh' , ['-c' , "cp -rf /data/test/* /tmp/test/"]); } ); $process->start(); ~~~ ### 在SwooleServer中添加用户自定义进程 ~~~ $server = new \Swoole\Server('127.0.0.1', 9501); /** * 自定义用户进程实现广播功能 * 循环接收管道消息,并发给服务器的所有连接 */ $process = new \Swoole\Process(function($process) use ($server) { // 用户进程内应当进行while(true)或EventLoop循环,否则用户进程会不停地退出重启 while (true) { $msg = $process->read(); // 创建的子进程可以调用$server对象提供的各个方法和属性 foreach($server->connections as $conn) { $server->send($conn, $msg); } } }); // 在swooleServer中新增进程时,子进程不需要start // 在Server启动时会自动创建进程,并执行指定的子进程函数 $server->addProcess($process); $server->on('receive', function ($serv, $fd, $reactor_id, $data) use ($process) { //群发收到的消息 $process->write($data); }); $server->start(); ~~~ 可以参考easyswoole的编码方式:http://www.easyswoole.com/Manual/3.x/Cn/_book/BaseUsage/process.html。 把复杂的业务逻辑写进类里面,然后实例出来调用`getProcess()` ### 进程池 ~~~ // 设置10个工作进程 $workerNum = 10; $pool = new \Swoole\Process\Pool($workerNum); // 配置事件回调 $pool->on("WorkerStart", function ($pool, $workerId) { // 得到Process对象,可以使用Process对象的方法 $process = $pool->getProcess(); $process->exec("/bin/sh", ['-c', 'ls -l']); }); $pool->on("WorkerStop", function ($pool, $workerId) { echo "Worker#{$workerId} is stopped\n"; }); // 启动工作进程 $pool->start(); ~~~ >[danger] 这边测试下来,`onWorkerStart`事件一直重复触发,按道理是只会在进程启动的时候执行一次。原因未知。有可能是单核虚拟机的问题。 ### 进程信号异步监听 [Linux信号列表](https://wiki.swoole.com/wiki/page/p-LinuxSignal.html) ~~~ // 监听SIGTERM信号(停止) \Swoole\Process::signal(SIGTERM, function($signo) { echo "shutdown."; }); ~~~ ![](https://i.loli.net/2019/04/26/5cc2db7825118.png) 查到当前的进程pid为19093, 然后 `kill -9 19093`, 杀掉之后就输出`shutdown.` ## 进程隔离与内存共享 进程和进程之间是隔离的。 * 不同的进程中PHP变量不是共享,即使是全局变量,在A进程内修改了它的值,在B进程内是无效的 * 如果需要在不同的Worker进程内共享数据,可以用`Redis`、`MySQL`、`文件`、`Swoole\Table`、`APCu`、`shmget`等工具实现 * 不同进程的文件句柄是隔离的,所以在A进程创建的Socket连接或打开的文件,在B进程内是无效,即使是将它的fd发送到B进程也是不可用的 ~~~ $server = new \Swoole\Http\Server('0.0.0.0', 9500); // 4个进程 $server->set([ 'worker_num' => 4]); $i = 1; $server->on('Request', function ($request, $response) { global $i; $response->end($i++); }); $server->start(); ~~~ 用两个浏览器访问上面的`HttpServer`,会发现输出的$i都不一样。如果是worker设置为1的话就一样。因为`$i`变量虽然是全局变量(`global`),但由于进程隔离的原因。假设有`4`个工作进程,在`进程1`中进行`$i++`,实际上只有`进程1`中的`$i`变成`2`了,其他另外`3`个进程内`$i`变量的值还是`1`。 使用`Swoole\Table`来做到内存共享(其实更建议`redis`) ~~~ $server = new \Swoole\Http\Server('0.0.0.0', 9500); // 4个进程 $server->set([ 'worker_num' => 4]); // 参数指定表格的最大行数,必须为2的n次方,如果小于1024则默认成1024,即1024是最小值 // Table基于行锁,所以单次set/get/del在多线程/多进程的环境下是安全的 // set/get/del等方法是原子操作,用户代码中不需要担心数据加锁和同步的问题 $table = new \Swoole\Table(8); // 字符串类型必须指定第三参数,如果是整型的话,注意溢出 $table->column('i',$table::TYPE_INT); $table->create(); $table->set('table',['i' => 1]); $server->on('Request', function ($request, $response) use($table) { $table->incr('table','i'); $response->end($table->get('table','i')); }); $server->start(); ~~~ 更多参考: https://wiki.swoole.com/wiki/page/p-table.html >[info] 进程在系统是非常昂贵的资源,创建进程消耗很大,要谨慎使用。如果要创建的子进程业务是要长期执行的,比如“监控文件文件变化”,可以使用子进程。其它短期任务,能用协程尽量用协程。