什么是daemon?PHP中如何实现daemon?

守护进程(daemon)是一类在后台运行的特殊进程,用于执行特定的系统任务。本篇文章带大家了解一下PHP中实现daemon的方法,介绍一下编程中需要注意的地方。

什么是daemon?PHP中如何实现daemon?

PHP实现守护进程可以通过 pcntlposix 扩展实现。

编程中需要注意的地方有:

  • 通过二次 pcntl_fork() 以及 posix_setsid 让主进程脱离终端
  • 通过 pcntl_signal() 忽略或者处理 SIGHUP 信号
  • 多进程程序需要通过二次 pcntl_fork() 或者 pcntl_signal() 忽略 SIGCHLD 信号防止子进程变成 Zombie 进程
  • 通过 umask() 设定文件权限掩码,防止继承文件权限而来的权限影响功能
  • 将运行进程的 STDIN/STDOUT/STDERR 重定向到 /dev/null 或者其他流上

如果要做的更好,还需要注意:

  • 如果通过 root 启动,运行时更换到低权限用户身份
  • 及时 chdir() 防止操作错误路径
  • 多进程程序考虑定时重启,防止内存泄露

什么是daemon

文章的主角守护进程(daemon),Wikipedia 上的定义是:

UNIX环境高级编程(第二版)(以下使用简称 APUE 指代) 13章有云:

这里注意到,daemon有如下特征:

  • 没有终端
  • 后台运行
  • 父进程 pid 为1

想要查看运行中的守护进程可以通过 ps -ax 或者 ps -ef 查看,其中 -x 表示会列出没有控制终端的进程。

实现关注点

二次 fork 与 setsid

fork 系统调用

fork 系统调用用于复制一个与父进程几乎完全相同的进程,新生成的子进程不同的地方在于与父进程有着不同的 pid 以及有不同的内存空间,根据代码逻辑实现,父子进程可以完成一样的工作,也可以不同。子进程会从父进程中继承比如文件描述符一类的资源。

PHP 中的 pcntl 扩展中实现了 pcntl_fork() 函数,用于在 PHP 中 fork 新的进程。

setsid 系统调用

setsid 系统调用则用于创建一个新的会话并设定进程组 id。

这里有几个概念:会话进程组

在 Linux 中,用户登录产生一个会话(Session),一个会话中包含一个或者多个进程组,一个进程组又包含多个进程。每个进程组有一个组长(Session Leader),它的 pid 就是进程组的组 id。进程组长一旦打开一个终端,这一个终端就被称为控制终端。一旦控制终端发生异常(断开、硬件错误等),会发出信号到进程组组长。

后台运行程序(如 shell 中以&结尾执行指令)在终端关闭之后也会被杀死,就是没有处理好控制终端断开时发出的SIGHUP信号,而SIGHUP信号对于进程的默认行为则是退出进程。

调用 setsid 系统调用之后,会让当前的进程新建一个进程组,如果在当前进程中不打开终端的话,那么这一个进程组就不会存在控制终端,也就不会出现因为关闭终端而杀死进程的问题。

PHP 中的 posix 扩展中实现了 posix_setsid() 函数,用于在 PHP 中设定新的进程组。

孤儿进程

父进程比子进程先退出,子进程就会变成孤儿进程。

init 进程会收养孤儿进程,即孤儿进程的 ppid 变为 1。

二次 fork 的作用

首先,setsid 系统调用不能由进程组组长调用,会返回-1。

二次 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 调用 posix_setsid 要求生成新的进程组,调用成功后当前进程组变为 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 信号处理

一个进程收到 SIGHUP 信号的默认动作是结束进程。

SIGHUP 会在如下情况下发出:

  • 控制终端断开,SIGHUP 发送到进程组组长
  • 进程组组长退出,SIGHUP 会发送到进程组中的前台进程
  • SIGHUP 常被用于通知进程重载配置文件(APUE 中提及,daemon 由于没有控制终端,被认为不可能会收到这一个信号,所以选择复用)

由于实际的工作进程不在前台进程组中,而且进程组的组长已经退出并且没有控制终端,不处理正常情况下当然也没有问题,然而为了防止偶然的收到 SIGHUP 导致进程退出,也为了遵循守护进程程序设计的惯例,还是应当处理这一信号。

Zombie 进程处理

何为 Zombie 进程

简单来说,子进程先于父进程退出,父进程没有调用 wait 系统调用处理,进程变为 Zombie 进程。

子进程先于父进程退出时,会向父进程发送 SIGCHLD 信号,如果父进程没有处理,子进程也会变为 Zombie 进程。

Zombie 进程会占用可 fork 的进程数,Zombie 进程过多会导致无法 fork 新的进程。

此外,Linux 系统中 ppid 为 init 进程的进程,变为 Zombie 后会由 init 进程回收管理。

Zombie 进程的处理

从 Zombie 进程的特点,对于多进程的daemon,可以通过两个途径解决这一问题:

  • 父进程处理 SIGCHLD 信号
  • 让子进程被 init 接管

父进程处理信号无需多说,注册信号处理回调函数,调用回收方法即可。

对于让子进程被 init 接管,则可以通过2次 fork 的方法,让第一次 fork 出的子进程 a 再 fork 出实际的工作进程 b,让 a 先行退出,使得 b 成为孤儿进程,这样就能被 init 进程托管了。

umask

umask 会从父进程中继承,影响创建文件的权限。

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分别指的是 STDIN/STDOUT/STDERR,即标准输入/输出/错误三个流。

样例

首先来看一个样例:

<?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);
}

登录后复制

上述代码几乎完成了文章最开始部分提及的各个方面,唯一不同的是没有对标准流做处理。通过 php not_redirect_std_stream_daemon.php 指令也能让程序在后台进行。

sleep 的间隙,关闭终端,会发现进程退出。

通过 strace 观察系统调用的情况:

➜  ~ 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条):

简单来说:

  • daemon 不应使用标准流
  • 0/1/2 要设定成 /dev/null

例程中使用:

for (i = 0; i < rl.rlim_max; i++)
	close(i);

fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);

登录后复制

实现了这一个功能。dup() (参考手册)系统调用会复制输入参数中的文件描述符,并复制到最小的未分配文件描述符上。所以上述例程可以理解为:

关闭所有可以打开的文件描述符,包括标准输入输出错误;
打开/dev/null并赋值给变量fd0,因为标准输入已经关闭了,所以/dev/null会绑定到0,即标准输入;
因为最小未分配文件描述符为1,复制文件描述符0到文件描述符1,即标准输出也绑定到/dev/null;
因为最小未分配文件描述符为2,复制文件描述符0到文件描述符2,即标准错误也绑定到/dev/null;复制代码

登录后复制

开源项目实现:Workerman

Workerman 中的 Worker.php 中的 resetStd() 方法实现了类似的操作。

/**
* 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,导致触发回收。所需要做的就是将这些变量变为全局变量,保证引用的存在。

关于什么是daemon?PHP中如何实现daemon?的文章就分享到这,如果对你有帮助欢迎继续关注我们哦

本文来自投稿,不代表重蔚自留地立场,如若转载,请注明出处https://www.cwhello.com/244247.html

如有侵犯您的合法权益请发邮件951076433@qq.com联系删除

(0)
php学习php学习订阅用户
上一篇 2023年3月29日
下一篇 2023年3月29日

相关推荐

  • PHP中的测试数据管理工具。

    PHP是一种广泛应用于Web开发的脚本语言,由于其易于学习和扩展性,已经成为大多数Web开发人员的首选语言。随着Web应用程序越来越复杂,测试也变得更加重要。为了成功地测试PHP代码,测试数据是必不可少的。在本文中…

    2023年5月28日
    00
  • 经验分享php url传递参数。

    PHP中通过URL传递参数,通常使用GET或POST方法。GET方法将参数附加到URL末尾,而POST方法将参数放在请求体中。 在Web开发中,URL传递参数是一种常见的数据交互方式,通过URL传递参数,我们可以在不同的页面之间共享…

    2024年7月15日
    00
  • PHP中的物联网。

    近年来随着物联网技术的不断发展,越来越多的应用场景涌现出来,许多企业也加速了其在物联网领域的布局。而PHP作为一种流行的编程语言,同样也在不断地发展和拓展。本文将讨论PHP在物联网领域的运用。物联网是指通…

    2023年5月28日
    017
  • PHP8.0中的类型约束处理库:Typehint

    PHP是一门非常受欢迎的编程语言,被广泛用于Web开发领域。随着版本的不断更新,PHP的功能也日益丰富。在2020年11月,PHP8.0版本正式发布,其中一个重要的新特性就是类型约束处理库——Typehint。Typehint是PHP8.0中新…

    2023年5月18日
    03
  • php+redis实现对200w用户的即时推送服务

    怎么实现对200w用户的即时推送,这个推送可以理解为调用第三方的接口,push,sms之类的东西。当时先写了一个demo 直接读取DB然后单个推送,结果。。。。可想而知于是设计一套基于redis+php多进程的方案,用着还不…

    2022年6月21日
    0122
  • PHP涉及的英语单词!

    PHP:PHP is HyperText Proprocessor hosts:hosts文件 DNS:Domain Name Server域名服务器 load:装载 Module:模块 LoadModule:装载模块 File:文件 Match:匹配 FilesMatch:文件匹配 AddType:添加类型 applic…

    2018年4月28日
    0624
  • PHP商城开发中的页面性能优化

    随着电商市场的逐步扩大,越来越多的企业选择建立自己的电商平台来开展业务。而在建立电商平台的过程中,开发人员需要考虑的不仅是平台的功能,还需要考虑页面的性能优化问题,以提高用户体验和网站的排名。本文将…

    2023年5月19日
    01
  • 说说编写php用什么软件,PHP编写软件。

    PHP是一种广泛使用的开源通用脚本语言,尤其适用于Web开发并可嵌入HTML中使用,编写PHP代码通常需要使用什么软件呢? 1. 使用文本编辑器 你可以使用任何文本编辑器来编写PHP代码,一些常见的选择包括Sublime Text,…

    2024年7月4日
    00

联系我们

QQ:951076433

在线咨询:点击这里给我发消息邮件:951076433@qq.com工作时间:周一至周五,9:30-18:30,节假日休息