perl fork多进程程序实例讲解perl的多进程程序开发
Forking in perl is a nice thing to do, and for some it’s a hard thing to understand. It can be pretty easy to get lost especially since there are 100 ways to the same thing. I’m going to attempt to explain a little bit of the inner workings of fork() in Perl.
First you have to understand what fork() returns. When you do:
my $pid = fork();
If it is the parent, $pid will be assigned the PID of the child.
If it is the child, $pid will be assigned 0.
If it cannot fork anymore because of no resources, $pid will be undefined.
To help show how a fork() program works, I’m going to use this sample script:
1 #!/usr/bin/perl 2 3 my $pid = fork(); 4 if (not defined $pid) { 5 print “resources not avilable. ”; 6 } elsif ($pid == 0) { 7 print “IM THE CHILD ”; 8 sleep 5; 9 print “IM THE CHILD2 ”; 10 exit(0); 11 } else { 12 print “IM THE PARENT ”; 13 waitpid($pid,0); 14 } 15 print “HIYA ”;
If run, you will see this:
1 $ ./f.pl 2 IM THE CHILD 3 IM THE PARENT 4 - sleep for 5 seconds - 5 IM THE CHILD2 6 HIYA
$ ps -ef | grep fork1.pl derek 6440 2888 0 15:45 pts/2 00:00:00 /usr/bin/perl ./fork1.pl derek 6441 6440 0 15:45 pts/2 00:00:00 /usr/bin/perl ./fork1.pl
This is a pretty simple script and self explanatory. It starts out with the fork and then checks the value of $pid through the if statements and executes the block of code accordingly. What you really have to understand is that when fork() is called, you now have 2 programs that are the same. So in this example, when we do my $pid = fork(); you now have 2 processes running. Each process will run the code. It looks like $pid is only being assigned one value here but it is actually being assigned two or even three values (undefined if necessary). When the parent runs checking through the if statements, it will catch on the last else statement here because $pid is assigned PID of the child. When the child runs through this block of code, it will catch on the if ($pid == 0) because the $pid is assigned 0 for a child. The waitpid() call just waits for the child to exit. If you do not do this it will become a zombie (defunct) process, which means it has become detached from it’s parent.
So here is exactly what happens when you run this:
- The program forks, you now have 2 processes, one is the child, one is the parent.
- if (not defined $pid) gets run on both processes, and die’s if resources aren’t available.
- elsif ($pid == 0) gets run on both processes, if its the child, print “IM THE CHILD”, sleep for 5 seconds and then print “IM THE CHILD 2″ and exit(0);
- While the above statement is running on the child, the parent is going along with the else {} block of code and prints “IM THE PARENT” and then waits for the child to exit.
NOTE: The exit(0) in the child block is very important.. you need the child to exit its process when it is done, so it will no longer exist.
fork
首先说说 fork 函数。这个函数用来创 建一个进程,不过创建方法有些不太好理解。 先看下面的程序 fork-test.pl。我是用perl写的,不过相同的功能也可以用 C 来完成。
1 #!/usr/bin/perl 2 #------------------------------------ 3 # fork-test.pl 4 print "Program started, pid=$$. 5 "; 6 if ($child_pid = fork()) { 7 print "I'm parent, my pid=$$, child's pid=$child_pid. 8 "; 9 } else { 10 print "I'm child, pid=$$. 11 "; 12 }
运行之后显示下面的结果。
Program started, pid=8934. I'm child, pid=8935. I'm parent, my pid=8934, child's pid=8935.
为什么 I'm child 和 I'm parent 都会被显示?这是因为 fork 调用时, 当前的进程会从 fork 的位置一分为二,fork 对两个进程的返回值不同。 在父进程中 fork 返回子进程(即另一个进程)的进程id,而在子进程中 fork 返回 0。 上例的执行过程如下图。
![fork-test.png fork-test.png](http://img0.itokit.com/uploadfile/2012/0408/20120408040621895.jpg)
上例中执行到 Program started 时,只有一个进程 8934,而执行到 fork 时, 进程分为两个,父进程为 8934,子进程为 8935。接下来父进程执行 if 分支, 输入“I'm parent..”,而子进程执行 else 分支,输出 “I'm child”。
SIGCHLD信号和僵尸进程
首先说说什么是僵尸进程(zombie process)。我们知道 Linux 使用进程表来管理进程, 每个进程都在进程表中占据一个位置。当我们用 fork 生成一个子进程, 然后该子进程退出时,系统不会自动回收该子进程所占位置。 此时虽然进程表中有这个子进程的信息,但实际上该子进程早已结束, 于是这个进程就成了“僵尸进程”。
僵尸进程虽然不占用系统资源,但是它会浪费进程表的位置。如果僵尸进程太多, 有可能会导致不能创建新进程。下面的例子 zombie-test.pl 演示了如何创建僵尸进程:
1 #!/usr/bin/perl 2 #------------------------------------ 3 # zombie-test.pl 4 5 sub child { 6 print "I'm child, pid=$$. 7 "; 8 } 9 10 while (1) { 11 if (fork() == 0) { 12 &child; # 如果当前进程是子进程,则执行 child 函数 13 exit; # 并退出 14 } else { 15 sleep 5; # 如果是父进程,则睡眠 5 秒 16 } 17 }
该程序每隔 5 秒创建一个子进程,子进程输出一行文字后退出。 执行该程序片刻之后,从其他终端用 ps -ef 命令可以看到进程状态。 标有 <defunct> 的就是僵尸进程。
harlee 11687 10870 0 02:01 pts/1 00:00:00 /usr/bin/perl perl/zombie-test.pl charlee 11688 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct> charlee 11691 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct> charlee 11695 11687 0 02:01 pts/1 00:00:00 [zombie-test.pl] <defunct>
如何避免僵尸进程?当子进程结束时,系统会向父进程发送 SIGCHLD 信号。 父进程只要在处理这个信号时回收子进程占用的资源即可。
利用 waitpid 回收僵尸进程
一个方法就是在父进程中利用 waitpid 函数。该函数回收指定进程的资源, 并返回已结束进程的进程id。若指定进程不存在,则返回 -1。 我们可以通过调用 waitpid(-1, WNOHANG) 来回收所有子进程。 Perl 提供了全局变量 %SIG,只要设置该变量即可安装信号处理程序。 下面的 waitpid_test1.pl 演示了使用方法。完整的代码可以从本文的附件中下载。
use POSIX ":sys_wait_h"; $SIG{CHLD} = \&REAPER; sub REAPER { my $pid; while (($pid = waitpid(-1, WNOHANG)) > 0) { # 进行一些处理 } }
执行这个程序并用 ps -ef 查看进程,可以发现僵尸进程不再出现了。
不过上面这个程序有个问题。Linux的信号是不能排队的, 如果信号到达进程时进程不能接收该信号,这个信号就会丢失。 REAPER 中包含比较耗时的 while 循环,如果在 REAPER 执行过程中 发生 SIGCHLD 信号,这个信号就会丢失。为了避免这种情况, 我们可以尽量减少信号处理的执行时间。参考下面的 waitpid_test2.pl。
our $zombies = 0; # 记录系统中僵尸进程的数目 $SIG{CHLD} = sub { $zombies++ }; # 信号处理程序中仅仅统计僵尸进程数目 # 主程序 while (1) { if (fork() == 0) { &child; # 如果当前进程是子进程,则执行 child 函数 exit; # 并退出 } else { &REAPER if $zombies; sleep 5; # 如果是父进程,则睡眠 5 秒 } }
实际上,waitpid_test2.pl 并不能及时回收结束的子进程—— 由于 REAPER 在主程序中执行,如果子进程结束时主程序尚未执行到 REAPER 一行, 那么系统中可能会出现相当数量的僵尸进程,直到主程序执行 REAPER 为止。 不过一般情况下这种方法已经足够用了。
忽略 SIGCHLD 回收僵尸进程
另一个比较简单的方法就是直接忽略 SIGCHLD 信号,系统会自动回收结束的子进程。 参见下面的 ignore_sigchld_test.pl。
$SIG{CHLD} = 'IGNORE'; # 忽略 SIGCHLD 信号
与前面的 waitpid 方法相比,此方法虽然方便, 但缺点就是父进程无法在子进程结束时做些处理。 可根据实际需要选择最合适的方法。
本文源代码下载perl-source.zip
################################################
#!/usr/bin/perl #------------------------------------ # ignore_sigchld_test.pl $SIG{CHLD} = 'IGNORE'; # 忽略 SIGCHLD 信号 sub child { print "I'm child, pid=$$. "; } while (1) { if (fork() == 0) { &child; # 如果当前进程是子进程,则执行 child 函数 exit; # 并退出 } else { sleep 5; # 如果是父进程,则睡眠 5 秒 } }
################################################
PERL进程、管道、信号
PERL灵活的进程函数是为了复制进程用于分担任务与程序的工作量。
PERL中复制进程有两种方法:fork()、system()与 exec()。
fork()部分:*NIX传统的复制进程方法
fork()函数:
作用:进程复制函数。
用 法:$pid=fork();
讲解:
无参数;当本进程为父进程时返回值为子进程的PID值,当进程为子进程时返回值为 0。
实例:
#!usr/bin/perl -w
$pid=fork(); #复制进程,并把返回值附入$pid
die "Error:$!n" unless defined $pid;
#制定程序的错误机 制,此步可略
if($pid!=0){ #条件选择,测试$pid值
print"This is a main pid!PID
is $$!n"; #$pid值不等于0,此为父进程(附:$$为保留变量,其值为此进程的PID)
}else{ #否则.....
print"This is a sub pid!PID is $$!n"; #$pid值为0,此为子进程
}
exit 1;
#退出程序
分 析实例:
楼上的程序没有父进程与子进程的明显分化,要将它们分开就要靠测试$pid的值,所以对fork()函数的调用来说条件语句是非常重要 的,需要通过它们来辨别fork() 的返回值。
getppid()函数:
作用:在子进程中调用此函数来获得父进程 PID值。
用法:$parent=getppid();
讲解:无参数,调用后其返回值为父进程的PID值。
实 例:
#!usr/bin/perl -w $pid=fork(); #复制进程 die "Error:$!n" unless defined $pid; #制定错误机制,此步可略 if($pid==0){ #当进程为子进程则进入条件,当进程为父进程则跳过到程序结束 $parent=getppid(); #通过getppid()得到父进程PID值 print"This is a sub pid:$$,the parent is $parentn"; #在STDOUT打印子进程PID值与其父进程ID值 } exit 1; #退出程序
注意:楼上的getppid()实例无法在WIN32下通过,建议使用*nix平台。
关于使用fork():通过 fork()创建的子进程共享了父进程的所有变量、句柄等当前值。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
system() 与exec()部分:更直观的进程调用
=============================================================
system()函数:
作用:直接生成子进程。
用法:$result=system("dir/w *.bat"); 或 $result=system("dir/w","*.bat");
讲解:参数为SHELL语句。当函数能正常 调用时返回值为0(注意:此与其他函数不一致),其他返回值均为错误。
实例:
#!usr/bin/perl -w print STDOUT system("dir/w","*.pl"); #把system()函数中的子进程SHELL语句"dir/w *.pl"的结果输出到STDOUT中 exit 1; #退出程序
#在c:根目录下运行结果为:
C:>perl mm.pl
驱动器 C 中的卷没有标签。
卷的序列 号是 2629-08EF
C: 的目录
aaa.pl bbb.pl zzz.pl xxx.pl SOCK.PL
connect.pl connect2.pl connect3.pl connect4.pl connect5.pl
connect6.pl connect7.pl udpc.pl udps.pl connect8.pl
connect10.pl fork.pl pidd.pl mm.pl
19 个文件 7,503 字节
0 个目录 5,514,014,720 可用字节
0
#注 意楼上最后的"0",这就是返回后的数值,楼上表示程序正常调用了system()函数中的shell语句了。
±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±±
exec()函数:
作用:以指定的SHELL语句代替原进程。
用 法:$result=exec("copy *.bat c:"); 或 $result=exec("copy","*.bat","c:");
讲 解:参数为SHELL语句,成功调用后返回值为undef,其他返回值均为失败。使用此函数后生成的新进程与原进程为同一进程,
有相同的 PID,共享变量语柄等一切当前值。
实例与system()相当,所以不作讨论了。
附:关于"Perl的管道"部分 内容博大精深,而本人又水平有限,所以只好草草了事,如果有哪里不妥还望各位前辈指点
管道是为了使不同进程可以进行通信的制定的。管道是*NIX编程的重点。
与其相关的函数有:open()、 pipe()等等。
open():作打开句柄用。
格式:open(FILEHANDLE,"符号+参数");
例 子:open(NIX,"|wc -lw"); #打开名为NIX的管道,其进程为‘wc -lw’,管道为写入。
open(DDD,"dir c:|"); #打开名为DDD的管道,其进程为‘dir c:’,管道为读取。
讲解:FILEHANDLE(句柄)包括很多成分, 如文件句柄、SOCKET句柄、管道句柄,此文只谈到管道句柄。
第二个参数为双引号或单引号相括着的管道符号与参数。管道符号为"|",把管道 符号放到参数的左边为‘只读’,放到右边为‘只写’,如果两边都有便为双向管道(既可读也可写)。
简单的管道实例:
#!usr/bin/perl -w open(AAA,"dir d:|"); #打开管道AAA,进程为"dir d:",只读 @bbb=<AAA>; #从管道中读取内容,并压入@bbb数组 print"@bbbn"; #把@bbb数组全部输出到STDOUT close AAA; #关闭管道AAA exit 1; #退出程序
pipe():创建管道对。
格式: pipe(READ,WRITE);
实 例:pipe(README,WRITEME); #创建了一个管道对,"README"用于读,"WRITEME"用于写。
$aaa=pipe(AAA,BBB); #创建了一个管道对,"AAA"用于读,"BBB"用于写,$aaa变量为调用pipe()的返回值。
讲解:正常调用后返回值为非零 数,第一个参数为被创建的读管道,第二个参数为被创建的写管道。此函数通常配合进程中
的fork()函数一同使用,步骤是先使用pipe()函 数建立管道对,再使用fork()创建新进程,在不同的进程关闭不同的
管道,这样就可以达到管道间通信的目的了。
close(): 关闭管道
格式: close(AAA);
close BBB;
实例与格式相当,也就不详细介绍了。
讲 解:close()在调用时能将子程序的终止代码放到特殊变量$?中;当关闭的是写管道时close()调用将进入堵塞状态直至另一
端完成它的 全部工作为止。
信号其实就是编程里俗称的中断,它使监视与控制其他进程变为有可能。
首先说说通用信号,通用信号归纳 起来可以组成以下列表:
============================================================= 信 号名 值 标注 解释 ------------------------------------------------------------------------ HUP 1 A 检测到挂起 INT 2 A 来自键盘的中断 QUIT 3 A 来自键盘的停止 ILL 4 A 非法指令 ABRT 6 C 失败 FPE 8 C 浮点异常 KILL 9 AF 终端信号 USR1 10 A 用户定义的信号1 SEGV 11 C 非法内存访问 USR2 12 A 用户定义的信号2 PIPE 13 A 写往没有读取者的管道 ALRM 14 A 来自闹钟的定时器信号 TERM 15 A 终端信号 CHLD 17 B 子进程终止 CONT 18 E 如果被停止则继续 STOP 19 DF 停止进程 TSTP 20 D tty键入的停止命令 TTIN 21 D 对后台进程的tty输入 TTOU 22 D 对后台进程的tty输出 ------------------------------------------------------------------------
著明:上表中‘值’列下没有列出的值所对应的信号为系统调用的非标准信号,在此
文不予以探讨。上表中的第三列‘标注’定义了当进程接 受到信号后的操作,
如:
A-----终止进程
B-----忽略进程信号
C-----终止进程并卸下内核
D----- 停止进程
E-----恢复进程
F-----不能截取或忽略进程信号
=============================================================
下面就以INT作范例演示一下调用过程吧:
#!usr/bin/perl -w #c:test11.pl my $aaa=0; #对计数器变量$aaa进行负值 while($aaa<3){ #进入循环体 print"Beginn"; #打印字符串到STDOUT sleep(5); #睡眠函数,参数为5秒 next unless $SIG{INT}=&demon; #选择结构,demon子程序值负于中断函数 } sub demon{ #demon子程序体 $aaa++; #计数器自加 print"Stop!n"; #打印字符串到STDOUT } exit 1; #退出程序
输出结果:在SHELL里不停的打印出"Begin"字样,相隔5秒,一但在键盘中执行程序
中断操作 (Control+"c"),屏幕就会打印出"Stop!"字样,不停地继续下去。
中断调用总结:调用需要使用到系统保留全局HASH 数组%SIG,即使用"$SIG{信号名}"
截取信号,截取后将其负某个代码(子函数)的地址值,这代码就是截
取后要执行的结果了。