perlfork
СОДЕРЖАНИЕ
- НАЗВАНИЕ
- СИНТАКСИС
- ОПИСАНИЕ
- ПРИМЕЧАНИЯ И ОГРАНИЧЕНИЯ
- ПРИМЕЧАНИЯ ПО ПЕРЕНОСИМОСТИ
- ОШИБКИ
- АВТОР
- СМОТРИТЕ ТАКЖЕ
НАЗВАНИЕ
perlfork - Эмуляция fork() в Perl
СИНТАКСИС
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 с тем же именем. На большинстве платформ Unix, где доступен системный вызов fork(), Perl's fork() просто вызывает его.
На некоторых платформах, таких как Windows, где системный вызов fork() недоступен, Perl может быть скомпилирован для эмуляции fork() на уровне интерпретатора. Хотя эмуляция разработана для максимальной совместимости с реальным fork() на уровне программы Perl, существуют важные различия, которые вытекают из того, что все псевдодочерние «процессы», созданные таким образом, живут в одном реальном процессе с точки зрения операционной системы.
Этот документ предоставляет общий обзор возможностей и ограничений эмуляции fork(). Обратите внимание, что обсуждаемые здесь проблемы не относятся к платформам, где доступен реальный fork() и Perl настроен на его использование.
ОПИСАНИЕ
Эмуляция fork() реализована на уровне интерпретатора Perl. Это означает, что вызов fork() фактически клонирует работающий интерпретатор и все его состояние, а клонированный интерпретатор выполняется в отдельном потоке, начиная выполнение в новом потоке сразу после точки вызова fork() в родительском процессе. Мы будем называть поток, реализующий этот дочерний «процесс», псевдопроцессом.
Для программы Perl, вызвавшей fork(), всё это должно быть прозрачным. Родительский процесс возвращается из fork() со значением идентификатора псевдопроцесса, которое может быть использовано в дальнейшем в функциях управления процессами; дочерний процесс возвращается из fork() со значением 0 для обозначения того, что это дочерний псевдопроцесс.
Поведение других функций Perl в разветвлённых псевдопроцессах
Большинство функций Perl ведут себя естественным образом в псевдопроцессах.
- $$ или $PROCESS_ID
-
Это специальная переменная, правильно установленная для идентификатора псевдопроцесса. Её можно использовать для идентификации псевдопроцессов в конкретной сессии. Обратите внимание, что это значение может быть переиспользовано, если после запуска одних псевдопроцессов другие были обработаны с помощью wait().
- %ENV
-
Каждый псевдопроцесс поддерживает свою виртуальную среду. Изменения в %ENV влияют на виртуальную среду и видны только внутри этого псевдопроцесса, а также в любых процессах (или псевдопроцессах), запущенных из него.
- chdir() и все остальные встроенные функции, принимающие имена файлов
-
Каждый псевдопроцесс поддерживает своё виртуальное представление текущей директории. Изменения текущей директории с помощью chdir() видны только внутри этого псевдопроцесса и в любых запускаемых из него процессах (или псевдопроцессах). Все обращения к файлам и директориям из псевдопроцесса правильно сопоставляют виртуальную рабочую директорию с реальной рабочей директорией.
- wait() и waitpid()
-
wait() и waitpid() могут принимать идентификатор псевдопроцесса, возвращаемый функцией fork(). Эти вызовы правильно ждут завершения псевдопроцесса и возвращают его статус.
- kill()
-
kill('KILL', ...)может быть использовано для завершения псевдопроцесса, передав ему ID, возвращённый функцией fork(). Результат kill для псевдопроцесса непредсказуем, и его следует использовать только в крайних случаях, поскольку операционная система не гарантирует целостности ресурсов процесса при завершении работающего потока. Процесс, реализующий псевдопроцессы, может быть заблокирован, и интерпретатор Perl зависнет. Обратите внимание, что использованиеkill('KILL', ...)на псевдопроцессе может привести к утечкам памяти, потому что поток, реализующий псевдопроцесс, не получает возможности очистить свои ресурсы.kill('TERM', ...)также может быть использовано для псевдопроцессов, но сигнал не будет доставлен, пока псевдопроцесс заблокирован системным вызовом, например, ожидая подключения сокета или пытаясь прочитать из сокета без доступных данных. Начиная с Perl 5.14, родительский процесс не ждёт завершения потомков, если они были сигнализированы сkill('TERM', ...)для предотвращения тупика при выходе из процесса. Вам нужно будет явно вызвать waitpid(), чтобы убедиться, что дочерний процесс успел завершить себя, но тогда вы также несёте ответственность за то, что дочерний процесс не блокируется на ввода-выводе. - exec()
-
Вызов exec() внутри псевдопроцесса фактически запускает запрошенную программу в отдельном процессе и ждёт её завершения перед выходом со статусом, таким же как у этого процесса. Это означает, что идентификатор процесса, сообщаемый внутри работающей программы, будет отличаться от того, что мог вернуть предыдущий Perl fork(). Аналогично, любые функции управления процессами, применённые к ID, возвращённому fork(), будут влиять на ожидающий псевдопроцесс, вызвавший exec(), а не на реальный процесс, которого он ожидает после exec().
Если exec() вызван внутри псевдопроцесса, методы DESTROY и блоки END всё ещё будут вызваны после возврата внешнего процесса.
- exit()
-
exit() всегда завершает только выполняющийся псевдопроцесс, автоматически ожидая завершения любых активных дочерних псевдопроцессов. Обратите внимание, что это означает, что весь процесс не завершится, пока все выполняющиеся псевдопроцессы не завершат свою работу. Ниже приведены некоторые ограничения с открытыми дескрипторами файлов.
- Открытые дескрипторы файлов, каталогов и сетевых сокетов
-
Все открытые дескрипторы дублируются в псевдопроцессах, поэтому закрытие любых дескрипторов в одном процессе не влияет на другие. Ниже приведены некоторые ограничения.
Ограничения ресурсов
С точки зрения операционной системы, псевдопроцессы, созданные с помощью эмуляции fork(), представляют собой просто потоки в одном процессе. Это означает, что любые ограничения на уровне процесса, наложенные операционной системой, применяются ко всем псевдопроцессам вместе. Это включает любые ограничения, наложенные операционной системой на количество открытых файлов, каталогов и сокетов, ограничения на использование дискового пространства, ограничения на размер памяти, ограничения на использование ЦП и т. д.
Уничтожение родительского процесса
Если родительский процесс уничтожается (как с помощью встроенной функции Perl kill(), так и с помощью каких-либо внешних средств), все псевдопроцессы также уничтожаются, и весь процесс завершается.
Жизненный цикл родительского процесса и псевдопроцессов
В нормальном ходе событий родительский процесс и каждый псевдопроцесс, запущенный им, ждут завершения своих соответствующих псевдопотомков перед выходом. Это означает, что родительский процесс и каждый дочерний псевдопроцесс, который также является псевдородителем, выйдут только после завершения их псевдопотомков.
Начиная с Perl 5.14, родитель не будет автоматически ждать() любого потомка, который был сигнализирован с помощью kill('TERM', ...), чтобы избежать тупика, если потомок блокируется на вводе-выводе и никогда не получает сигнал.
ПРИМЕЧАНИЯ И ОГРАНИЧЕНИЯ
- BEGIN блоки
-
Эмуляция 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()). Таким образом, файлы можно закрывать независимо в родительском и дочернем процессах, но учтите, что дублированные дескрипторы по-прежнему будут использовать один и тот же указатель позиции. Изменение позиции в родительском процессе изменит её в дочернем, и наоборот. Можно избежать этого, открывая файлы, которым нужны отдельные указатели позиции, в дочернем процессе.
В некоторых операционных системах, особенно Solaris и Unixware, вызов
exit()из дочернего процесса будет сбрасывать и закрывать открытые дескрипторы файлов в родительском процессе, тем самым повреждая дескрипторы. В таких системах рекомендуется вызывать_exit()вместо этого._exit()доступен в Perl через модульPOSIX. Обратитесь к страницам руководства вашей системы для получения дополнительной информации. - Открытые дескрипторы каталогов
-
Perl полностью прочитает все открытые дескрипторы каталогов, пока не достигнет конца потока. Затем он вернётся к исходной позиции с помощью seekdir() и все последующие запросы readdir() будут выполняться из кэшированного буфера. Это означает, что ни дескриптор каталога, хранящийся в родительском процессе, ни в дочернем, не увидят никаких изменений, внесённых в каталог после вызова fork().
Обратите внимание, что rewinddir() имеет аналогичное ограничение в Windows и также не заставит readdir() повторно прочитать каталог. Только новый открытый дескриптор каталога отобразит изменения в каталоге.
- Forking pipe 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 pipe open() будут поддерживаться в будущем.
- Глобальное состояние, поддерживаемое XSUB
-
Внешие подпрограммы (XSUB), поддерживающие собственное глобальное состояние, могут не работать корректно. Таким XSUB необходимо либо поддерживать блокировки для защиты одновременного доступа к глобальным данным из разных псевдопроцессов, либо хранить все своё состояние в таблице символов Perl, которая копируется естественным образом при вызове fork(). В ближайшем будущем будет обеспечен механизм обратного вызова, предоставляющий расширениям возможность клонировать своё состояние.
- Интерпретатор, встроенный в большее приложение
-
Эмуляция fork() может не вести себя ожидаемым образом, если она выполняется в приложении, которое встраивает интерпретатор Perl и вызывает API Perl, которые могут оценивать фрагменты Perl-кода. Это связано с тем, что эмуляция имеет знания только о собственных структурах данных интерпретатора Perl и не знает ничего о состоянии содержащего приложения. Например, любое состояние, хранящееся в стеке вызовов самого приложения, недоступно.
- Потокобезопасность расширений
-
Поскольку эмуляция fork() выполняет код в нескольких потоках, расширения, вызывающие не потокобезопасные библиотеки, могут не работать надёжно при вызове fork(). По мере того как поддержка многопоточности Perl становится всё более распространённой даже на платформах с родной функцией fork(), ожидается, что такие расширения будут исправлены для потокобезопасности.
ОСОБЕННОСТИ ПЕРЕНОСИМОСТИ
В переносимом Perl-коде kill(9, $child) не следует использовать в виртуальных процессах. Убийство виртуального процесса небезопасно и имеет непредсказуемые результаты. Смотрите "kill()", выше.
ОШИБКИ
-
Использование псевдо-идентификаторов процессов в виде отрицательных целых чисел нарушает целостность для целочисленной переменной
-1, поскольку функции wait() и waitpid() обрабатывают это число как специальное. Подразумевается, что в текущей реализации система никогда не выделяет идентификатор потока1для пользовательских потоков. В будущем будет реализовано более подходящее представление для псевдо-идентификаторов процессов. -
В некоторых случаях системные дескрипторы, созданные операторами pipe(), socket() и accept(), очевидно, не дублируются корректно в псевдопроцессах. Это происходит только в некоторых ситуациях, но когда это происходит, это может привести к тупиковым ситуациям между сторонами чтения и записи трубок или к невозможности отправки или получения данных через сокеты.
-
Этот документ может быть неполным в некоторых аспектах.
АВТОР
Поддержка конкурирующих интерпретаторов и эмуляции fork() была реализована компанией ActiveState при финансовой поддержке корпорации Microsoft.
Этот документ написан и поддерживается Gurusamy Sarathy <gsar@activestate.com>.
СМОТРИТЕ ТАКЖЕ
© 1993–2020 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.32.0/perlfork