PHP Advanced Programming Daemon
http://netkiller.github.io/journal/php.daemon.html
Neo Chen (陈京峰) 氏、netkiller、 7NYT
中国広東省深セン市龍華新区民志街西山美地
518131
+86 13113668890
+86 755 29812080
著作権 © 2014 http: / /netkiller.github.io
Copyright Statement
転載する場合は、記事およびこの声明の出典と著者情報を必ず明記してください。ドキュメントソース:
|
|
| 2014-09-01 公開 2015-08-31 更新 2015-10-20 に更新、グレースフル リスタートを追加 |
私の一連のドキュメント
Netkiller アーキテクト ノート
Netkiller 開発者マニュアル
Netkiller PHP マニュアル
Netkiller Python マニュアル
Netkiller テスト マニュアル
Netkiller 暗号化マニュアル
Netkiller Linux マニュアル
Netkiller CentOS マニュアル | Netkiller FreeBSD マニュアル | Netkiller シェル マニュアル | Netkiller セキュリティ マニュアル | |
Netkiller モニタリング マニュアル | Netkiller ストレージ マニュアル | Netkiller メール マニュアル | Netkiller Docbook マニュアル札 | Netkiller プロジェクト ハンドブック |
Netkiller PostgreSQL ハンドブック | Netkiller MySQL ヘルプ | Netkiller NoSQL ヘルプ | Netkiller LDAP ヘルプNetkiller ネットワーク ヘルプ | |
Netkiller H3C | Netkiller マルチメディア ハンドブック | Netkiller Perl ハンドブック | Netkiller アマチュア無線ハンドブック | Netkiller DevOps ハンドブック |
iBook を使用して現在のドキュメントを読むことができます
目次
デーモンプロセスは、ターミナルから切り離され、バックグラウンドで実行されるプロセスです。デーモンプロセスは端末から分離されているため、プロセス実行中の情報が端末に表示されることはなく、端末が生成する端末情報によってプロセスが中断されることはありません。 たとえば、Apache、nginx、mysql はすべてデーモンです
2. デーモンを開発する理由
多くのプログラムは、ターミナルや UI との対話を持たず、他のものを使用する場合があります。 TCP/UDP ソケット、UNIX ソケット、fifo など、他のプログラムと対話するためのメソッド。プログラムが開始されると、条件が満たされるまでバックグラウンドでタスクの処理を開始します。3. アプリケーション開発にデーモン プロセスを使用する場合
現在のニーズを例として挙げると、プログラムを実行し、特定のポートをリッスンし、サーバーによって開始されたデータを受信し続ける必要があります。データを分析して処理し、その結果をデータベースに書き込みます。ZeroMQ を使用してデータの送受信を実装します。 このプログラムの開発にデーモン プロセスを使用しない場合、プログラムが実行されると、現在の端末ウィンドウ フレームが占有され、現在の端末キーボード入力の影響を受ける可能性があり、プログラムが誤って終了する可能性があります。
4. デーモンのセキュリティ問題
脆弱性によりプログラムがハッカーによって制御された場合、攻撃者はそのプログラムを継承することしかできないように、プログラムが非スーパーユーザーとして実行されることを望んでいます。実行権限があり、スーパーユーザー権限を取得できません。 ポートの競合などの問題が発生するため、プログラムは 1 つのインスタンスのみを実行し、同僚が 3 つ以上のプログラムを開くことを許可しないことを願っています。
5. デーモンプロセスの開発方法
例 1. マルチスレッドデーモンプロセスの例<?phpclass ExampleWorker extends Worker { #public function __construct(Logging $logger) { # $this->logger = $logger; #} #protected $logger; protected static $dbh; public function __construct() { } public function run(){ $dbhost = '192.168.2.1'; // 数据库服务器 $dbport = 3306; $dbuser = 'www'; // 数据库用户名 $dbpass = 'qwer123'; // 数据库密码 $dbname = 'example'; // 数据库名 self::$dbh = new PDO("mysql:host=$dbhost;port=$dbport;dbname=$dbname", $dbuser, $dbpass, array( /* PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\'', */ PDO::MYSQL_ATTR_COMPRESS => true, PDO::ATTR_PERSISTENT => true ) ); } protected function getInstance(){ return self::$dbh; }}/* the collectable class implements machinery for Pool::collect */class Fee extends Stackable { public function __construct($msg) { $trades = explode(",", $msg); $this->data = $trades; print_r($trades); } public function run() { #$this->worker->logger->log("%s executing in Thread #%lu", __CLASS__, $this->worker->getThreadId() ); try { $dbh = $this->worker->getInstance(); $insert = "INSERT INTO fee(ticket, login, volume, `status`) VALUES(:ticket, :login, :volume,'N')"; $sth = $dbh->prepare($insert); $sth->bindValue(':ticket', $this->data[0]); $sth->bindValue(':login', $this->data[1]); $sth->bindValue(':volume', $this->data[2]); $sth->execute(); $sth = null; /* ...... */ $update = "UPDATE fee SET `status` = 'Y' WHERE ticket = :ticket and `status` = 'N'"; $sth = $dbh->prepare($update); $sth->bindValue(':ticket', $this->data[0]); $sth->execute(); //echo $sth->queryString; //$dbh = null; } catch(PDOException $e) { $error = sprintf("%s,%s\n", $mobile, $id ); file_put_contents("mobile_error.log", $error, FILE_APPEND); } }}class Example { /* config */ const LISTEN = "tcp://192.168.2.15:5555"; const MAXCONN = 100; const pidfile = __CLASS__; const uid = 80; const gid = 80; protected $pool = NULL; protected $zmq = NULL; public function __construct() { $this->pidfile = '/var/run/'.self::pidfile.'.pid'; } private function daemon(){ if (file_exists($this->pidfile)) { echo "The file $this->pidfile exists.\n"; exit(); } $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // we are the parent //pcntl_wait($status); //Protect against Zombie children exit($pid); } else { // we are the child file_put_contents($this->pidfile, getmypid()); posix_setuid(self::uid); posix_setgid(self::gid); return(getmypid()); } } private function start(){ $pid = $this->daemon(); $this->pool = new Pool(self::MAXCONN, \ExampleWorker::class, []); $this->zmq = new ZMQSocket(new ZMQContext(), ZMQ::SOCKET_REP); $this->zmq->bind(self::LISTEN); /* Loop receiving and echoing back */ while ($message = $this->zmq->recv()) { //print_r($message); //if($trades){ $this->pool->submit(new Fee($message)); $this->zmq->send('TRUE'); //}else{ // $this->zmq->send('FALSE'); //} } $pool->shutdown(); } private function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, 9); unlink($this->pidfile); } } private function help($proc){ printf("%s start | stop | help \n", $proc); } public function main($argv){ if(count($argv) < 2){ printf("please input help parameter\n"); exit(); } if($argv[1] === 'stop'){ $this->stop(); }else if($argv[1] === 'start'){ $this->start(); }else{ $this->help($argv[0]); } }}$cgse = new Example();$cgse->main($argv); ログイン後にコピー
例 2.メッセージキューとデーモンプロセス <?phpdeclare(ticks = 1);require_once( __DIR__.'/autoload.class.php' );umask(077); class EDM { protected $queue; public function __construct() { global $argc, $argv; $this->argc = $argc; $this->argv = $argv; $this->pidfile = $this->argv[0].".pid"; $this->config = new Config('mq'); $this->logging = new Logging(__DIR__.'/log/'.$this->argv[0].'.'.date('Y-m-d').'.log'); //.H:i:s //print_r( $this->config->getArray('mq') ); //pcntl_signal(SIGHUP, array(&$this,"restart")); } protected function msgqueue(){ $exchangeName = 'email'; //交换机名 $queueName = 'email'; //队列名 $routeKey = 'email'; //路由key //创建连接和channel $connection = new AMQPConnection($this->config->getArray('mq')); if (!$connection->connect()) { die("Cannot connect to the broker!\n"); } $this->channel = new AMQPChannel($connection); $this->exchange = new AMQPExchange($this->channel); $this->exchange->setName($exchangeName); $this->exchange->setType(AMQP_EX_TYPE_DIRECT); //direct类型 $this->exchange->setFlags(AMQP_DURABLE); //持久化 $this->exchange->declare(); //echo "Exchange Status:".$this->exchange->declare()."\n"; //创建队列 $this->queue = new AMQPQueue($this->channel); $this->queue->setName($queueName); $this->queue->setFlags(AMQP_DURABLE); //持久化 $this->queue->declare(); //echo "Message Total:".$this->queue->declare()."\n"; //绑定交换机与队列,并指定路由键 $bind = $this->queue->bind($exchangeName, $routeKey); //echo 'Queue Bind: '.$bind."\n"; //阻塞模式接收消息 while(true){ //$this->queue->consume('processMessage', AMQP_AUTOACK); //自动ACK应答 $this->queue->consume(function($envelope, $queue) { $msg = $envelope->getBody(); $queue->ack($envelope->getDeliveryTag()); //手动发送ACK应答 $this->logging->info('('.'+'.')'.$msg); //$this->logging->debug("Message Total:".$this->queue->declare()); }); $this->channel->qos(0,1); //echo "Message Total:".$this->queue->declare()."\n"; } $conn->disconnect(); } protected function start(){ if (file_exists($this->pidfile)) { printf("%s already running\n", $this->argv[0]); exit(0); } $this->logging->warning("start"); $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { //pcntl_wait($status); //等待子进程中断,防止子进程成为僵尸进程。 exit(0); } else { posix_setsid(); //printf("pid: %s\n", posix_getpid()); file_put_contents($this->pidfile, posix_getpid()); //posix_kill(posix_getpid(), SIGHUP); $this->msgqueue(); } } protected function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, SIGTERM); //posix_kill($pid, SIGKILL); unlink($this->pidfile); $this->logging->warning("stop"); }else{ printf("%s haven't running\n", $this->argv[0]); } } protected function restart(){ $this->stop(); $this->start(); } protected function status(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); printf("%s already running, pid = %s\n", $this->argv[0], $pid); }else{ printf("%s haven't running\n", $this->argv[0]); } } protected function usage(){ printf("Usage: %s {start | stop | restart | status}\n", $this->argv[0]); } public function main(){ //print_r($this->argv); if($this->argc != 2){ $this->usage(); }else{ if($this->argv[1] == 'start'){ $this->start(); }else if($this->argv[1] == 'stop'){ $this->stop(); }else if($this->argv[1] == 'restart'){ $this->restart(); }else if($this->argv[1] == 'status'){ $this->status(); }else{ $this->usage(); } } }}$edm = New EDM();$edm->main(); ログイン後にコピー
5.1. プログラムの起動
以下はプログラム起動後にバックグラウンドに入るコードです プロセスIDファイルが存在する場合は、現在のプロセスの状態を判断します。コード file_exists($this- >pidfile) によってプログラムは実行されていますが、その後プロセスが強制終了されるため、実行する前にファイルを手動で削除する必要がありますprivate function daemon(){ if (file_exists($this->pidfile)) { echo "The file $this->pidfile exists.\n"; exit(); } $pid = pcntl_fork(); if ($pid == -1) { die('could not fork'); } else if ($pid) { // we are the parent //pcntl_wait($status); //Protect against Zombie children exit($pid); } else { // we are the child file_put_contents($this->pidfile, getmypid()); posix_setuid(self::uid); posix_setgid(self::gid); return(getmypid()); } } ログイン後にコピー プログラムの開始後、親プロセスが起動されますを実行すると、子プロセスがバックグラウンドで実行され、子プロセスの権限が root から指定されたユーザーに切り替えられ、同時にプロセス ID ファイルの書き込みが行われます。
5.2. プログラムが停止します
プログラムは停止し、pid ファイルを読み取り、posix_kill($pid, 9) を呼び出し、最後にファイルを削除します。private function stop(){ if (file_exists($this->pidfile)) { $pid = file_get_contents($this->pidfile); posix_kill($pid, 9); unlink($this->pidfile); } } ログイン後にコピー 5.3. シングルトン モード
これは、各スレッドがこのデータベース接続を確立および終了する場合に非常に重要であり、データベースに大きなオーバーヘッドが発生します。 。protected function getInstance(){ return self::$dbh;} ログイン後にコピー 5.4. グレースフル リスタートを実現する
いわゆるグレースフル リスタートとは、プロセスが終了しない状況に加え、変数のリセット、設定ファイルの更新、ログのリセットなどを含むリロードを指します。 /start または、再起動するとプロセスが終了して再起動され、プロセス ID が変更されます。同時に、すぐに終了するとビジネスが中断されます。したがって、多くのデーモン プロセスは、いわゆるグレースフル リスタートであるリロード機能を提供します。 リロードの実装原則は、プロセスに SIGHUP シグナルを送信することです。kill コマンドを使用して kill -s SIGHUP 64881 を送信することも、ライブラリ関数を使用して posix_kill(posix_getpid(), SIGUSR1) を実装することもできます。設定ファイル<?phppcntl_signal(SIGTERM, function($signo) { echo "\n This signal is called. [$signo] \n"; Status::$state = -1;});pcntl_signal(SIGHUP, function($signo) { echo "\n This signal is called. [$signo] \n"; Status::$state = 1; Status::$ini = parse_ini_file('test.ini');});class Status{ public static $state = 0; public static $ini = null;}$pid = pcntl_fork();if ($pid == -1) { die('could not fork');}if($pid) { // parent} else { $loop = true; Status::$ini = parse_ini_file('test.ini'); while($loop) { print_r(Status::$ini); while(true) { // Dispatching... pcntl_signal_dispatch(); if(Status::$state == -1) { // Do something and end loop. $loop = false; break; } if(Status::$state == 1) { printf("This program is reload.\r\n"); Status::$state = 0; break; } echo '.'; sleep(1); } echo "\n"; } echo "Finish \n"; exit();} ログイン後にコピー テスト方法、まずデーモンを実行します [root@netkiller pcntl]# cat test.ini [db]host=192.168.0.1port=3306 ログイン後にコピー 次に、設定ファイルを変更して user=test 設定項目を追加します # php signal.reload.php Array( [host] => 192.168.0.1 [port] => 3306) ログイン後にコピー 別のターミナル ウィンドウで、ps コマンドを使用して PID を見つけます。プロセスを削除し、kill コマンドを使用して SIGHUP シグナルを送信し、ps でプロセスを確認すると、プロセスの PID が変更されていないことがわかります うわー設定ファイルがリロードされました This signal is called. [1] This program is reload.Array( [host] => 192.168.0.1 [port] => 3306 [user] => test) ログイン後にコピー 优雅重启完成。
6. 进程意外退出解决方案如果是非常重要的进程,必须要保证程序正常运行,一旦出现任何异常退出,都需要做即时做处理。下面的程序可能检查进程是否异常退出,如果退出便立即启动。 #!/bin/shLOGFILE=/var/log/$(basename $0 .sh).logPATTERN="my.php"RECOVERY="/path/to/my.php start"while truedo TIMEPOINT=$(date -d "today" +"%Y-%m-%d_%H:%M:%S") PROC=$(pgrep -o -f ${PATTERN}) #echo ${PROC} if [ -z "${PROC}" ]; then ${RECOVERY} >> $LOGFILE echo "[${TIMEPOINT}] ${PATTERN} ${RECOVERY}" >> $LOGFILE #else #echo "[${TIMEPOINT}] ${PATTERN} ${PROC}" >> $LOGFILE fisleep 5done & ログイン後にコピー ログイン後にコピー |