Вывод и правило Broken pipe
Почему start() по умолчанию задаёт outputTarget в
/dev/null и что на самом деле идёт не так, когда фоновый процесс пишет в pipe,
который никто не читает.
Какую поломку это предотвращает
Когда outputTarget равен null, start() направляет stdout и stderr дочернего процесса в
pipe’ы обратно к родителю. У pipe фиксированный буфер ОС (~64 КБ). Если родитель его
не вычитывает:
- Дочерний процесс продолжает
echo; буфер заполняется. - Следующий
write()блокируется — дочерний процесс застревает и не делает работу. - Если родитель завершился или закрыл свой конец чтения, вместо этого запись вызывает
SIGPIPE/ «Broken pipe», убивая дочерний процесс.
Для fire-and-forget задачи — а это частый случай — это тихий убийца: задача будто бы запускается, а потом застревает или умирает по причинам, никак не связанным с её логикой.
Почему /dev/null — безопасное значение по умолчанию
При значении по умолчанию outputTarget = '/dev/null' start() вообще не открывает pipe —
ОС отдаёт stdout/stderr прямо в null-устройство. Ничего не буферизуется, ничего не блокируется,
а родителю не нужно управлять жизненным циклом. Можно запустить задачу и спокойно выбросить
объект Thread.
Компромисс
/dev/null безопасен, потому что его по определению никто не читает. Как только вы хотите
читать вывод, вы осознанно переходите на pipe (null) и берёте на себя обязанность его
вычитывать.
Три режима вывода
outputTarget |
Подключение | Когда |
|---|---|---|
'/dev/null' (по умолчанию) |
Отбрасывается ОС, без pipe | Фоновые fire-and-forget задачи |
'/path/to/file.log' |
Дописывается в файл | Хранить запись; staging/продакшн |
null |
Направляется родителю через pipe (неблокирующий) | Локальная разработка — вы активно читаете |
При файловой цели и stdout, и stderr открываются в режиме дописывания, поэтому переиспользовать
один лог-файл между запусками безопасно. При null pipe’ы переводятся в неблокирующий режим,
и вы читаете их через readOutput() / readError().
Чтение вывода из pipe
readOutput() / readError() вызывают stream_get_contents на неблокирующих pipe’ах и
возвращают всё, что пришло с момента последнего вызова. Два следствия, которые стоит знать:
- Вычитывайте в цикле, пока
isAlive()— одно чтение не захватит весь вывод долгой задачи, а переполнение буфера снова приведёт к Broken pipe. - После
join()они возвращают''—join()закрывает и очищает pipe’ы при завершении, поэтому читайте нужное до join (или в цикле, пока процесс жив).
--debug (флаг debugMode) ортогонален: он включает E_ALL + display_errors в дочернем
процессе, чтобы предупреждения и notices PHP появлялись в выбранной вами цели.
Связанное
- Жизненный цикл runner — где подключаются дескрипторы
- Логирование и отладка — практическое руководство
- Справочник API — параметры
start()