Ускорение разветвления больших процессов в Linux?
Какой самый быстрый и лучший способ для современного Linux добиться того же эффекта, что и fork
- execve
комбо из большого процесса?
Моя проблема заключается в том, что процесс разветвления имеет размер ~500 МБайт, а простой тест производительности позволяет получить всего около 50 форкс / с из процесса (ср. ~1600 форкс / с из процесса минимального размера), что слишком медленно для предполагаемого применения.
Некоторое прибегая к помощи прибывает vfork
как изобретенный в качестве решения этой проблемы... но и предупреждения о том, чтобы не использовать его. Современный Linux, похоже, приобрел связанный clone
а также posix_spawn
звонки; это может помочь? Какая современная замена для vfork
?
Я использую 64-битный Debian Lenny на i7 (проект может перейти на Squeeze, если posix_spawn
помог бы).
5 ответов
Результат: я собирался пойти по пути порожденного вспомогательного подпроцесса, как было предложено другими ответами здесь, но потом я наткнулся на это, используя огромную поддержку страниц для улучшения производительности форка.
Попробовав сам, используя libhugetlbfs, чтобы просто заставить все mallocs моего приложения выделять огромные страницы, я теперь получаю около 2400 форкс / с независимо от размера процесса (в любом случае, в диапазоне, который меня интересует). Удивительно.
В Linux вы можете использовать posix_spawn(2)
с POSIX_SPAWN_USEVFORK
флаг, чтобы избежать издержек при копировании таблиц страниц при разветвлении из большого процесса.
См. Минимизация использования памяти для создания подпроцессов приложения для хорошего обзора posix_spawn(2)
, его преимущества и некоторые примеры.
Чтобы воспользоваться vfork(2)
, убедись, что ты #define _GNU_SOURCE
до #include <spawn.h>
а потом просто posix_spawnattr_setflags(&attr, POSIX_SPAWN_USEVFORK)
Я могу подтвердить, что это работает на Debian Lenny и обеспечивает значительное ускорение при форкировании большого процесса.
benchmarking the various spawns over 1000 runs at 100M RSS
user system total real
fspawn (fork/exec): 0.100000 15.460000 40.570000 ( 41.366389)
pspawn (posix_spawn): 0.010000 0.010000 0.540000 ( 0.970577)
Вы на самом деле измеряли, сколько времени занимают вилки? Цитируя страницу, на которую вы ссылались,
У Linux никогда не было этой проблемы; Поскольку Linux использовал внутреннюю семантику копирования при записи, Linux копирует страницы только после их изменения (на самом деле, есть еще несколько таблиц, которые необходимо скопировать; в большинстве случаев их издержки незначительны)
Таким образом, количество вил не показывает, насколько большими будут накладные расходы. Вы должны измерять время, затрачиваемое на вилки, и (что является общим советом) только на вилки, которые вы фактически выполняете, а не на эталонный тест максимальной производительности.
Но если вы действительно поймете, что разветвление большого процесса является медленным, вы можете запустить небольшой вспомогательный процесс, направить мастер-процесс на его вход и получить команды для exec
от него. Небольшой процесс будет fork
а также exec
эти команды.
posix_spawn ()
Эта функция, насколько я понимаю, реализована через fork
/exec
на настольных системах. Однако во встроенных системах (особенно в тех, у которых нет MMU на плате) процессы порождаются через системный вызов, интерфейс к которому posix_spawn
или аналогичная функция. Цитирую информативный раздел описания стандарта POSIXposix_spawn
:
Обмен, как правило, слишком медленный для среды реального времени.
Динамическая трансляция адресов недоступна везде, где POSIX может быть полезен.
Процессы слишком полезны, чтобы просто отключить POSIX, когда он должен выполняться без трансляции адресов или других служб MMU.
Таким образом, POSIX нужны примитивы для создания процессов и выполнения файлов, которые могут быть эффективно реализованы без трансляции адресов или других сервисов MMU.
Я не думаю, что вы получите выгоду от этой функции на рабочем столе, если ваша цель - минимизировать затраты времени.
Если вы заранее знаете количество подпроцессов, возможно, было бы целесообразно предварительно запустить приложение при запуске, а затем распространять информацию execv через канал. В качестве альтернативы, если в вашей программе есть какое-то "затишье", может быть разумно заблаговременно раскошелиться на один или два подпроцесса для более быстрого выполнения в более позднее время. Ни один из этих вариантов не решит проблему напрямую, но если какой-либо подход подходит для вашего приложения, он может позволить вам обойти проблему.
Я наткнулся на это сообщение в блоге: http://blog.famzah.net/2009/11/20/a-much-faster-popen-and-system-implementation-for-linux/
pid = clone(fn, stack_aligned, CLONE_VM | SIGCHLD, arg);
Выдержка:
Системный вызов clone() приходит на помощь. Используя clone(), мы создаем дочерний процесс, который имеет следующие особенности:
- Дочерний работает в том же пространстве памяти, что и родительский. Это означает, что при создании дочернего процесса структуры памяти не копируются. В результате этого любое изменение любой переменной, не связанной со стеком, сделанное дочерним элементом, будет видимым родительским процессом. Это похоже на потоки, и поэтому полностью отличается от fork(), а также очень опасно - мы не хотим, чтобы ребенок испортил родительский.
- Дочерний элемент начинается с функции входа, которая вызывается сразу после создания дочернего элемента. Это как потоки, и в отличие от fork().
- У дочернего элемента есть отдельное пространство стека, которое похоже на потоки и fork(), но полностью отличается от vfork().
- Самое важное: этот нитевидный дочерний процесс может вызывать exec().
Вкратце, вызывая clone следующим образом, мы создаем дочерний процесс, который очень похож на поток, но все же может вызывать exec():
Однако я думаю, что это все еще может быть предметом проблемы setuid:
http://ewontfix.com/7/ "setuid and vfork"
Теперь мы дошли до худшего. Потоки и vfork позволяют вам попасть в ситуацию, когда два процесса совместно используют пространство памяти и работают одновременно. Теперь, что произойдет, если другой поток в родительском вызове вызовет setuid (или любую другую функцию, влияющую на привилегии)? В итоге вы получите два процесса с разными уровнями привилегий, работающих в общем адресном пространстве. И это плохо.
Рассмотрим, например, многопоточный серверный демон, изначально запускаемый от имени root, который использует posix_spawn, наивно реализованный с помощью vfork, для запуска внешней команды. Не имеет значения, запускается ли эта команда от имени root или с низкими привилегиями, поскольку она является фиксированной командной строкой с фиксированной средой и не может сделать ничего вредного. (В качестве глупого примера, скажем, это дата выполнения как внешняя команда, потому что программист не мог понять, как использовать strftime.)
Поскольку это не имеет значения, он вызывает setuid в другом потоке без какой-либо синхронизации с запуском внешней программы с намерением перейти к обычному пользователю и выполнить предоставленный пользователем код (возможно, скрипт или модуль, полученный dlopen) как пользователь. К сожалению, он просто дал этому пользователю разрешение на отображение нового кода поверх исполняемого кода posix_spawn или на изменение строк, которые posix_spawn передает exec в дочерний элемент. Упс.