美文网首页
聊聊守护进程这点事

聊聊守护进程这点事

作者: 稀饭不加糖C | 来源:发表于2020-02-11 08:21 被阅读0次

    前言

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

    什么是守护进程?

    定义一:

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

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

    最后

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

    相关文章

      网友评论

          本文标题:聊聊守护进程这点事

          本文链接:https://www.haomeiwen.com/subject/lwytfhtx.html