不一样进程之间的通讯或进程间通讯(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,尽管它们均可以关闭套接字。