Пакет · thread · глубокое погружение

Swoole и доставка payload

Почему stdin-канал, работающий везде, ломается под Swoole, и как альтернативные режимы доставки передают сериализованную задачу с нулём канальных дескрипторов на стороне родителя. О выборе режима см. справочник по режимам payload.

Каскад утечки fd

По умолчанию payload передаётся через stdin-канал, создаваемый proc_open (дескриптор 0). Под Swoole с SWOOLE_HOOK_ALL каждая операция с файловым дескриптором перехватывается и оборачивается в корутину, и этот канальный fd утекает во внутреннюю таблицу Swoole:

  1. Родитель записывает payload и вызывает fclose($pipe).
  2. Swoole освобождает fd через socket_free_defer, считая его сокетом, — и получает EBADF.
  3. Fd остаётся «грязным» во внутренней таблице Swoole.
  4. При следующем запросе ядро переиспользует тот же номер fd для нового канала.
  5. Swoole видит номер как уже зарегистрированный → posix_spawn() failed: Bad file descriptor.

Решение — вообще не создавать канальный fd на стороне родителя. Это делают два транспорта.

Выбор автоматический

AdaptiveEngine (по умолчанию) обнаруживает активный рантайм Swoole — внутри корутины или при включённом SWOOLE_HOOK_ALL — и самостоятельно переключается на TempFileTransport, так что путь без конфигурации уже безопасен для Swoole. Привязывайте транспорт явно только для того, чтобы переопределить этот выбор: Thread::bindEngine(new AdaptiveEngine(transport: new ShmTransport())).

TempFileTransport

Payload записывается во временный файл в sys_get_temp_dir(), ему выставляется chmod 0600 до записи любых данных, и он передаётся дочернему процессу как stdin (['file', $path, 'r']). В момент возврата из proc_open родитель удаляет файл через unlink — дочерний процесс уже держит дескриптор, поэтому читает payload целиком, хотя на диске не остаётся записи в каталоге.

  • Канальный fd в родителе никогда не создаётся.
  • Без дополнительных расширений — работает везде, где доступен для записи sys_get_temp_dir().
  • Универсальный, безопасный вариант по умолчанию для Swoole / ReactPHP / Amp.

ShmTransport

Payload записывается в сегмент разделяемой памяти System V (shmop_open с флагом 'n', права 0600). Ключ сегмента передаётся дочернему процессу в командной строке как --shmkey; stdin устанавливается в /dev/null. Дочерний процесс читает сегмент, немедленно вызывает shmop_delete, затем продолжает работу.

  • Ни канального fd, ни дискового I/O — payload целиком живёт в RAM.
  • Требует ext-shmop.
  • Лучший вариант для очень крупных payload или высокоинтенсивной отправки, где задержка записи temp-файла проявляется в профиле.

Очистка, устойчивая к сбоям

Обычно дочерний процесс удаляет свой сегмент сам. Но если он упадёт до чтения, сегмент бы утёк — поэтому ключ переносится out-of-band и освобождается позже. stage() возвращает его в StagedPayload::$ref; родительский ProcessHandle держит этот подготовленный payload, и когда процесс пожинается (join()/reap()/деструктор → finish()), он вызывает PayloadTransport::cleanup(), который через shmop_delete удаляет любой уцелевший сегмент. Таким образом, освобождение происходит в более позднем вызове, чем start(), и Thread не приходится самому отслеживать ключ.

Уникальные ключи

Каждый stage() выводит ключ через crc32(uniqid('__wtr_thread_', true) . $seq) & 0x7fffffff — статический счётчик $seq держит его уникальным внутри процесса, а маска & 0x7fffffff сбрасывает знаковый бит, так что ключ является неотрицательным int на любой сборке. Он повторяет попытку до пяти раз при коллизии, поэтому конкурентная отправка от множества объектов Thread никогда не разделяет один сегмент.

Выбор

text
Внутри Swoole / ReactPHP / Amp?
├── Нет → PipeTransport (по умолчанию в CLI)
└── Да  → ext-shmop доступен И payload большой / высокочастотный?
        ├── Да  → ShmTransport        (только RAM, ноль дискового I/O)
        └── Нет → TempFileTransport    (без расширений, работает везде)

Под активным рантаймом Swoole AdaptiveEngine уже выбирает TempFileTransport за вас — безопасный универсальный вариант; берите ShmTransport только после того, как измерите узкое место temp-файла.

Связанное