本系列文章讨论Perl的三个关键功能:进程(Processes)、管道(Pipes)和信号(Signals)。通过建立一个新进程,Perl程序可以运行另一个程序甚至是它自己的拷贝。管道允许Perl脚本刚其它进行交换数据,而信号使Perl脚本监视和控制其它进程成为可能。本文讨论的是其中的:管道。 管道(Pipe)两个进程间交换数据。随程序而定,两个进程可能运行在相同的机器上,也可能是运行在LAN(局域网,Local Area Network)的两台机器上,也有可能是互联网上的其中一个。这两个进程会彼此协作。 Perl的管道是IPC(进程间通讯, interprocess communication)的最简单形式。管道是当前脚本的一个文件句柄连接到另一进程的标准输入或标准输出。Perl的管道在UNIX、VMS和Windows已完全实现并在Macintosh的MPW环境有限实现。 操作一个管道open()的两个参数用于打开管道。首先,第一个参数是文件句柄的名字。第二个参数,是一个程序和它的所有参数,要么在前面、要么在后面跟着管道的符号“ | ”。该命令应该完全按照所使用的操作系统默认的shell来输入,UNIX里是Bourne shell(“sh”),WINDOWS里是DOS/NT命令提示符。你可能需要指定该命令的完整路径,比如/usr/bin/ls或者依赖PATH环境变量来查找。 如果管道符号在程序前面,文件句柄将标准输入发送给它的数据全部写入。如果管道符号在程序后面,文件句柄打开并读取,并把所有读取到的数据传递给程序的标准输出。 举例来说,在UNIX中,ls -l命令将返回当前目录下所有文件的列表。将“ ls -l | ”传递给 open(),我们可以打开一个管道从这个命令里读取: open (LSFH,"ls -l |") or die "无法打开 ls -l: $!"; while (my $line = <LSFH>) { print "我看到了: $line\n"; } close LSFH; 这个片段简单地响应了ls -l命令返回的每一行。 open (WC,"| wc -lw") or die "无法打开单词统计: $!"; print WC "这是第1行。\n"; print WC "这是另一行。\n"; print WC "这是最后一行。\n"; print WC "哦,我说谎了。\n"; close WC; IO::Filehandle通过 open() 方法来支持管道: $wc = IO::Filehandle->open("| wc - lw") or die "无法打开单词统计: $!"; 使用管道来看看完整的实用的例子。这个 whos_there.pl 程序打开一个到UNIX who命令的管道并统计当前系统的用户登录次数。 #!/usr/bin/perl #whos_there.pl - 统计登记当前系统的用户登录次数 ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #1 use warnings; #2 my %who; #3 open(WHOFH, "who |") or die "无法打开who: $!"; #4 while(<WHOFH>) { #5 next unless m/^(\S+)/; #6 $who{$1}++; #7 } #8 close WHOFH; #12 #输出统计结果 foreach(sort {$who{$b} <=> $who{$a}} keys %who) { #9 printf "%10s %d\n", $_, $who{$_}; #10 } #11 输出结果: % whos_there.pl lorui 81 root 25 zhang3 1 它显示用户“lorui”和“root”分别登录81次和25次,其它用户只登录了一次。这些用户按登录次数的多少来排序。 lorui pts/23 Jan 24 16:52 (centos.) 这些字段分别是:用户名,该用户在终端使用的名字;登录时间以及他登录的远程机器。我们使用一个正则模式来提取用户名,然后将用户名存放到%who哈希中,这样一来,用户名成了该哈希的键,每个用户的登录资料成了哈希的值。 和管道一起的 open() 和 close()稍微得到了增加。它们提供有关子进程的附加信息。当打开一个管道时, open()返回命令的PID。这是一个非零的唯一整数,可使用信号来监视和控制子进程。你可以保存这个PID,也可以忽略它而仅把它作为 open() 函数的返回值对待。 反引号运算符:让管道变得简单Perl的反引号运行符(`)是创建单一读取程序输出管道的便捷方法。反引号的行为和双引号的行为相似,只有一点不同:反引号会解析并执行其中的命令,比如: $ls_output = `ls`; 这将运行ls命令,捕获其输出并将输出赋给 $ls_output 标量。 在内部,Perl打开一个管道来执行命令,读取它的所有输出并打印到标准输出,关闭管道并返回命令的输出作为该操作的结果。通常操作结果都以换行符结尾,可通过 chomp() 来移除。 和双引号一样,反引号解析标量和数组。比如,我们可以创建一个包含参数的变量传递给ls命令,像这样: $arguments = '-l -F'; $ls_output = `ls $arguments`; 命令的标准错误不通过反引号重定向。如果子进程输出任何诊断或错误信息,它们将和你的输出信息混合在一起。在UNIX系统中,你可以使用Bourne shll的输出重定向系统将子进程的标准错误和标准输出联合在一直,就像这样: $ls_output = `ls 2>&1`; 现在 $ls_output 将包含命令的标准错误和标准输出。 pipe()函数:让管道更加强大一种强大但稍显复杂的创建管道的方法是使用Perl内置的 pipe()函数。 pipe()创建一对文件句柄:一个用来读一个一来写。任何写到一个文件句柄的数据可以从另一个句柄读取。 $result = pipe(READHANDLE, WRITEHANDLE)创建一对文件句柄连接到管道。第一个参数是读数据的句柄,第二个是写数据的句柄。如果成功, pipe() 返回一个真值。 为什么 pipe() 这么有用?它一般与 fork() 函数配合使用,来创建父子进程对,以交换数据。父进程保持一个文件句柄同时关闭另一个,与此同时子进程做相反的工作。父子进程现在可以通过管道进行通讯,以实现并行工作。 % facfib.pl 8 factorial(1) => 1 factorial(2) => 2 factorial(3) => 6 factorial(4) => 24 factorial(5) => 120 fibonacci(1) => 1 factorial(6) => 720 fibonacci(2) => 1 factorial(7) => 5040 fibonacci(3) => 2 factorial(8) => 40320 fibonacci(4) => 3 fibonacci(5) => 5 fibonacci(6) => 8 fibonacci(7) => 13 fibonacci(8) => 21 以下是代码: #!/usr/bin/perl #facfib.pl - 计算阶乘和斐波纳契级数的位置 #1 ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; my $arg = shift || 10; #3 pipe(READER, WRITER) or die "无法打开管道: $!\n"; #4 if (fork == 0) { #第一个子进程写到WRITER #5 close READER; #6 select WRITER; $| = 1; #7 factorial($arg); #8 exit 0; #9 } #10 if (fork == 0) {#第二个子进程写到WRITER #11 close READER; #12 select WRITER; $| = 1; #13 my $result = fibonacci($arg); #14 exit 0; #15 } #16 #父进程关闭WRITER并从READER读取 #17 close WRITER; #18 print while <READER>; #19 sub factorial { #20 my $target = shift; #21 for(my $result = 1, my $i = 1; $i <= $target; $i++) { #22 print "factorial($i) => ", $result *= $i, "\n"; #23 } #24 } #25 sub fibonacci { #26 my $target = shift; #27 my ($a, $b) = (1, 0); #28 for(my $i = 1; $i <= $target; $i++) { #29 my $c = $a + $b; #30 print "fibonacci($i) => $c\n"; #31 ($a, $b) = ($b, $c); #32 } #33 }#34 1到3行:初始化模块。开启严格的语法检查并移除并保存命令行参数。如果没有给定参数,使用默认值10。 pipe(READER,WRITER) or die "pipe no good: $!"; my $child = fork(); die "Can't fork: $!" unless defined $child; if ($child == 0) { # child process close READER; # child doesn't need this open (STDOUT,">&WRITER"); # STDOUT now goes to writer exec $cmd,$args; die "exec failed: $!"; } close WRITER; # parent doesn't need this 在代码的结尾部分,READER附属于$cmd的标准输出,跟下面的紧凑代码效果一样: open (READER,"$cmd $args |") or die "pipe no good: $!"; 双向管道open() 和 pipe() 协作创建的是单向的管道文件句柄。如果你想对其它进程同时进行读写,那就没那么幸运了。特别是,下面这个貌似具有前瞻性的代码是不能运行的: open(FH,"| $cmd |"); 一种方法是调用两次 pipe() 方法,创建两对链接的文件句柄。一对用于父进程等待子进程,另一对用于子进程到父进程,某种程度上像双车道的高速公路。我们不想深入这种方法,但它是标准模块IPC::Open2 和 IPC::Open3 用来创建和设定附加到STDIN、STDOUT和STDERR文件句柄子进程的方法。 管道和普通文件句柄的区别有时候需要测试一个句柄打开的是文件还是管道。Perl提供了如下测试方法:
如果文件句柄打开的是一个管道,那么 -p 测试将返回真: print "我有一个管道!\n" if -p FILEHANDLE; -t 和 -s 测试区别其它特殊类型的文件句柄。如果文件句柄在终端(Windows的命令行),那么 -t 测试返回真。程序可以利用它来测试STDIN,以确定该程序是通过终端还是通过一个文件的标准重定向运行: print "运行于终端,禁用配置提示.\n" unless -t STDIN; -s 测试用于标识一个文件句柄是否通过网络套接字打开: print "网络已激活.\n" if -S FH 还有很多文件测试函数来获取文件大小、修改时间、所有权和其它信息。请通过 perlfunc 查看详情。 令人不安的管道错误当你的脚本从文件句柄中打开一个管道,并且在管道结束后退出或者简单地关闭这个程序,你的程序将收到文件句柄的EOF。那么,在相反的情况下会发什么——当你的脚本在写入管道时,你的程序意外终止或过早地关闭了管道连接? #!/usr/bin/perl # write_ten.pl ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; open (PIPE, "| read_three.pl") or die "无法打开管道:$!"; #3 select PIPE; $| = 1; select STDOUT; #4 my $count = 0; #5 for(1..10) { #6 warn "正在写入第 $_ 行\n"; #7 print PIPE "这是第 $_ 行\n" and $count++; #8 sleep 1; #9 } #10 close PIPE or die "无法关闭管道:$!"; #11 print "共写入 $count 行文本\n"; #12 代码片段 read_three.pl: #!/usr/bin/perl # read_three.pl ############################################## # (c) 2011 LoRui(i@, www.) # ############################################## use strict; #2 use warnings; for(1..3) { #3 last unless defined(my $line = <>); #4 warn "read_three.pl 获取:$line"; #5 } #6 运行结果: % write_ten.pl 正在写入第 1 行 read_three.pl 获取:这是第 1 行 正在写入第 2 行 read_three.pl 获取:这是第 2 行 正在写入第 3 行 read_three.pl 获取:这是第 3 行 正在写入第 4 行 Broken pipe 前3行都如预期的那样工作,当 write_ten.pl 尝试写入第4行文本时,脚本报告 Broken pipe 错误。这表明打印“正在写入第x行”的语句并没有返回真值,即管道没有执行。 |
|