PHP 编写守护进程

作者: JONE 分类: PHPer 发布时间: 2020-05-19 14:55

原文地址:https://learnku.com/articles/22483

PHP 创建守护进程

进程根据状态可以分为三种进程,守护进程,僵尸进程,孤儿进程。今天我们着重来分析下守护进程。
守护进程
简介

守护进程 (daemon) 是一类在后台运行的特殊进程,用于执行特定的系统任务。很多守护进程在系统引导的时候启动,并且一直运行直到系统关闭。另一些只在需要的时候才启动,完成任务后就自动结束。
创建步骤

创建子进程,终止父进程
由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在 shell 终端里造成一个已经运行完毕的假象。之后所有的工作都在子进程中完成,而用户在 shell 终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式 I 上做到了与控制终端的脱离。

在子进程中创建新会话
这个步骤是创建守护进程中最重要的一步,在这里使用的是系统函数 setsid。setsid 函数用于创建一个新的会话,并担任该会话组的组长。调用 setsid 的三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制和让进程摆脱原控制终端的控制。
在调用 fork 函数时,子进程全盘拷贝父进程的会话期 (session,是一个或多个进程组的集合)、进程组、控制终端等,虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。setsid 函数能够使进程完全独立出来,从而脱离所有其他进程的控制。

改变工作目录
使用 fork 创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载,因此,把当前工作目录换成其他的路径,如 “/” 或 “/tmp” 等。改变工作目录的常见函数是 chdir。

重设文件创建掩码
文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用 fork 函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。因此,把文件创建掩码设置为 0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是 umask,通常的使用方法为 umask (0)。

关闭文件描述符
用 fork 新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写,但它们一样消耗系统资源,可能导致所在的文件系统无法卸载。
直接上代码

注:运行环境是 linux 系统,并且要在 cli 模式下运行。

文件名:deamon.php

<?php
/**
* User: streetlamp
* Date: 2019/1/9
* Time: 15:14
*/

class Deamon
{
protected $_pidFile;

public function __construct()
{
$this->_pidFile = '/var/www/html/queue/public/pid.log';
$this->_checkPcntl();
}

/**
* 创建守护进程核心函数
* @return string|void
*/
private function _demonize()
{
if (php_sapi_name() != 'cli') {
die('Should run in CLI');
}
//创建子进程
$pid = pcntl_fork();
if ($pid == -1) {
return 'fork faile';
} elseif ($pid) {
//终止父进程
exit('parent process');
}
//在子进程中创建新的会话
if (posix_setsid() === -1) {
die('Could not detach');
}
//改变工作目录
chdir('/');
//重设文件创建的掩码
umask(0);
$fp = fopen($this->_pidFile, 'w') or die("Can't create pid file");
//把当前进程的id写入到文件中
fwrite($fp, posix_getpid());
fclose($fp);
//关闭文件描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
//运行守护进程的逻辑
$this->job();
return;
}

/**
* 守护进程的任务
*/
private function job()
{
//TODO 你的守护经常需要执行的任务
while (true) {
file_put_contents('/var/www/html/queue/public/job.log', 'job' . PHP_EOL, FILE_APPEND);
sleep(5);
}
}

/**
* 获取守护进程的id
* @return int
*/
private function _getPid()
{
//判断存放守护进程id的文件是否存在
if (!file_exists($this->_pidFile)) {
return 0;
}
$pid = intval(file_get_contents($this->_pidFile));
if (posix_kill($pid, SIG_DFL)) {//判断该进程是否正常运行中
return $pid;
} else {
unlink($this->_pidFile);
return 0;
}
}

/**
* 判断pcntl拓展
*/
private function _checkPcntl()
{
!function_exists('pcntl_signal') && die('Error:Need PHP Pcntl extension!');
}

private function _message($message)
{
printf("%s %d %d %s" . PHP_EOL, date("Y-m-d H:i:s"), posix_getpid(), posix_getppid(), $message);
}

/**
* 开启守护进程
*/
private function start()
{
if ($this->_getPid() > 0) {
$this->_message('Running');
} else {
$this->_demonize();
$this->_message('Start');
}
}

/**
* 停止守护进程
*/
private function stop()
{
$pid = $this->_getPid();
if ($pid > 0) {
//通过向进程id发送终止信号来停止进程
posix_kill($pid, SIGTERM);
unlink($this->_pidFile);
echo 'Stoped' . PHP_EOL;
} else {
echo "Not Running" . PHP_EOL;
}
}

private function status()
{
if ($this->_getPid() > 0) {
$this->_message('Is Running');
} else {
echo 'Not Running' . PHP_EOL;
}
}

public function run($argv)
{
$param = is_array($argv) && count($argv) == 2 ? $argv[1] : null;
switch ($param) {
case 'start':
$this->start();
break;
case 'stop':
$this->stop();
break;
case 'status':
$this->status();
break;
default:
echo "Argv start|stop|status " . PHP_EOL;
break;
}
}

}

$deamon = new \Deamon();
$deamon->run($argv);

运行方式(运行以上示例代码,不同环境可能需要修改文件写入路径相关的代码)

开启守护进程:php demon.php start
停止守护进程:php demon.php stop
查看守护进程的状态:php demon.php status