php 进程池设计与实现,phper必学!

phper 为什么要学习进程池

在php开发过程中经常使用的 php-fpm 使用的进程模型就是进程池,学习进程池知识能让我们更好理解php-fpm 的运行模式,进程池也是php中主流的并发服务器解决方案
在这里插入图片描述
包含我们的 Workerman 也是用的是进程池,编写一个简单的进程池可以帮助我们更好学习Workerman 源码,了解Workerman 为何如此设计
在这里插入图片描述

池的概念

池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配

很明显,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用设施,它避免了服务器对内核的频繁访问。

进程池技术的应用至少由以下两部分组成:

资源进程:预先创建好的空闲进程,管理进程会把工作分发到空闲进程来处理。

管理进程:管理进程负责创建资源进程,把工作交给空闲资源进程处理,回收已经处理完工作的资源进程。

为什么要有进程池?

动态创建进程缺点

操作系统繁忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。
那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么?

首先,动态创建进程(或线程)是比较耗费时间的,这将导致较慢的客
户响应

即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。

动态创建的子进程(或子线程)通常只用来为一个客户服务(除非我们做特殊的处理),这将导致系统上产生大量的细微进程(或线程)。进程(或线程)间的切换将消耗大量CPU时间。

动态创建的子进程是当前进程的完整映像。当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能

因此我们不能无限制的根据任务去开启或者结束进程。

进程池的优点

进程池是由服务器预先创建的一组子进程,这些子进程的数目在3~10个之间(当然,这只是典型情况)。具体看服务器配置

进程池中的所有子进程都运行着相同的代码,并具有相同的属性,比如优先级、PGID等。因为进程池在服务器启动之初就创建好了,所以每个子进程都相对“干净”,即它们没有打开不必要的文件描述符(从父进程继承而来),也不会错误地使用大块的堆内存(从父进程复制得到)。

选择子进程为新任务服务的方式

当有新的任务到来时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比于动态创建子进程,选择一个已经存在的子进程的代价显然要小得多。至于主进程选择哪个子进程来为新任务服务,则有两种方式:

第一种:主进程使用某种算法来主动选择子进程。最简单、最常用的算法是随机算法和Round Robin(轮流选取)算法,但更优秀、更智能的算法将使任务在各个工作进程中更均匀地分配,从而减轻服务器的整体压力。

第二种:主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。

本篇文章使用第一种实现任务调度
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。这边我使用的是ipc消息队列进行父子进程间的通信

进程池模型

在这里插入图片描述

服务端

<?php

namespace php_pool;
class Process
{
    public $_pid;//进程pid
    public $_msqid;//进程通信消息队列msqid
}

class Server
{
    public $_sockFile = "pool.sock";// 定义一个sock 文件名 用于unix域通信
    public $_processNum = 3;//默认进程池启动 worker 进程
    public $_keyFile = "pool.php";

    public $idx;//方便子进程获取进程对象

    public $_process = []; // 进程池进程对象数组
    public $_sockfd; // 存放当前进程sock 实例
    public $_run = true; //运行开关

    public $_roll = 0; //轮询算法参数

    public $exitpid = []; //程序退出,回收子进程

	/**
	* 信号处理函数
	*/
    public function sigExitHandler($signo)
    {
        $this->_run = false;
    }

    public function __construct($num = 3)
    {
        $this->_processNum = $num;
        // 安装 SIGINT 信号  Ctrl+c 触发该信号
        pcntl_signal(SIGINT, [$this, "sigExitHandler"]);
        $this->forkWorker();// 创建一些 worker 进程
        cli_set_process_title('master'); // 设置主进程名 方便查看
        $this->Listen(); // 监听请求
        // 回收 worker 进程,避免僵尸进程
        while (1) {
            $pid = pcntl_wait($status);
            if ($pid > 0) {
                $this->exitpid[] = $pid;
            }
            if (count($this->exitpid) == $this->_processNum) {
                break;
            }
        }
        /** @var Process $p */
        foreach ($this->_process as $p) {
        	// 移除消息队列
            msg_remove_queue($p->_msqid);
        }
        // 主进程退出
        fprintf(STDOUT, "master shutdown\n");
    }
	
    public function forkWorker()
    {
    	// 实例化 worker 进程对象
        $processObj = new Process();
        for ($i = 0; $i < $this->_processNum; $i++) {
            $key = ftok($this->_keyFile, $i);
            $mqsid = msg_get_queue($key);// 创建 worker 通信的消息队列
            $process = clone $processObj; // 克隆 worker 进程对象
            $process->_msqid = $mqsid; 
            $this->_process[$i] = $process;
            $this->idx = $i; // 方便 worker 进程使用进程对象
            /**
             * @var Process $this
             */
            $this->_process[$i]->_pid = pcntl_fork();// 派生 worker 子进程
            if ($this->_process[$i]->_pid == 0) {//子进程逻辑
                $this->Worker(); // 启动 worker 子进程
            } else {//父进程逻辑
                continue;
            }
        }
    }

    public function Listen()
    {
    	// 创建 sock 文件描述符实例
        $this->_sockfd = socket_create(AF_UNIX, SOCK_STREAM, 0);
        // 创建异常判断
        if (!is_resource($this->_sockfd)) {
            fprintf(STDOUT, "socket create fail:%s\n", socket_strerror(socket_last_error($this->_sockfd)));

        }
        // 移除上传遗留的 pool.sock 避免重复绑定错误
        unlink($this->_sockFile);
        // 将sockfd 套接字绑定到文件上
        if (!socket_bind($this->_sockfd, $this->_sockFile)) {
            fprintf(STDOUT, "socket bind fail\n", socket_strerror(socket_last_error($this->_sockfd)));
        }
		// 监听套接字上的连接
        socket_listen($this->_sockfd, 10);
        // 启动事件循环操作
        $this->evenLoop();
    }
	
	// worker 轮询算法实现
    public function selectWorker($data)
    {
        /**
         * @var Process $process
         */
         //轮询获取 worker 进程实例
        $process = $this->_process[$this->_roll++ % $this->_processNum];
        // 获取worker 消息队列 msq_id 
        $msgid = $process->_msqid;
        // 往worker 进程消息队列 投递 请求任务 msg_send 函数设置为非阻塞
        if (msg_send($msgid, 1, $data, true, false)) {
            fprintf(STDOUT, "send ok\n");
        }
    }

    public function evenLoop()
    {
		// 获取 sock 套接字 注册到 socket_select 可读事件
        $readFds = [$this->_sockfd];
	
        $writeFds = [];
        $exFds = [];
        while ($this->_run) {
        	//socket_select 函数就可以对集合$readFds中的数据是否发生可读行为进行监听【可写、异常等 我们暂且不表设置为空】以达到在同一个进程中实时处理多个IO的目的 接受套接字数组并等待它们改变状态。这里我们只监听了可读事件
            $ret = socket_select($readFds, $writeFds, $exFds, null, null);
            \pcntl_signal_dispatch();// 分发信号
   			// socket_select 函数异常直接退出
            if (false === $ret) {
                break;
            } else if ($ret === 0) {//没有事件发生返回 0
                continue;
            }
			
            if($readFds){
                foreach ($readFds as $fd){
                    if($fd == $this->_sockfd){
                    	// 接收客户端请求
                        $connfd = socket_accept($fd);
                        // 读取客户端发来数据
                        $data = socket_read($connfd,1024);
                        if($data){
                        	// 选择一个 worker 来处理 客户端请求
                            $this->selectWorker($data);
                        }
                        // 给客户端 一个响应
                        socket_write($connfd,"ok",2);
                        // 关闭客户端连接
                        socket_close($connfd);
                    }
                }
            }
        }
       	// 主进程退出
        // 关闭 sock 实例
        socket_close($this->_sockfd);
        /**
         * @var Process $p
         */
		// 给所有 worker 进程发送进程退出消息
        foreach ($this->_process as $p) {
            if (msg_send($p->_msqid, 1, 'quit')) {
                fprintf(STDOUT, "master send quit ok\n");
            }
        }

    }

	// 处理业务逻辑 worker 进程,具体流程由业务逻辑决定
    public function Worker()
    {
        fprintf(STDOUT, "child pid=%d start\n", posix_getpid());
        /**
         * @var Process $process
         */
        $process = $this->_process[$this->idx];
        $msgid = $process->_msqid;
        while (1) {
        	// 获取主进程投递过来的客户端请求消息
            if (msg_receive($msgid, 0, $msgType, 1024, $msg)) {
                fprintf(STDOUT, "child pid =%d recv:%s\n", posix_getpid(), $msg);
                // 监听退出命令
                if (strncasecmp($msg, 'quit', 4) == 0) {
                    break;
                }
            }
        }

        fprintf(STDOUT, "child pid=%d chutdown\n", posix_getpid());
        exit(0);
    }
}

(new Server(3));

客户端

<?php

namespace php_pool;
$sockFile = "pool.sock";
$_sockfd = socket_create(AF_UNIX, SOCK_STREAM, 0);
// 连接服务端
if(socket_connect($_sockfd,$sockFile)){
        $data = 'hello';
        // 发送请求消息给服务端
        socket_write($_sockfd,$data,strlen($data));
        // 接收服务端响应
        echo socket_read($_sockfd,1024)."\n\r";
        //关闭连接
        socket_close($_sockfd);
}

首先我们运行服务端代码,服务运行后进程池有3个 worker 进程,没问题
在这里插入图片描述
接下来我们运行客户端代码,发个 hello 消息给服务端,让它给我们处理一下,多次运行服务端消息接收正常,服务端每次都轮询一个进程池里的worker 进程为我们处理任务
在这里插入图片描述
最后测试服务端退出,所有 worker 进程 正常退出 主进程回收完子进程资源也正常退出,不过 socket_select 函数报了警告,不过不用理会,这个是我们按下 ctrl+c 导致系统调用中断导致的警告,可以忽略
在这里插入图片描述

结语

进程池的设计根据业务不同写法会有所差异,但大致流程都差不多,本次编写的进程池也只是学习使用,并不特别完善,如果有问题请联系我改正谢谢!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
作为一个PHP开发人员,以下是一些你可以学习和提升自己的建议: 1. 深入学习PHP语言:不断深入学习PHP语言本身的特性和功能,包括掌握PHP的高级特性、面向对象编程、异常处理、命名空间等。阅读官方文档、参考书籍以及在线教程,可以帮助你更好地理解和应用PHP。 2. 掌握PHP框架:学习和熟练掌握流行的PHP框架,如Laravel、Symfony、CodeIgniter等。框架可以提供更高效和可维护的开发方式,并且具有许多常用功能和最佳实践。通过使用框架,你可以加快开发速度并构建更高质量的应用程序。 3. 学习前端技术:作为一个PHP开发人员,熟悉前端技术(如HTML、CSS、JavaScript)是非常有益的。这样你可以更全面地开发Web应用程序,实现更好的用户体验。学习流行的前端框架(如React、Vue.js)也是一个很好的选择。 4. 掌握数据库技术:数据库在Web开发中起着重要作用,所以掌握数据库技术对于一个PHP开发人员来说是必不可少的。学习SQL语言,了解关系型数据库(如MySQL)和非关系型数据库(如MongoDB)的使用和优化技巧。 5. 参与开源项目和社区:积极参与开源项目和社区,与其他开发者交流和分享经验。通过参与开源项目,你可以学习到其他人的最佳实践,并有机会提升自己的编程能力。 6. 持续学习和自我提升:Web开发技术不断演进,持续学习是非常重要的。跟踪行业的最新趋势和技术,参加培训课程、研讨会和在线教育平台上的课程,保持对新技术的好奇心,并将其应用到实际项目中。 7. 开发项目经验:除了学习,实际项目经验也是提升自己的重要途径。尝试参与一些小型项目或者个人项目,通过实践中不断积累经验和解决问题,提高自己的编码能力和技术水平。 总之,持续学习、实践和参与社区是提升自己作为PHP开发人员的关键。不断提升自己的技术能力,关注行业发展趋势,扩展自己的技术广度和深度,将有助于你在PHP开发领域取得更好的成绩。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值