本系列文章讨论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命令返回的每一行。
另一个有关管道输出的例子,UNIX的 wc -lw命令将统计文本文件的行数(选项“-l”)和单词数(选项“-w”),并发送到标准输入。下面的代码片段使用管道打开这个命令,写入几行文本给它,然后关闭管道。当程序运行时,统计得到的单词数和行数将通过wc打印到命令窗口:
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@lorui.com, www.lorui.com) #
##############################################

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次,其它用户只登录了一次。这些用户按登录次数的多少来排序。
1到3行:初始化脚本。通过 use strict 来开启严格的语法检查。它捕获错误键入的变量、不恰当地使用全局变量、引用字符串失败和其它潜在的错误。我们创建一个哈希(hash)%who 来存储用户和登记次数。
第4行:打开到 who 命令的管道。我们在名为WHOFH的文件句柄上使用 open(),使用 who | 作为第二个参数。如果 open() 调用失败,打印错误信息并终止程序运行。
5到8行:读取who命令的输出。我们每次读取并处理一行who的输出,每行who命令的输出看起来像这样:
lorui pts/23 Jan 24 16:52 (centos.lorui.com)

这些字段分别是:用户名,该用户在终端使用的名字;登录时间以及他登录的远程机器。我们使用一个正则模式来提取用户名,然后将用户名存放到%who哈希中,这样一来,用户名成了该哈希的键,每个用户的登录资料成了哈希的值。
将在文件结尾处(EOF,End Of File)终止。
9到11行:打印结果。我们以登录次数来将%who的键进行排序,并打印每个用户的登录资料。在这里使用了 printf() 格式,“%10s %d \n”告诉 pritnf()将其第一个参数格式化为右对齐的10个字符长,如果不足10,则以空格填充。然后以十进制整数打印第二个参数。
第12行:关闭管道。现在,已经完成管道操作了,所以我们使用 close() 来关闭它。

和管道一起的 open()close()稍微得到了增加。它们提供有关子进程的附加信息。当打开一个管道时, open()返回命令的PID。这是一个非零的唯一整数,可使用信号来监视和控制子进程。你可以保存这个PID,也可以忽略它而仅把它作为 open() 函数的返回值对待。
当管道关闭时, close()将进程序的退出代码存储到了全局变量 $? 中。和大多数Perl习惯相反, $?为零表示命令成功,非零表示命令失败。
另一方面,在使用 close() 关闭管道时, close()函数调用将被屏蔽,直到所有工作完成并退出。如果你在一个管道读取到EOF之前关闭了它,程序将得到一个PIPE信号。

反引号运算符:让管道变得简单


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() 函数配合使用,来创建父子进程对,以交换数据。父进程保持一个文件句柄同时关闭另一个,与此同时子进程做相反的工作。父子进程现在可以通过管道进行通讯,以实现并行工作。
一个简短的例子可以阐述这项技术的功能。给定一个正整数,脚本facifib.pl计算其阶乘和该值在斐波纳契级数的位置。利用现代的多处理器机器,这个计算将出现在两个子进程里,而且由父进程启动它们。当你运行这个程序的时候,你可以看到类似下面的结果:
% 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@lorui.com, www.lorui.com) #
##############################################

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。
第4行:创建链接到管道。难过 pipe() 来创建到管道的链接。READER 将在main进程(父进程)中使用,用来从子进程中读取它们使用WRITER写入的结果。
5到10行:创建第一个子进程。调用 fork() 克隆当前进程。在父进程中, fork()返回非零的子进程PID。在子进程中, fork()返回数字0。如果我们看到 fork()的结果是0,我们知道我们在子进程里。我们关闭READER文件句柄,因为我们不需要它。我们 select() WRITER,让它成为默认的输出句柄,并且通过将 $| 变量的值设为真值来开启自动清除(autoflush)模式。这是必须的,以确保我们一在子进程进行写,父进程就能获取到相关信息。
现在我们用命令行整型参数调用 factorial() 函数【译注:在Perl应该称之为“子例程”。由于本文大量出现“子进程”,为避免混淆,本文使用“函数”这一说法。】。之后,子进程完成其工作,于是 exit() (退出)。我们的 WRITER 副本将自动关闭。
11到16行:创建第二个子进程。回到父进程,我们再次调用 fork() 来创建第二个子进程。这个进程调用 fibonacci() 函数而不是 factorial()。
17到19:处理来自子进程的消息。在父进程中,我们之所以关闭WRITER,是因为我们已不再需要它。我们从READER每次读取一行,然后打印结果。它将包含两个子进程的输出。当最后一个子进程完成并关闭它的WRITER文件句柄时,READER返回undef,并将EOF返回给我们。我们可以 close() (关闭)READER并检查其返回的代码,或者让Perl在我们退出的时候自动关闭文件句柄,就像我们的代码那样。
20到25行:factorial()函数。我们通过传递参数到一个循环体来计算阶乘。在计算的每一步骤,我们打印计算结果。因为已通过 select() 将WRITER设为默认文件句柄,所有这个管道的 print() 语句最终将被父进程读取。
26到34行:fibonacci()函数。
pipe() 函数也可以通过 open() 创建一个连接到另一个程序的文件句柄。我们不使用这种方法,但它的大概流程是:父进程 fork(),子进程使用一对文件句柄中的一个重新打开STDIN或STDOUT,然后 exec() 通过参数得到程序。这里是一个示例:
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文件句柄子进程的方法。
更优雅的方法是,通过 socketpair() 函数创建一个双向的管道。和 pipe() 一样,它也创建两个链接的文件句柄,和前面创建的单向连接不同的是,它创建的是两个文件句柄都是可读写的。数据从一个句柄写入,再从另一个读出,反之亦然。
由于 socketpair() 函数和用于网络通讯的 socket() 函数很相似,所以我们将在后续文章里专门讨论。

管道和普通文件句柄的区别


有时候需要测试一个句柄打开的是文件还是管道。Perl提供了如下测试方法:

















测试方法 描述
-p 文件句柄是一个管道
-t 文件句柄在终端打开
-s 文件句柄是一个套接字(socket)

如果文件句柄打开的是一个管道,那么 -p 测试将返回真:
print "我有一个管道!\n" if -p FILEHANDLE;

-t-s 测试区别其它特殊类型的文件句柄。如果文件句柄在终端(Windows的命令行),那么 -t 测试返回真。程序可以利用它来测试STDIN,以确定该程序是通过终端还是通过一个文件的标准重定向运行:
print "运行于终端,禁用配置提示.\n"
unless -t STDIN;


-s 测试用于标识一个文件句柄是否通过网络套接字打开:
print "网络已激活.\n" if -S FH


还有很多文件测试函数来获取文件大小、修改时间、所有权和其它信息。请通过 perlfunc 查看详情。

令人不安的管道错误


当你的脚本从文件句柄中打开一个管道,并且在管道结束后退出或者简单地关闭这个程序,你的程序将收到文件句柄的EOF。那么,在相反的情况下会发什么——当你的脚本在写入管道时,你的程序意外终止或过早地关闭了管道连接?
我们可以写两个小的Perl脚本来找到答案。第一个脚本命名为 write_ten.pl,打开一个管道到第二个程序,并写入十行数据给它。这个脚本检查 print() 的返回值,如果返回真值,$count 变量的值加1。当 write_ten.pl 完成后,显示 $count 的值,以标明管道成功写入的行数。第二个程序是 read_three.pl,从标准输入读取3行文本,然后退出。
两个脚本显示在下面。值得注意的是,write_ten.pl 的管道运行在自动清除模式而不是缓存模式,所以能立刻回显每一行文本。write_ten.pl在每写入一行文本后 sleep() (暂停)一秒,让 read_three.pl 偶尔报告其接收到的文本。两个脚本一起,可以让我们很容易地看到发生了什么。当我们运行 write_ten.pl里,我们可以看到下面的结果。
代码片段 write_ten.pl:
#!/usr/bin/perl
# write_ten.pl
##############################################
# (c) 2011 LoRui(i@lorui.com, www.lorui.com) #
##############################################

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@lorui.com, www.lorui.com) #
##############################################

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行”的语句并没有返回真值,即管道没有执行。
当程序尝试写入到一个管道并且没有程序来读取这些数据,将返回一个PIPE异常。这个异常从PIPE信号传递给写入者。默认的,这个信号直接终止程序运行。同样的错误也发生在网络应用程序中。当发送者尝试把数据传递给远程程序时,可能造成程序退出或停止接收。
为了有效的处理管道,你必须搭配一个信号处理器,这是我们的下一课题。

知识共享许可协议
本作品采用知识共享署名 3.0 Unported许可协议进行许可。

上一篇 下一篇