Pull to refresh

Comments 29

Сильное решение, объёмный труд. Молодцы!
А зачем без MMU и без их эмуляции?
Автор написал, что он занимается встроенными системами, а для них зачастую используют процы без MMU, например, cortex-Mx.
Интересно, какая мотивация использовать vfork вместо потока?
Вообще, сейчас полчаса мучительно вспоминал, где читал про vfork. И вспомнил. «Linux. Системное программирование», Р. Лав. Там, в частности говорится, что в современных ядрах Linux fork «ленивый», в том смысле, что реальное копирование виртуальных страниц осуществляется лишь при изменении оных (насколько помню, в BSD это не так). Осмелюсь привести выдержку из книги, посвященную vfork.
Скрытый текст
До внедрения страниц, поддерживающих копирование при записи, разработчики UNIX были вынуждены реализовывать бесполезное копирование адресного про­ странства в течение ветвления, сразу за которым следовало выполнение exec. В связи с этим разработчики BSD представили в 3.0 BSD системный вызов, кото­рый называется vfork:
#include <sys/types.h>
#include <unistd.h>
pid_t vfork (void);
Успешное исполнение vfork() работает так же, как и fork(), кроме того, что дочерний процесс должен немедленно успешно вызвать одну из функций exec или завершиться, вызвав _exit() (будет рассмотрен в следующем разделе). Система vfork() не выполняет копирования адресного пространства и таблиц страниц, от­ носящихся к родительскому процессу, пока дочерний не завершится или не вы­ полнит новый двоичный образ. Между этим родительский и дочерний процессы совместно используют — без семантики копирования при записи — свое адресное пространство и страницы с табличными записями. Фактически единственная ра­бота, выполняемая vfork(), — дублирование внутренней структуры данных ядра. Следовательно, дочерний процесс не должен модифицировать никаких данных в памяти адресного пространства.
Системный вызов vfork() является архаизмом и не должен реализовываться в Linux, хотя, надо отдать должное, даже с учетом копирования при записи vfork() работает быстрее fork(), так как отпадает необходимость в самом копировании страниц. В любом случае появление страниц с поддержкой копирования при за­писи, любые аргументы в пользу альтернатив fork() утрачивают силу. Действительно, до появления ядра Linux 2.2.0 vfork() был просто оболочкой для fork(). Требования для vfork() менее строгие, чем для fork(), поэтому данная реализация вполне осуществима.
Строго говоря, ни одно внедрение vfork() не застраховано от ошибок. Представь­те, что вызов exec завершился сбоем: родительский процесс будет находиться в за­ блокированном состоянии, пока дочерний процесс не найдет способа разрешить эту ситуацию или не завершится. В программах лучше использовать fork() напрямую.
Возможно fork и ленивый, действительно таблицы помещаются флагом copy-on-write, что кстати в статье указано, Но тогда не понятно, зачем же в Linux добавили clone, если могли бы использовать fork. Вот ссылка на википедию, статья fork
Вы задаёте неправильный вопрос и потому никак не можете на него получить правильный ответ :-)

asm-generic/unistd.h

#define __NR_clone 220

#define __NR_vfork 1071

#define __NR_fork 1079

Догадываетесь о чём я? Подсказка: номера сисколлам выдаются более-менее в порядке их появления в ядре :-)
Хорошо. Перефразирую:
Почему же сначала добавили clone, а не полноценную поддержку fork.

Так лучше?:)
Не совсем. Тут уже начинает работать принцип: зачем задавать вопрос, ответ на который вы знаете? Или действительно не знаете? Не верю.

Чисто для справки, чтобы не было ненужных споров (это из исходников ядра):
SYSCALL_DEFINE5(clone, unsigned long, clone_flags, unsigned long, newsp,
		 int __user *, parent_tidptr,
		 int __user *, child_tidptr,
		 int, tls_val)
{
	return do_fork(clone_flags, newsp, 0, parent_tidptr, child_tidptr);
}
SYSCALL_DEFINE0(fork)
{
	return do_fork(SIGCHLD, 0, 0, NULL, NULL);
}
SYSCALL_DEFINE0(vfork)
{
	return do_fork(CLONE_VFORK | CLONE_VM | SIGCHLD, 0,
			0, NULL, NULL);
}

Извиняюсь, читал большей частью код, а текст — по диагонали, потому абзац про pthreads пропустил. Вопрос снимается
vfork — это всегда новый процесс, с новым исполняемым кодом. Идея замены его потоком не подходит никак.
Мы же про Linux говорим, так? С точки зрения ядра там поток и процесс практически идентичны. В данном контексте vfork от pthread будет отличаться процессом-родителем, файловыми дескрипторами, ну и обработчики сигналов. Я к тому, что если требуется наличие общего адресного пространства с процессом-родителем, то это pthread. Если нужен клон процесса с отдельным адресным пространством, то это fork. А зачем vfork?
vfork — это оптимизированный fork, на тот случай, когда дочерний процесс сразу вызывает exec.
Fork — это копия процесса. Vfork — то же самое, но без адресного пространства. Можете называть это «оптимизацией». Также можно pthread_create называть «оптимизацией» vfork. Но оптимизация подразумевает, что делается то же самое, но меньшей ценой. Тут это не так. Вот нынешний «ленивый» fork в Linux — это оптимизация.
можно pthread_create называть «оптимизацией» vfork

Нельзя, потому что pthread всегда запускается на новом стеке, в то время как vfork получает все регистры от родителя и гадит прямо на стек родителя.
оптимизация подразумевает, что делается то же самое, но меньшей ценой

В рамках дополнительных ограничений наложенных на него, vfork делает именно это.
vfork не делает того же, что форк. (точка) Это не оптимизация. Про треды вы могли не уточнять.
Вот вам ещё немного точек: [..........], не нервничайте так, всё будет хорошо.
Нельзя, потому что pthread всегда запускается на новом стеке, в то время как vfork получает все регистры от родителя и гадит прямо на стек родителя.
Куда там запускается pthread никого не волнует. Системный вызов возвращается в обоих процессах в одну и ту же точку. И да, изначально в Linux и fork(2) и vfork(2) были реализованы поверх clone(2). Отдельные системные вызовы появились относительно недавно (ну как недавно… лет десять назад) для того, чтобы полностью поддержать какие-то тонкие моменты из спецификации POSIX, связанные с сигналами.
Вы это просто так сказали, или в поддержку какого-то тезиса?
Системный вызов возвращается в обоих процессах в одну и ту же точку.

Только в разном состоянии. И в случае vfork родитель не продолжит выполнения до тех пор, пока потомок не вызовет exec.
Вы это просто так сказали, или в поддержку какого-то тезиса?
Системный вызов возвращается в обоих процессах в одну и ту же точку.
Это в ответ на ваше утверждение про то, что pthread всегда запускается на новом стеке. Нет, он не запускается на новом стеке, у двух процессов в момент возврата из сисколла стек общий. Переключение на другой стек делает уже библиотека pthreads, но её обёртку, как бы, использовать не обязательно.
Нет, он не запускается на новом стеке

Интересно, где это так?
Вот я читаю uClibc/libpthread/nptl/pthread_create.c и почти первой строчкой в функции __pthread_create_2_1 стоит
  int err = ALLOCATE_STACK (iattr, &pd);

Читаю glibc — почти то же самое.
Oops. Почему-то решил, что обсуждаются clone(2) и fork(2), а не pthread_create(3) и fork(2) — просто как-то не приходило в голову никогда сравнивать системные вызовы и библиотечные функции, они у меня в голове как-то немного в разных местах живут. Извиняюсь за дезинформацию. Да, разумеется, pthread_create(2) — это не «оптимизация»…
Если мне не изменяет память, то как раз наоброт. Изначально fork в Linux был простым и ленивым, и необходимости в vfork не было. Затем появлялись новые граничные условия, fork усложнялся, в результате от ленивости пришлось уйти. И тогда возникла необходимость в vfork. Чёрт, вот не могу найти статью по этому поводу, буквально два месяца назад поднимал.
Я читал, что vfork появился в BSD, когда они сказали, что не хотят каждый раз создавать адресное пространство. Потом это внесли в POSIX, а Linux долго использовал vfork как обертку над fork.
Да, что гадать, я считаю ман первоисточником, а там именно твоя точка зрения рассказана. Т.е. или статья гнала или я не так понял.

Единственно, что до ядра 2.2.0-pre6 vfork и fork были синонимами. После vfork стал тем, кем он есть. Ну а по сути своей в Linux они сводятся в системному вызову clone() с разными флагами. В BSD схожая картина была.

Ну и для информации разработчикам: POSIX.1-2008 removes the specification of vfork().
Если нужен клон процесса с отдельным адресным пространством, то это fork. А зачем vfork?

vfork — это если нужен процесс с отдельным адресным пространством, но не полноценный клон, а достаточный лишь для выполнения вызова exec(), который очень часто следует за fork'ом. И в таком смысле vfork можно назвать оптимизацией.

В общем же смысле это конечно не оптимизация, какой-нибудь андроидовый zygote можно реализовать только fork'ом.

Я бы назвал vfork костылём, но пришедшимся, видимо, очень кстати, поскольку он покрывает очень частый сценарий использования fork в связке с exec, достигая того же самого, но меньшей кровью.
Полностью согласен. Но замечу, что в том варианте, когда за fork следует exec, «оптимизация» при помощи vfork не является краеугольным камнем производительности системы.
Не соглашусь. В статье приводится сравнительная таблица, Из нее следует, что в некоторых случаях получается приличный выигрышь в производительности. Могу привести один случай. Создавалась встроенная система, ATM коммутатор, в ней использовался синтезируемый процессор Microblaze, в этом процессоре нет полноценной MMU поддержки, а есть только TLB. Для управления устройством решили использовать готовые решения ну и обернуть все это в скрипты. Обнаружились жуткие тормоза, окозалось что вот этот самый fork выполняется десятки-сотни милисекунд. То есть это была самая жрущая операция, которую стоило оптимизировать. Может это и редкий пример, но он реальный. Поэтому когда оптимизация дается таким простым образом, мне кажется это надо использовать.
Не всегда доступен posix_spawn, а для exec нужен именно процесс, т.к. при exec текущий замещается новым. Если это будет поток, у тебя всё приложение заместится.
Мощная статья, спасибо.

Рекомендую вместо странного сайта linuxmanpages использовать online-страницы мэйнтэйнера manpages, man7.org. Там всегда самые свежие версии манов. vfork (2).

Немного смутил неконсистентный порядок констант и переменных в if:
if (-1 == pid) {
        /* ... */
}
if (pid == 0) {
        /* ... */
}

if (0 > child_pid) {
        /* ... */
}
/* ... */

if (res < 0) {
        /* ... */
}

Sign up to leave a comment.