本文关于Perl进程的内容主体来自于《Pro Perl》的第21章。html
Perl中可使用fork
函数来建立新的进程,它会调用操做系统的fork系统调用来建立新进程。shell
fork是Unix系统中的函数,在Windows中不原生支持fork。但从Perl 5.8开始,Perl提供了一个模拟的fork使其能够无视平台的差别,它是使用Perl解释器线程来实现的fork,由于解释器线程不自动共享数据,因此用来fork进程正好。换句话说,Perl 5.8开始fork是能够随意用来建立进程的。bash
fork函数会派生本身,经过本身克隆出一个子进程。这个克隆过程是完整的,由于子进程和父进程在克隆的过程当中是彻底一致的,子进程和父进程共享代码,克隆完成后才设置一些各进程独有的属性,好比有本身的文件句柄(已经文件句柄上的锁)、进程ID、优先级等等属性。less
在fork新进程以后,就会有两个近乎彻底同样的进程在并行运行。fork有两个返回值,一个是给父进程的返回值,这个返回值是fork出来的子进程的PID(若是fork失败,则返回undef),一个是给子进程的返回值,这个返回值为0。因此,经过fork的返回值能够判断出进程是子进程仍是父进程。函数
if (my $pid = fork) { print "parent process\n"; print "child \$pid: $pid\n"; } else { print "child process\n"; exit 0; }
这段程序的运行结果的顺序是随机的,这是由于没法保证多个进程的调度顺序。例以下面是某两次运行的结果:工具
[root]$ perl fork.pl parent process child $pid: 22 child process [root]$ perl fork.pl parent process child process child $pid: 24
因为fork可能会失败(例如达到了进程数量的最大限制值),因此上面的代码不太健壮,并且fork进程后,一般比较期待看到子进程的代码而非父进程的,父进程的代码一般在子进程的下面。因此改写成以下代码:操作系统
defined( my $pid = fork ) or die "Failed to fork: $!"; unless($pid) { # 子进程在此 print "Child process\n"; exit 0; } # 父进程在此 print "parent process\n"; print "Child process PID: $pid";
fork奇特的地方就在于针对不一样的进程返回了不一样的值,更严格地说是返回了两次。但任何一个函数都只能返回一次,由于一个return语句就结束函数了,那fork是如何实现两次返回的?线程
对于$pid = fork
这个语句,将其分红两个部分,一个是fork操做,一个是返回值赋值操做。在fork克隆完但fork还没结束时就已经有了两个进程,这两个进程的代码都同样,都在运行fork,两个fork都要赋值给$pid
。能够认为是两个进程在执行fork,或者从程序的角度上看,是两个程序去调用了两次fork。code
虽然说fork返回了两次,但实际上fork函数的返回值只有一个,只不过在不一样环境下返回不一样的值,fork只需一个环境判断就能够知道该返回哪一个值:父进程的fork函数返回值要赋值给父进程的$pid
,子进程的fork函数返回值要赋值给子进程的$pid
。htm
若是须要搞懂这个细节,请参见fork、文件句柄、文件描述符和锁的关系。
fork出来的子进程一般须要有一个退出语句,例如exit,不然子进程在执行完本身的代码后,有可能会执行父进程的代码,由于子进程和父进程是共享代码的。
例如:
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子进程代码段 print "In Child process\n"; # (1) } # 父进程代码 print "parent process here\n"; # (2) print "The pid is: $pid\n"; # (3)
子进程执行完(1)后,由于它也有(2)和(3)的代码,因此子进程会继续执行(2)和(3)。但实际上,(2)和(3)本该是给父进程执行的。
为了不这样的问题,要么将父进程的代码放进unless的else语句块中,要么在子进程代码块中加入exit语句保证在执行完语句后退出进程。
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子进程代码段 print "In Child process\n"; # (1) exit 0; } # 父进程代码 print "parent process here\n"; # (2) print "The pid is: $pid\n"; # (3)
更常常地,fork会结合exec家族的函数来加载其它程序替换当前进程中的程序,exec家族函数有一个共同的特性:执行完所加载的程序后自动退出进程。因此,就再也不须要在子进程中加入exit语句。
defined (my $pid = fork) or die "Can't fork child process: $!"; unless ($pid) { # 子进程代码段 print "In Child process\n"; exec 'date +"%F %T"'; } # 父进程代码 print "parent process here\n"; print "The pid is: $pid\n";
exec函数的返回值是多余的,历来都不须要检查exec的返回值,但exec是否成功调用某个程序是须要检查的,例如上面没法调用date命令。但由于exec是执行完后就当即退出的,因此能够直接在exec后面加上错误处理语句,如die,只要能运行到die,说明exec失败了。
unless ($pid) { # 子进程代码段 print "In Child process\n"; exec 'date +"%F %T"'; die "Exec failed: $!"; }
须要注意的是,exec COMMAND
的COMMAND失败不表明exec失败,exec是发起系统调用,只有这个系统调用的过程当中失败才算是失败,例如没法发起调用。COMMAND执行失败和exec已经无关,例如date命令不存在也已经表示exec成功发起了系统调用,因此不会运行到die语句。
当前进程的PID可使用特殊变量$$
来获取,或者对应的英文形式$PID
、$PROCESS_ID
也能够获取。
print "my PID is $$\n";
对于Unix,能够经过子进程找出其父进程的PID,在Perl中可使用getppid
函数获取父进程的PID。
$parent_PID = getppid;
因而,能够发送HUP信号给父进程:
kill "HUP", getppid;
当想要将信号发送给多个进程而非单个进程时,进程组的重要性就体现出来了。每一个进程在fork出来的时候,就加入了一个进程组,对于没有父进程的进程,它本身独立成组,组ID即为它本身的PID。对于有父进程的子进程,在被建立时会继承父进程的进程组。注意是继承父进程的进程组,而不是以父进程为进程组。固然,若是父进程是本身的进程组,那么子进程初始时会在父进程的组中。
但须要注意的是,并不是子进程就必定在父进程所在的进程组中。若是真是这样的话,那么Linux下全部的进程都在init/systemd这个祖先进程的组中,但实际上并不是如此。操做系统容许进程改变本身的进程组(稍后就介绍使用Perl如何改变进程组),例如本身成组。实际上,在shell下执行命令时,都是本身成立本身的进程组的(可pstree -g查看所属进程组号),尽管它们都有父进程。
使用getpgrp PID
能够获取PID进程所在的进程组。例如,获取当前进程所在的进程组:
getpgrp $$; getpgrp; # 等价
对于获取当前进程的进程组,更具可移植性的方式是将一个false值(通常使用数值0)为getpgrp的参数。
getpgrp 0;
下面是一个检查子进程、父进程所在进程组的示例:
defined (my $pid = fork ) or die "Can't fork process:\n"; unless($pid) { print "(Child)->PID: $$\n"; print "(Child)->PPID: @{ [ getppid ] }\n"; print "(Child)->GroupID: @{ [ getpgrp $$ ] }\n"; print "(Child)->ParentGroupID: @{ [ getpgrp getppid ] }\n"; sleep 2; # 为了让后面的pstree收集子进程信息 exit 0; } print "(Parent)->GroupID: @{[ getpgrp $$ ]}\n"; print "(Parent)->PPID: @{[ getppid ]}\n"; system "pstree -p | grep 'perl'";
执行的结果:
(Parent)->GroupID: 155 (Parent)->PPID: 4 (Child)->PID: 156 (Child)->PPID: 155 (Child)->GroupID: 155 (Child)->ParentGroupID: 155 init(1)-+-init(3)---bash(4)---perl(155)-+-perl(156)
可见,子进程和父进程的进程组都是155,这个155正是父进程自身。
实际上查看进程组的需求很少,由于几乎已经能够知道进程和父进程在同一个进程组中,除非咱们单独设置了进程所在的进程组。
设置进程所在进程组的方式是使用setpgrp
函数,第一个参数是要设置的进程ID,第二个参数是要加入到哪一个进程组。
setpgrp $pid, $pgid;
进程不只能够加入到任何已存在的进程组中,还能够本身成立一个进程组并加入到本身的组中,只需将setpgrp的两个参数都设置为相同的PID值便可。例如,当前进程加入本身的组:
setpgrp $$, $$; setpgrp;
一样的,为了可移植性,使用false值做为setpgrp的参数:
setpgrp 0, 0;
设置进程组通常用来隔离子进程和父进程,或者说让子进程脱离父进程,以避免收到父进程发送的信号。好比让终端中的进程(它们是终端进程的子进程)脱离终端,这样发送信号给终端进程来终止终端时,只有脱离终端的子进程才能继续存活,终端进程自身以及其它终端子进程都将死亡。而在父进程死亡后,脱离了父进程的子进程都将成为孤儿进程(orphan process),孤儿进程都会转移到PID=1的init或systemd祖先进程下,但这些子进程仍然在本身的进程组中。
脱离父进程
子进程脱离了父进程所在进程组后,不会当即转移走,而是继续留在父进程下面,这是由于进程组和父子进程之间的关系不是彻底对等关系,脱离进程组不表明子进程就再也不是父进程的子进程了,它仍然是。只有在父进程终止时,子进程由于收不到信号而得以继续存活,但每一个进程都必须有父进程(除了pid=1的init/systemd进程),因此操做系统会让子进程转移到进程的祖先init/systemd下由它们负责管理。因此,在shell中使用nohup类工具将进程脱离终端时,进程仍在bash进程的下面,只有关闭终端时,子进程才转移到init/systemd进程下。
更通用的,设置进程组能够用来实现所谓的daemon类进程:和建立它们的父进程分离并独立存活的进程。
要发送信号给进程组,只需使用kill函数,并传递一个负数的PID值做为第二个参数,这表示将信号发送给该PID所在的进程组,该组里全部的进程都将收到该信号。例如,发送HUP信号给当前进程所在的进程组,这样
kill "HUP", -$$;
下面是一个daemon类程序的示例:
#!/usr/bin/env perl use strict; use warnings; defined (my $pid = fork) or die "Can't fork child: $!"; # 子进程 unless($pid) { setpgrp 0,0; # 脱离组 alarm 10; # 计时器10秒 while(1){ foreach (0..2){ print "A\n" if $_ == 0; print "B\n" if $_ == 1; print "C\n" if $_ == 2; } sleep 2; } } # 父进程中 print "Daemon Process created: $pid\n"; sleep 1; # 给子进程一点时间来脱离进程组 kill 9, -$$; # 杀掉本身以及没有脱离组的子进程
这段代码的逻辑很简单:父进程建立子进程后睡眠一秒钟以给子进程脱离组一点时间,而后父进程就自杀(发送终止信号给本身),而子进程本身加入本身的组,而后在后台运行一个循环,每一个循环都输出A、B、C后睡眠2秒,并经过设置一个alarm计时器在10秒后终止子进程。
上面的示例中,重点就在于父进程自杀后,子进程仍然在运行。