• Perl多进程


    本文关于Perl进程的内容主体来自于《Pro Perl》的第21章。

    创建新进程

    Perl中可以使用fork函数来创建新的进程,它会调用操作系统的fork系统调用来创建新进程。

    fork是Unix系统中的函数,在Windows中不原生支持fork。但从Perl 5.8开始,Perl提供了一个模拟的fork使其可以无视平台的差异,它是使用Perl解释器线程来实现的fork,因为解释器线程不自动共享数据,所以用来fork进程正好。换句话说,Perl 5.8开始fork是可以随意用来创建进程的。

    fork函数会派生自己,通过自己克隆出一个子进程。这个克隆过程是完整的,因为子进程和父进程在克隆的过程中是完全一致的,子进程和父进程共享代码,克隆完成后才设置一些各进程独有的属性,比如有自己的文件句柄(已经文件句柄上的锁)、进程ID、优先级等等属性。

    在fork新进程之后,就会有两个近乎完全一样的进程在并行运行。fork有两个返回值,一个是给父进程的返回值,这个返回值是fork出来的子进程的PID(如果fork失败,则返回undef),一个是给子进程的返回值,这个返回值为0。所以,通过fork的返回值可以判断出进程是子进程还是父进程

    if (my $pid = fork) {
        print "parent process
    ";
        print "child $pid: $pid
    ";
    } else {
        print "child process
    ";
        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
    ";
        exit 0;
    }
    
    # 父进程在此
    print "parent process
    ";
    print "Child process PID: $pid";
    

    fork为什么有两个返回值

    fork奇特的地方就在于针对不同的进程返回了不同的值,更严格地说是返回了两次。但任何一个函数都只能返回一次,因为一个return语句就结束函数了,那fork是如何实现两次返回的?

    对于$pid = fork这个语句,将其分成两个部分,一个是fork操作,一个是返回值赋值操作。在fork克隆完但fork还没结束时就已经有了两个进程,这两个进程的代码都一样,都在运行fork,两个fork都要赋值给$pid。可以认为是两个进程在执行fork,或者从程序的角度上看,是两个程序去调用了两次fork。

    虽说fork返回了两次,但实际上fork函数的返回值只有一个,只不过在不同环境下返回不同的值,fork只需一个环境判断就可以知道该返回哪个值:父进程的fork函数返回值要赋值给父进程的$pid,子进程的fork函数返回值要赋值给子进程的$pid

    fork父子进程、文件句柄和文件锁的关系

    如果需要搞懂这个细节,请参见fork、文件句柄、文件描述符和锁的关系

    fork + exec

    fork出来的子进程通常需要有一个退出语句,例如exit,否则子进程在执行完自己的代码后,有可能会执行父进程的代码,因为子进程和父进程是共享代码的。

    例如:

    defined (my $pid = fork) 
        or die "Can't fork child process: $!";
    
    unless ($pid) {
        # 子进程代码段
        print "In Child process
    ";   # (1)
    }
    
    # 父进程代码
    print "parent process here
    ";   # (2)
    print "The pid is: $pid
    ";      # (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
    ";   # (1)
        exit 0;
    }
    
    # 父进程代码
    print "parent process here
    ";   # (2)
    print "The pid is: $pid
    ";      # (3)
    

    更经常地,fork会结合exec家族的函数来加载其它程序替换当前进程中的程序,exec家族函数有一个共同的特性:执行完所加载的程序后自动退出进程。所以,就不再需要在子进程中加入exit语句。

    defined (my $pid = fork) 
        or die "Can't fork child process: $!";
    
    unless ($pid) {
        # 子进程代码段
        print "In Child process
    ";
        exec 'date +"%F %T"';
    }
    
    # 父进程代码
    print "parent process here
    ";
    print "The pid is: $pid
    ";
    

    exec函数的返回值是多余的,从来都不需要检查exec的返回值,但exec是否成功调用某个程序是需要检查的,例如上面无法调用date命令。但因为exec是执行完后就立即退出的,所以可以直接在exec后面加上错误处理语句,如die,只要能运行到die,说明exec失败了。

    unless ($pid) {
        # 子进程代码段
        print "In Child process
    ";
        exec 'date +"%F %T"';
        die "Exec failed: $!";
    }
    

    需要注意的是,exec COMMAND的COMMAND失败不代表exec失败,exec是发起系统调用,只有这个系统调用的过程中失败才算是失败,例如无法发起调用。COMMAND执行失败和exec已经无关,例如date命令不存在也已经表示exec成功发起了系统调用,所以不会运行到die语句。

    关于进程ID

    当前进程的PID可以使用特殊变量$$来获取,或者对应的英文形式$PID$PROCESS_ID也可以获取。

    print "my PID is $$
    ";
    

    对于Unix,可以通过子进程找出其父进程的PID,在Perl中可以使用getppid函数获取父进程的PID。

    $parent_PID = getppid;
    

    于是,可以发送HUP信号给父进程:

    kill "HUP", getppid;
    

    进程组和daemon

    当想要将信号发送给多个进程而非单个进程时,进程组的重要性就体现出来了。每个进程在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:
    ";
    
    unless($pid) {
            print "(Child)->PID: $$
    ";
            print "(Child)->PPID: @{ [ getppid ] }
    ";
            print "(Child)->GroupID: @{ [ getpgrp $$ ] }
    ";
            print "(Child)->ParentGroupID: @{ [ getpgrp getppid ] }
    ";
            sleep 2;   # 为了让后面的pstree收集子进程信息
            exit 0;
    }
    
    print "(Parent)->GroupID: @{[ getpgrp $$ ]}
    ";
    print "(Parent)->PPID: @{[ getppid ]}
    ";
    
    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;
    

    daemon类进程

    设置进程组一般用来隔离子进程和父进程,或者说让子进程脱离父进程,以免收到父进程发送的信号。比如让终端中的进程(它们是终端进程的子进程)脱离终端,这样发送信号给终端进程来终止终端时,只有脱离终端的子进程才能继续存活,终端进程自身以及其它终端子进程都将死亡。而在父进程死亡后,脱离了父进程的子进程都将成为孤儿进程(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
    " if $_ == 0;
                print "B
    " if $_ == 1;
                print "C
    " if $_ == 2;
            }
            sleep 2;
        }
    }
    
    # 父进程中
    print "Daemon Process created: $pid
    ";
    sleep 1; # 给子进程一点时间来脱离进程组
    kill 9, -$$;  # 杀掉自己以及没有脱离组的子进程
    

    这段代码的逻辑很简单:父进程创建子进程后睡眠一秒钟以给子进程脱离组一点时间,然后父进程就自杀(发送终止信号给自己),而子进程自己加入自己的组,然后在后台运行一个循环,每个循环都输出A、B、C后睡眠2秒,并通过设置一个alarm计时器在10秒后终止子进程。

    上面的示例中,重点就在于父进程自杀后,子进程仍然在运行。

  • 相关阅读:
    博客园二号地址:
    VisionMaster4.0.0二次开发教程(每日更新博客)
    5.观察者模式
    4.策略模式
    微信小程序radio的样式修改
    Linux下mysql安装
    idea2020 设置Run Dashboard
    一个方便统计页面 PV/UV、触发和交互的轻量级前端埋点工具
    登录认证-实名认证-产品设计
    启示录—产品经理
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10386933.html
Copyright © 2020-2023  润新知