守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。本篇文章带大家了解一下PHP中实现daemon的方法,介绍一下编程中需要注意的地方。
|
守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。本篇文章带大家了解一下PHP中实现daemon的方法,介绍一下编程中需要注意的地方。
PHP实现守护进程可以通过 编程中需要注意的地方有:
如果要做的更好,还需要注意:
什么是daemon文章的主角守护进程(daemon),Wikipedia 上的定义是:
UNIX环境高级编程(第二版)(以下使用简称 APUE 指代) 13章有云:
这里注意到,daemon有如下特征:
想要查看运行中的守护进程可以通过 实现关注点二次 fork 与 setsidfork 系统调用fork 系统调用用于复制一个与父进程几乎完全相同的进程,新生成的子进程不同的地方在于与父进程有着不同的 pid 以及有不同的内存空间,根据代码逻辑实现,父子进程可以完成一样的工作,也可以不同。子进程会从父进程中继承比如文件描述符一类的资源。 PHP 中的 setsid 系统调用setsid 系统调用则用于创建一个新的会话并设定进程组 id。 这里有几个概念: 在 Linux 中,用户登录产生一个会话(Session),一个会话中包含一个或者多个进程组,一个进程组又包含多个进程。每个进程组有一个组长(Session Leader),它的 pid 就是进程组的组 id。进程组长一旦打开一个终端,这一个终端就被称为控制终端。一旦控制终端发生异常(断开、硬件错误等),会发出信号到进程组组长。 后台运行程序(如 shell 中以 调用 PHP 中的 孤儿进程父进程比子进程先退出,子进程就会变成孤儿进程。 init 进程会收养孤儿进程,即孤儿进程的 ppid 变为 1。 二次 fork 的作用首先, 二次 fork 操作的样例代码如下: $pid1 = pcntl_fork();
if ($pid1 > 0) {
exit(0);
} else if ($pid1 < 0) {
exit("Failed to fork 1\n");
}
if (-1 == posix_setsid()) {
exit("Failed to setsid\n");
}
$pid2 = pcntl_fork();
if ($pid2 > 0) {
exit(0);
} else if ($pid2 < 0) {
exit("Failed to fork 2\n");
}假定我们在终端中执行应用程序,进程为 a,第一次 fork 会生成子进程 b,如果 fork 成功,父进程 a 退出。b 作为孤儿进程,被 init 进程托管。 此时,进程 b 处于进程组 a 中,进程 b 调用 此时进程 b 事实上已经脱离任何的控制终端,例程: <?php
cli_set_process_title('process_a');
$pidA = pcntl_fork();
if ($pidA > 0) {
exit(0);
} else if ($pidA < 0) {
exit(1);
}
cli_set_process_title('process_b');
if (-1 === posix_setsid()) {
exit(2);
}
while(true) {
sleep(1);
}执行程序之后: ? ~ php56 2fork1.php ? ~ ps ax | grep -v grep | grep -E 'process_|PID' PID TTY STAT TIME COMMAND 28203 ? Ss 0:00 process_b 从 ps 的结果来看,process_b 的 TTY 已经变成了 代码走到这里,似乎已经完成了功能,关闭终端之后 process_b 也没有被杀死,但是为什么还要进行第二次 fork 操作呢? StackOverflow 上的一个回答写的很好:
这是为了防止实际的工作的进程主动关联或者意外关联控制终端,再次 fork 之后生成的新进程由于不是进程组组长,是不能申请关联控制终端的。 综上,二次 fork 与 setsid 的作用是生成新的进程组,防止工作进程关联控制终端。 SIGHUP 信号处理一个进程收到 而
由于实际的工作进程不在前台进程组中,而且进程组的组长已经退出并且没有控制终端,不处理正常情况下当然也没有问题,然而为了防止偶然的收到 Zombie 进程处理何为 Zombie 进程简单来说,子进程先于父进程退出,父进程没有调用 子进程先于父进程退出时,会向父进程发送 Zombie 进程会占用可 fork 的进程数,Zombie 进程过多会导致无法 fork 新的进程。 此外,Linux 系统中 ppid 为 init 进程的进程,变为 Zombie 后会由 init 进程回收管理。 Zombie 进程的处理从 Zombie 进程的特点,对于多进程的daemon,可以通过两个途径解决这一问题:
父进程处理信号无需多说,注册信号处理回调函数,调用回收方法即可。 对于让子进程被 init 接管,则可以通过2次 fork 的方法,让第一次 fork 出的子进程 a 再 fork 出实际的工作进程 b,让 a 先行退出,使得 b 成为孤儿进程,这样就能被 init 进程托管了。 umaskumask 会从父进程中继承,影响创建文件的权限。 PHP 手册上提到:
如果父进程的 umask 没有设定好,那么在执行一些文件操作时,会出现意想不到的效果: ? ~ cat test_umask.php
<?php
chdir('/tmp');
umask(0066);
mkdir('test_umask', 0777);
? ~ php test_umask.php
? ~ ll /tmp | grep umask
drwx--x--x 2 root root 4.0K 8月 22 17:35 test_umask所以,为了保证每一次都能按照预期的权限操作文件,需要置0 umask 值。 重定向0/1/2这里的0/1/2分别指的是 样例首先来看一个样例: <?php
// not_redirect_std_stream_daemon.php
$pid1 = pcntl_fork();
if ($pid1 > 0) {
exit(0);
} else if ($pid1 < 0) {
exit("Failed to fork 1\n");
}
if (-1 == posix_setsid()) {
exit("Failed to setsid\n");
}
$pid2 = pcntl_fork();
if ($pid2 > 0) {
exit(0);
} else if ($pid2 < 0) {
exit("Failed to fork 2\n");
}
umask(0);
declare(ticks = 1);
pcntl_signal(SIGHUP, SIG_IGN);
echo getmypid() . "\n";
while(true) {
echo time() . "\n";
sleep(10);
}上述代码几乎完成了文章最开始部分提及的各个方面,唯一不同的是没有对标准流做处理。通过 在 通过 ? ~ strace -p 6723
Process 6723 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = 0
write(1, "1503417004\n", 11) = 11
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({10, 0}, 0x7fff71a30ec0) = 0
write(1, "1503417014\n", 11) = -1 EIO (Input/output error)
close(2) = 0
close(1) = 0
munmap(0x7f35abf59000, 4096) = 0
close(0) = 0发现发生了 EIO 错误,导致进程退出。 原因很简单,即我们编写的 daemon 程序使用了当时启动时终端提供的标准流,当终端关闭时,标准流变得不可读不可写,一旦尝试读写,会导致进程退出。 在信海龙的博文《一个echo引起的进程崩溃》中也提到过类似的问题。 解决方案APUE 样例APUE 13.3中提到过一条编程规则(第6条):
简单来说:
例程中使用: for (i = 0; i < rl.rlim_max; i++)
close(i);
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);实现了这一个功能。 关闭所有可以打开的文件描述符,包括标准输入输出错误; 打开/dev/null并赋值给变量fd0,因为标准输入已经关闭了,所以/dev/null会绑定到0,即标准输入; 因为最小未分配文件描述符为1,复制文件描述符0到文件描述符1,即标准输出也绑定到/dev/null; 因为最小未分配文件描述符为2,复制文件描述符0到文件描述符2,即标准错误也绑定到/dev/null;复制代码 开源项目实现:WorkermanWorkerman 中的 Worker.php 中的 /**
* Redirect standard input and output.
*
* @throws Exception
*/
public static function resetStd()
{
if (!self::$daemonize) {
return;
}
global $STDOUT, $STDERR;
$handle = fopen(self::$stdoutFile, "a");
if ($handle) {
unset($handle);
@fclose(STDOUT);
@fclose(STDERR);
$STDOUT = fopen(self::$stdoutFile, "a");
$STDERR = fopen(self::$stdoutFile, "a");
} else {
throw new Exception('can not open stdoutFile ' . self::$stdoutFile);
}
}Workerman 中如此实现,结合博文,可能与 PHP 的 GC 机制有关,对于 fd 0 1 2来说,PHP 会维持对这三个资源的引用计数,在直接 fclose 之后,会使得这几个 fd 对应的资源类型的变量引用计数为0,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。 推荐学习:《PHP视频教程》 以上就是什么是daemon?PHP中如何实现daemon?的详细内容,更多请关注模板之家(www.mb5.com.cn)其它相关文章! |
