本文作者是 workerman 作者 walkor 2014 年的作品,自己研究 wokerman 的过程中偶然发现,希望对其他研究 wokerman 或者想通过 PHP 写服务的同学带来帮助。 wokerman 官网 www.workerman.net
很少有用 PHP 写服务的,然而有些场景又要求能有一个这样的服务器程序,它能够与 PHP 无缝结合,并且提供高可靠靠性能的服务,并且提供现有架构所没有的一些高级特性,例如支持自定义协议,支持长连接等等。
PPM(PHP-Process-Manager)是我用 PHP 开发的一款进程管理框架,集成了socket服务功能,无需安装nginx、apache、 PHP -fpm,只需要安装 PHP -cli即可。支持libevent事件轮询库,支持服务平滑重启,支持磁盘文件监控及自动更新,支持各种协议包括自定义协议,最主要的由于是用 PHP 写的,它能与 PHP 无缝结合,非常适合用 PHP 写后端业务逻辑。
从这里可以知道 wokerman 原先叫 PPM, 后来才改名 wokerman。
下面我讲下PPM进程控制与管理的做法
为了充分发挥服务器多核的优势,socket服务都会采用多线程及多进程的模型对外提供服务。用 PHP 写服务必然涉及到进程管理或这线程管理方面的东西,由于PPM使用的是多进程模型,这里主要讲下 PHP 进程控制与管理方面的内容。
1、如何成为 daemon 程序?
2、如何实现多进程?
3、如何监控子进程是否退出?
4、进程间如何通信?
以下都是在linux环境中(windows不支持 PHP 多进程),并且是在 PHP -cli模式下。
1、PHP 如何成为daemon程序 废话不多说,直接上代码
// 设置umask
umask(0);
// fork一次
$pid = pcntl_fork();
if(-1 == $pid)
{
// 出错退出
exit("Daemonize fail ,can not fork");
}
elseif($pid > 0)
{
// 父进程,退出
exit(0);
}
// 子进程使之成为session leader
if(-1 == posix_setsid())
{
// 出错退出
exit("Daemonize fail ,setsid fail");
}
// 现在已经是daemon进程了
2、PHP 如何实现多进程 类似上面使用 pcntl_fork 函数,也不多说,看代码
$pid = pcntl_fork();
if($pid > 0)
{
// 父进程
}
elseif(0 == $pid)
{
// 子进程
}
else
{
// 出错
}
3、PHP 如何监控子进程是否退出 PHP 监控子进程退出有几种办法
1、监听 SIGCHLD 信号 在linux系统中,一个进程终止或者停止时,它的父进程会收到一个SIGCHLD信号,在 PHP 中可以用
pcntl_sigwaitinfo、pcntl_sigtimedwait、pcntl_signal等函数都检测到此信号。父收到SIGCHLD信号,说明有子进程退出了。要注意的是信号可能会重叠,当有多个SIGCHLD信号到达父进程时,父进程可能只检测到一个SIGCHLD信号。需要循环用pcntl_wait/pcntl_waitpid函数检测到底有几个子进程退出以及退出的状态。
一个监控SIGCHLD信号的示例
pcntl_sigtimedwait(array(SIGCHLD), $siginfo);
while(($pid = pcntl_waitpid(-1, $status, WUNTRACED | WNOHANG)) != 0){
// 退出的子进程pid
if($pid>0){
// pid为$pid的子进程退出了,这里可以重新pcntl_fork一个新的子进程
}
else{
// 出错了
}
}
上面这个例子进程会阻塞在 pcntl_sigtimedwait 上,同时我们可以用 pcntl_sigtimedwait 监听更多的信号,例如同时监听 SIGINT 终止信号来实现整个服务的停止,同时监听 SIGHUP 来实现服务的平滑重启等等
2、直接调用pcntl_wait/pcntl_waitpid监控
while(($pid = pcntl_waitpid(-1, $status, WUNTRACED)) != 0){
// 退出的子进程pid
if($pid>0){
// pid为$pid的子进程退出了,这里可以重新pcntl_fork一个新的子进程
}
else{
// 出错了
}
}
这里和上面的例子少了一个pnntl_sigtimedwait调用,直接使用pcntl_waitpid,注意pcntl_waitpid第三个参数没有传WNOHANG,则整个进程会阻塞在pcntl_waitpid上。
3、在进程间建立socket或者管道 在进程间建立socket或者管道,然后用select等IO复用技术监听socket或者管道可读,如果可读但是没有读出数据,那么说明进程间的socket或者管道已经断开了,很可能对方进程已经退出了。
// 创建管道
$channel = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP);
stream_set_blocking($channel[0], 0);
stream_set_blocking($channel[1], 0);
// 创建子进程
$pid = pcntl_fork();
// 父进程
if($pid > 0)
{
fclose($channel[1]);
$write = $e = null;
while(1)
{
$read = array($channel[0]);
if(@stream_select($read, $write, $e, 1))
{
foreach($read as $channel_to_check)
{
if('' == fread($channel_to_check))
{
// $channel_to_check(这里是channel[0])对应进程可能退出了
}
}
}
}
}
// 子进程
elseif($pid === 0)
{
fclose($channel[0]);
}
上面的例子整个进程会阻塞在stream_select调用上面。建立socket监听可读事件,不局限与父子进程间监控进程退出,任何进程间建立链接后,都可以实现监控进程退出事件,而且进程间可以通过这个链接实现进程间通讯。
4、定时发送信号 0 检测
while(1)
{
// 使用posix_kill需要当前进程所有者与被检测$pid所有者是同一个或者当前进程所有者拥有足够的权限
if(!posix_kill($pid, 0))
{
// 进程$pid已经退出了
}
sleep(1);
}
这个方法最简单,只要有权限,它可以检测任何进程是否存活。缺点是不能及时的检测到进程退出。
4、进程间如何通信 PHP 可用的进程间通讯方法很多
1、共享内存 shm*、shmop* // 注意多进程写的时候要考虑互斥
2、消息队列msg_* // 直接使用系统的消息队列,简单高效,不用考虑互斥问题
3、信号量sem_* // 用于互斥使用某个资源
4、管道 PIPE管道、全双工管道、FIFO命名管道
5、socket 命名、无名、unix socket
等等..
具体使用哪种,看实际需要而定。