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 提供了一个 fork() 关键字,它对应于同名的 Unix 系统调用。在大多数支持 fork() 系统调用的类 Unix 平台上,Perl 的 fork() 只是简单地调用它。
在某些平台(如 Windows)上,fork() 系统调用不可用,Perl 可以被构建为在解释器级别模拟 fork()。虽然模拟旨在尽可能地与 Perl 程序级别的真实 fork() 兼容,但由于所有创建的伪子“进程”在操作系统看来都位于同一个真实进程中,因此存在一些重要的差异。
本文档概述了 fork() 模拟的功能和限制。请注意,此处讨论的问题不适用于支持真实 fork() 且 Perl 已配置为使用它的平台。
fork() 的模拟是在 Perl 解释器级别实现的。这意味着运行 fork() 实际上会克隆正在运行的解释器及其所有状态,并在单独的线程中运行克隆的解释器,从父进程中调用 fork() 的位置开始在新的线程中执行。我们将实现此子“进程”的线程称为伪进程。
对于调用 fork() 的 Perl 程序,所有这些都设计为透明的。父进程从 fork() 返回一个伪进程 ID,该 ID 可以随后用于任何进程操作函数;子进程从 fork() 返回值 0
以表示它是子伪进程。
大多数 Perl 特性在伪进程中以自然的方式运行。
此特殊变量正确设置为伪进程 ID。它可以用于识别特定会话中的伪进程。请注意,如果在其他伪进程被 wait() 之后启动了任何伪进程,则此值可能会被回收。
每个伪进程都维护自己的虚拟环境。对 %ENV 的修改会影响虚拟环境,并且仅在该伪进程中以及从其启动的任何进程(或伪进程)中可见。
每个伪进程都维护自己对当前目录的虚拟概念。使用 chdir() 对当前目录的修改仅在该伪进程中以及从其启动的任何进程(或伪进程)中可见。来自伪进程的所有文件和目录访问将正确地将虚拟工作目录映射到实际工作目录。
wait() 和 waitpid() 可以传递由 fork() 返回的伪进程 ID。这些调用将正确等待伪进程的终止并返回其状态。
kill('KILL', ...)
可以通过传递由 fork() 返回的 ID 来用于终止伪进程。kill 对伪进程的结果是不可预测的,并且不应在紧急情况下使用,因为操作系统可能无法保证进程资源在运行线程终止时的完整性。实现伪进程的进程可能会被阻塞,并且 Perl 解释器会挂起。请注意,在伪进程() 上使用 kill('KILL', ...)
通常会导致内存泄漏,因为实现伪进程的线程没有机会清理其资源。
kill('TERM', ...)
也可用于伪进程,但当伪进程被系统调用阻塞时,例如等待套接字连接,或尝试从没有可用数据的套接字读取数据,信号将不会被传递。从 Perl 5.14 开始,父进程在使用 kill('TERM', ...)
信号子进程后,不会等待子进程退出,以避免进程退出时的死锁。您必须显式调用 waitpid() 以确保子进程有时间清理自身,但您也需要负责确保子进程不会阻塞在 I/O 上。
在伪进程中调用 exec() 实际上会在一个单独的进程中生成请求的可执行文件,并等待它完成,然后以与该进程相同的退出状态退出。这意味着在运行的可执行文件中报告的进程 ID 将与之前 Perl fork() 可能返回的进程 ID 不同。类似地,应用于 fork() 返回的 ID 的任何进程操作函数将影响调用 exec() 的等待伪进程,而不是它在 exec() 之后等待的真实进程。
当在伪进程中调用 exec() 时,DESTROY 方法和 END 块仍然会在外部进程返回后被调用。
exit() 始终只退出正在执行的伪进程,在自动 wait()-ing 任何未完成的子伪进程之后。请注意,这意味着除非所有正在运行的伪进程都已退出,否则整个进程将不会退出。有关打开文件句柄的一些限制,请参见下文。
所有打开的句柄都在伪进程中被 dup()-ed,因此在一个进程中关闭任何句柄不会影响其他进程。有关一些限制,请参见下文。
在操作系统看来,通过 fork() 模拟创建的伪进程只是同一个进程中的线程。这意味着操作系统施加的任何进程级限制都适用于所有伪进程的总和。这包括操作系统对打开的文件、目录和套接字句柄数量、磁盘空间使用量、内存大小、CPU 利用率等的任何限制。
如果父进程被杀死(无论是使用 Perl 的 kill() 内建函数,还是使用一些外部手段),所有伪进程也会被杀死,整个进程退出。
在正常情况下,父进程和它启动的每个伪进程都会等待它们各自的伪子进程完成,然后再退出。这意味着父进程和它创建的每个也是伪父进程的伪子进程,只有在它们的伪子进程退出后才会退出。
从 Perl 5.14 开始,父进程不会自动 wait() 任何被 kill('TERM', ...)
信号的子进程,以避免在子进程阻塞在 I/O 上且永远不会收到信号的情况下出现死锁。
fork() 模拟在 BEGIN 块中调用时不会完全正确地工作。分叉的副本将运行 BEGIN 块的内容,但不会在 BEGIN 块之后继续解析源代码流。例如,考虑以下代码
BEGIN {
fork and exit; # fork child and exit the parent
print "inner\n";
}
print "outer\n";
这将打印
inner
而不是预期的
inner
outer
这种限制源于在解析过程中克隆和重启 Perl 解析器使用的堆栈的根本技术困难。
在 fork() 时打开的任何文件句柄都将被 dup()-ed。因此,可以在父进程和子进程中独立关闭文件,但要注意,dup()-ed 句柄仍然共享相同的 seek 指针。更改父进程中的 seek 位置将更改子进程中的 seek 位置,反之亦然。可以通过在子进程中分别打开需要不同 seek 指针的文件来避免这种情况。
在某些操作系统上,特别是 Solaris 和 Unixware,从子进程调用 exit()
将刷新并关闭父进程中的打开的文件句柄,从而破坏文件句柄。在这些系统上,建议改为调用 _exit()
。_exit()
可通过 POSIX
模块在 Perl 中使用。有关此方面的更多信息,请参阅您系统的联机帮助页。
Perl 会完全从所有打开的目录句柄中读取,直到它们到达流的末尾。然后它会使用 seekdir() 返回到原始位置,所有未来的 readdir() 请求都将从缓存缓冲区中满足。这意味着父进程持有的目录句柄和子进程持有的目录句柄都不会看到 fork() 调用后对目录所做的任何更改。
请注意,rewinddir() 在 Windows 上也有类似的限制,也不会强制 readdir() 再次读取目录。只有新打开的目录句柄才会反映对目录的更改。
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);
}
分叉管道打开() 结构将在未来得到支持。
维护自身全局状态的外部子例程 (XSUB) 可能无法正常工作。此类 XSUB 或者需要维护锁以保护不同伪进程对全局数据的并发访问,或者将所有状态都维护在 Perl 符号表上,该符号表在调用 fork() 时会自然复制。在不久的将来将提供一个回调机制,为扩展提供一个克隆其状态的机会。
当 fork() 模拟在嵌入 Perl 解释器并调用可以评估 Perl 代码片段的 Perl API 的应用程序中执行时,它可能不会按预期工作。这源于模拟只了解 Perl 解释器自己的数据结构,而不知道包含应用程序的状态。例如,应用程序自己的调用堆栈上携带的任何状态都无法访问。
由于 fork() 模拟在多个线程中运行代码,因此在调用 fork() 时,调用非线程安全库的扩展可能无法可靠地工作。随着 Perl 的线程支持在具有原生 fork() 的平台上逐渐得到更广泛的采用,预计这些扩展将针对线程安全进行修复。
在可移植的 Perl 代码中,kill(9, $child)
不应用于派生进程。杀死派生进程是不安全的,并且会导致不可预测的结果。请参阅上面的 "kill()"。
由于 wait() 和 waitpid() 函数将该数字视为特殊数字,因此将伪进程 ID 设置为负整数对于整数 -1
会失效。当前实现中的默示假设是系统永远不会为用户线程分配 1
的线程 ID。将在未来实现更好的伪进程 ID 表示方法。
在某些情况下,pipe()、socket() 和 accept() 运算符创建的操作系统级句柄在伪进程中似乎没有被准确地复制。这只会发生在某些情况下,但在发生这种情况时,可能会导致管道句柄的读写端之间出现死锁,或者无法通过套接字句柄发送或接收数据。
本文档在某些方面可能不完整。
并发解释器和 fork() 模拟的支持由 ActiveState 实现,由微软公司提供资金。
本文档由 Gurusamy Sarathy <[email protected]> 撰写和维护。