PHP实现系统编程(五)--- 编写守护进程详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhang197093/article/details/79170881

(一)进程组、会话、控制终端、控制进程等概念

进程组:每个进程都有一个所属的进程组 (process group),进程组有一个进程组长(process group leader),进程组ID即为这个进程组长的进程号,所以判断一个进程是否为进程组组长,只需比较该进称号是否和它的进程组id相等即可,PHP中可以用函数 posix_getpgrp() 获取当前进程的进程组id,用 posix_getpid() 获取当前进程的进程号。

<?php

function isGroupLeader() {
    return posix_getpgrp() == posix_getpid();
}

$pid = pcntl_fork();

if($pid == 0) {
    echo '子进程:' . PHP_EOL;
}
elseif($pid > 0) {
    sleep(2);
    echo '父进程:' . PHP_EOL;
}

echo "当前进程组gid:" . posix_getpgrp() . PHP_EOL;
echo "当前进程号pid:" . posix_getpid() . PHP_EOL;

if (isGroupLeader()) {
    echo 'Is a process group leader' . PHP_EOL;
}
else {
    echo 'Is not a process group leader' . PHP_EOL;
}
以上例程会输出:

子进程:
当前进程组gid:15827
当前进程号pid:15828
Is not a process group leader
父进程:
当前进程组gid:15827
当前进程号pid:15827
Is a process group leader
会话:会话(session)是若干进程组的集合,会话中的一个进程组为会话组长(session leader),会话ID即为这个会话组长的进程组id,PHP中可以使用函数 posix_getsid(int $pid)  来获取指定进程的会话id,也可以使用函数 posix_setsid() 来创建一个新的会话,此时该进程成为新会话的会话组长,该函数调用成功返回新创建的会话ID,或者在失败出错时返回-1, 注意linux中调用 posix_setsid() 函数的进程不能是进程组长,否则会调用失败,这是由于一个进程组中的进程不能同时跨多个会话

linux 中关于setsid的文档介绍:

setsid()  creates  a  new  session  if  the calling process is not a process group leader.  The calling process is the leader of the new session, the process group
       leader of the new process group, and has no controlling tty.  The process group ID and session ID of the calling process are set to the PID of the calling process.
       The calling process will be the only process in this new process group and in this new session.
<?php

function isGroupLeader() {
    return posix_getpgrp() == posix_getpid();
}

echo "当前会话id: " . posix_getsid(0) . PHP_EOL; //传0表示获取当前进程的会话id

if (isGroupLeader()) {
    echo "当前进程是进程组长\n";
}

$ret = posix_setsid();  //创建一个新会话
var_dump($ret);  //由于当前进程是进程组长,此处会返回-1, 表示调用失败
以上例程会输出:

当前会话id: 13000
当前进程是进程组长
int(-1)
那该如何新建会话呢,我们注意到前面使用pcntl_fork() 创建了一个子进程后,这个子进程就不是进程组长,所以可以利用子进程来创建新会话。

扫描二维码关注公众号,回复: 3870465 查看本文章

<?php
function isGroupLeader()
{
     return posix_getpgrp() == posix_getpid();
}
echo "当前进程所属会话ID:" . posix_getsid(0) . PHP_EOL;

$pid = pcntl_fork();

if ($pid > 0) {
    exit(0); // 让父进程退出
}
elseif ($pid == 0) {
    if (isGroupLeader()) {
        echo "是进程组组长\n";
    } else {
        echo "不是进程组组长\n";
    }
    echo "进程组ID:" . posix_getpgrp() . PHP_EOL;  
    echo "进程号pid: " . posix_getpid() . PHP_EOL;

    $ret = posix_setsid();
    var_dump($ret);

    echo "当前进程所属会话ID:" . posix_getsid(0) . PHP_EOL;
}
以上例程会输出:

当前进程所属会话ID:13000
[root@localhost php]# 不是进程组组长
进程组ID:15856
进程号pid: 15857
int(15857)
当前进程所属会话ID:15857
利用子进程成功创建了新的会话。

控制终端控制进程:(终端是所有输入输出设备的总称,比如键盘,鼠标,显示器都是一个终端)一个会话可以有一个控制终端,一个控制终端被一个会话独占。会话刚创建的时候是没有控制终端的,但会话组长可以申请打开一个终端,如果这个终端不是其他会话的控制终端,这时的终端将会成为会话的控制终端,会话组长叫做控制进程。

linux下判断一个会话是否拥有控制终端,我们可以尝试打开一个特殊的文件 /dev/tty , 他指向了真实的控制终端,如果打开成功说明拥有控制终端,反之则没有控制终端。

<?php
function isGroupLeader()
{
     return posix_getpgrp() == posix_getpid();
}

$pid = pcntl_fork();

if ($pid > 0) {
        sleep(1);
        $fp = fopen("/dev/tty", "rb");
        if ($fp) {
            echo "父进程会话 " . posix_getsid(0) . " 拥有控制终端\n";
        } else {
            echo "父进程会话 " . posix_getsid(0) . " 不拥有控制终端\n";
        }

    exit(0); // 让父进程退出
}
elseif ($pid == 0) {
    if (isGroupLeader()) {
        echo "是进程组组长\n";
    } else {
        echo "不是进程组组长\n";
    }

    $ret = posix_setsid();
    var_dump($ret);

    $fp = fopen("/dev/tty", "rb");
    if ($fp) {
            echo "子进程会话 " . posix_getsid(0) . " 拥有控制终端\n";
    }   else {
            echo "子进程会话 " . posix_getsid(0) . " 不拥有控制终端\n";
    }
}
上述例程子进程新建了一个会话,然后父子进程都尝试打开文件 /dev/tty,例程输出如下:

不是进程组组长
int(15906)
PHP Warning:  fopen(/dev/tty): failed to open stream: No such device or address in /root/php/setsid.php on line 30

Warning: fopen(/dev/tty): failed to open stream: No such device or address in /root/php/setsid.php on line 30
子进程会话 15906 不拥有控制终端
父进程会话 13000 拥有控制终端

产生SIGHUP信号

1、当一个会话失去控制终端时,内核会向该会话的控制进程发送一个 SIGHUP 信号,而通常会话的控制进程是shell进程,shell在收到一个 SIGHUP 信号时,会向由它创建的所有进程组(前台或后台进程组)也发送一个SIGHUP信号,然后退出,进程收到一个SIGHUP信号的默认处理方式就是退出进程,当然进程也可以自定义信号处理或者忽略它。

2、另外,当控制进程终止时,内核也会向终端的前台进程组的所有成员发送SIGHUP信号。

<?php
$callback = function($signo){
        $sigstr = 'unkown signal';
        switch($signo) {
        case SIGINT:
            $sigstr = 'SIGINT';
            break;
        case SIGHUP:
            $sigstr = 'SIGHUP';
            break;
        case SIGTSTP:
            $sigstr = 'SIGTSTP';
            break;
        }
       file_put_contents("daemon.txt", "catch signal $sigstr\n", FILE_APPEND);
};

pcntl_signal(SIGINT, $callback);
pcntl_signal(SIGHUP, $callback);
pcntl_signal(SIGTSTP, $callback);

while(1)
{
    sleep(100);
    pcntl_signal_dispatch();
}
使用 php sighup.php运行起该程序,然后直接关掉终端,重新登录shell,会发现该程序仍在运行,daemon.txt 文件中会 记录捕获到的SIGHUP信号。

[root@localhost php]# cat daemon.txt 
catch signal SIGHUP
[root@localhost php]# ps aux | grep sighup.php 
root     18438  0.0  0.4 191600  8996 ?        S    16:48   0:00 php sighup.php
root     18443  0.0  0.0 103328   896 pts/0    S+   16:53   0:00 grep sighup.php
同时linux下提供了一个nohup命令,可以让进程忽略所有的SIGHUP信号,例如

[root@localhost php]# nohup php sighup.php 
nohup: 忽略输入并把输出追加到"nohup.out"

(二)标准输入、标准输出、标准错误输出

php中有三个默认打开的文件句柄 STDIN,STDOUT, STDERR 分别对应上述三个文件描述符,而由于标准输入输出是和终端相关的,对于守护进程来说并没有什么用,可以直接关闭,但是直接关闭可能会造成一个问题,请看下面这段代码

<?php
fclose(STDOUT);

$fp = fopen("stdout.log", "a");

echo "hello world\n";
运行上述代码时,屏幕不会输出echo的信息,而是写入到打开的文件中了,这是由于关闭STDOUT文件句柄后,释放了对应的文件描述符,而linux打开文件总是使用最小的可用文件描述符,所以这个文件描述符现在指向fopen打开的文件了,导致原本写到标准输出的信息现在写到了文件里。为了避免这种怪异的行为,我们在关闭这三个文件句柄之后可以立即打开 linux提供的黑洞文件 /dev/null,比如:

<?php
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
fopen('/dev/null', 'r');
fopen('/dev/null', 'w');
fopen('/dev/null', 'w');

$fp = fopen("stdout.log", "a");

echo "hello world\n";                     

上面这个例程关闭STDIN,STDOUT, STDERR立马打开 /dev/null 三次,这样echo的信息会直接写到黑洞中,避免了前面出现的怪异的问题。


(三)编写守护进程涉及的其他问题

编写守护进程还涉及工作目录、文件掩码、信号处理、热更新、安全的启动停止等等问题,这里先留给大家自己百度,后期有空再来补充。


(四)一个守护进程的示例

<?php

//由于进程组长无法创建会话,fork一个子进程并让父进程退出,以便可以创建新会话
switch(pcntl_fork()) {
    case -1:
            exit("fork error");
            break;
    case 0: 
            break;
    default:
            exit(0); //父进程退出
}

posix_setsid();  //创建新会话,脱离原来的控制终端

//再次fork并让父进程退出, 子进程不再是会话首进程,让其永远无法打开一个控制终端
switch(pcntl_fork()) {
    case -1:
        exit("fork error");
        break;
    case 0:
        break;
    default:
        exit(0); //父进程退出
}

//关闭标准输入输出
fclose(STDIN);
fclose(STDOUT);
fclose(STDERR);
fopen('/dev/null', 'r');
fopen('/dev/null', 'w');
fopen('/dev/null', 'w');

//切换工作目录
chdir('/');

//清除文件掩码
umask(0);

//由于内核不会再为进程产生SIGHUP信号,我们可以使用该信号来实现热重启
pcntl_signal(SIGHUP, function($signo){
    //重新加载配置文件,重新打开日志文件等等
});

for(;;)
{
     pcntl_signal_dispatch();  //处理信号回调
    //实现业务逻辑
}


to be continue!




猜你喜欢

转载自blog.csdn.net/zhang197093/article/details/79170881