聊聊守护进程这点事

前言

我们经常使用守护进程,却不是很清楚其原理。本文就来聊下什么是守护进程,如何一步一步使用代码来实现守护进程。

什么是守护进程?

定义一:

在一个多任务的电脑操作系统中,守护进程是一种在后台执行的电脑程序。此类程序会被以进程的形式初始化。守护进程程序的名称通常以字母“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);
}

上述代码只是示例使用,实际使用还需要封装一下。

最后

如有描述不当之处,还望及时指正。感谢!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272

推荐阅读更多精彩内容