Perl多進程

本文關於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爲何有兩個返回值

fork奇特的地方就在於針對不一樣的進程返回了不一樣的值,更嚴格地說是返回了兩次。但任何一個函數都只能返回一次,由於一個return語句就結束函數了,那fork是如何實現兩次返回的?線程

對於$pid = fork這個語句,將其分紅兩個部分,一個是fork操做,一個是返回值賦值操做。在fork克隆完但fork還沒結束時就已經有了兩個進程,這兩個進程的代碼都同樣,都在運行fork,兩個fork都要賦值給$pid。能夠認爲是兩個進程在執行fork,或者從程序的角度上看,是兩個程序去調用了兩次fork。code

雖然說fork返回了兩次,但實際上fork函數的返回值只有一個,只不過在不一樣環境下返回不一樣的值,fork只需一個環境判斷就能夠知道該返回哪一個值:父進程的fork函數返回值要賦值給父進程的$pid,子進程的fork函數返回值要賦值給子進程的$pidhtm

fork父子進程、文件句柄和文件鎖的關係

若是須要搞懂這個細節,請參見fork、文件句柄、文件描述符和鎖的關係

fork + exec

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語句。

關於進程ID

當前進程的PID可使用特殊變量$$來獲取,或者對應的英文形式$PID$PROCESS_ID也能夠獲取。

print "my PID is $$\n";

對於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:\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;

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\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秒後終止子進程。

上面的示例中,重點就在於父進程自殺後,子進程仍然在運行。

相關文章
相關標籤/搜索