博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
PHP高级编程之守护进程,实现优雅重启
阅读量:6293 次
发布时间:2019-06-22

本文共 10374 字,大约阅读时间需要 34 分钟。

PHP高级编程之守护进程

http://netkiller.github.io/journal/php.daemon.html

MrNeo Chen (陈景峰)netkiller, BG7NYT

中国广东省深圳市龙华新区民治街道溪山美地
518131
+86 13113668890
+86 755 29812080

版权 © 2014 http://netkiller.github.io

版权声明

转载请与作者联系,转载时请务必标明文章原始出处和作者信息及本声明。

文档出处:

扫描二维码进入 Netkiller 微信订阅号

读者群:128659835 请注明“读者”

 

2014-09-01

摘要

2014-09-01 发表

2015-08-31 更新

2015-10-20 更新,增加优雅重启

我的系列文档

您可以使用阅读当前文档


目录

1. 什么是守护进程

守护进程是脱离于终端并且在后台运行的进程。守护进程脱离于终端是为了避免进程在执行过程中的信息在任何终端上显示并且进程也不会被任何终端所产生的终端信息所打断。

例如 apache, nginx,  都是守护进程

2. 为什么开发守护进程

很多程序以服务形式存在,他没有终端或UI交互,它可能采用其他方式与其他程序交互,如TCP/UDP Socket, UNIX Socket, fifo。程序一旦启动便进入后台,直到满足条件他便开始处理任务。

3. 何时采用守护进程开发应用程序

以我当前的需求为例,我需要运行一个程序,然后监听某端口,持续接受服务端发起的数据,然后对数据分析处理,再将结果写入到中; 我采用ZeroMQ实现数据收发。

如果我不采用守护进程方式开发该程序,程序一旦运行就会占用当前终端窗框,还有受到当前终端键盘输入影响,有可能程序误退出。

4. 守护进程的安全问题

我们希望程序在非超级用户运行,这样一旦由于程序出现漏洞被骇客控制,攻击者只能继承运行权限,而无法获得超级用户权限。

我们希望程序只能运行一个实例,不运行同事开启两个以上的程序,因为会出现端口冲突等等问题。

5. 怎样开发守护进程

例 1. 多线程守护进程例示
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. 消息队列与守护进程
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文件来判断,当前进程状态,如果进程ID文件存在表示程序在运行中,通过代码file_exists($this->pidfile)实现,但而后进程被kill需要手工删除该文件才能运行

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切换到指定用户,同时将pid写入进程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. 实现优雅重启

所谓优雅重启是指进程不退出的情况加实现重新载入包含重置变量,刷新配置文件,重置日志等等

stop/start 或者 restart都会退出进程,重新启动,导致进程ID改变,同时瞬间退出导致业务闪断。所以很多守护进程都会提供一个reload功能,者就是所谓的优雅重启。

reload 实现原理是给进程发送SIGHUP信号,可以通过kill命令发送 kill -s SIGHUP 64881,也可以通过库函数实现 posix_kill(posix_getpid(), SIGUSR1);

创建配置文件

[root@netkiller pcntl]# cat test.ini [db]host=192.168.0.1port=3306

测试方法,首先运行该守护进程

# php signal.reload.php Array(    [host] => 192.168.0.1    [port] => 3306)

现在修改配置文件,增加user=test配置项

[root@netkiller pcntl]# cat test.ini [db]host=192.168.0.1port=3306user=test

发送信号,在另一个终端窗口,通过ps命令找到该进程的PID,然后使用kill命令发送SIGHUP信号,然后再通过ps查看进程,你会发现进程PID没有改变

[root@netkiller pcntl]# ps ax | grep reload64881 pts/0    S      0:00 php -c /srv/php/etc/php-cli.ini signal.reload.php65073 pts/1    S+     0:00 grep --color=auto reload[root@netkiller pcntl]# kill -s SIGHUP 64881[root@netkiller pcntl]# ps ax | grep reload64881 pts/0    S      0:00 php -c /srv/php/etc/php-cli.ini signal.reload.php65093 pts/1    S+     0:00 grep --color=auto reload

配置文件被重新载入

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 &
你可能感兴趣的文章
Web服务器的配置与管理(4) 配置访问权限和安全
查看>>
sql注入之order by猜列数问题
查看>>
将域用户加入本地power user组的脚本
查看>>
python range()内建函数
查看>>
Git 远程分支的pull与push
查看>>
React源码学习——ReactClass
查看>>
电脑爱好者GHOSTWIN764位V4.0
查看>>
MYSQL——常用运算符和函数
查看>>
JS获取上传文件的大小
查看>>
Lync Server 2010迁移至Lync Server 2013故障排错Part1:缺少McsStandalone.msi
查看>>
域控制器建立教程
查看>>
RHCE 学习笔记(20) ACL
查看>>
Django 和 Ajax 简介
查看>>
Qt的一个颜色选取按钮QColorButton
查看>>
perl 散列数组
查看>>
puppet之service管理
查看>>
Exchange2010server证书申请及分配服务
查看>>
Cassandra 处理客户端请求
查看>>
[WinApi]邮槽通信C/S实例
查看>>
linux NFS配置:NFS相关概念及其配置与查看
查看>>