不一樣進程之間的通訊或進程間通訊(InterProcess Communication, IPC),是一個涉及多個方面的主題。Perl提供了多種進程間通訊的方式,本文將逐一介紹。本文的內容主體來自於《Pro Perl》的第21章。前端
管道是兩個文件描述符(文件句柄)經過一根管道鏈接起來,一端的文件句柄讀,另外一端的文件句柄寫,從而實現進程間的通訊。shell
Perl使用pipe
函數能夠建立單向管道,也就是一端只可讀、一端只可寫的管道,因此它須要兩個文件句柄參數。編程
pipe READ_FH, WRITE_FH;
默認狀況下,Perl會對IO進行緩衝,向寫入端文件句柄寫入數據時會暫時緩衝在文件句柄的緩衝中,而不會當即放進管道,也就是說讀入端沒法當即讀取到這段數據。對於管道這種數據實時通訊的機制,應該關閉緩衝,而是讓它在須要寫入數據的時候當即刷到管道中。安全
pipe READ_FH, WRITE_FH; # when write to WRITE_FH select WRITE_FH; $| = 1; # 或者使用IO::Handle設置autoflush(1) WRITE_FH->autoflush(1);
下面是一個父子進程間經過單向pipe通訊的示例:父進程寫、子進程讀網絡
#!/usr/bin/perl use strict; use warnings; pipe READ_FH, WRITE_FH; unless(fork){ # Child Process read from pipe alarm 5; while(<READ_FH>){ print "Child Readed: $_"; } exit; } # Parent Process write to pipe select WRITE_FH; $| = 1; for (1..3){ print WRITE_FH "message: $_\n"; sleep 1; }
再來一個示例,是父子進程之間經過兩個管道實現來回通訊的簡單實現:less
#!/usr/bin/perl use strict; use warnings; pipe CREAD_FH, CWRITE_FH; pipe PREAD_FH, PWRITE_FH; my $msg = "S"; unless(fork) { # 子進程:關閉不用的Pipe端 close PREAD_FH; close CWRITE_FH; while(<CREAD_FH>){ chomp; print "Child got message: $_\n"; syswrite PWRITE_FH, "C$_\n"; } } # 父進程:關閉不用的Pipe端 close CREAD_FH; close PWRITE_FH; syswrite CWRITE_FH, "$msg\n"; while(<PREAD_FH>){ chomp; print "Parent got message: $_\n"; syswrite CWRITE_FH, "P$_\n"; sleep 1; }
上面使用了系統底層的syswrite
函數(與之對應的是sysread),它們寫入、讀取數據時會繞過IO Buffer。並且這裏必須不能使用緩衝,不然會出現死鎖:父子進程都將等待讀數據。dom
在後文還會介紹套接字實現的雙向通訊,並再次實現這個示例。socket
IO::Pipe
模塊也能夠用來建立管道,它建立的是裸管道(raw pipe)對象(面向對象的對象),能夠經過調用reader和writer方法來將Raw Pipe對象轉換成IO::Handle
的只讀、只寫文件句柄。例如:函數
use IO::Pipe my $pipe = new IO::Pipe unless (fork){ # Child Process $pipe->reader; # $pipe is now a read-only IO::Handle } # parent Process $pipe->writer; # $pipe is now a write-only IO::Handle # remeber to disable IO buffering
例如:this
#!/usr/bin/perl use strict; use warnings; use IO::Pipe; my $pipe = IO::Pipe->new(); unless(fork) { # 子進程 alarm 5; $pipe->reader(); while(<$pipe>){ chomp; print "$_\n"; } } # 父進程 $pipe->writer(); $pipe->autoflush(1); for (1..3){ print {$pipe} "message: $_\n"; sleep 1; }
open函數打開文件句柄的時候,能夠經過管道符號"|"將文件句柄和外部調用的命令之間創建管道。
例如,將perl從文件句柄中讀取的數據交給外部命令cat -n
進行處理:
#!/usr/bin/perl open LOG, "| cat -n" or die "Can't open file: $!"; while(<LOG>){ print $_; }
再例如,將外部命令cat -n
的執行結果交給perl文件句柄:
#!/usr/bin/perl open LOG, "cat -n test.log |" or die "Can't open file: $!"; while(<LOG>){ print "from pipe: $_"; }
除了上面這種將管道符號"|"寫在左右兩邊的方式,還有另一種方式:-|
和|-
,其中"-"能夠認爲是外部命令:
-|
|-
|
寫在左邊,表示句柄到外部命令,等價於|-
,|
寫在右邊,表示外部命令到句柄,等價於-|
如下幾種寫法是等價的:
open LOG, "|tr '[a-z]' '[A-Z]'"; open LOG, "|-", "tr '[a-z]' '[A-Z]'"; open LOG, "|-", "tr", '[a-z]', '[A-Z]'; open LOG, "cat -n '$file'|"; open LOG, "-|", "cat -n '$file'"; open LOG, "-|", "cat", "-n", $file;
在open創建管道的時候(不管管道符號在左邊仍是右邊),調用的外部命令會打開一個新進程(子進程),open的返回值就是這個子進程的pid,可使用waitpid
去爲這個子進程收屍。對於|-
和-|
模式,外部命令能夠寫在子進程中(見下文避免子shell示例中用法)。
注意:open在調用管道的時候,返回值纔是子進程的pid(對父進程,對子進程仍然爲0,和fork是同樣的)。在正常open文件句柄的時候,返回的是非0值表示open成功。
# 管道在右邊 my $pid = open LOG1, "sleep 5 |"; print "child pid: $pid\n"; # sleep進程的pid while(<LOG1>){ print "$_\n"; } # 管道在左邊 my $pid = open LOG2, "| sleep 5"; print "child pid: $pid\n"; # sleep進程的pid for("a".."d"){ print LOG2 "$_\n"; }
實際上,使用open調用管道的時候,若是要執行的命令中包含了一些shell的特殊符號,那麼Perl就會打開一個子shell做爲子進程,再經過這個子shell來解釋外部命令,就像system
函數同樣。若是能避免這種行爲,則儘可能避免,這種行爲有時候不是太安全。
避免的方式是使用exec或system函數,並將外部命令和命令的參數以列表的方式傳遞給它們(目的是爲了分隔命令和參數)。由於open調用-|
或|-
時,會啓動一個新進程,咱們能夠在這個新子進程中執行exec函數來替換這個子進程,並將命令的參數以列表的方式傳遞給exec。
#!/usr/bin/perl use strict; use warnings; # "-|"後沒有給外部命令,而是留在後面給定 defined(my $pid = open FH, "-|") or die "Can' fork: $!"; unless($pid){ # 子進程 exec qw(ps -ef); # 使用exec分離命令和參數 } # 父進程 print "Child Process PID: $pid\n"; while(<FH>){ chomp; print "psCMD: $_\n"; }
更簡潔一點:
#!/usr/bin/perl use strict; use warnings; open(PS, "-|") or exec 'ps', '-ef'; while(<PS>){ chomp; print "psCMD: $_\n"; }
管道還能夠繼續傳遞給管道:
open LOG, "|tr '[a-z]' '[A-Z]' | cat -n";
但這種管道是單向管道,沒法提供既可讀又可寫的功能,即| COMMAND |
這種模式是沒法實現的。可是,能夠將單向管道的寫入數據輸出到一個臨時文件中,而後讀取端從這個臨時文件中讀取。
open LOG, "|sort >/tmp/output$$"; ... open RESULT, "/tmp/output$$"; unlink "/tmp/output$$";
實際上,| COMMAND |
這種雙向管道能夠用IPC::open2
或IPC::open3
來實現。
open函數只能打開一個文件句柄,要麼是輸入文件句柄,要麼是輸出文件句柄,因此沒法使用open來實現| COMMAND |
模式的雙向通訊。
IPC::open2
和IPC::open3
能夠在運行外部命令(以fork+exec的方式)的同時打開2個(open2)或3個文件句柄(open3),它們打開的文件句柄都鏈接到外部命令,一個用於讀取外部命令的結果,一個用於輸出給外部命令,若是使用open3,則還有一個用於外部命令的錯誤輸出,就像是爲子進程準備了獨屬於子進程的STDIN、STDOUT和STDERR同樣。注意,它們都返回子進程的pid(對子進程則返回0,就像fork同樣)。
以下:
use IPC::Open2; my $pid = open2(*RD, *WR, @CMD_AND_ARGS); use IPC::Open3; my $pid = open3(*WR, *RD, *ERR, @CMD_AND_ARGS);
顯然,open2和open3的文件句柄參數的順序不同,一個讀在前,一個寫在前,因此必定要仔細檢查,它多是萬惡之源。或者,只使用open3來避免這個問題。另外,若是隻想要其中一個或2個文件句柄,可使用shift
做爲open2/3的參數。例如:
open2(shift, *WR, 'CMD', 'ARG');
其中WR文件句柄用於向外部命令輸出數據,RD文件句柄用於從外部命令的結果中讀取數據。它們和外部命令的鏈接關係以下所示:
|---------> |-------->| ↑ ↓ ↑ ↓ WR | COMMAND | RD
例如:從標準輸入中讀取數據,經過Writer句柄寫入給bc命令進行計算,再經過Reader句柄從bc命令讀取出計算結果。
#!/usr/bin/perl use strict; use warnings; use IPC::Open2; local(*Reader, *Writer); my $pid = open2(\*Reader, \*Writer, "bc"); my $res; while(<STDIN>){ # 讀取標準輸入 print Writer $_; # 將標準輸入經過Writer寫入給bc $res = <Reader>; # 從Reader中讀取bc的計算結果 print STDOUT "$res"; # 輸出計算結果到標準輸出 }
執行幾回該程序:
$ echo "3 + 3" | perl bc.pl 6 $ echo "3 * 3" | perl bc.pl 9 $ echo "3 - 1" | perl bc.pl 2
因爲typeglobs是比較老式的編程方式,因此能夠傳遞IO::Handle
對象來實現相同的功能:
use IPC::Open2; use IO::Handle; my $Rd = IO::Handle->new(); my $Wr = IO::Handle->new(); my $pid = open2($Rd, $Wr, 'CMD', 'ARG');
或者直接傳遞未賦值的詞法變量(詞法變量默認會初始化):
use IPC::Open2; my ($rd, $wr); my $pid = open2($rd, $wr, "CMD", "ARG");
咱們並不是必定要本身編寫WR | COMMAND | RD
中WR和RD部分的代碼來提供數據、讀取數據,能夠在WR處使用<&FH1
來表示直接從FH1文件句柄中讀取數據寫入給COMMAND,在RD處使用>&FH2
來表示直接將結果輸出給FH2文件句柄。也就是說,COMMAND從FH1文件句柄中讀取輸入,將執行結果輸出給FH2。即:
open2(>&RD, <&WR, 'CMD');
在使用open2和open3的時候,必須注意IO緩衝問題。
對於這種模式的雙向管道:
|---------> |-------->| ↑ ↓ ↑ ↓ WR | COMMAND | RD
須要注意的是,WR文件句柄是自動關閉IO buffer的,因此向外部命令傳遞的數據都能當即被COMMAND讀取。可是,咱們沒法控制COMMAND是否緩衝IO,也就是說,咱們沒法保證RD能當即從COMMAND讀取到數據,這取決於COMMAND的程序設計。像bc命令是計算一行輸出一行的,RD能理解讀取到計算結果,而sort這樣的命令須要將全部數據都讀入到緩衝中排序完成後纔會輸出,這時外部命令沒法知道WR是否已經寫完了數據,外部命令將所以而一直等待,致使RD也將出現等待。正由於沒法保證外部命令是否緩衝,將很容易出現死鎖問題。
例以下面這個簡單的sort示例:
#!/usr/bin/perl use strict; use warnings; use IPC::Open2; my($rd, $wr); my $pid = open2($rd, $wr, "sort"); while(<>){ print {$wr} "$_"; } close $wr; # 這一行是必須的 while(<$rd>){ print "$_"; }
上面的代碼邏輯很簡單,從標準輸入中讀取數據,而後排序,而後讀出結果輸出到標準輸出。但這裏的細節是sort命令會等待全部數據都被讀入緩衝後再進行排序操做,因此這裏使用close()提早關閉WR來通知sort命令數據已經寫入完成了,因而sort當即開始排序,RD將從中讀取到結果。
若是註釋上面的close(),將致使死鎖問題,能夠一試。
因爲open2和open3都使用fork+exec來運行外部命令,它們中的任何一個失敗都會致使open2/3失敗,可是open並不會返回失敗,而是直接拋出異常。對於子進程的exec失敗來講,它將發送SIGPIPE信號,而子進程並不會探測並處理這個信號,咱們必須本身去處理,例如捕捉、忽略信號。
open2/3不會等待子進程的退出,也不會爲它收屍。若是是短小的程序,可能操做系統會直接幫助收屍了,但若是程序執行時間較長,那麼須要手動去收屍。收屍很簡單,直接用waitpid
便可。例如使用非阻塞版本的waitpid來收屍:
use IPC::Open2; use POSIX qw(WNOHANG); my $pid = open2($rd, $wr, 'CMD'); until (waitpid $pid, WNOHANG){ # 直到沒有子進程可等待了 # do something sleep 1; # 每秒去輪詢一次 }
必需要注意,until裏面的代碼不能有阻塞代碼(嚴格地說是該段代碼對$rd和$wr的操做沒有阻塞),不然就不會繼續調用到waitpid,從而出現死鎖。
雖然沒有直接的雙向管道(bidirectional pipe),可是能夠建立兩個文件句柄,每一個文件句柄都是雙向的,從而將它們實現成相似於管道的雙向管道。好比前文示例中建立的兩個父子進程來回通訊的管道,好比套接字,他們都是雙向通訊的。
兩個套接字之間,每一個套接字均可以進行讀、寫,並且它能夠跨網絡、跨主機進行通訊,固然也能夠在本機內不一樣進程間直接通訊。對於本機進程間的雙向通訊來講,使用網絡套接字進行通訊比較重量級,使用Unix套接字則更輕量級,更高效率,由於Unix套接字省去了許多網絡通訊的內容。本文也只介紹Unix套接字,在後面介紹網絡編程的時候再解釋網絡套接字。
socketpair
函數能夠用來建立Unix套接字,它沒有任何網絡相關的內容。它建立兩個來回通訊的匿名套接字,看上去就像是雙向管道同樣(不適用於Windows系統,由於Windows上沒有Unix套接字的概念)。實際上,對於Perl來講,有些操做系統平臺中的管道(單向的)就是經過socketpair函數來實現的,它在建立了兩個都可讀寫的套接字後,關閉一個套接字的讀和另外一個套接字的寫,就實現了單向管道。
要建立一個套接字,除了要給定套接字文件句柄(文件描述符),還須要有3個必要的部分:domain、type和與之關聯的協議。以socketpair函數爲例:
socketpair SOCK1, SOCK2, DOMAIN, TYPE, PROTOCOL
其中(可執行man 2 socket
):
PF_INET
、PF_INET6
、PF_UNIX
等,其中"PF"可換成"AF",PF表示protocal family,AF表示address family,但它們能夠混用且能夠認爲等價SOCK_STREAM
(對應TCP)、SOCK_DGRAM
(對應UDP)、SOCK_SEQPACKET
(基本等價於TCP,但稍有不一樣)、SOCK_RAW
、SOCK_PACKET
(對應鏈路層,文檔中已經指明不建議使用)可是這些對於socketpair函數來講基本是多餘的,由於socketpair建立的套接字不涉及網絡通訊或文件系統通訊,不須要監聽鏈接,不須要綁定地址,不須要關係協議類型,由於操做系統沒有底層的協議API符合socketpair建立的套接字類型。因此,咱們才認爲Unix Domain套接字比網絡套接字要輕量級的多。
使用流類型的套接字,以便於咱們能夠像一個普通文件句柄同樣取操做套接字,此外不須要關心協議,因此指定爲PF_UNSPEC,或者指定爲0。
例如,使用socketpair建立父子進程之間雙向通訊的Unix Domain套接字:
use Socket; socketpair PARENT, CHILD, AF_UNIX, SOCK_STREAM, PF_UNSPEC; # 能夠加上判斷 socketpair ... or die "$!";
它將創建以下形式的兩個雙向通訊的套接字:
進程1 進程2 ----------------------------- PARENT -----------> CHILD CHILD -----------> PARENT
也就是寫入PARENT端,數據自動流入到CHILD端,只能從CHILD端讀取PARENT端的寫入。反之,只能從PARENT端讀取CHILD端的寫入。
下面是使用socketpair建立的socket實現前文使用雙管道實現父子進程雙向通訊的等價示例:
#!/usr/bin/perl use strict; use warnings; use Socket; use IO::Handle; socketpair PARENT, CHILD, AF_UNIX, SOCK_STREAM, AF_UNSPEC; PARENT->autoflush(1); CHILD->autoflush(1); my $msg = "S"; unless (fork) { # 子進程 close PARENT; while(<CHILD>){ chomp; print "Child Got: $_\n"; print CHILD "C$_\n"; } } # 父進程 close CHILD; print PARENT "$msg\n"; while(<PARENT>){ chomp; print "Parent Got: $_\n"; print PARENT "P$_\n"; sleep 1; }
有些時候,不是必定要用上套接字的雙向通訊功能,好比一端數據已經寫入完成了,可是當前端還在讀取或等待讀取,那麼能夠關閉該端的寫入操做,反之能夠關閉讀取操做。甚至,能夠直接像close同樣關閉套接字。
shutdown函數可用來關閉用不上的那端套接字,shutdown函數的用法:
shutdown(SOCKET, 0); # I/we have stopped reading data shutdown(SOCKET, 1); # I/we have stopped writing data shutdown(SOCKET, 2); # I/we have stopped using this socket
已經解釋的很清楚了,第二個參數爲0表示關閉套接字讀操做(SHUT_RD
,即成爲write-only套接字),爲1表示關閉套接字寫操做(SHUT_WR
,即成爲read-only套接字),爲2表示禁用該套接字(SHUT_RDWR
)。且上面使用了I/we
第一人稱,I表示當前進程中的某套接字,we表示多個進程中的同一個套接字。
shutdown函數和close函數的區別在於某些狀況下shutdown函數比較適用,好比想告訴對端我已經完成了寫但尚未完成讀(或者反之),並且shutdown會關閉經過多個進程中的同個套接字(也就是說,close隻影響當前進程打開的某套接字,而shutdown則影響全部進程的這個套接字)。
正由於shutdown會影響全部進程的同一個套接字,因此對於同時讀寫的套接字來講,不要隨意使用shutdown。正如上面的示例中,看上去子進程只使用CHILD套接字,父進程只使用PARENT套接字,因此使用shutdown關閉子進程的PARENT,關閉父進程的CHILD,就像前面使用的close同樣。但實際結果倒是,兩個進程的CHILD和PARENT都將關閉。因此,能用close的地方不表明能用shutdown,儘管它們均可以關閉套接字。