聊聊守护进程这点事
前言
我们经常使用守护进程,却不是很清楚其原理。本文就来聊下什么是守护进程,如何一步一步使用代码来实现守护进程。
什么是守护进程?
定义一:
在一个多任务的电脑操作系统中,守护进程是一种在后台执行的电脑程序。此类程序会被以进程的形式初始化。守护进程程序的名称通常以字母“d”结尾:例如,syslogd就是指管理系统日志的守护进程。
通常,守护进程没有任何存在的父进程(即PPID=1),且在UNIX系统进程层级中直接位于init之下。守护进程程序通常通过如下方法使自己成为守护进程:对一个子进程运行fork,然后使其父进程立即终止,使得这个子进程能在init下运行。这种方法通常被称为“脱壳”。
定义二:
守护进程也成精灵进程( daemon )是生存周期较长的一种进程。它们常常在系统自举时启动,仅在系统关闭时才终止。因为他们没有控制终端,所以说他们是在后台运行的。
守护进程特征:
- 没有终端
- 后台运行
- 父进程PID为0
想要查看运行中的守护进程可以通过 ps -ax 或者 ps -ef 查看,其中 -x 表示会列出没有控制终端的进程。
进程组
是一个或多个进程的集合,进程组有进程组ID来唯一标识。除了进程号(PID)之外,进程组ID也是一个进程的必备属性。每个进程组都有一个组长进程,其组长进程的进程号等于进程组ID,且该进程组ID不会因组长进程的退出而受到影响。
会话周期
会话周期是一个或多个进程组的集合。通常一个会话开始于用户登录,终止于用户退出,在此期间该用户运行的所有进程都属于这个会话期。
如何创建一个守护进程?
1.成为后台进程
fork子进程且父进程退出,控制终端将子进程放入后台执行,方法是在进程中调用fork(),然后父进程终止,所有后续工作在子进程中进行。
用fork创建子进程,父进程退出,子进程成为孤儿进程被init接管,子进程变为后台进程。
2.在子进程中创建新会话
先介绍一下Linux中的 进程 和 控制终端,登陆会话 和 进程 组之间的关系。进程属于一个进程组,进程组号(GID)就是进程组长的进程号(PID)。登陆会话可以包含多个进程组,这些进程组共享一个控制终端,这个控制终端通常是创建进程的登陆终端。控制终端、登陆会话和进程组通常是从父进程继承下来的,我们的目的就是要让子进程脱离它们的控制。方法是在子进程中调用posix_setsid()使之成为会话组长。setsid的作用就是让进程摆脱原会话和原进程组的控制。
Linux内核通过维护会话和进程组来管理多用户进程。每个进程是一个进程组的成员,而每个进程组又是某个会话的成员。一般而言,当用户在某个终端上登录时,一个新的会话就开始了。进程组由组中的领头进程标识,领头进程的进程标识符就是进程组的组标识符。类似的,每个会话也对应有一个领头进程。同一会话中的进程通过该会话的领头进程和一个终端相连,该终端作为这个会话的控制终端。一个会话只能有一个控制终端,而一个控制终端只能控制一个会话。用户通过控制终端,可以向该控制终端所控制的会话中的进程发送键盘信号。同一会话中只能有一个前台进程组,属于前台进程组的进程可从控制终端获得输入,而其他进程均是后台进程,可能分属于不同的后台进程组。
3. 改变当前目录为根目录
进程活动时,其工作目录所在的文件系统不能卸载,一般需要将工作目录改变到根目录。对于需要写运行日志的进程将工作目录改变到特定目录如chdir('/')
,如有需要,也可以把当前工作目录换成其他路径。
4. 重设文件权限掩码
进程从父进程那里继承了文件创建掩模,它可能修改守护进程所创建的文件的存取位。为防止这一点,通过 umask(0)
可以将文件掩模清除,如果应用程序根本就不涉及创建新文件或是文件访问权限的限定,这一步不是必须的。
5. 关闭文件描述符
同文件权限掩码一样,新进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不被我们的Daemon进程读或写,但它们一样消耗系统资源,而且可能导致所在的文件系统无法卸载。文件描述符为0、1、2的三个文件(分别代表标准输入、标准输出、标准错误),也需要被关闭,在PHP中只需要 fclose()
就可以了。
守护进程示例
<?php
use Exception;
/**
* 守护进程基类
* @package Wanglelecc\Process
*
* @Author wll
* @Time 2020-01-31 15:15
*/
class Daemon
{
/**
* @var string
*/
private $stdin = '/dev/null';
/**
* @var string
*/
private $stdout = '/tmp/console.log';
/**
* @var string
*/
private $stderr = '/tmp/console.error.log';
/**
* 守护进程
*
* @throws SystemException
*
* @author wll <wanglelecc@gmail.com>
* @date 2020-01-31 16:25
*/
public function daemonize(): void
{
global $stdin, $stdout, $stderr;
// 创建一个子进程
$pid = pcntl_fork();
if ($pid == -1) {
throw new Exception("进程创建失败", 1);
} elseif ($pid > 0) {
//父进程退出,子进程被1号进程收养
exit(0);
}
//创建一个新的会话,脱离终端控制,更改子进程为组长进程
$sid = posix_setsid();
if ($sid == -1) {
throw new Exception('进程创建新会话失败');
}
//修改进程的工作目录,由于子进程会继承父进程的工作目录,修改工作目录释放对父进程工作目录的占用
chdir('/');
//重设文件掩码
umask(0);
/**
* 通过上一步,我们创建了一个新的会话组长,进程组长,且脱离了终端,但是会话组长可以申请重新打开一个终端,为了避免
* 这种情况,我们再次创建一个子进程,并退出当前进程,这样运行的进程就不再是会话组长。
*/
$pid = pcntl_fork();
if ($pid == -1) {
throw new Exception("进程创建失败", 1);
} elseif ($pid > 0) {
//再一次退出父进程,子进程成为最终的守护进程
exit(0);
}
//关闭守护进程不是用的标准输入、输出、错误数据的描述符
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
/**
* 如果关闭了标准输入/输出/错误描述符
* 那么打开的前三个文件描述符将成为新的标准输入/输出/错误的文件描述符
* 使用的$stdin,$stdout,$stderr就是普通的变量
* 必须指定为全局变量,否则文件描述符将在函数执行完毕后被释放
*/
$stdin = fopen($this->stdin, 'r');
$stdout = fopen($this->stdout, 'a+');
$stderr = fopen($this->stderr, 'a+');
}
}
// 使用
$daemon = new Daemon();
$daemon->daemonize();
while(true){
// 处理业务
echo "test...".PHP_EOL;
sleep(1);
}
上述代码只是示例使用,实际使用还需要封装一下。
最后
如有描述不当之处,还望及时指正。感谢!