PHP實現系統編程(五)--- 編寫守護進程詳解

(一)進程組、會話、控制終端、控制進程等概念

進程組:每個進程都有一個所屬的進程組 (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() 創建了一個子進程後,這個子進程就不是進程組長,所以可以利用子進程來創建新會話。

<?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!




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章