Perl程式:殭屍程式和孤兒程式

駿馬金龍發表於2019-03-11

部落格園的我:駿馬金龍: https://www.cnblogs.com/f-ck-need-u

概念

殭屍程式:當子程式退出時,父程式還沒有(使用wait或waitpid)接收其退出狀態時,子程式就成了殭屍程式
孤兒程式:當子程式還在執行時,父程式先退出了,子程式就會成為孤兒程式被pid=1的init/systemd程式收養

需要說明的是,殭屍程式的父程式死掉後,殭屍程式也會被pid=1的init/systemd程式收養,而init/systemd程式會定期清理其下殭屍程式,並在它的任意子程式退出時檢查它的領土下是否有殭屍程式存在,從而保證init/systemd下不會有太多殭屍程式。

殭屍程式模擬

#!/usr/bin/perl
#
use strict;
use warnings;

defined(my $pid = fork) or die "fork failed: $!";

unless($pid){
    # child process
    print "I am child process\n";
    exit;
}

# parent process
print "I am parent process\n";
sleep(2);
system("ps -o pid,ppid,state,tty,command");
print "parent process exiting\n";
exit;
複製程式碼

執行結果:

I am parent process
I am child process
   PID   PPID S TT       COMMAND
 22342  22340 S pts/0    -bash
 22647  22342 S pts/0    perl zombie2.pl
 22648  22647 Z pts/0    [perl] <defunct>
 22649  22647 R pts/0    ps -o pid,ppid,state,tty,command
parent process exiting
複製程式碼

孤兒程式模擬

#!/usr/bin/perl
use strict;
use warnings;

defined(my $pid = fork) or die "fork failed: $!";
unless($pid){
    # 子程式
    print "second child, ppid=",getppid(),"\n";
    sleep(5);
    print "second child, ppid=",getppid(),"\n";
    exit 0;
}

# 父程式
sleep 1;
複製程式碼

結果:

second child, ppid=22683
# 5秒之後輸出
second child, ppid=1
複製程式碼

解決殭屍程式的方式

殭屍程式是因為沒有使用wait/waitpid接收子程式的退出狀態,只要使用wait/waitpid接收該子程式的退出狀態,父程式就會為子程式收屍善後。

另外,當子程式退出時,核心會立即傳送SIGCHLD訊號給父程式告知其該子程式退出了。

有幾種方式可以應對殭屍程式:

  • 直接在父程式中使用wait/waitpid等待所有子程式退出(不能留下任一個子程式)
  • 父程式中定義SIGCHLD訊號的處理程式,並在該訊號處理程式中呼叫wait/waitpid為每個退出的子程式收屍
  • 連續fork兩次,在第二次fork中執行主程式碼,第一次fork的子程式立即退出並在父程式中被收屍。這使得第一個退出的子程式不會成為殭屍程式,也使得第二個子程式立即成為孤兒程式被pid=1的init/systemd收養,從而保證其不會成為殭屍程式。這樣,需要想要成為孤兒的已經孤兒了,父程式卻可以繼續執行父程式的程式碼。如果只fork一次,想要子程式孤兒,父程式繼續執行程式碼是不可能的,因為只有父程式退出,子程式才會孤兒

這三種方式中,前兩種用的比較多,第三種比較技巧化,但是也有其用處,比如實現脫離終端的程式。

等待所有子程式退出

父程式中等待所有子程式退出的方式:

until(wait == -1){}
until(waitpid -1, 0 == -1){}
until(waitpid -1, WNOHANG == -1){}
複製程式碼

例如:

#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw(WNOHANG);

# fork 5個子程式
for (1..5) {
    defined(my $pid = fork) or die "fork error: $!";
    unless($pid){
        # 子程式
        print "I am child: $_\n";
        sleep 1;
        exit 0;
    }
}

# 每秒非阻塞wait一次
until(waitpid(-1, WNOHANG) == -1){
    print "any children still exists\n";
    sleep 1;
}

print "all child exits\n";
system("ps -o pid,ppid,state,tty,command");
exit 0;
複製程式碼

執行結果:

I am child: 1
I am child: 2
I am child: 3
any children still exists
I am child: 5
I am child: 4
any children still exists
any children still exists
any children still exists
any children still exists
any children still exists
any children still exists
all child exits
   PID   PPID S TT       COMMAND
 22342  22340 S pts/0    -bash
 24547  22342 S pts/0    perl waitallchild.pl
 24553  24547 R pts/0    ps -o pid,ppid,state,tty,command
複製程式碼

這裡輸出了多個"any children...",是因為waitpid對於每個等待到的pid都返回一次,此外如果檢查的時候沒有任何退出的子程式,也會每秒返回一次。

最終的結果中顯示沒有殭屍程式的存在。

SIGCHLD處理程式收掉殭屍程式

#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw(WNOHANG);

sub reap_child;

# 註冊SIGCHLD訊號的處理程式
$SIG{CHLD}=\&reap_child;

# fork 5個子程式
for (1..5){
    defined(my $pid = fork) or die "fork failed: $!";
    unless($pid){
        # 子程式
        print "I am child: $_\n";
        sleep 1;
        exit 0;
    } else {
        print "child $_: pid=$pid\n";
    }
}

# 父程式
sleep 20;
system("ps -o pid,ppid,state,tty,command");

sub reap_child {
    print "SIGCHLD triggered at:",~~localtime, "\n";
    # 只要有子程式退出,就收屍
    while((my $kid = waitpid -1, WNOHANG) > 0){
        print "$kid reaped\n";
    }
}
複製程式碼

執行結果:

child 1: pid=24857
I am child: 1
child 2: pid=24858
I am child: 2
child 3: pid=24859
I am child: 3
child 4: pid=24860
I am child: 4
child 5: pid=24861
I am child: 5
SIGCHLD triggered at:Mon Feb 25 13:49:43 2019
24857 reaped
24859 reaped
24860 reaped
   PID   PPID S TT       COMMAND
 22342  22340 S pts/0    -bash
 24856  22342 S pts/0    perl reap_zombie.pl
 24858  24856 Z pts/0    [perl] <defunct>
 24861  24856 Z pts/0    [perl] <defunct>
 24862  24856 R pts/0    ps -o pid,ppid,state,tty,command
SIGCHLD triggered at:Mon Feb 25 13:49:43 2019
24858 reaped
24861 reaped
複製程式碼

發現只需1-2秒程式就終止了,但父程式明明就sleep 20了,為什麼?還有結果好像很奇怪?不僅有兩個殭屍程式還只觸發了兩次SIGCHLD訊號處理程式。

上面觸發了兩次SIGCHLD訊號處理程式,因為第二次觸發的是system()開啟的子程式ps命令退出時觸發的。

之所以1-2秒就結束,是因為子程式結束時,核心傳送SIGCHLD訊號給父程式,會中斷父程式的sleep睡眠。

只觸發兩次訊號處理程式就能收走5個子程式,其中第一次觸發收走了3個子程式,第二次觸發收走了2個子程式,是因為waitpid會返回所有等待到的子程式pid,第一次等待到了3個子程式的退出,第二次等待到了2個子程式的退出。

那麼為什麼system()中的ps退出時沒有被SIGCHLD訊號處理程式中的waitpid收走?這是因為system()函式自身就帶有了wait阻塞函式,它自己會收走經過它fork出來的子程式,使得雖然ps的退出觸發了SIGCHLD,但ps的退出狀態值已經清空了,無法被訊號處理程式中的waitpid處理。

fork兩次收掉殭屍程式

程式碼如下:

#!/usr/bin/env perl
use strict;
use warnings;

defined(my $pid = fork) or die "fork failed: $!";
unless($pid){
    # 第一個子程式
    # 繼續fork一個孫子程式:第二個子程式
    defined(my $kid = fork) or die "fork failed: $!";
    if($kid){
        # 第一個子程式5秒後退出
        sleep 5;
        exit 0;
    }

    # 孫子程式
    sleep(10);
    print "second child, ppid=",getppid(),"\n";
    exit 0;
}

# 為第一個子程式收屍
(waitpid $pid, 0 == $pid) or die "waitpid error: $!";

exit 0;
複製程式碼

上面的程式碼中,在5秒後第一個子程式退出並被父程式收屍,第二個程式將成為孤兒程式被pid=1的程式收養。

相關文章