PHP多程式開發[快問快答系列]

豎橫山發表於2022-12-13
echo $$ //輸出當前bash程式
strace -s 65500 -p 程式號    //列印程式系統呼叫
kill -s 10 pid //傳送訊號
kill -s SIGUSR2 pid //傳送訊號
pstree -ap //檢視程式樹
ps -ajx //檢視程式資訊
ps 命令欄位解析:
PPID:父程式ID
PID:程式ID
PGID:程式組ID
SID:會話ID
TTY:所在終端
STAT:程式狀態 R執行 Z殭屍 S睡眠 T停止 D睡眠無法被喚醒
UID:unix使用者ID
COMMAND:啟動命令

一般指可執行檔案,在Linux系統中它按ELF格式進行儲存,沒有字尾可言, file命令可以檢視elf檔案的具體型別
ELF全程Executable Linkable Format 可執行可連結格式
ELF分為四大種類

  • EXEC 可執行檔案
  • REL 可重定位檔案,也稱為靜態庫檔案,連結器連結之後成為動態庫檔案,比如event.so sockets.so curl.so
  • Shared Object File 共享目標檔案
  • core dump 儲存程式產生的異常資訊
    可透過objdump/readelf 命令檢視ELF檔案相關資訊
  • tty 物理終端
    tty是最令人熟悉的了,在Linux中,/dev/ttyX代表的都是上述的物理終端,其中,/dev/tty1~/dev/tty63代表的是本地終端,也就是接到本機的鍵盤顯示器可以操作的終端
    /dev/console 當前焦點終端

  • pts 偽終端
    透過tcp/ip協議實現的終端,比如用SSH進行的登入,或者telnet, 那麼你將得到一個叫做/dev/pts/X的偽終端同時在
    /proc/bash pid/fd 生成三個識別符號指向當前的/dev/pts/X
    0 標準輸入 滑鼠,鍵盤
    1 標準輸出 顯示器
    2 標準錯誤 顯示器
    PHPMultiple processesNOTE

程式退出

  1. 執行到最後一行語句
  2. 執行時遇到return 時
  3. 執行時遇到exit()函式的時候
  4. 程式異常的時候
  5. 程式接收到中斷訊號

程式結束時並不會真的退出,還會駐留在內在中,pcntl_wait(pcntl_waitpid)函式來獲取程式的終止狀態碼同時該函式會釋放終止程式的記憶體空間,如果不這麼做,會產生很多殭屍程式佔用大量的記憶體空間

孤兒程式

父程式執行完,子程式在執行,則子程式會被頭號程式init接管,這型別的程式成為孤兒程式

殭屍程式

子程式執行完,父程式沒有呼叫pcntl_wait()回收,程式狀態變成Z+

守護程式

父程式是init程式,一般在系統啟動時開始執行,除非強行終止,否則直到系統關機都保持執行。守護程式經常以超級使用者(root)許可權執行,因為它們要使用特殊的埠(1-1024)或訪問某些特殊的資源。

多個程式組成一個程式組,每個程式組只有一個組長,組長的PID就是程式組的ID;組內所有程式退出時,程式組才會消失,可以透過ps -ajx 命令檢視 pgid

PHP多程式開發[快問快答系列]

多個程式組組成一個會話,每個會話都有一個會話首程式。會話的特點
1) 使用setsid()函式可以建立一個新的會話
2) 會話首程式無法呼叫setsid,會報錯
3) 非會話首程式程式可呼叫setsid建立出一個新的會話,這個行為會導致該程式會建立一個新的程式組且自身為該程式組組長,該程式會建立出一個新的會話組且自身為該會話組組長,該程式會脫離當前命令列控制終端
現實上的比喻就是除了老闆之後,員工都可以呼叫 我上我也行() 這個函式變成老闆且不受原公司的控制

訊號是程式間通訊的其中一種方式,平時用到的kill -9 pid,指的不是用第九種方式殺死程式,而是傳送訊號值為9的訊號給程式,而剛好訊號9是SIGKILL,功能是停止程式,檢視作業系統支援的訊號命令: kill -l

PHP多程式開發[快問快答系列]
一般使用1-31, 注意看沒有32,33這兩個訊號
訊號的產生來源可能是:

  • 鍵盤上按了Ctrl+C會產生SIGINT訊號,關閉終端會產生SIGHUP訊號
  • 硬體也能產生訊號
  • 使用kill命令
  • 軟體產生,比如在管道里當一側準備寫管道時可能會產生SIGPIPE訊號

當一個程式收到一個訊號時,三個可選操作

  • 作業系統預設的方式,比如SIGKILL就是殺死程式
  • 忽略掉這個訊號,pcntl_signal(SIGKILL, SIG_IGN, false);程式收到SIGKILL命令時將不為所動
  • 有自己的想法,pcntl_signal(SIGKILL, function($signal){//自己的想法}, false); 這樣將會觸發自定義回撥

pcntl_signal() 訊號處理器是會被子程式繼承的,所以fork()之前最後先行處理訊號處理器

//需要安裝posix擴充套件
posix_getpid();    //獲取程式ID
posix_getppid();//獲取父程式ID
posix_getpgid(posix_getppid());//獲取程式組ID
posix_getpgrp());//同上
posix_getsid(posix_getpid()));//獲取會話ID
posix_getuid();//獲取當前登入使用者UID
posix_getgid();//獲取當前登入使用者組UID
posix_geteuid();//獲取當前有效使用者UID
posix_getguid();//獲取當前有效使用者組UID
posix_kill();//傳送訊號
//建立一個計時器,在指定的秒數後向程式傳送一個SIGALRM訊號。每次對 pcntl_alarm()的呼叫都會取消之前設定的alarm訊號。如果seconds設定為0,將不會建立alarm訊號。 
pcntl_alarm(int $seconds);
//在當前程式當前位置產生子程式,子程式會複製父程式的程式碼段和資料段(Copy on write 寫時複製,當子程式要修改記憶體空間時,作業系統會分配新的記憶體給子程式),ELF檔案的結構,如果父程式先退出,子程式變成孤兒程式,被pid=1程式接管
pcntl_fork();
//安裝一個訊號處理器
pcntl_signal(int $signo, callback $handler);
//呼叫等待訊號的處理器,觸發全部未執行的訊號回撥
pcntl_signal_dispatch()
//設定或檢索阻塞訊號
pcntl_sigprocmask(int $how, array $set[, array &$oldset])
//等待或返回fork的子程式狀態,wait函式掛起當前程式的執行直到一個子程式退出或接收到一個訊號要求中斷當前程式或呼叫一個訊號處理函式。用此函式時已經退出(俗稱殭屍程式),此函式立刻返回。子程式使用的所有系統資源將被釋放。
pcntl_wait($status)
//加個WNOHANG引數,不掛起父程式,如果沒有子程式退出返回0,如果有子程式退出返回子程式pid,如果返回-1表示父程式已經沒有子程式
pcntl_wait($status, WNOHANG)
//基本同pcntl_wait,waitpid可以指定子程式id
pcntl_waitpid ($pid ,$status)
pcntl_waitpid ($pid ,$status, WNOHANG)
//檢查狀態程式碼是否代表一個正常的退出。引數 status 是提供給成功呼叫 pcntl_waitpid() 時的狀態引數。
pcntl_wifexited($status)
//返回一箇中斷的子程式的返回程式碼  當php exit(10)時,這個函式返回10,這個函式僅在函式pcntl_wifexited()返回 TRUE.時有效
pcntl_wexitstatus($status)
//檢查子程式狀態碼是否代表由於某個訊號而中斷。引數 status 是提供給成功呼叫 pcntl_waitpid() 時的狀態引數。
pcntl_wifsignaled($status)
//返回導致子程式中斷的訊號
pcntl_wtermsig($status)
//檢查子程式當前是否已經停止,此函式只有作用於pcntl_wait使用了WUNTRACED作為 option的時候
pcntl_wifstopped($status)
//返回導致子程式停止的訊號
pcntl_wstopsig($status)
//檢索由最後一個失敗的pcntl函式設定的錯誤數
pcntl_errno() 
pcntl_get_last_error()
//檢索與給定errno關聯的系統錯誤訊息
pcntl_strerror(pcntl_errno())
<?php
$o_redis = new Redis();
$o_redis->connect( '127.0.0.1', 6379 );
// 使用for迴圈搞出3個子程式來
for ( $i = 1; $i <= 3; $i++ ) {
  $i_pid = pcntl_fork();
  if ( 0 == $i_pid ) {
    // 使用while保證三個子程式不會退出...
    while( true ) {
      sleep( 1 );
    }
  }
}
// 使用while保證主程式不會退出...
while( true ) { 
  sleep( 1 );
}
netstat -ant |grep 6379

PHP多程式開發[快問快答系列]
說明父程式和三個子程式一共四個程式,實際上共享了一個Redis長連線

因為Redis是一個單程式單執行緒的伺服器,所以接收到的命令都是順序執行順序返回的,所以當客戶端多個程式共享一個redis連線時,當有四個程式向Redis服務端發起請求,返回四個結果,誰先搶到就是誰的,正確的做法是每個子程式建立一個Redis連線,或者用連線池

$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 子程式10秒鐘後退出.
    for ($i = 1; $i <= 10; $i++) {
        sleep(1);
        echo "我的父程式是:" . posix_getppid() . PHP_EOL;
    }
} else if ($i_pid > 0) {
    // 父程式休眠2s後退出.
    sleep(2);
}
$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 子程式10s後退出,變成殭屍程式
    sleep(10);
} else if ($i_pid > 0) {
    // 父程式休眠1000s後退出.
    sleep(1000);
}
$i_pid = pcntl_fork();
if (0 == $i_pid) {
    // 在子程式中
    for ($i = 1; $i <= 10; $i++) {
        sleep(1);
        echo "子程式PID " . posix_getpid() . "倒數計時 : " . $i . PHP_EOL;
    }
} else if ($i_pid > 0) {
    $i_ret = pcntl_wait($status);
    echo $i_ret . ' : ' . $status . PHP_EOL;
    // while保持父程式不退出
    while (true) {
        sleep(1);
    }
}
<?php
// fork出十個子程式
for ($i = 1; $i <= 10; $i++) {
    $i_pid = pcntl_fork();
    // 每個子程式隨機執行1-5秒鐘
    if (0 == $i_pid) {
        $i_rand_time = mt_rand(1, 5);
        sleep($i_rand_time);
        exit;
    } // 父程式收集所有子程式PID
    else if ($i_pid > 0) {

    }
}
while (true) {
    // sleep使父程式不會因while導致CPU爆炸.
    sleep(1);
    //設定WNOHANG引數不會阻塞,就是需要外層包個迴圈
    $pid = pcntl_wait($status, WNOHANG);
    if ($pid == 0) {   //目前還沒有結束的子程式
        continue;
    }
    if ($pid == -1) { //已經結束啦 很藍的啦
        exit("所有程式均已終止" . PHP_EOL);
    }
    // 如果子程式是正常結束
    if (pcntl_wifexited($status)) {
        // 獲取子程式結束時候的 返回錯誤碼
        $i_code = pcntl_wexitstatus($status);
        echo $pid . "正常結束,最終返回:" . $i_code . PHP_EOL;
    }
    // 如果子程式是被訊號終止
    if (pcntl_wifsignaled($status)) {
        // 獲取是哪個訊號終止的該程式
        $i_signal = pcntl_wtermsig($status);
        echo $pid . "由訊號結束,訊號為:" . $i_signal . PHP_EOL;
    }
    // 如果子程式是[臨時掛起]
    if (pcntl_wifstopped($status)) {
        // 獲取是哪個訊號讓他掛起
        $i_signal = pcntl_wstopsig($status);
        echo $pid . "被掛起,掛起訊號為:" . $i_signal . PHP_EOL;
    }
}
$pid = pcntl_fork();
if ($pid > 0) { //1)在父程式中執行fork並exit推出
    exit();
} elseif ($pid == 0) {
    if (posix_setsid() < 0) {   //2)在子程式中呼叫setsid函式建立新的會話
        exit();
    }
    chdir('/'); //3)在子程式中呼叫chdir函式,讓根目錄 ” / ” 成為子程式的工作目錄
    umask(0);   //4)在子程式中呼叫umask函式,設定程式的umask為0
    echo "create success, pid = " . posix_getpid();
    //5)在子程式中關閉任何不需要的檔案描述符
    fclose(STDIN);
    fclose(STDOUT);
    fclose(STDERR);
}
//可以把上面封裝成函式daemon();
while (true) {} //具體業務
for ($i = 1; $i <= 4; $i++) {
    $i_pid = pcntl_fork();
    if (0 == $i_pid) { //子程式
        cli_set_process_title("Worker Process"); //修改子程式的名字
        while (true) {
            sleep(1);
        }
    }
}
cli_set_process_title("Master Process");    //修改父程式的名字
while (true) {
    sleep(1);
}

PHP多程式開發[快問快答系列]

// 訊號處理回撥
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm訊號." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2訊號." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1訊號." . PHP_EOL;
            break;
        default:
            echo "其他訊號." . PHP_EOL;
    }
}
// 給程式安裝3個訊號處理回撥
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//傳送一個訊號給當前程式
    posix_kill(posix_getpid(), SIGUSR1);
    pcntl_signal_dispatch(); //調一次分發一次訊號,呼叫之前,訊號累積在佇列裡
    posix_kill(posix_getpid(), SIGUSR2);
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下
}

PHP多程式開發[快問快答系列]
其中第1,2行與第3,4,5,6行中間隔了一秒,體會一下pcntl_signal_dispatch這個函式

//php7.1及以上才能用這個函式
pcntl_async_signals(true);
// 訊號處理回撥
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm訊號." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2訊號." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1訊號." . PHP_EOL;
            break;
        default:
            echo "其他訊號." . PHP_EOL;
    }
}
// 給程式安裝訊號...
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//傳送一個訊號給當前程式
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下
}
pcntl_async_signals(true);
// 訊號處理回撥
function signal_handler($signal)
{
    switch ($signal) {
        case SIGTERM:
            echo "sigterm訊號." . PHP_EOL;
            break;
        case SIGUSR2:
            echo "sigusr2訊號." . PHP_EOL;
            break;
        case SIGUSR1:
            echo "sigusr1訊號." . PHP_EOL;
            break;
        default:
            echo "其他訊號." . PHP_EOL;
    }
}
// 給程式安裝訊號...
pcntl_signal(SIGTERM, "signal_handler");
pcntl_signal(SIGUSR1, "signal_handler");
pcntl_signal(SIGUSR2, "signal_handler");
//把SIGUSR1阻塞,收到這個訊號先不處理
pcntl_sigprocmask(SIG_BLOCK, [SIGUSR1], $a_oldset);
$counter = 0;
while (true) {
    posix_kill(posix_getpid(), SIGUSR1);//傳送一個訊號給當前程式
    posix_kill(posix_getpid(), SIGUSR2);
    sleep(1);   //稍微休息一下
    if ($counter++ == 5) {
        //解除SIGUSR1訊號阻塞,並立刻執行SIGUSR1處理回撥函式
        pcntl_sigprocmask(SIG_UNBLOCK, [SIGUSR1], $a_oldset);
    }
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結
遇強則強,太強另說

相關文章