正文
有时程序需要在命令行退出后继续执行,这就是守护进程模式,如队列消费者,当你在命令行启动消费者进程,退出命令行后,消费者依旧从队列中消费队列数据。
守护进程是一个在后台运行并且不受任何终端控制的进程。Unix操作系统有很多典型的守护进程(其数目根据需要或20—50不等), 它们在后台运行,执行不同的管理任务。用户使守护进程独立于所有终端是因为,在守护进程从一个终端启动的情况下, 这同一个终端可能被其他的用户使用。例如,用户从一个终端启动守护进程后退出,然后另外一个人也登录到这个终端。 用户不希望后者在使用该终端的过程中,接收到守护进程的任何错误信息。 同样,由终端键入的任何信号(例如中断信号)也不应该影响先前在该终端启动的任何守护进程的运行。 虽然让服务器后台运行很容易(只要shell命令行以&结尾即可),但用户还应该做些工作,让程序本身能够自动进入后台,且不依赖于任何终端。 守护进程没有控制终端,因此当某些情况发生时,不管是一般的报告性信息,还是需由管理员处理的紧急信息,都需要以某种方式输出。
三个概念
1.进程组。
一些相关的进程可以组成一个进程组,每个进程组都会有一个组ID(正整数),每个进程组都会有一个组长进程, 组长进程的ID等于进程组ID。组长进程可以创建新的进程组以及该进程组中的其他进程。 一个进程组是有生命周期的, 即便是组长进程挂了,只有组里还有其他的活口,那就算该进程组依然存活,只有到组里最后一个进程也挂了,那该进程组真的才是彻底没了。
2.会话。
一些相关的进程组组成了一个会话。在*NIX下,是通过setsid()创建一个新的会话。但是值得注意的是,组长进程不能创建会话, 简单理解就是在组长进程中,执行setsid函数会报错,这点很重要。所以一般都是组长进程执行fork创建子进程,然后主进程退出, 因为子进程的进程ID是新分配的,而子进程的进程组ID是继承父进程的,所以子进程就注定不可能是组长进程, 从而可以确保子进程中一定可以执行setsid函数。在执行setsid函数时候,一般会发生下面三个比较重要的事情:
该进程会创建一个新的进程组,该进程为进程组组长(或者你可以认为这是一种提升);
该进程会创建一个会话组并成为该会话的会话首进程(会话首进程就是创建该会话的进程);
该进程会失去控制终端。如果该进程本来就没有控制终端,则罢了;如果有,那么该进程也将脱离该控制终端,与之失去联系。
3.控制终端。
每个会话可能会拥有一个控制终端(看着比较玄学,你可以暂时理解为就一个那种黑乎乎的命令行窗口), 建立与控制终端连接的会话首进程叫做控制进程。
创建步骤
1.创建子进程,终止父进程
由于守护进程是脱离控制终端的,因此首先创建子进程,终止父进程,使得程序在shell终端里造成一个已经运行完毕的假象。 之后所有的工作都在子进程中完成,而用户在shell终端里则可以执行其他的命令,从而使得程序以僵尸进程形式运行,在形式上做到了与控制终端的脱离。
2.在子进程中创建新会话
这个步骤是创建守护进程中最重要的一步,在这里使用的是系统函数setsid。setsid函数用于创建一个新的会话,并担任该会话组的组长。 调用setsid有三个作用:让进程摆脱原会话的控制、让进程摆脱原进程组的控制、让进程摆脱原控制终端的控制。 在调用fork函数时,子进程全盘拷贝父进程的会话期(session,是一个或多个进程组的集合)、进程组、控制终端等, 虽然父进程退出了,但原先的会话期、进程组、控制终端等并没有改变,因此,那还不是真正意义上使两者独立开来。 setsid函数能够使进程完全独立出来,从而脱离所有其他进程的控制。
3.改变工作目录
使用fork创建的子进程也继承了父进程的当前工作目录。由于在进程运行过程中,当前目录所在的文件系统不能卸载, 因此,把当前工作目录换成其他的路径,如“/”或“/tmp”等。改变工作目录的常见函数是chdir()。
4.重设文件创建掩码
文件创建掩码是指屏蔽掉文件创建时的对应位。由于使用fork函数新建的子进程继承了父进程的文件创建掩码,这就给该子进程使用文件带来了诸多的麻烦。 因此,把文件创建掩码设置为0,可以大大增强该守护进程的灵活性。设置文件创建掩码的函数是umask,通常的使用方法为umask(0)。
为什么需要 umask(0) ?
当你在 linux 调用 umask 的时候你会看到一个掩码值,这个掩码决定了你创建文件权限范围,例如本人当前机器的 umask 为0022。 文件的最大权限是 0666,而目录的最大权限是 0777, 那么当前用户的创建的目录权限就是 0755,对于当前用户而言就是 rwx-rx-rx 权限。 而文件则是 0644,对于当前用户而言 rw-r-r 权限。所以如果没有重置掩码的话,那么对于目录而言就是 0755,而文件则是 0644 了。
5.关闭文件描述符
用fork新建的子进程会从父进程那里继承一些已经打开了的文件。这些被打开的文件可能永远不会被守护进程读或写, 但它们一样消耗系统资源,可能导致所在的文件系统无法卸载。
fclose($fatherFile); 关闭文件描述符。
fclose(STDIN);fclose(STDOUT);fclose(STDERR); 关闭标准输入输出与错误显示。
6.处理SIGCHLD信号
处理SIGCHLD信号并不是必须的。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。 如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。 如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。
代码实现:
<?php
$fatherFile = fopen('test.txt');
// 创建子进程,终止父进程
$pid = pcntl_fork();
if ($pid < 0) {
exit('fork error.');
} else if ($pid > 0) {
exit(0);
}
// 在子进程中创建新会话
if (posix_setsid() < 0) {
exit('setsid error.');
}
// 改变工作目录
chdir('/tmp');
// 重设文件创建掩码
umask(0);
// 关闭文件描述符
fclose($fatherFile);
// 关闭标准输入输出
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
细节:
// 获取进程ID
var_dump(posix_getpid());
// 获取进程组ID
var_dump(posix_getpgid(posix_getpid()));
// 获取进程会话ID
var_dump(posix_getsid(posix_getpid()));
三者结果相同,说明了该进程是进程组的组长,也是会话首领。
旧事重提
过去写过一些相关内容,可以参照阅读:
PHP 多进程编程 https://ibaiyang.github.io/blog/php/2019/10/10/PHP-多进程编程.html
PHP posix_setsid()守护进程 https://ibaiyang.github.io/blog/php/2019/04/19/PHP-posix_setsid()守护进程.html
PHP RabbitMQ相关 https://ibaiyang.github.io/blog/php/2019/04/18/PHP-RabbitMQ相关.html
PHP RabbitMQ延迟队列实现 https://ibaiyang.github.io/blog/php/2019/04/19/PHP-RabbitMQ延迟队列实现.html
用PHP写的守护进程存在一个问题:进程异常中止后,不知道状态也无法重启。
如果能有一个监护进程的管理程序就好了:可以实时知道守护进程的运行状态,守护进程异常中止后,可以重启守护进程。 现在有一个python写的程序Supervisor实现了这个功能。
Supervisor是用Python开发的一套通用的进程管理程序,能将一个普通的命令行进程变为后台daemon,并监控进程状态,异常退出时能自动重启。 它是通过fork/exec的方式把这些被管理的进程当作supervisor的子进程来启动,这样只要在supervisor的配置文件中, 把要管理的进程的可执行文件的路径写进去即可。也实现当子进程挂掉的时候,父进程可以准确获取子进程挂掉的信息的, 可以选择是否自己启动和报警。supervisor还提供了一个功能,可以为supervisord或者每个子进程,设置一个非root的user, 这个user就可以管理它对应的进程。
拓展阅读
有一些PHP应用总结的比较好的内容, 可以在这里读一些: https://github.com/elarity/advanced-php 。
参考资料
PHP 实现守护进程 https://www.jianshu.com/p/161d9981112a
PHP高级编程之守护进程 http://netkiller.github.io/journal/php.daemon.html http://www.netkiller.cn/journal/php.daemon.html
PHP 实现守护进程 https://www.php.cn/php-weizijiaocheng-428099.html
php写守护进程(Daemon) https://www.cnblogs.com/onephp/p/9203029.html
Supervisor使用详解 https://www.jianshu.com/p/0b9054b33db3
Linux 信号说明列表 https://blog.csdn.net/tengzhaorong/article/details/9744869