Perl程序:殭屍程序和孤兒程序
概念
殭屍程序:當子程序退出時,父程序還沒有(使用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 PIDPPID S TTCOMMAND 2234222340 S pts/0-bash 2264722342 S pts/0perl zombie2.pl 2264822647 Z pts/0[perl] <defunct> 2264922647 R pts/0ps -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收養,從而保證其不會成為殭屍程序
這三種方式中,前兩種用的比較多,第三種比較技巧化,但是也有其用處。
等待所有子程序退出
父程序中等待所有子程序退出的方式:
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 PIDPPID S TTCOMMAND 2234222340 S pts/0-bash 2454722342 S pts/0perl waitallchild.pl 2455324547 R pts/0ps -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 PIDPPID S TTCOMMAND 2234222340 S pts/0-bash 2485622342 S pts/0perl reap_zombie.pl 2485824856 Z pts/0[perl] <defunct> 2486124856 Z pts/0[perl] <defunct> 2486224856 R pts/0ps -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的程序收養。