Perl進程間通訊

不一樣進程之間的通訊或進程間通訊(InterProcess Communication, IPC),是一個涉及多個方面的主題。Perl提供了多種進程間通訊的方式,本文將逐一介紹。本文的內容主體來自於《Pro Perl》的第21章。前端

單向管道(unidirectional pipe)

管道是兩個文件描述符(文件句柄)經過一根管道鏈接起來,一端的文件句柄讀,另外一端的文件句柄寫,從而實現進程間的通訊。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建立管道

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和管道

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::open2IPC::open3來實現。

IPC::Open2和IPC::Open3

open函數只能打開一個文件句柄,要麼是輸入文件句柄,要麼是輸出文件句柄,因此沒法使用open來實現| COMMAND |模式的雙向通訊。

IPC::open2IPC::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的陷阱

在使用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的收屍

因爲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):

  • Damain部分指定通訊類型:PF_INETPF_INET6PF_UNIX等,其中"PF"可換成"AF",PF表示protocal family,AF表示address family,但它們能夠混用且能夠認爲等價
  • type部分指定通訊語義:SOCK_STREAM(對應TCP)、SOCK_DGRAM(對應UDP)、SOCK_SEQPACKET(基本等價於TCP,但稍有不一樣)、SOCK_RAWSOCK_PACKET(對應鏈路層,文檔中已經指明不建議使用)
  • Protocol部分依賴於type的類型:對於type只有一種協議類型的type,能夠指定爲0讓操做系統自動選擇該惟一的協議,若是type的協議類型不是惟一的,則須要明確指定

可是這些對於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;
}

shutdown函數關閉套接字

有些時候,不是必定要用上套接字的雙向通訊功能,好比一端數據已經寫入完成了,可是當前端還在讀取或等待讀取,那麼能夠關閉該端的寫入操做,反之能夠關閉讀取操做。甚至,能夠直接像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,儘管它們均可以關閉套接字

相關文章
相關標籤/搜索