目錄

名稱

perlfork - Perl 的 fork() 模擬

概要

NOTE:  As of the 5.8.0 release, fork() emulation has considerably
matured.  However, there are still a few known bugs and differences
from real fork() that might affect you.  See the "BUGS" and
"CAVEATS AND LIMITATIONS" sections below.

Perl 提供了一個對應於 Unix 系統調用的 fork() 的關鍵字。 在大多數支持 fork() 系統調用的類 Unix 平台上,Perl 的 fork() 簡單地調用它。

在一些平台上,如 Windows,其中 fork() 系統調用不可用,Perl 可以在解釋器級別模擬 fork()。 儘管模擬設計得盡可能與 Perl 程序的真正 fork() 兼容,但由於所有以這種方式創建的虛擬子“進程”在操作系統看來都與同一個真實進程中存在,因此存在某些重要的區別。

本文概述了 fork() 模擬的功能和限制。 請注意,這裡討論的問題不適用於具有真正 fork() 的平台並且 Perl 已配置為使用它的平台。

描述

fork() 的模擬是在 Perl 解釋器的層次上實現的。一般來說,這意味著運行 fork() 會實際複製運行中的解釋器及其所有狀態,並在單獨的線程中運行複製的解釋器,在新線程中從父程序中調用 fork() 後的點開始執行。我們將實現此子「進程」的線程稱為偽進程。

對於調用 fork() 的 Perl 程序,所有這些都設計為透明。父進程從 fork() 返回時會返回一個偽進程 ID,該 ID 可以隨後在任何進程操作函數中使用;子進程從 fork() 返回時返回值為 0,表示它是子偽進程。

行為在 forked 偽進程中的其他 Perl 功能

大多數 Perl 功能在偽進程中的行為都很自然。

$$ 或 $PROCESS_ID

這個特殊變量被正確設置為偽進程 ID。它可用於識別特定會話中的偽進程。請注意,如果在等待其他偽進程之後啟動了任何偽進程,則此值將被回收利用。

%ENV

每個偽進程都維護自己的虛擬環境。修改 %ENV 將影響虛擬環境,僅在該偽進程內可見,在從中啟動的任何進程(或偽進程)中也是如此。

chdir() 及所有其他接受文件名的內建

每個偽進程都維護自己的虛擬當前目錄的概念。使用 chdir() 修改當前目錄僅在該偽進程內可見,在從中啟動的任何進程(或偽進程)中也是如此。所有來自偽進程的文件和目錄訪問將正確地將虛擬工作目錄映射到實際工作目錄。

wait() 及 waitpid()

wait() 及 waitpid() 可以傳遞由 fork() 返回的偽進程 ID。這些調用將正確等待偽進程的終止並返回其狀態。

kill()

kill('KILL', ...) 可以用於終止由 fork() 返回的 ID 所表示的虛擬進程。對虛擬進程的 kill 操作是不可預測的,除非在極端情況下使用,因為當運行的線程被終止時,操作系統可能無法保證進程資源的完整性。實現虛擬進程的進程可能會被阻塞,Perl 解譯器會掛起。請注意,在虛擬進程上使用 kill('KILL', ...) 可能通常會導致內存泄漏,因為實現虛擬進程的線程沒有機會清理其資源。

kill('TERM', ...) 也可用於虛擬進程,但當虛擬進程被系統調用阻塞時,例如等待連接套接字或嘗試從沒有可用數據的套接字讀取時,信號將不會被傳遞。從 Perl 5.14 開始,父進程將不會等待子進程在被信號化後退出,以避免進程退出時死鎖。您將需要顯式調用 waitpid() 以確保子進程有時間清理自身,但您也需要確保子進程不會在 I/O 上阻塞。

exec()

在虛擬進程內調用 exec() 實際上會在一個單獨的進程中生成所請求的可執行文件,並在它完成後退出,並具有相同的退出狀態作為該進程。這意味著運行可執行文件中報告的進程 ID 將與之前 Perl fork() 可能返回的不同。同樣,對 fork() 返回的 ID 應用的任何進程操作函數將影響調用 exec() 的等待虛擬進程,而不是 exec() 後等待的實際進程。

當 exec() 在虛擬進程內調用時,外部進程返回後仍將調用 DESTROY 方法和 END 块。

exit()

exit() 總是退出執行中的虛擬進程,自動等待任何未完成的子虛擬進程。請注意,這意味著除非所有運行中的虛擬進程都退出,否則整個進程將不會退出。有關打開文件處理的一些限制,請參閱下文。

打開文件、目錄和網絡套接字的處理

所有打開的處理在虛擬進程中都被 dup()-ed,因此在一個進程中關閉任何處理並不影響其他進程。有關一些限制,請參閱下文。

資源限制

在操作系統眼中,通過 fork() 模擬創建的虛擬進程只是同一進程中的線程。這意味著操作系統施加的任何進程級限制都適用於所有虛擬進程。這包括操作系統對打開文件、目錄和套接字處理的數量、磁盤空間使用限制、內存大小限制、CPU 利用率限制等。

殺死父進程

如果父進程被殺死(無論是使用 Perl 的 kill() 內建函數,還是使用某些外部手段),所有的虛擬進程也會被殺死,整個進程都會退出。

父進程和虛擬進程的生命周期

在正常情況下,父進程和它啟動的每個虛擬進程都會等待它們各自的虛擬子進程完成後再退出。這意味著只有在它們的虛擬子進程退出後,父進程和它啟動的每個同時也是虛擬父進程的虛擬子進程才會退出。

從 Perl 5.14 開始,父進程將不會自動等待任何已被信號化為 kill('TERM', ...) 的子進程,以避免在子進程在 I/O 上阻塞且永遠不接收信號時造成死鎖。

注意事項和限制

BEGIN 塊

當從 BEGIN 塊中調用 fork() 模擬時,它將無法完全正確地工作。分叉的副本將運行 BEGIN 塊的內容,但不會在 BEGIN 塊之後繼續解析源流。例如,考慮以下代碼

BEGIN {
    fork and exit;          # fork child and exit the parent
    print "inner\n";
}
print "outer\n";

這將打印

inner

而不是預期的

inner
outer

這個限制源於在解析過程中克隆和重新啟動 Perl 解析器使用的堆棧的基本技術困難。

打開的文件句柄

在 fork() 被調用時打開的任何文件句柄將被複製。因此,這些文件可以在父進程和子進程中獨立地關閉,但請注意,複製的句柄仍然共享相同的查找指針。在父進程中改變查找位置將會同時改變子進程中的查找位置,反之亦然。可以通過在子進程中分別打開需要獨立查找指針的文件來避免這種情況。

在某些操作系統中,特別是 Solaris 和 Unixware,從子進程調用 exit() 將會刷新並關閉父進程中打開的文件句柄,從而破壞文件句柄。在這些系統上,建議使用 _exit()_exit() 通過 Perl 的 POSIX 模塊可用。請參考您系統的 man 頁面以獲取有關此的更多信息。

打開的目錄句柄

Perl會完整地從所有打開的目錄處理程序中讀取,直到它們達到流的末尾。然後,它將seekdir()返回到原始位置,所有未來的readdir()請求將從緩存緩衝區中滿足。這意味著在fork()調用之後,既父進程持有的目錄處理程序,也子進程持有的目錄處理程序將不會看到對目錄所做的任何更改。

請注意,Windows上的rewinddir()也有類似的限制,它也不會強制readdir()再次讀取目錄。只有新打開的目錄處理程序才會反映對目錄所做的更改。

尚未實現的forking管道open()

open(FOO, "|-")open(BAR, "-|")結構尚未實現。這個限制可以通過在新代碼中明確創建一個管道來輕鬆解決。以下示例顯示了如何向分叉的子進程寫入

# simulate open(FOO, "|-")
sub pipe_to_fork ($) {
    my $parent = shift;
    pipe my $child, $parent or die;
    my $pid = fork();
    die "fork() failed: $!" unless defined $pid;
    if ($pid) {
        close $child;
    }
    else {
        close $parent;
        open(STDIN, "<&=" . fileno($child)) or die;
    }
    $pid;
}

if (pipe_to_fork('FOO')) {
    # parent
    print FOO "pipe_to_fork\n";
    close FOO;
}
else {
    # child
    while (<STDIN>) { print; }
    exit(0);
}

這個則從子進程中讀取

# simulate open(FOO, "-|")
sub pipe_from_fork ($) {
    my $parent = shift;
    pipe $parent, my $child or die;
    my $pid = fork();
    die "fork() failed: $!" unless defined $pid;
    if ($pid) {
        close $child;
    }
    else {
        close $parent;
        open(STDOUT, ">&=" . fileno($child)) or die;
    }
    $pid;
}

if (pipe_from_fork('BAR')) {
    # parent
    while (<BAR>) { print; }
    close BAR;
}
else {
    # child
    print "pipe_from_fork\n";
    exit(0);
}

forking管道open()結構將在將來得到支持。

XSUBs維護的全局狀態

維護自己全局狀態的外部子程式(XSUBs)可能無法正常工作。這樣的XSUBs將需要維護鎖,以保護來自不同虛擬進程的全局數據的同時訪問,或者將所有狀態保存在Perl符號表上,這在調用fork()時自然被復制。將提供一種回調機制,為擴展提供機會來克隆它們的狀態。

嵌入在較大應用程序中的解釋器

當在嵌入Perl解釋器並調用可以評估Perl代碼片段的Perl API的應用程序中執行fork()仿真時,fork()仿真可能不會按預期運行。這是因為仿真僅知道有關Perl解釋器自身的數據結構,對包含應用程序的狀態一無所知。例如,應用程序自身調用棧上的任何狀態都無法到達。

擴展的線程安全性

由於fork()仿真在多個線程中運行代碼,因此調用不安全的非線程安全庫的擴展在調用fork()時可能無法可靠工作。隨著Perl的線程支持逐漸在即使是具有本地fork()的平台上也被廣泛採用,這些擴展有望被修復以適應線程安全性。

可移植性注意事項

在可移植的Perl代碼中,不應在分叉的進程上使用kill(9, $child)。殺死分叉的進程是不安全的,並且具有不可預測的結果。請參閱上面的"kill()"

錯誤

作者

並發解釋器和fork()模擬的支持是由ActiveState實現的,並由Microsoft Corporation提供資金支持。

本文檔由Gurusamy Sarathy <gsar@activestate.com>編寫和維護。

參見

perlfunc中的"fork"perlipc