Perl信號處理

来源:https://www.cnblogs.com/f-ck-need-u/archive/2019/02/15/10386248.html
-Advertisement-
Play Games

信號處理 操作系統可以通過信號(signal)處理機制來實現一些功能:程式註冊好待監視的信號處理機制,在程式運行過程中如果產生了對應的信號,則會按照註冊好的處理方式進行處理。 signal基礎 每個進程都記錄了一個信號(signal)索引表,並註冊了各種信號的處理方式,每當收到信號的時候,會立即停止 ...


本文關於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 \n" 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 $_\n";
        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.\n";
    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\n";
}

$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 "\nHurry up!: ";
    alarm 5;
}

$SIG{ALRM} = \&alarmhandler;

alarm 5;
print "Hit a key: ";
my $key = ReadKey 0;
print "\n You typed '$key' \n";

# cancel alarm
alarm 0;

# reset readmode
ReadMode 'restore';

上面的alarm 0其實是多餘的,因為只要輸入了字元後,基本上立即就到達了程式的結尾而正常結束,所以不需要alarm 0來取消計時器。但在稍微大一點的程式中,取消計時器是很有必要的,因為我們不知道什麼時候程式結束。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ...
  • 1、冒泡排序 2、選擇排序 3、插入排序 4、希爾排序 5、歸併排序 6、快速排序 7、堆排序 8、計數排序 9、桶排序 10、基數排序 ...
  • sizeof和strlen區別 sizeof是關鍵字,在編譯時就能計算出值,可以計算任何類型 strlen是函數,只有在運行時才能去計算,且只能計算字元型的. 對於數組時,strlen是判斷’\0’為標誌結尾的,而sizeof則計算的是數組整個空間示例如下: 列印如下: 可以看到當我們的buf1沒有 ...
  • 第一題:extract變數覆蓋 知識簡介 extract()函數語法: 題目信息 Topic Link:http://123.206.87.240:9009/1.php 利用extract()函數的變數覆蓋漏洞原理構造payload 漏洞產生原因:extract()函數當只有一個參數時,預設的第二參 ...
  • 好多網站對於爬蟲中沒有進行瀏覽器偽裝的會進行反爬, 以糗事百科網站為例 下麵提供了三種方法添加headers,使爬蟲能夠偽裝成瀏覽器訪問。 備註: 方法二和方法三中省略了 方法一:通過opener添加header 方法二:通過opener批量添加header 方法三:通過Request添加heade ...
  • 實際開發過程中,經常會修改代碼重啟應用,每次手動重啟既麻煩開發效率又低,所以熱部署對於開發來說顯得十分必要,本文將介紹如何在IntelliJ IDEA(版本2018.2.5)中實現SpringBoot項目熱部署。 本文使用spring-boot-devtools實現的熱部署,按照下麵兩個步驟操作完成 ...
  • 不是電腦相關專業畢業的,從來沒用過leetcode,最近在學習數據結構和演算法,用leetcode練練手。 新手上路,代碼如有不妥之處,儘管指出來。 今天抽空做的第一個題:Two Sum(最簡單的呃呃呃) 題目如下: 解決思路: 現有數組nums[p-r],首先將nums從小至大排序,然後將nums ...
  • 二分查找特別好理解,就類似於快排和歸併當中用到的分治的思想,每次取中間數與目標數相比較,然後確定是大了還是小了,區間折半。 就比如: 小紅選中了1-100中的某個數字(這個數字是56),要小明來猜,產生如下對話: 小明第一次猜測:68 小紅:大了 小明第二次猜測:35 小紅:小了 小明第三次猜測:5 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...