• Perl信号处理


    本文关于Perl信号处理的内容主体来自于《Pro Perl》的第21章。

    信号处理

    操作系统可以通过信号(signal)处理机制来实现一些功能:程序注册好待监视的信号处理机制,在程序运行过程中如果产生了对应的信号,则会按照注册好的处理方式进行处理。

    signal基础

    每个进程都记录了一个信号(signal)索引表,并注册了各种信号的处理方式,每当收到信号的时候,会立即停止执行操作并处理对应的信号。

    绝大多数信号都有默认处理机制,但Perl支持用户自己重新定义接收到信号时的处理方式。在Perl中,信号处理的方式注册在一个hash变量%SIG中,key为信号的名称,value有几种可能的值:

    • DEFAULT或undef:表示采取所接收信号的默认处理方式
    • IGNORE:表示忽略接收到的该信号
    • 子程序引用:如&subref或匿名子程序sub { codeblock },表示接收到该信号时,执行该子程序
    • 子程序:强烈建议不使用该类值

    要想查看支持的信号,可以遍历一下%SIG,或者直接在Linux下使用kill -l命令:

    $ perl -le 'print join qq/ /, sort keys %SIG'
    

    要查看信号对应的数值,可以去Config的sig_name里查找:

    #!/usr/bin/perl
    use strict;
    use warnings;
    
    use Config;
    my @signals = split ' ', $Config{sig_name};
    for (0..$#signals){
        print "$_ $signals 
    " unless $signals[$_] =~ /^NUM/;
    }
    

    记住几个常见的即可(数值|KEY|NAME):

    • 0 | ZERO | SIGZERO:检查进程是否存在
    • 1 | HUP | SIGHUP:发送HUP信号给终端来终止终端上的所有进程(终端的子进程),对daemon类程序还常重新定义该信号用来重新加载配置文件并reload服务
    • 2 | INT | SIGINT:中断进程,可被捕捉和忽略,几乎等同于sigterm,所以也会尽可能的释放执行clean-up,释放资源,保存状态等(CTRL+C)
    • 3 | QUIT | SIGQUIT:从键盘发出杀死(终止)进程的信号,优先级较高,可能还会发出core dump行为
    • 9 | KILL | SIGKILL:强制终止进程,该信号不可被捕捉。该信号是人为强制终止,而不是让操作系统内核去终止进程,所以进程收到该信号后不会执行任何clean-up行为,所以资源不会释放,状态不会保存
    • 10 | USR1 | SIGUSR1:用户自定义信号1
    • 12 | USR2 | SIGUSR2:用户自定义信号2
    • 13 | PIPE | SIGPIPE:已关闭的管道。当正在读的、或正在写入的管道已被对方关闭时,将触发该信号
    • 14 | ALRM | SIGALRM:alarm信号,当当前进程的alarm计时器(alarm定时器即一个定时器)到期了,将触发该信号。在Microsoft系统上未实现该信号
    • 15 | TERM | SIGTERM:杀死(终止)进程,可被捕捉和忽略,几乎等同于sigint信号,会尽可能的释放执行clean-up,释放资源,保存状态等,优先级高于INT,但低于QUIT和KILL
    • 17 | CHLD | SIGCHLD:当子进程中断或退出时,发送该信号告知父进程自己已完成,父进程收到信号将告知内核清理进程列表。所以该信号可以解除僵尸进程,也可以让非正常退出的进程工作得以正常的clean-up,释放资源,保存状态等
    • 18 | CONT | SIGCONT:发送此信号使得stopped进程进入running,该信号主要用于jobs,例如bg & fg 都会发送该信号。可以直接发送此信号给stopped进程使其运行起来
    • 19 | STOP | SIGSTOP:该信号是不可被捕捉和忽略的进程停止信息,收到信号后会进入stopped状态,直到接收到CONT信号后才继续运行
    • 20 | TSTP | SIGTSTP:该信号是可被忽略的进程停止信号(CTRL+Z)
    • 28 | WINCH | SIGWINCH:进程所在的控制终端或控制窗口大小发生了改变(例如拉大拉小图形界面程序的框框)会发送该信号。对于后台进程,由于没有窗口的概念,常常重新定义该信号用来实现graceful stop
    • 29 | IO | SIGIO:异步IO事件。如果文件句柄设置为异步IO(即O_ASYNC),当该文件句柄中产生了任何事件(例如可写事件)时都会发送该信号

    安全的信号

    需要注意的是,对于具有安全信号处理机制的语言(不止是Perl),需要保证在运行一条语句(严格地说是opcode)的时候不会被操作系统的信号处理机制中断,只有在当前正在处理的语句结束后,才会中断

    例如,在Perl进行IO的时候,信号不会终止正在进行的IO操作,而是在这次IO完成后再终止。再例如,正在执行排序操作的时候,不会在排序的过程中终止,而是当前排序过程完成后再终止。

    安全的信号机制优点很明显,它可以让程序更加健壮。但是缺点也很明显,因为有些操作可能会花费比较长的时间,然后才终止进程。当然,大多数时候这个缺点并不是什么大问题,但是有些情况下对时间长短的控制要求非常精确(比如反导弹系统,必须在一个很短的时间内计算出一些数据,这种程序很可能会直接定制操作系统实现特殊的功能),这样的情况就不适合使用这种安全的信号处理机制。

    从Perl 5.8开始,Perl就默认使用safe模式的信号处理机制。如果想要在Perl上使用非安全的信号处理机制,需要设置环境变量PERL_SIGNALS=unsafe

    信号处理

    前面说过,要想定制信号处理方式,只需在%SIG中注册对应的value即可。其中value有几种可能的值:

    • DEFAULT或undef:表示采取所接收信号的默认处理方式
    • IGNORE:表示忽略接收到的该信号
    • 子程序引用:如&subref或匿名子程序sub { codeblock },表示接收到该信号时,执行该子程序
    • 子程序:强烈建议不使用该类值

    注意,自定义信号处理方式,对于无法捕获的信号无影响,如SIGKILL信号是不可被捕捉的信号。

    例如,忽略INT信号,使得CTRL+C无效:

    $SIG{INT}='IGNORE';
    

    以下是一个完整的perl示例:

    #!/usr/bin/env perl
    use strict;
    use warnings;
    
    $SIG{INT} = 'IGNORE';
    
    for (1..3){
            print "hello $_
    ";
            sleep 2;
    }
    

    执行这个perl程序的时候,按下ctrl + c将无法终止程序,而是正常运行完。

    再例如,设置alarm信号为默认值'DEFAULT',alarm信号的默认处理机制是终止调用alarm的进程。

    $SIG{ALRM} = 'DEFAULT';
    

    设置信号的处理方式为一个自定义的子程序:

    $SIG{USR1} = &usr1handler;
    

    注意使用的是子程序引用,不要直接使用子程序。实际上,如果%SIG的value部分,如果不是子程序引用,也不是'DEFAULT'或IGNORE,其它字符串都表示以main包(不是当前包)的该子程序作为信号处理方式。例如:

    $SIG{USR1} = 'DEFLT';
    

    等价于:

    $SIG{USR1} = &main::DEFLT;
    

    而很多时候,这个子程序是不存在的。所以,请注意value部分的拼写。

    还可以直接定义一个匿名子程序作为信号处理的值。例如,收到INT信号时,清理一些临时文件(如pid文件):

    $SIG{INT} = sub {
        warn "received SIGINT, removing PID file and exiting.
    ";
        unlink "/var/run/perlapp.pid";
        exit 0;
    };
    

    正常的%SIG写法注册信号时,一次只能注册一个信号:

    $SIG{INT} = &handler;
    

    但可以通过下面的方式一次性注册多个信号处理方式

    %SIG = (%SIG, INT => IGNORE, PIPE => &handler, HUP => &handler);
    

    之所以能这么展开,是因为Perl在列表上下文会将列表、数组、hash(它们本质上都是列表)压扁展开,所以括号中的%SIG会展开成一个列表,然后重新定义了INT、PIPE、HUP信号的值,由于hash类型的key必须是唯一的,所以重新定义的key的值会覆盖已有的值。

    die和warn的信号处理

    Perl除了支持信号处理机制,还支持错误处理,特别是die和warn这两个行为(以及Carp模块中对应的crap和croak)。

    $SIG{__WARN__} = &yoursub;
    $SIG{__DIE__} = &yoursub;
    

    这些并不是真的信号,而是伪信号,Perl提供伪信号处理机制让我们定制一些事件的处理方式。在%SIG中并没有为这些伪信号设置默认值,所以如果需要设置伪信号的事件处理,需要手动设置,正如上面设置的方式。

    上面的前缀和后缀双下划线是可选的,只是为了让伪信号和真信号进行区分。当然,Perl并不允许我们在%SIG中随意创建信号名。

    写一个信号处理子程序

    如果某个信号的所注册的是一个子程序引用,那么在接收到这个信号的时候,会调用这个子程序,并传递信号的名称作为参数给子程序。

    例如:

    #!/usr/bin/perl 
    use strict;
    use warnings;
    
    sub handler {
        my $sig = shift;
        print "Caught SIGNAL: $sig
    ";
    }
    
    $SIG{INT} = &handler;
    
    for (1..3){
        sleep 2;
    }
    

    有些操作系统(特别是BSD系统)会在调用一次子程序后注销信号处理子程序,所以要想继续注册该信号的处理方式,可以在子程序中的开头(在开头加是为了避免信号触发后子程序调用过程中有新的信号进来)加上重新安装子程序的语句:

    sub handler{
        $sig = shift;
        # reinstall handler
        $SIG{$sig} = &handler;
        ...
        ...其它代码...
        ...
    }
    

    很多时候,并不希望正在处理某个信号的时候再次接收该信号(因为这个时候接收同样的信号是多余的行为),这时可以在子程序的开头将信号处理设置为"IGNORE"来忽略可能的新信号,再在子程序的结尾设置回原来的信号处理方式。

    下面的代码展示了这种处理逻辑:

    sub handler {
        $SIG{$_[0]} = 'IGNORE';
        ... do something ...
        $SIG{$_[0]} = &handler;
    }
    

    或者,更简便的方式是使用local关键字来修饰%SIG中对应的信号:

    sub handler {
        local $SIG{$_[0]} = 'IGNORE';
        ... do something ...
    }
    

    local关键字是在局部范围内操作全局变量,在退出范围时恢复全局变量。所以,上面的代码中,只有在handler函数内部临时设置了信号处理方式为"IGNORE",退出子程序后又恢复原来的信号处理方式。

    糟糕的信号处理子程序

    其实信号处理机制中隐含了一个关键点:强烈建议不要在信号处理程序中分配新内存。例如,新建一个变量保存某个值。

    例如,下面的示例中,就在每次信号处理的过程中,新建一个元素空间保存每个被触发的信号计数器的值:

    my %sigcount;
    sub allocatinghandler {
        $sigcount{$_[0]}++;
    }
    

    上面是不太好的编程方式,而下面修改后的代码则更好,因为在第一次调用子程序的时候,就分配好了一些空间(每个信号默认值都为0),在每次自增计数器计数的时候不会再新分配内存:

    %sigcount = map { $_ => 0 } keys %SIG;
    
    sub nonallocatinghandler {
        $sigcount{$_[0]}++;
    }
    

    发送信号(解释HUP信号和0信号)

    在Unix系统中,使用kill命令发送信号。在Perl中,也可以使用kill函数来发送信号。

    Perl kill函数至少两个参数,第一个参数是要发送的信号名,第二个或者后面的参数是待发送信号的PID。Perl kill的返回值为成功交付信号的进程数量(因为有些信号忽略的进程没必要计算是否接收了信号,所以忽略的信号不计数):

    # 发送INT信号给多个进程
    kill 'INT', @mychildren;
    
    # 更易读的方式
    kill INT => @mychildren, $grandpatoo;
    
    # 进程自杀
    kill KILL => $$;
    kill (9, $$);     # 使用数值格式的信号
    kill 9, $$;
    
    # 发送信号给父进程
    kill USR1 => getppid;
    

    其中getppid函数用来获取父进程的PID。

    向一个负数的PID发送信号,表示将信号发送给该PID所在进程组(包括子进程、兄弟进程,甚至可能会包括父进程)。例如,下面的语句表示发送HUP信号给当前进程自身所在的进程组:

    kill HUP => -$$;
    

    HUP信号经常会发送给父进程,然后父进程会发送给其所有子进程来终止它们,并重新初始化它们。例如apache httpd可以发送一个HUP信号给main进程,来重新fork子进程。当然,在这过程中,父进程自身可能并不希望被HUP终止,所以这时常为父进程设置信号忽略。如下:

    sub huphandler{
        local $SIG{HUP} = 'IGNORE';
        kill HUP => -$$;
    }
    

    信号0是特殊的信号,它不会有任何操作,仅仅用来检查进程是否存在。因为kill返回值是正确接收信号的进程数量,如果进程存在,0信号就会被接收但却不会做任何处理,但kill的返回值却为1。例如,检查某个子进程是否存在:

    kill (0 => $child) or warn "Child $child is dead!";
    

    SIGALRM信号:ALARM

    alarm常用来做一个计时器,计时到了就发送ALRM信号来终止计时器所在进程。

    可以通过alarm函数设置一个计时器,它的参数是0或正数,正数表示计时多少秒,0表示取消当前已有的计时器。每个进程只能有一个alarm计时器。

    # 30秒的计时器
    alarm 30;
    

    计时器计时到了,就会立即发送ALRM信号,该信号默认行为是终止当前进程,除非设置了ALRM信号的处理方式。例如,下面定义了一个2秒的计时器,后面还睡眠5秒:

    $ perl -le 'alarm 2;sleep 5;'
    

    在睡眠5秒的过程中,大概在第二秒后就直接终止进程了,而不是等到5秒都睡眠完。

    需要注意的是,前面说过安全的信号处理机制会等待当前正在执行的opcode执行完再处理信号,所以alarm定义的计时器可能并不那么精确,出现一点点的误差是经常性的。

    重新设置计时器会覆盖之前已有的计时器。例如:

    alarm 30;   # 30秒的计时器
    ... do something ...
    alarm 5;    # 覆盖前面的定时器,重新定义一个5秒的计时器
    

    alarm函数的参数设置为0表示取消已有的alarm计时器,但注意取消计时器不会发送SIGALRM信号。

    alarm 0;
    

    计时器有时候非常好用,它是非阻塞模式的sleep,可以让我们回到交互模式下并计时。例如,下面的示例中要求在5秒内输入一个字符,如果没输入就一直提示"Hurry UP:",并继续设置5秒的计时器等待输入,由于ReadKey是阻塞的,只要一输入就不再阻塞,于是进入后续语句并很快到达程序的尾部并正常结束。

    #!/usr/bin/perl
    use strict;
    use warnings;
    use Term::ReadKey;
    
    # Make read blocking until a key is pressed, and turn on autoflushing (no
    # buffered IO)
    ReadMode 'cbreak';
    $| = 1;
    
    sub alarmhandler {
        print "
    Hurry up!: ";
        alarm 5;
    }
    
    $SIG{ALRM} = &alarmhandler;
    
    alarm 5;
    print "Hit a key: ";
    my $key = ReadKey 0;
    print "
     You typed '$key' 
    ";
    
    # cancel alarm
    alarm 0;
    
    # reset readmode
    ReadMode 'restore';
    

    上面的alarm 0其实是多余的,因为只要输入了字符后,基本上立即就到达了程序的结尾而正常结束,所以不需要alarm 0来取消计时器。但在稍微大一点的程序中,取消计时器是很有必要的,因为我们不知道什么时候程序结束。

  • 相关阅读:
    在MPTCP中引入流量监控——bwm-ng的使用说明
    Ubuntu下配置MPTCP
    实现两台MPTCP主机之间的通信——VSFTPD的配置与使用
    Google 辟谣,Android 和 Chrome OS 不合并
    paper-7
    计算机网络方面国际三大顶尖会议
    谷歌物联网操作系统Android Things揭开面纱
    张纯如
    Android binder机制之 5 --(创建binder服务)
    【BZOJ 1491】[NOI2007]社交网络
  • 原文地址:https://www.cnblogs.com/f-ck-need-u/p/10386248.html
Copyright © 2020-2023  润新知