随着互联网的发展,php快速开发的特点,现在越来越多的团队将php作为服务端的编程语言,
大家都知道php是单线程,但使用PCNTL和POSIX等扩展实现多进程编程,相比多线程编程,多进程就容易的多。在使用php开发服务端时,很多时候避免不了和多进程打交道,个人才疏学浅,有疏漏。请望指正。
php创建守护进程
开始之前, 请确认已安装扩展pcntl和posix。请使用
php -m
创建守护进程就是让进程脱离终端,独自在后台运行,我们可以让父进程
需要注意的地方有:
- 我们通过通过二次pcntl_fork()和posix_setsid 让主进程脱离终端
- 通过pcntl_signal() 忽略或处理SIGHUP信号
- 多进程需要通过二次pcntl_fork() 或者 pcntl_signal 忽略SIGHUP 信号防止子进程变成Zombie(僵尸)进程
- 通过umask()设定文件权限掩码防止继承文件权限带来的权限影响功能
- 将运行进程的 STDIN/STDOUT/STDERR 重定向到 /dev/null
如果要做的更好。我们还需要:
- 如果通过root启动,运行时更换到低权限用户身份
- 及时chdir() 防止操作错误的路径
- 多进程时要考虑定时重启,防止内存泄漏
在这我们需要关注以下知识点
一、 二次fork和setsid
- fork系统的调用
fork系统的调用是用于复制一个父进程几乎完全的相同的进程,新生成的子进程不同的地方在于父进程有着不同的pid以及不同的内存空间,根据代码逻辑实现,父进程可以完成一样的工作,也可以不同,子进程会从父进程中继承比如文件描述符一类的资源。
PHP 中的pcntl扩展中实现了pcntl_fork()函数,用于php中fork新的进程
- setsid系统调用
setsid系统调用则用于创建一个新的会话并设定进程组id。
这里有几个概念: 会话 进程组。
在Linux中,用户登录产生一个会话session,一个会话中包含一个或者多个进程组,一个进程组又包含多个进程,每个进程组有一个组长,它的pid就是进程组的组id。进程组长一旦打开终端,这个终端就被称为控制终端。一但控制终端发生异常。会发送信号到进程组组长
后台运行程序在终端关闭之后也会被杀死,就是没有处理好控制终端断开时发出的SIGGUP信号,而SIGHUP信号对于进程的默认行为则是退出进程。
调用setsid系统调用之后,会让当前的进程新建一个进程组。如果在当前进程中不打开终端的话,那么这一个进程组就不会存在控制终端,也就不会出现因为关闭终端杀死进程的问题。
php中的POSIX扩展中实现了posix_setsid()函数,用于在php中设定新的进程组。
孤儿进程
父进程比子进程先退出,子进程就会变成孤儿进程。
init进程也就是初始进程就会收养孤儿进程,即孤儿进程的ppid变为1。
二次fork的作用
StackOverflow 上的一个回答写的很好:
The second fork(2) is there to ensure that the new process is not a session leader, so it won’t be able to (accidentally) allocate a controlling terminal, since daemons are not supposed to ever have a controlling terminal.
这是为了防止实际的工作的进程主动关联控制终端或意外关联控制终端,在此fork之后生成新的进程由于不是进程组组长,是不能申请关联控制终端的。
二次fork与setsid的作用是生成新的进程组,防止工作进程关联控制终端。
SIGHUP 信号处理
一个进程收到SIGHUP的信号默认动作是结束进程。
SIGHUP会在如下情况下发出:
- 终端断开,SIGHUP 发送到进程组组长
- 进程组组长退出,SIGHUP会发送到进程组中的前台进程
- SIGHUP常被用于通知进程重载配置文件
使用PHP代码来实现
1、设置守护进程
/**
* 使服务守护进程化.
*
* @return void
*/
protected function deamon()
{
umask(0); // 为后面的子进程让出最大权限
$pid = pcntl_fork();
if (-1 == $pid) {
exit("创建子进程失败" . PHP_EOL);
} elseif ($pid) {
exit();
}
posix_setsid(); // 使当前进程成为session leader
$pidAgain = pcntl_fork();
if (-1 == $pidAgain) {
exit("再次创建子进程失败" . PHP_EOL);
} elseif ($pidAgain) {
exit(posix_getpgid(posix_getppid()). PHP_EOL);
}
}
2、处理任务
/**
* 处理请求.
*
* @return void
*/
protected function handleTask()
{
while (true) {
// process task
sleep(2); // 模拟处理请求
//exec('yii rpc-server/rpc-server');
$amqp = yii::$app->params['amqp'];
//建立一个到RabbitMQ服务器的连接
$this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]);
$this->channel = $this->connection->channel();
//接下来,我们创建一个通道
$this->channel->queue_declare('rpc_queue',false,false,false,false);
//回调
$callback = function($req){
$n = intval($req->body);
file_put_contents(date('Ymd').'txt' , $n."\n" , FILE_APPEND | LOCK_EX );
$msg = new AMQPMessage(
(string) $n,
array('correlation_id' => $req->get('correlation_id'))
);
$req->delivery_info['channel']->basic_publish(
$msg,'', $req->get('reply_to')
);
$req->delivery_info['channel']->basic_ack(
$req->delivery_info['delivery_tag']
);
};
$this->channel->basic_qos(null,1,null);
$this->channel->basic_consume('rpc_queue','',false,false,false,false,$callback);
while (count($this->channel->callbacks)) {
$this->channel->wait();
}
$this->channel->close();
$this->connection->close();
}
}
3、合并
/**
* 启动服务.
*
* @return void
*/
public function actionStart()
{
$this->deamon(); // 守护进程化
$this->handleTask(); // 开始处理任务
}
代码整合
<?php
/**
* Created by TestServer.php.
* User: gongzhiyang
* Date: 19/6/27
* Time: 5:14 下午
*/
namespace console\controllers;
use yii;
use yii\console\Controller;
use PhpAmqpLib\Connection\AMQPStreamConnection;
use PhpAmqpLib\Message\AMQPMessage;
class TestServerController extends Controller
{
/**
* 启动服务.
*
* @return void
*/
public function actionStart()
{
$this->deamon(); // 守护进程化
$this->handleTask(); // 开始处理任务
}
/**
* 使服务守护进程化.
*
* @return void
*/
protected function deamon()
{
umask(0); // 为后面的子进程让出最大权限
$pid = pcntl_fork();
if (-1 == $pid) {
exit("创建子进程失败" . PHP_EOL);
} elseif ($pid) {
exit();
}
posix_setsid(); // 使当前进程成为session leader
$pidAgain = pcntl_fork();
if (-1 == $pidAgain) {
exit("再次创建子进程失败" . PHP_EOL);
} elseif ($pidAgain) {
exit(posix_getpgid(posix_getppid()). PHP_EOL);
}
}
/**
* 处理请求.
*
* @return void
*/
protected function handleTask()
{
while (true) {
// process task
sleep(2); // 模拟处理请求
//exec('yii rpc-server/rpc-server');
$amqp = yii::$app->params['amqp'];
//建立一个到RabbitMQ服务器的连接
$this->connection = new AMQPStreamConnection($amqp["host"], $amqp["port"], $amqp["user"], $amqp["password"]);
$this->channel = $this->connection->channel();
//接下来,我们创建一个通道
$this->channel->queue_declare('rpc_queue',false,false,false,false);
//回调
$callback = function($req){
$n = intval($req->body);
file_put_contents(date('Ymd').'txt' , $n."\n" , FILE_APPEND | LOCK_EX );
$msg = new AMQPMessage(
(string) $n,
array('correlation_id' => $req->get('correlation_id'))
);
$req->delivery_info['channel']->basic_publish(
$msg,'', $req->get('reply_to')
);
$req->delivery_info['channel']->basic_ack(
$req->delivery_info['delivery_tag']
);
};
$this->channel->basic_qos(null,1,null);
$this->channel->basic_consume('rpc_queue','',false,false,false,false,$callback);
while (count($this->channel->callbacks)) {
$this->channel->wait();
}
$this->channel->close();
$this->connection->close();
}
}
}
启动
gongzgiyangdeMacBook-Air:~ gongzhiyang$ yii test-server/start
11410
gongzgiyangdeMacBook-Air:~ gongzhiyang$ ps -ef | grep php
501 5410 1 0 4:08下午 ?? 0:00.07 php /usr/bin/yii test-server/start
501 5888 1 0 4:10下午 ?? 18:03.16 /Applications/PhpStorm.app/Contents/MacOS/phpstorm
501 8723 1 0 4:25下午 ?? 0:00.04 php /usr/bin/yii test-server/start
501 11065 1 0 4:37下午 ?? 0:00.98 php /usr/bin/yii rpc-server/rpc-server
501 11414 1 0 4:39下午 ?? 0:00.02 php /usr/bin/yii test-server/start
501 11504 11147 0 4:39下午 ttys003 0:00.01 grep php