zoukankan      html  css  js  c++  java
  • Perl进程间通信

    不同进程之间的通信或进程间通信(InterProcess Communication, IPC),是一个涉及多个方面的主题。Perl提供了多种进程间通信的方式,本文将逐一介绍。本文的内容主体来自于《Pro Perl》的第21章。

    单向管道(unidirectional pipe)

    管道是两个文件描述符(文件句柄)通过一根管道连接起来,一端的文件句柄读,另一端的文件句柄写,从而实现进程间的通信。

    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: $_
    ";
        sleep 1;
    }
    

    双向通信:双管道

    再来一个示例,是父子进程之间通过两个管道实现来回通信的简单实现:

    #!/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: $_
    ";
            syswrite PWRITE_FH, "C$_
    ";
        }
    }
    
    # 父进程:关闭不用的Pipe端
    close CREAD_FH;
    close PWRITE_FH;
    
    syswrite CWRITE_FH, "$msg
    ";
    while(<PREAD_FH>){
        chomp;
        print "Parent got message: $_
    ";
        syswrite CWRITE_FH, "P$_
    ";
        sleep 1;
    }
    

    上面使用了系统底层的syswrite函数(与之对应的是sysread),它们写入、读取数据时会绕过IO Buffer。而且这里必须不能使用缓冲,否则会出现死锁:父子进程都将等待读数据。

    在后文还会介绍套接字实现的双向通信,并再次实现这个示例。

    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
    

    例如:

    #!/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 "$_
    ";
            }
    }
    
    # 父进程
    $pipe->writer();
    $pipe->autoflush(1);
    for (1..3){
            print {$pipe} "message: $_
    ";
            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
    ";  # sleep进程的pid
    
    while(<LOG1>){
        print "$_
    ";
    }
    
    
    # 管道在左边
    my $pid = open LOG2, "| sleep 5";
    print "child pid: $pid
    ";   # sleep进程的pid
    
    for("a".."d"){
        print LOG2 "$_
    ";
    }
    

    实际上,使用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
    ";
    while(<FH>){
        chomp;
        print "psCMD: $_
    ";
    }
    

    更简洁一点:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    open(PS, "-|") or exec 'ps', '-ef';
    
    while(<PS>){
        chomp;
        print "psCMD: $_
    ";
    }
    

    管道还可以继续传递给管道:

    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: $_
    ";
            print CHILD "C$_
    ";
        }
    }
    
    # 父进程
    close CHILD;
    print PARENT "$msg
    ";
    while(<PARENT>){
        chomp;
        print "Parent Got: $_
    ";
        print PARENT "P$_
    ";
        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,尽管它们都可以关闭套接字

  • 相关阅读:
    关闭各种浏览器自动更新的方法
    新中新question
    linux忘记root密码后的解决办法
    Linux下的命令
    windows未启用Administrator账户
    Jenkins安装以及邮件配置
    pl/sql不能复制粘贴
    python-装饰器&自动化框架搭建
    进程练习——生成指定规格的图片
    python-类的各种方法
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10400540.html
Copyright © 2011-2022 走看看