1. 程式人生 > 其它 >PHP高階程式設計之守護程序

PHP高階程式設計之守護程序

PHP高階程式設計之守護程序

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

摘要

2014-09-01 發表

2015-08-31 更新

我的系列文件

Netkiller Architect 手札

Netkiller Developer 手札

Netkiller PHP 手札

Netkiller Python 手札

Netkiller Testing 手札

Netkiller Cryptography 手札

Netkiller Linux 手札

Netkiller Debian 手札

Netkiller CentOS 手札

Netkiller FreeBSD 手札

Netkiller Shell 手札

Netkiller Security 手札

Netkiller Web 手札

Netkiller Monitoring 手札

Netkiller Storage 手札

Netkiller Mail 手札

Netkiller Docbook 手札

Netkiller Project 手札

Netkiller Database 手札

Netkiller PostgreSQL 手札

Netkiller MySQL 手札

Netkiller NoSQL 手札

Netkiller LDAP 手札

Netkiller Network 手札

Netkiller Cisco IOS 手札

Netkiller H3C 手札

Netkiller Multimedia 手札

Netkiller Perl 手札

Netkiller Amateur Radio 手札

Netkiller DevOps 手札

您可以使用iBook閱讀當前文件


目錄

  • 1. 什麼是守護程序
  • 2. 為什麼開發守護程序
  • 3. 何時採用守護程序開發應用程式
  • 4. 守護程序的安全問題
  • 5. 怎樣開發守護程序
    • 5.1. 程式啟動
    • 5.2. 程式停止
    • 5.3. 單例模式
  • 6. 程序意外退出解決方案

1. 什麼是守護程序

守護程序是脫離於終端並且在後臺執行的程序。守護程序脫離於終端是為了避免程序在執行過程中的資訊在任何終端上顯示並且程序也不會被任何終端所產生的終端資訊所打斷。

例如 apache, nginx, mysql 都是守護程序

2. 為什麼開發守護程序

很多程式以服務形式存在,他沒有終端或UI互動,它可能採用其他方式與其他程式互動,如TCP/UDP Socket, UNIX Socket, fifo。程式一旦啟動便進入後臺,直到滿足條件他便開始處理任務。

3. 何時採用守護程序開發應用程式

以我當前的需求為例,我需要執行一個程式,然後監聽某埠,持續接受服務端發起的資料,然後對資料分析處理,再將結果寫入到資料庫中; 我採用ZeroMQ實現資料收發。

如果我不採用守護程序方式開發該程式,程式一旦執行就會佔用當前終端窗框,還有受到當前終端鍵盤輸入影響,有可能程式誤退出。

4. 守護程序的安全問題

我們希望程式在非超級使用者執行,這樣一旦由於程式出現漏洞被駭客控制,攻擊者只能繼承執行許可權,而無法獲得超級使用者許可權。

我們希望程式只能執行一個例項,不運行同事開啟兩個以上的程式,因為會出現埠衝突等等問題。

5. 怎樣開發守護程序

例 1. 多執行緒守護程序例示

			<?php
class 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,%sn", $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 parametern");
			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. 訊息佇列與守護程序

			<?php
declare(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 runningn", $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: %sn", 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 runningn", $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 = %sn", $this->argv[0], $pid);
		}else{
			printf("%s haven't runningn", $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;
}			

6. 程序意外退出解決方案

如果是非常重要的程序,必須要保證程式正常執行,一旦出現任何異常退出,都需要做即時做處理。下面的程式可能檢查程序是否異常退出,如果退出便立即啟動。

		#!/bin/sh

LOGFILE=/var/log/$(basename $0 .sh).log
PATTERN="my.php"
RECOVERY="/path/to/my.php start"

while true
do
        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
        fi
sleep 5
done &