PHP 如何创建守护(daemon)进程
先讲几个概念
守护进程:
Linux中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。守护进程常常在系统引导装入时启动,在系统关闭时终止。
进程组:
是一个或多个进程的集合。进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID。且该进程组ID不会因组长进程的退出而受到影响。
会话周期:
会话期是一个或多个进程组的集合。通常,一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
创建一个守护进程大体这样:
1、fork子进程,父进程退出
为避免挂起,控制终端将 Daemon 放入后台执行,方法是在进程中调用 fork(),然后使父进程终止,我们所有后续工作都在子进程中完成。
<?php $pid = pcntl_fork(); // 父进程 和 子进程 都会执行下面代码 if ($pid == -1) { // 当 pid 为 -1 的时候表示创建子进程失败,这时返回-1。 return false; } else if ($pid) { // 父进程会得到子进程号,所以这里是父进程执行的逻辑 pcntl_wait($status); // 等待子进程中断,防止子进程成为僵尸进程。 } else { // 子进程得到的 $pid 为 0,所以这里是 子进程 执行的逻辑。 }
2、在子进程中创建新会话
先介绍一下 Linux 中的 进程 与 控制终端,登录会话 和 进程组 之间的关系:
进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登录会话可以包含多个进程组。这些进程组共享一个控制终端。这个控制终端通常是创建进程的登录终端。 控制终端,登录会话和进程组通常是从父进程继承下来的。我们的目的就是要摆脱它们,使之不受它们的影响。方法是在第1点的基础上,调用 posix_setsid(); 使进程成为会话组长。setsid有几个作用:让进程摆脱原会话的控制;让进程摆脱原进程组的控制;
3、改变当前目录为根目录
进程活动时,其工作目录所在的文件系统不能卸下。一般需要将工作目录改变到根目录。对于需要转储核心,写运行日志的进程将工作目录改变到特定目录如 chdir("/"),如果有特殊的需要,我们也可以把当前工作目录换成其他的路径,比如 /tmp。
4、重设文件权限掩码
进程从父进程那里继承了文件创建掩模。它可能修改守护进程所创建的文件的存取位。为防止这一点,将文件创建掩模清除:umask(0),如果你的应用程序根本就不涉及创建新文件或是文件访问权限的设定,这一步不是必须的。
5、关闭文件描述符
同文件权限掩码一样,新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不被我们的daemon进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸下。文件描述符为 0、1 和 2 的三个文件,输入、输出 和 报错 这三个文件也需要被关闭。在 PHP 中只需要 fclose() 就可以了。
fclose(STDIN); fclose(STDOUT); fclose(STDERR);
6、守护进程退出,处理 SIGCHLD 信号
当用户需要外部停止守护进程运行时,往往会使用 kill 命令停止该守护进程。所以,守护进程中需要编码来实现 kill 发出的 signal 信号处理,达到进程的正常退出。处理 SIGCHLD 信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。
<?php // 使用 ticks 需要 PHP 4.3.0 以上版本 declare(ticks = 1); // 信号处理函数 function sig_handler($signo) { switch ($signo) { case SIGTERM: // 处理 SIGTERM 信号 - 进程终止 exit; break; case SIGHUP: // 处理 SIGHUP 信号 - 终止控制终端或进程 break; case SIGUSR1: // 用户信号 echo "Caught SIGUSR1...\n"; break; default: // 处理所有其他信号 } } echo "Installing signal handler...\n"; // 安装信号处理器 pcntl_signal(SIGTERM, "sig_handler"); pcntl_signal(SIGHUP, "sig_handler"); pcntl_signal(SIGUSR1, "sig_handler"); // 或者在PHP 4.3.0以上版本可以使用对象方法 // pcntl_signal(SIGUSR1, array($obj, "do_something"); echo "Generating signal SIGTERM to self...\n"; // 向当前进程发送 SIGUSR1 信号 posix_kill(posix_getpid(), SIGUSR1); echo "Done\n";
下面实现一个守护进程的示例代码
<?php class Deamon { private $_pidFile; private $_jobs = array(); private $_infoDir; public function __construct($dir = '/tmp') { $this->_setInfoDir($dir); $this->_pidFile = rtrim($this->_infoDir, '/') . '/' . __CLASS__ . '_pid.log'; $this->_checkPcntl(); } private function _demonize() { if (php_sapi_name() != 'cli') { die('Should run in CLI'); } $pid = pcntl_fork(); if ($pid < 0) { die("Can't Fork!"); } else if ($pid > 0) { exit(); } if (posix_setsid() === -1) { die('Could not detach'); } chdir('/'); umask(0); $fp = fopen($this->_pidFile, 'w') or die("Can't create pid file"); fwrite($fp, posix_getpid()); fclose($fp); if (!empty($this->_jobs)) { foreach ($this->_jobs as $job) { if (!empty($job['argv'])) { call_user_func($job['function'], $job['argv']); } else { call_user_func($job['function']); } } } return; } private function _setInfoDir($dir = null) { if (is_dir($dir)) { $this->_infoDir = $dir; } else { $this->_infoDir = __DIR__; } } private function _checkPcntl() { !function_exists('pcntl_signal') && die('Error:Need PHP Pcntl extension!'); } private function _getPid() { 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; } } private function _message($message) { printf("%s %d %d %s" . PHP_EOL, date("Y-m-d H:i:s"), posix_getpid(), posix_getppid(), $message); } public function start() { if ($this->_getPid() > 0) { $this->_message('Running'); } else { $this->_demonize(); $this->_message('Start'); } } public function stop() { $pid = $this->_getPid(); if ($pid > 0) { posix_kill($pid, SIGTERM); unlink($this->_pidFile); echo 'Stoped' . PHP_EOL; } else { echo "Not Running" . PHP_EOL; } } public function status() { if ($this->_getPid() > 0) { $this->_message('Is Running'); } else { echo 'Not Running' . PHP_EOL; } } public function addJobs($jobs = array()) { if (!isset($jobs['function']) || empty($jobs['function'])) { $this->_message('Need function param'); } if (!isset($jobs['argv']) || empty($jobs['argv'])) { $jobs['argv'] = ""; } $this->_jobs[] = $jobs; } 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->addJobs(array( 'function' => 'test', 'argv' => 'Go' )); $deamon->run($argv); function test($param) { $i = 0; while (true) { echo 'Now is ', $param . PHP_EOL; $i++; sleep(5); } }
延伸阅读: