前言

在一个多任务的计算机操作系统中,守护进程(英语:daemon,/ˈdiːmən//ˈdeɪmən/)是一种在后台执行的计算机程序。此类程序会被以进程的形式初始化。守护进程程序的名称通常以字母“d”结尾:例如,syslogd就是指管理系统日志的守护进程。

daemon 程序是一直运行的服务端程序,又称为守护进程。通常在系统后台运行,没有控制终端不与前台交互,daemon 程序一般作为系统服务使用。daemon  是长时间运行的进程,通常在系统启动后就运行,在系统关闭时才结束。一般说Daemon程序在后台运行,是因为它没有控制终端,无法和前台的用户交互。daemon程序一般都作为服务程序使用,等待客户端程序与它通信。我们也把运行的daemon程序称作守护进程。

通常,守护进程没有任何存在的父进程(即PPID=1),且在UNIX系统进程层级中直接位于init之下。守护进程程序通常通过如下方法使自己成为守护进程:对一个子进程运行fork,然后使其父进程立即终止,使得这个子进程能在init下运行。这种方法通常被称为“脱壳”。

系统通常在启动时一同起动守护进程。守护进程为对网络请求,硬件活动等进行响应,或其他通过某些任务对其他应用程序的请求进行回应提供支持。守护进程也能够对硬件进行配置(如在某些Linux系统上的devfsd),运行计划任务(例如cron),以及运行其他任务。每个进程都有一个父进程,子进程退出,父进程能得到子进程退出的状态。

守护进程简单地说就是可以脱离终端而在后台运行的进程 . 这在Linux中是非常常见的一种进程 , 比如apache或者mysql等服务启动后 , 就会以守护进程的方式进驻在内存中 。守护程序是在后台运行的应用程序,而不是由用户直接操作。守护进程的例子是Cron和MySQL。 使用PHP守护进程非常简单,并且需要使用PHP 4.1或更高版本编译参数:--enable-pcntl

假如有个耗时间的任务需要跑在后台 : 将所有mysql中user表中的2000万用户全部导入到redis中做预热缓存 , 那么这个任务估计一时半会是不会结束的 , 这个时候就需要编写一个php脚本以daemon形式运行在系统中 , 结束后自动推出。

在Linux中 , 有三种方式实现脚本后台化 :

1 . 在命令后添加一个&符号

比如 php task.php & . 这个方法的缺点在于 如果terminal终端关闭 , 无论是正常关闭还是非正常关闭 , 这个php进程都会随着终端关闭而关闭 , 其次是代码中如果有echo或者print_r之类的输出文本 , 会被输出到当前的终端窗口中 。

2 . 使用nohup命令

比如 nohup php task.php & . 默认情况下 , 代码中echo或者print_r之类输出的文本会被输出到php代码同级目录的nohup.out文件中 . 如果你用exit命令或者关闭按钮等正常手段关闭终端 , 该进程不会被关闭 , 依然会在后台持续运行 . 但是如果终端遇到异常退出或者终止 , 该php进程也会随即退出 . 本质上 , 也并非稳定可靠的daemon方案 。

3 . 通过 pcntl 与 posix 扩展实现

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

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

daemon有如下特征:

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

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

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 中设定新的进程组。

二次 fork 的作用

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

二次 fork 操作的样例代码如下:

$pid1 = pcntl_fork();

if ($pid1 > 0) {
// 父进程会得到子进程号,所以这里是父进程执行的逻辑
exit('parent process. 1'."\n");
} 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('parent process. 2'."\n");
} else if ($pid2 < 0) {
exit("Failed to fork 2\n");
}

pcntl_fork() 函数创建一个子进程,这个子进程仅PID(进程号) 和PPID(父进程号)与其父进程不同。

返回值

  成功时,在父进程执行线程内返回产生的子进程的PID,在子进程执行线程内返回 0,失败时,在 父进程上下文返回 -1,不会创建子进程,并且会引发一个PHP错误。

假定我们在终端中执行应用程序,进程为 a,第一次 fork 会生成子进程 b,如果 fork 成功,父进程 a 退出。b 作为孤儿进程,被 init 进程托管。

此时,进程 b 处于进程组 a 中,进程 b 调用 posix_setsid 要求生成新的进程组,调用成功后当前进程组变为 b。

php fork2.php
parent process. 1
parent process. 2

此时进程 b 事实上已经脱离任何的控制终端,例程:

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

执行程序之后:  

$ php cli-title.php
$ ps ax | grep -v grep | grep -E 'process_|PID'
PID TTY STAT TIME COMMAND
15725 ? Ss 0:00 process_b

重新打开一个shell窗口,效果一样,都在呢

从 ps 的结果来看,process_b 的 TTY 已经变成了 ,即没有对应的控制终端。

代码走到这里,似乎已经完成了功能,关闭终端之后 process_b 也没有被杀死,但是为什么还要进行第二次 fork 操作呢?

StackOverflow 上的一个回答写的很好:

The second fork(2) is there to ensure that the new process is not a session leader, so it won’t be able to (accidentally) allocate a controlling terminal, since daemons are not supposed to ever have a controlling terminal.

这是为了防止实际的工作的进程主动关联或者意外关联控制终端,再次 fork 之后生成的新进程由于不是进程组组长,是不能申请关联控制终端的。

综上,二次 fork 与 setsid 的作用是生成新的进程组,防止工作进程关联控制终端。 

写一个demo测试下

<?php
// 第一次fork系统调用
$pid_A = pcntl_fork(); // 父进程 和 子进程 都会执行下面代码
if ($pid_A < 0) {
// 错误处理: 创建子进程失败时返回-1.
exit('A fork error ');
} else if ($pid_A > 0) {
// 父进程会得到子进程号,所以这里是父进程执行的逻辑
exit("A parent process exit \n");
} // B 作为孤儿进程,被 init 进程托管,此时,进程 B 处于进程组 A 中 // 子进程得到的$pid为0, 所以以下是子进程执行的逻辑,受控制终端的影响,控制终端关闭则这里也会退出 // [子进程] 控制终端未关闭前,将当前子进程提升会会话组组长,及进程组的leader
// 进程 B 调用 posix_setsid 要求生成新的进程组,调用成功后当前进程组变为 B
if (-1 == posix_setsid()) {
exit("Failed to setsid\n");
} // 此时进程 B 已经脱离任何的控制终端 // [子进程] 这时候在【进程组B】中,重新fork系统调用(二次fork)
$pid_B = pcntl_fork();
if ($pid_B < 0) {
exit('B fork error ');
} else if ($pid_B > 0) {
exit("B parent process exit \n");
} // [新子进程] 这里是新生成的进程组,不受控制终端的影响,写写自己的业务逻辑代码
for ($i = 1; $i <= 100; $i++) {
sleep(1);
file_put_contents('daemon.log',$i . "--" . date("Y-m-d H:i:s", time()) . "\n",FILE_APPEND);
}

Window 下跑回直接抛出异常

php runtime\daemon.php
PHP Fatal error: Uncaught Error: Call to undefined function pcntl_fork() in D:\phpStudy\PHPTutorial\WWW\notes\runtime\daemon.php:13
Stack trace:
#0 {main}
thrown in D:\phpStudy\PHPTutorial\WWW\notes\runtime\daemon.php on line 13

Linux 下执行,输出结果

php daemon.php
...
97--2018-09-07 03:50:09
98--2018-09-07 03:50:10
99--2018-09-07 03:50:11
100--2018-09-07 03:50:12

所以,现在即使关闭了终端,改脚本任然在后台守护进程运行

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对脚本之家的支持。

参考:

1、https://liaoaoyang.cn/articles/2017/08/22/php-daemon/

2、https://blog.ti-node.com/blog/6343348095782223873

PHP7 网络编程(二)daemon守护进程的更多相关文章

  1. Linux网络编程学习(四) -----守护进程的建立(第三章)

    本文介绍一个例程daemon_init() #include <sys/types.h> #include <signal.h> #include <unistd.h&g ...

  2. Linux网络编程(二)

    Linux网络编程(二) 使用多进程实现服务器并发访问. 采用多进程的方式实现服务器的并发访问的经典范例. 程序实现功能: 1.客户端从标准输入读入一行文字,发送到服务器. 2.服务器接收到客户端发来 ...

  3. python并发编程基础之守护进程、队列、锁

    并发编程2 1.守护进程 什么是守护进程? 表示进程A守护进程B,当被守护进程B结束后,进程A也就结束. from multiprocessing import Process import time ...

  4. nohup命令、setsid命令、Daemon(守护进程)简要梳理

    nohup命令 当用户注销(logout)或者网络断开时,终端会收到 HUP(hangup)信号从而关闭其所有子进程.因此,我们的解决办法就有两种途径:要么让进程忽略 HUP 信号,要么让进程运行在新 ...

  5. Python 3 并发编程多进程之守护进程

    Python 3 并发编程多进程之守护进程 主进程创建守护进程 其一:守护进程会在主进程代码执行结束后就终止 其二:守护进程内无法再开启子进程,否则抛出异常:AssertionError: daemo ...

  6. python网络编程基础(线程与进程、并行与并发、同步与异步、阻塞与非阻塞、CPU密集型与IO密集型)

    python网络编程基础(线程与进程.并行与并发.同步与异步.阻塞与非阻塞.CPU密集型与IO密集型) 目录 线程与进程 并行与并发 同步与异步 阻塞与非阻塞 CPU密集型与IO密集型 线程与进程 进 ...

  7. Python学习笔记整理总结【网络编程】【线程/进程/协程/IO多路模型/select/poll/epoll/selector】

    一.socket(单链接) 1.socket:应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口.在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socke ...

  8. Python网络编程之线程,进程

    一. 线程: 基本使用 线程锁 线程池 队列(生产者消费者模型) 二. 进程:  基本使用  进程锁 进程池 进程数据共享 三. 协程: gevent greenlet 四. 缓存: memcache ...

  9. C#网络编程二:Socket编程

    一:什么是SOCKET socket的英文原义是"孔"或"插座".作为进程通信机制,取后一种意思.通常也称作"套接字",用于描述IP地址和端 ...

随机推荐

  1. 信用算力基于 RocketMQ 实现金融级数据服务的实践

    微服务架构已成为了互联网的热门话题之一,而这也是互联网技术发展的必然阶段.然而,微服务概念的提出者 Martin Fowler 却强调:分布式调用的第一原则就是不要分布式. 纵观微服务实施过程中的弊端 ...

  2. js 时间类函数

    js 时间类是  Date() var currtime = new Date();// 实例一个时间,是当前时间 接收一个时间戳为参数 var time2=new Date(currtime.get ...

  3. Jmeter工具之上传图片,上传音频文件接口

    https://www.jianshu.com/p/f23f7fe20bf3 互联网时代的来临,不同手机上安装的APP,还是PC端的应用软件或多或多都会涉及到图片的上传,那么在Jmeter工具如何模拟 ...

  4. mysql5.6更改datadir数据存储目录

    环境需求: 有些数据存储场景可能需要将数据放到指定的挂载路径或目录,mysql默认存放数据路径在:/var/lib/mysql下. 测试环境: 操作步骤: 1. 查看当前存储目录 [root@mysq ...

  5. BZOJ3932: [CQOI2015]任务查询系统 主席树

    3932: [CQOI2015]任务查询系统 Time Limit: 20 Sec  Memory Limit: 512 MBSubmit: 4869  Solved: 1652[Submit][St ...

  6. 微信小程序支付(PHP后端)

    1.申请开通小程序支付,我们正式开通的微信支付是在微信公众平台上,我们需要绑定之前的微信商户平台即可,这一点不过多强调 2.小程序支付开发步骤 (1).统一下单 大家看到微信的统一下单接口密密麻麻的一 ...

  7. Android 新架构组件 -- WorkManager

    Android WorkManager compile "android.arch.work:work-runtime:1.0.0-alpha02" 继承Worker类并实现doW ...

  8. JavaScript FormData的详细介绍及使用

    本文转自:https://blog.csdn.net/liupeifeng3514/article/details/78988001 FormData的详细介绍及使用请点击此处,那里对FormData ...

  9. 利用captcha库绘制验证码

    #导包 from captcha.image import ImageCaptcha from PIL import Image import random import time import os ...

  10. laravel基于redis实现的一个简单的秒杀系统

    说明:网上很多redis秒杀系统的文章,看的都是一头雾水,然后自己来实现一个,也方便以后自己学习 实现的方式是用的redis的list队列,框架为laravel 核心部分为list的pop操作,此操作 ...