隨着互聯網的發展,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