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

概念

殭屍進程:當子進程退出時,父進程尚未(使用wait或waitpid)接收其退出狀態時,子進程就成了殭屍進程
孤兒進程:當子進程還在運行時,父進程先退出了,子進程就會成爲孤兒進程被pid=1的init/systemd進程收養bash

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

殭屍進程模擬

#!/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;

結果:code

second child, ppid=22683
# 5秒以後輸出
second child, ppid=1

解決殭屍進程的方式

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

另外,當子進程退出時,內核會當即發送SIGCHLD信號給父進程告知其該子進程退出了。get

有幾種方式能夠應對殭屍進程:it

  • 直接在父進程中使用wait/waitpid等待全部子進程退出(不能留下任一個子進程)
  • 父進程中定義SIGCHLD信號的處理程序,並在該信號處理程序中調用wait/waitpid爲每一個退出的子進程收屍
  • 連續fork兩次,在第二次fork中執行主代碼,第一次fork的子進程當即退出並在父進程中被收屍。這使得第一個退出的子進程不會成爲殭屍進程,也使得第二個子進程當即成爲孤兒進程被pid=1的init/systemd收養,從而保證其不會成爲殭屍進程。這樣,須要想要成爲孤兒的已經孤兒了,父進程卻能夠繼續執行父進程的代碼。若是隻fork一次,想要子進程孤兒,父進程繼續執行代碼是不可能的,由於只有父進程退出,子進程纔會孤兒

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

等待全部子進程退出

父進程中等待全部子進程退出的方式:終端

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的進程收養。

相關文章
相關標籤/搜索
本站公眾號
   歡迎關注本站公眾號,獲取更多信息