本文關於Perl進程的內容主體來自於《Pro Perl》的第21章。html
Perl中可使用fork
函數來建立新的進程,它會調用操做系統的fork系統調用來建立新進程。shell
fork是Unix系統中的函數,在Windows中不原生支持fork。但從Perl 5.8開始,Perl提供了一個模擬的fork使其能夠無視平臺的差別,它是使用Perl解釋器線程來實現的fork,由於解釋器線程不自動共享數據,因此用來fork進程正好。換句話說,Perl 5.8開始fork是能夠隨意用來建立進程的。bash
fork函數會派生本身,經過本身克隆出一個子進程。這個克隆過程是完整的,由於子進程和父進程在克隆的過程當中是徹底一致的,子進程和父進程共享代碼,克隆完成後才設置一些各進程獨有的屬性,好比有本身的文件句柄(已經文件句柄上的鎖)、進程ID、優先級等等屬性。less
在fork新進程以後,就會有兩個近乎徹底同樣的進程在並行運行。fork有兩個返回值,一個是給父進程的返回值,這個返回值是fork出來的子進程的PID(若是fork失敗,則返回undef),一個是給子進程的返回值,這個返回值爲0。因此,經過fork的返回值能夠判斷出進程是子進程仍是父進程。函數
if (my $pid = fork) { print "parent process\n"; print "child \$pid: $pid\n"; } else { print "child process\n"; exit 0; }
這段程序的運行結果的順序是隨機的,這是由於沒法保證多個進程的調度順序。例以下面是某兩次運行的結果:工具
[root]$ perl fork.pl parent process child $pid: 22 child process [root]$ perl fork.pl parent process child process child $pid: 24
因爲fork可能會失敗(例如達到了進程數量的最大限制值),因此上面的代碼不太健壯,並且fork進程後,一般比較期待看到子進程的代碼而非父進程的,父進程的代碼一般在子進程的下面。因此改寫成以下代碼:操作系統
defined( my $pid = fork ) or die "Failed to fork: $!"; unless($pid) { # 子進程在此 print "Child process\n"; exit 0; } # 父進程在此 print "parent process\n"; print "Child process PID: $pid";
fork奇特的地方就在於針對不一樣的進程返回了不一樣的值,更嚴格地說是返回了兩次。但任何一個函數都只能返回一次,由於一個return語句就結束函數了,那fork是如何實現兩次返回的?線程
對於$pid = fork
這個語句,將其分紅兩個部分,一個是fork操做,一個是返回值賦值操做。在fork克隆完但fork還沒結束時就已經有了兩個進程,這兩個進程的代碼都同樣,都在運行fork,兩個fork都要賦值給$pid
。能夠認爲是兩個進程在執行fork,或者從程序的角度上看,是兩個程序去調用了兩次fork。code
雖然說fork返回了兩次,但實際上fork函數的返回值只有一個,只不過在不一樣環境下返回不一樣的值,fork只需一個環境判斷就能夠知道該返回哪一個值:父進程的fork函數返回值要賦值給父進程的$pid
,子進程的fork函數返回值要賦值給子進程的$pid
。htm
若是須要搞懂這個細節,請參見fork、文件句柄、文件描述符和鎖的關係。
fork出來的子進程一般須要有一個退出語句,例如exit,不然子進程在執行完本身的代碼後,有可能會執行父進程的代碼,由於子進程和父進程是共享代碼的。
例如:
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子進程代碼段 print "In Child process\n"; # (1) } # 父進程代碼 print "parent process here\n"; # (2) print "The pid is: $pid\n"; # (3)
子進程執行完(1)後,由於它也有(2)和(3)的代碼,因此子進程會繼續執行(2)和(3)。但實際上,(2)和(3)本該是給父進程執行的。
爲了不這樣的問題,要麼將父進程的代碼放進unless的else語句塊中,要麼在子進程代碼塊中加入exit語句保證在執行完語句後退出進程。
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子進程代碼段 print "In Child process\n"; # (1) exit 0; } # 父進程代碼 print "parent process here\n"; # (2) print "The pid is: $pid\n"; # (3)
更常常地,fork會結合exec家族的函數來加載其它程序替換當前進程中的程序,exec家族函數有一個共同的特性:執行完所加載的程序後自動退出進程。因此,就再也不須要在子進程中加入exit語句。
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子進程代碼段 print "In Child process\n"; exec 'date +"%F %T"'; } # 父進程代碼 print "parent process here\n"; print "The pid is: $pid\n";
exec函數的返回值是多餘的,歷來都不須要檢查exec的返回值,但exec是否成功調用某個程序是須要檢查的,例如上面沒法調用date命令。但由於exec是執行完後就當即退出的,因此能夠直接在exec後面加上錯誤處理語句,如die,只要能運行到die,說明exec失敗了。
unless ($pid) { # 子進程代碼段 print "In Child process\n"; exec 'date +"%F %T"'; die "Exec failed: $!"; }
須要注意的是,exec COMMAND
的COMMAND失敗不表明exec失敗,exec是發起系統調用,只有這個系統調用的過程當中失敗纔算是失敗,例如沒法發起調用。COMMAND執行失敗和exec已經無關,例如date命令不存在也已經表示exec成功發起了系統調用,因此不會運行到die語句。
當前進程的PID可使用特殊變量$$
來獲取,或者對應的英文形式$PID
、$PROCESS_ID
也能夠獲取。
print "my PID is $$\n";
對於Unix,能夠經過子進程找出其父進程的PID,在Perl中可使用getppid
函數獲取父進程的PID。
$parent_PID = getppid;
因而,能夠發送HUP信號給父進程:
kill "HUP", getppid;
當想要將信號發送給多個進程而非單個進程時,進程組的重要性就體現出來了。每一個進程在fork出來的時候,就加入了一個進程組,對於沒有父進程的進程,它本身獨立成組,組ID即爲它本身的PID。對於有父進程的子進程,在被建立時會繼承父進程的進程組。注意是繼承父進程的進程組,而不是以父進程爲進程組。固然,若是父進程是本身的進程組,那麼子進程初始時會在父進程的組中。
但須要注意的是,並不是子進程就必定在父進程所在的進程組中。若是真是這樣的話,那麼Linux下全部的進程都在init/systemd這個祖先進程的組中,但實際上並不是如此。操做系統容許進程改變本身的進程組(稍後就介紹使用Perl如何改變進程組),例如本身成組。實際上,在shell下執行命令時,都是本身成立本身的進程組的(可pstree -g查看所屬進程組號),儘管它們都有父進程。
使用getpgrp PID
能夠獲取PID進程所在的進程組。例如,獲取當前進程所在的進程組:
getpgrp $$; getpgrp; # 等價
對於獲取當前進程的進程組,更具可移植性的方式是將一個false值(通常使用數值0)爲getpgrp的參數。
getpgrp 0;
下面是一個檢查子進程、父進程所在進程組的示例:
defined (my $pid = fork ) or die "Can't fork process:\n"; unless($pid) { print "(Child)->PID: $$\n"; print "(Child)->PPID: @{ [ getppid ] }\n"; print "(Child)->GroupID: @{ [ getpgrp $$ ] }\n"; print "(Child)->ParentGroupID: @{ [ getpgrp getppid ] }\n"; sleep 2; # 爲了讓後面的pstree收集子進程信息 exit 0; } print "(Parent)->GroupID: @{[ getpgrp $$ ]}\n"; print "(Parent)->PPID: @{[ getppid ]}\n"; system "pstree -p | grep 'perl'";
執行的結果:
(Parent)->GroupID: 155 (Parent)->PPID: 4 (Child)->PID: 156 (Child)->PPID: 155 (Child)->GroupID: 155 (Child)->ParentGroupID: 155 init(1)-+-init(3)---bash(4)---perl(155)-+-perl(156)
可見,子進程和父進程的進程組都是155,這個155正是父進程自身。
實際上查看進程組的需求很少,由於幾乎已經能夠知道進程和父進程在同一個進程組中,除非咱們單獨設置了進程所在的進程組。
設置進程所在進程組的方式是使用setpgrp
函數,第一個參數是要設置的進程ID,第二個參數是要加入到哪一個進程組。
setpgrp $pid, $pgid;
進程不只能夠加入到任何已存在的進程組中,還能夠本身成立一個進程組並加入到本身的組中,只需將setpgrp的兩個參數都設置爲相同的PID值便可。例如,當前進程加入本身的組:
setpgrp $$, $$; setpgrp;
一樣的,爲了可移植性,使用false值做爲setpgrp的參數:
setpgrp 0, 0;
設置進程組通常用來隔離子進程和父進程,或者說讓子進程脫離父進程,以避免收到父進程發送的信號。好比讓終端中的進程(它們是終端進程的子進程)脫離終端,這樣發送信號給終端進程來終止終端時,只有脫離終端的子進程才能繼續存活,終端進程自身以及其它終端子進程都將死亡。而在父進程死亡後,脫離了父進程的子進程都將成爲孤兒進程(orphan process),孤兒進程都會轉移到PID=1的init或systemd祖先進程下,但這些子進程仍然在本身的進程組中。
脫離父進程
子進程脫離了父進程所在進程組後,不會當即轉移走,而是繼續留在父進程下面,這是由於進程組和父子進程之間的關係不是徹底對等關係,脫離進程組不表明子進程就再也不是父進程的子進程了,它仍然是。只有在父進程終止時,子進程由於收不到信號而得以繼續存活,但每一個進程都必須有父進程(除了pid=1的init/systemd進程),因此操做系統會讓子進程轉移到進程的祖先init/systemd下由它們負責管理。因此,在shell中使用nohup類工具將進程脫離終端時,進程仍在bash進程的下面,只有關閉終端時,子進程才轉移到init/systemd進程下。
更通用的,設置進程組能夠用來實現所謂的daemon類進程:和建立它們的父進程分離並獨立存活的進程。
要發送信號給進程組,只需使用kill函數,並傳遞一個負數的PID值做爲第二個參數,這表示將信號發送給該PID所在的進程組,該組裏全部的進程都將收到該信號。例如,發送HUP信號給當前進程所在的進程組,這樣
kill "HUP", -$$;
下面是一個daemon類程序的示例:
#!/usr/bin/env perl use strict; use warnings; defined (my $pid = fork) or die "Can't fork child: $!"; # 子進程 unless($pid) { setpgrp 0,0; # 脫離組 alarm 10; # 計時器10秒 while(1){ foreach (0..2){ print "A\n" if $_ == 0; print "B\n" if $_ == 1; print "C\n" if $_ == 2; } sleep 2; } } # 父進程中 print "Daemon Process created: $pid\n"; sleep 1; # 給子進程一點時間來脫離進程組 kill 9, -$$; # 殺掉本身以及沒有脫離組的子進程
這段代碼的邏輯很簡單:父進程建立子進程後睡眠一秒鐘以給子進程脫離組一點時間,而後父進程就自殺(發送終止信號給本身),而子進程本身加入本身的組,而後在後臺運行一個循環,每一個循環都輸出A、B、C後睡眠2秒,並經過設置一個alarm計時器在10秒後終止子進程。
上面的示例中,重點就在於父進程自殺後,子進程仍然在運行。