Forking in perl is a nice thing to do, and for some it’s a hard thing to understand. It can be pretty easy to get lost especially since there are 100 ways to the same thing. I’m going to attempt to explain a little bit of the inner workings of fork() in Perl.
First you have to understand what fork() returns. When you do:
my $pid = fork();
If it is the parent, $pid will be assigned the PID of the child.
If it is the child, $pid will be assigned 0.
If it cannot fork anymore because of no resources, $pid will be undefined.
To help show how a fork() program works, I’m going to use this sample script:
1 #!/usr/bin/perl 2 3 my $pid = fork(); 4 if (not defined $pid) { 5 print “resources not avilable. ”; 6 } elsif ($pid == 0) { 7 print “IM THE CHILD ”; 8 sleep 5; 9 print “IM THE CHILD2 ”; 10 exit(0); 11 } else { 12 print “IM THE PARENT ”; 13 waitpid($pid,0); 14 } 15 print “HIYA ”;
If run, you will see this:
1 $ ./f.pl 2 IM THE CHILD 3 IM THE PARENT 4 - sleep for 5 seconds - 5 IM THE CHILD2 6 HIYA
$ ps -ef | grep fork1.pl derek 6440 2888 0 15:45 pts/2 00:00:00 /usr/bin/perl ./fork1.pl derek 6441 6440 0 15:45 pts/2 00:00:00 /usr/bin/perl ./fork1.pl
This is a pretty simple script and self explanatory. It starts out with the fork and then checks the value of $pid through the if statements and executes the block of code accordingly. What you really have to understand is that when fork() is called, you now have 2 programs that are the same. So in this example, when we do my $pid = fork(); you now have 2 processes running. Each process will run the code. It looks like $pid is only being assigned one value here but it is actually being assigned two or even three values (undefined if necessary). When the parent runs checking through the if statements, it will catch on the last else statement here because $pid is assigned PID of the child. When the child runs through this block of code, it will catch on the if ($pid == 0) because the $pid is assigned 0 for a child. The waitpid() call just waits for the child to exit. If you do not do this it will become a zombie (defunct) process, which means it has become detached from it’s parent.
So here is exactly what happens when you run this:
- The program forks, you now have 2 processes, one is the child, one is the parent.
- if (not defined $pid) gets run on both processes, and die’s if resources aren’t available.
- elsif ($pid == 0) gets run on both processes, if its the child, print “IM THE CHILD”, sleep for 5 seconds and then print “IM THE CHILD 2″ and exit(0);
- While the above statement is running on the child, the parent is going along with the else {} block of code and prints “IM THE PARENT” and then waits for the child to exit.
NOTE: The exit(0) in the child block is very important.. you need the child to exit its process when it is done, so it will no longer exist.
fork
首先說說 fork 函數。這個函數用來創 建一個進程,不過創建方法有些不太好理解。 先看下面的程序 fork-test.pl。我是用perl寫的,不過相同的功能也可以用 C 來完成。
1 #!/usr/bin/perl 2 #------------------------------------ 3 # fork-test.pl 4 print "Program started, pid=$$. 5 "; 6 if ($child_pid = fork()) { 7 print "I'm parent, my pid=$$, child's pid=$child_pid. 8 "; 9 } else { 10 print "I'm child, pid=$$. 11 "; 12 }
運行之后顯示下面的結果。
Program started, pid=8934. I'm child, pid=8935. I'm parent, my pid=8934, child's pid=8935.
為什么 I'm child 和 I'm parent 都會被顯示?這是因為 fork 調用時, 當前的進程會從 fork 的位置一分為二,fork 對兩個進程的返回值不同。 在父進程中 fork 返回子進程(即另一個進程)的進程id,而在子進程中 fork 返回 0。 上例的執行過程如下圖。

上例中執行到 Program started 時,只有一個進程 8934,而執行到 fork 時, 進程分為兩個,父進程為 8934,子進程為 8935。接下來父進程執行 if 分支, 輸入“I'm parent..”,而子進程執行 else 分支,輸出 “I'm child”。
SIGCHLD信號和僵屍進程
首先說說什么是僵屍進程(zombie process)。我們知道 Linux 使用進程表來管理進程, 每個進程都在進程表中占據一個位置。當我們用 fork 生成一個子進程, 然后該子進程退出時,系統不會自動回收該子進程所占位置。 此時雖然進程表中有這個子進程的信息,但實際上該子進程早已結束, 於是這個進程就成了“僵屍進程”。
僵屍進程雖然不占用系統資源,但是它會浪費進程表的位置。如果僵屍進程太多, 有可能會導致不能創建新進程。下面的例子 zombie-test.pl 演示了如何創建僵屍進程:
1 #!/usr/bin/perl 2 #------------------------------------ 3 # zombie-test.pl 4 5 sub child { 6 print "I'm child, pid=$$. 7 "; 8 } 9 10 while (1) { 11 if (fork() == 0) { 12 &child; # 如果當前進程是子進程,則執行 child 函數 13 exit; # 並退出 14 } else { 15 sleep 5; # 如果是父進程,則睡眠 5 秒 16 } 17 }
該程序每隔 5 秒創建一個子進程,子進程輸出一行文字后退出。 執行該程序片刻之后,從其他終端用 ps -ef 命令可以看到進程狀態。 標有 <defunct> 的就是僵屍進程。
harlee 11687 10870 0 02:01 pts/1 00:00:00 /usr/bin/perl perl/zombie-test.pl charlee 11688 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct> charlee 11691 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct> charlee 11695 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct>
如何避免僵屍進程?當子進程結束時,系統會向父進程發送 SIGCHLD 信號。 父進程只要在處理這個信號時回收子進程占用的資源即可。
利用 waitpid 回收僵屍進程
一個方法就是在父進程中利用 waitpid 函數。該函數回收指定進程的資源, 並返回已結束進程的進程id。若指定進程不存在,則返回 -1。 我們可以通過調用 waitpid(-1, WNOHANG) 來回收所有子進程。 Perl 提供了全局變量 %SIG,只要設置該變量即可安裝信號處理程序。 下面的 waitpid_test1.pl 演示了使用方法。完整的代碼可以從本文的附件中下載。
use POSIX ":sys_wait_h"; $SIG{CHLD} = \&REAPER; sub REAPER { my $pid; while (($pid = waitpid(-1, WNOHANG)) > 0) { # 進行一些處理 } }
執行這個程序並用 ps -ef 查看進程,可以發現僵屍進程不再出現了。
不過上面這個程序有個問題。Linux的信號是不能排隊的, 如果信號到達進程時進程不能接收該信號,這個信號就會丟失。 REAPER 中包含比較耗時的 while 循環,如果在 REAPER 執行過程中 發生 SIGCHLD 信號,這個信號就會丟失。為了避免這種情況, 我們可以盡量減少信號處理的執行時間。參考下面的 waitpid_test2.pl。
our $zombies = 0; # 記錄系統中僵屍進程的數目 $SIG{CHLD} = sub { $zombies++ }; # 信號處理程序中僅僅統計僵屍進程數目 # 主程序 while (1) { if (fork() == 0) { &child; # 如果當前進程是子進程,則執行 child 函數 exit; # 並退出 } else { &REAPER if $zombies; sleep 5; # 如果是父進程,則睡眠 5 秒 } }
實際上,waitpid_test2.pl 並不能及時回收結束的子進程—— 由於 REAPER 在主程序中執行,如果子進程結束時主程序尚未執行到 REAPER 一行, 那么系統中可能會出現相當數量的僵屍進程,直到主程序執行 REAPER 為止。 不過一般情況下這種方法已經足夠用了。
忽略 SIGCHLD 回收僵屍進程
另一個比較簡單的方法就是直接忽略 SIGCHLD 信號,系統會自動回收結束的子進程。 參見下面的 ignore_sigchld_test.pl。
$SIG{CHLD} = 'IGNORE'; # 忽略 SIGCHLD 信號
與前面的 waitpid 方法相比,此方法雖然方便, 但缺點就是父進程無法在子進程結束時做些處理。 可根據實際需要選擇最合適的方法。
本文源代碼下載perl-source.zip
################################################
#!/usr/bin/perl #------------------------------------ # ignore_sigchld_test.pl $SIG{CHLD} = 'IGNORE'; # 忽略 SIGCHLD 信號 sub child { print "I'm child, pid=$$. "; } while (1) { if (fork() == 0) { &child; # 如果當前進程是子進程,則執行 child 函數 exit; # 並退出 } else { sleep 5; # 如果是父進程,則睡眠 5 秒 } }
################################################
PERL進程、管道、信號
PERL靈活的進程函數是為了復制進程用於分擔任務與程序的工作量。
PERL中復制進程有兩種方法:fork()、system()與 exec()。
fork()部分:*NIX傳統的復制進程方法
fork()函數:
作用:進程復制函數。
用 法:$pid=fork();
講解:
無參數;當本進程為父進程時返回值為子進程的PID值,當進程為子進程時返回值為 0。
實例:
#!usr/bin/perl -w
$pid=fork(); #復制進程,並把返回值附入$pid
die "Error:$!n" unless defined $pid;
#制定程序的錯誤機 制,此步可略
if($pid!=0){ #條件選擇,測試$pid值
print"This is a main pid!PID
is $$!n"; #$pid值不等於0,此為父進程(附:$$為保留變量,其值為此進程的PID)
}else{ #否則.....
print"This is a sub pid!PID is $$!n"; #$pid值為0,此為子進程
}
exit 1;
#退出程序
分 析實例:
樓上的程序沒有父進程與子進程的明顯分化,要將它們分開就要靠測試$pid的值,所以對fork()函數的調用來說條件語句是非常重要 的,需要通過它們來辨別fork() 的返回值。
getppid()函數:
作用:在子進程中調用此函數來獲得父進程 PID值。
用法:$parent=getppid();
講解:無參數,調用后其返回值為父進程的PID值。
實 例:
#!usr/bin/perl -w $pid=fork(); #復制進程 die "Error:$!n" unless defined $pid; #制定錯誤機制,此步可略 if($pid==0){ #當進程為子進程則進入條件,當進程為父進程則跳過到程序結束 $parent=getppid(); #通過getppid()得到父進程PID值 print"This is a sub pid:$$,the parent is $parentn"; #在STDOUT打印子進程PID值與其父進程ID值 } exit 1; #退出程序
注意:樓上的getppid()實例無法在WIN32下通過,建議使用*nix平台。
關於使用fork():通過 fork()創建的子進程共享了父進程的所有變量、句柄等當前值。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
system() 與exec()部分:更直觀的進程調用
=============================================================
system()函數:
作用:直接生成子進程。
用法:$result=system("dir/w *.bat"); 或 $result=system("dir/w","*.bat");
講解:參數為SHELL語句。當函數能正常 調用時返回值為0(注意:此與其他函數不一致),其他返回值均為錯誤。
實例:
#!usr/bin/perl -w print STDOUT system("dir/w","*.pl"); #把system()函數中的子進程SHELL語句"dir/w *.pl"的結果輸出到STDOUT中 exit 1; #退出程序
#在c:根目錄下運行結果為:
C:>perl mm.pl
驅動器 C 中的卷沒有標簽。
卷的序列 號是 2629-08EF
C: 的目錄
aaa.pl bbb.pl zzz.pl xxx.pl SOCK.PL
connect.pl connect2.pl connect3.pl connect4.pl connect5.pl
connect6.pl connect7.pl udpc.pl udps.pl connect8.pl
connect10.pl fork.pl pidd.pl mm.pl
19 個文件 7,503 字節
0 個目錄 5,514,014,720 可用字節
0
#注 意樓上最后的"0",這就是返回后的數值,樓上表示程序正常調用了system()函數中的shell語句了。
±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
exec()函數:
作用:以指定的SHELL語句代替原進程。
用 法:$result=exec("copy *.bat c:"); 或 $result=exec("copy","*.bat","c:");
講 解:參數為SHELL語句,成功調用后返回值為undef,其他返回值均為失敗。使用此函數后生成的新進程與原進程為同一進程,
有相同的 PID,共享變量語柄等一切當前值。
實例與system()相當,所以不作討論了。
附:關於"Perl的管道"部分 內容博大精深,而本人又水平有限,所以只好草草了事,如果有哪里不妥還望各位前輩指點
管道是為了使不同進程可以進行通信的制定的。管道是*NIX編程的重點。
與其相關的函數有:open()、 pipe()等等。
open():作打開句柄用。
格式:open(FILEHANDLE,"符號+參數");
例 子:open(NIX,"|wc -lw"); #打開名為NIX的管道,其進程為‘wc -lw’,管道為寫入。
open(DDD,"dir c:|"); #打開名為DDD的管道,其進程為‘dir c:’,管道為讀取。
講解:FILEHANDLE(句柄)包括很多成分, 如文件句柄、SOCKET句柄、管道句柄,此文只談到管道句柄。
第二個參數為雙引號或單引號相括着的管道符號與參數。管道符號為"|",把管道 符號放到參數的左邊為‘只讀’,放到右邊為‘只寫’,如果兩邊都有便為雙向管道(既可讀也可寫)。
簡單的管道實例:
#!usr/bin/perl -w open(AAA,"dir d:|"); #打開管道AAA,進程為"dir d:",只讀 @bbb=<AAA>; #從管道中讀取內容,並壓入@bbb數組 print"@bbbn"; #把@bbb數組全部輸出到STDOUT close AAA; #關閉管道AAA exit 1; #退出程序
pipe():創建管道對。
格式: pipe(READ,WRITE);
實 例:pipe(README,WRITEME); #創建了一個管道對,"README"用於讀,"WRITEME"用於寫。
$aaa=pipe(AAA,BBB); #創建了一個管道對,"AAA"用於讀,"BBB"用於寫,$aaa變量為調用pipe()的返回值。
講解:正常調用后返回值為非零 數,第一個參數為被創建的讀管道,第二個參數為被創建的寫管道。此函數通常配合進程中
的fork()函數一同使用,步驟是先使用pipe()函 數建立管道對,再使用fork()創建新進程,在不同的進程關閉不同的
管道,這樣就可以達到管道間通信的目的了。
close(): 關閉管道
格式: close(AAA);
close BBB;
實例與格式相當,也就不詳細介紹了。
講 解:close()在調用時能將子程序的終止代碼放到特殊變量$?中;當關閉的是寫管道時close()調用將進入堵塞狀態直至另一
端完成它的 全部工作為止。
信號其實就是編程里俗稱的中斷,它使監視與控制其他進程變為有可能。
首先說說通用信號,通用信號歸納 起來可以組成以下列表:
============================================================= 信 號名 值 標注 解釋 ------------------------------------------------------------------------ HUP 1 A 檢測到掛起 INT 2 A 來自鍵盤的中斷 QUIT 3 A 來自鍵盤的停止 ILL 4 A 非法指令 ABRT 6 C 失敗 FPE 8 C 浮點異常 KILL 9 AF 終端信號 USR1 10 A 用戶定義的信號1 SEGV 11 C 非法內存訪問 USR2 12 A 用戶定義的信號2 PIPE 13 A 寫往沒有讀取者的管道 ALRM 14 A 來自鬧鍾的定時器信號 TERM 15 A 終端信號 CHLD 17 B 子進程終止 CONT 18 E 如果被停止則繼續 STOP 19 DF 停止進程 TSTP 20 D tty鍵入的停止命令 TTIN 21 D 對后台進程的tty輸入 TTOU 22 D 對后台進程的tty輸出 ------------------------------------------------------------------------
著明:上表中‘值’列下沒有列出的值所對應的信號為系統調用的非標准信號,在此
文不予以探討。上表中的第三列‘標注’定義了當進程接 受到信號后的操作,
如:
A-----終止進程
B-----忽略進程信號
C-----終止進程並卸下內核
D----- 停止進程
E-----恢復進程
F-----不能截取或忽略進程信號
=============================================================
下面就以INT作范例演示一下調用過程吧:
#!usr/bin/perl -w #c:test11.pl my $aaa=0; #對計數器變量$aaa進行負值 while($aaa<3){ #進入循環體 print"Beginn"; #打印字符串到STDOUT sleep(5); #睡眠函數,參數為5秒 next unless $SIG{INT}=&demon; #選擇結構,demon子程序值負於中斷函數 } sub demon{ #demon子程序體 $aaa++; #計數器自加 print"Stop!n"; #打印字符串到STDOUT } exit 1; #退出程序
輸出結果:在SHELL里不停的打印出"Begin"字樣,相隔5秒,一但在鍵盤中執行程序
中斷操作 (Control+"c"),屏幕就會打印出"Stop!"字樣,不停地繼續下去。
中斷調用總結:調用需要使用到系統保留全局HASH 數組%SIG,即使用"$SIG{信號名}"
截取信號,截取后將其負某個代碼(子函數)的地址值,這代碼就是截
取后要執行的結果了。