部落格園的我:駿馬金龍: 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的程式收養。