В чем причина создания двойного форка при создании демона?

165

Я пытаюсь создать демон в Python. Я нашел следующий вопрос , в котором есть несколько полезных ресурсов, за которыми я сейчас следую, но мне любопытно, зачем нужна двойная вилка. Я покопался в гугле и нашел множество ресурсов, объявляющих, что это необходимо, но не зачем.

Некоторые упоминают, что это предотвращает приобретение демоном управляющего терминала. Как бы это сделать без второй развилки? Каковы последствия?

Shabbyrobe
источник
2
Одна из сложностей, связанных с выполнением двойной ветвления, заключается в том, что родитель не может легко получить PID процесса внука ( fork()вызов возвращает PID дочернего элемента родителю, поэтому легко получить PID дочернего процесса, но не так просто получить PID процесса внука ).
Крейг МакКуин

Ответы:

105

Глядя на код, указанный в вопросе, обоснование:

Раздвиньте второго ребенка и немедленно выйдите, чтобы предотвратить появление зомби. Это приводит к тому, что второй дочерний процесс становится осиротевшим, что делает процесс init ответственным за его очистку. И, поскольку первый дочерний объект является лидером сеанса без управляющего терминала, он может приобрести его, открыв терминал в будущем (системы на основе System V). Этот второй ответвление гарантирует, что дочерний элемент больше не является лидером сеанса, предотвращая получение демоном управляющего терминала.

Таким образом, он должен гарантировать, что демон заново переименован в init (на случай, если процесс, запускающий демона, является долгоживущим), и исключает любые шансы демона на повторный захват управляющего tty. Так что, если ни один из этих случаев не применим, то одной вилки должно быть достаточно. " Сетевое программирование Unix - Стивенс " имеет хороший раздел на эту тему.

гулянка
источник
28
Это не совсем правильно. Стандартный способ создания демона - просто сделать p=fork(); if(p) exit(); setsid(). В этом случае родитель также выходит, и первый дочерний процесс переопределяется. Магия двойных вилок требуется только для предотвращения приобретения демона tty.
parasietje
1
Итак, насколько я понимаю, если моя программа запустится и forksбудет childзапущен процесс, этот самый первый дочерний процесс будет session leaderи сможет открыть терминал TTY. Но если я снова отключусь от этого потомка и прерву этого первого потомка, второй раздвоенный потомок не будет session leaderи не сможет открыть терминал TTY. Это утверждение правильно?
tonix
2
@tonix: просто разветвление не создает лидера сессии. Это сделано setsid(). Таким образом, первый разветвленный процесс становится лидером сеанса после вызова, setsid()а затем мы снова выполняем ветвление, так что последний, разветвленный процесс больше не является лидером сеанса. Помимо требования setsid()быть лидером сессии, вы на месте.
dbmikus
169

Я пытался понять двойную вилку и наткнулся на этот вопрос здесь. После долгих исследований это то, что я понял. Надеюсь, это поможет прояснить ситуацию для тех, у кого такой же вопрос.

В Unix каждый процесс принадлежит группе, которая, в свою очередь, принадлежит сеансу. Вот иерархия ...

Сеанс (SID) → Группа процессов (PGID) → Процесс (PID)

Первый процесс в группе процессов становится лидером группы процессов, а первый процесс в сеансе становится лидером сеанса. С каждым сеансом может быть связан один TTY. Только лидер сеанса может взять под контроль TTY. Для того, чтобы процесс был по-настоящему демонизирован (запущен в фоновом режиме), мы должны убедиться, что лидер сеанса убит, чтобы исключить возможность того, что сеанс когда-либо получит контроль над TTY.

Я запустил программу-демон Сандера Маречала на Python с этого сайта на моем Ubuntu. Вот результаты с моими комментариями.

1. `Parent`    = PID: 28084, PGID: 28084, SID: 28046
2. `Fork#1`    = PID: 28085, PGID: 28084, SID: 28046
3. `Decouple#1`= PID: 28085, PGID: 28085, SID: 28085
4. `Fork#2`    = PID: 28086, PGID: 28085, SID: 28085

Обратите внимание, что процесс является лидером сеанса после Decouple#1, потому что это PID = SID. Это все еще может взять под контроль TTY.

Обратите внимание, что Fork#2больше не является лидером сессии PID != SID. Этот процесс никогда не может взять под контроль TTY. Истинно демонизирован.

Лично я нахожу терминологическую форк-двойку непонятной. Лучшей идиомой может быть вилка-развязка-вилка.

Дополнительные ссылки, представляющие интерес:

Правин Голлакота
источник
Двойная ветвь также предотвращает создание зомби, когда родительский процесс выполняется дольше, и по какой-то причине удаляет обработчик по умолчанию для сигнала, информирующего о том, что процесс умер.
Трисмегистос
Но второй оператор for может также вызвать развязку и стать лидером сеанса, а затем приобрести терминал.
Трисмегистос
2
Это неправда. Первый fork()уже предотвращает создание зомби, при условии, что вы закроете родителя.
parasietje
1
Минимальный пример для получения приведенных выше результатов: gist.github.com/cannium/7aa58f13c834920bb32c
can.
1
Было бы хорошо позвонить setsid() перед синглом fork()? На самом деле я думаю, что ответы на этот вопрос ответят на это.
Крейг МакКуин
118

Строго говоря, двойная вилка не имеет ничего общего с повторным воспитанием демона как потомка init. Все, что необходимо для повторного воспитания ребенка, это то, что родитель должен выйти. Это может быть сделано только с одной вилкой. Кроме того, двойное разветвление само по себе не переопределяет процесс демона init; родитель демона должен выйти. Другими словами, родитель всегда выходит, когда разветвляет правильного демона, чтобы процесс демона был переназначен init.

Так почему двойная вилка? POSIX.1-2008 Раздел 11.1.3, « Терминал управления », содержит ответ (выделение добавлено):

Управляющий терминал для сеанса назначается руководителем сеанса в зависимости от реализации. Если лидер сеанса не имеет управляющего терминала и открывает файл терминального устройства, который еще не связан с сеансом, без использования O_NOCTTYопции (см. open()), То определяется, будет ли реализация управляющим терминалом лидера сеанса. Если процесс, который не является лидером сеанса, открывает файл терминала или используется O_NOCTTYопция open(), то этот терминал не должен становиться управляющим терминалом вызывающего процесса .

Это говорит нам о том, что если процесс-демон делает что-то вроде этого ...

int fd = open("/dev/console", O_RDWR);

... тогда процесс-демон может быть выбран в /dev/consoleкачестве управляющего терминала в зависимости от того, является ли процесс-демон лидером сеанса, и в зависимости от реализации системы. Программа может гарантировать, что вышеуказанный вызов не получит управляющий терминал, если программа сначала гарантирует, что он не является лидером сеанса.

Обычно при запуске демона setsidвызывается (от дочернего процесса после вызова fork), чтобы отделить демон от его управляющего терминала. Однако вызов setsidтакже означает, что вызывающий процесс будет лидером сеанса нового сеанса, что оставляет открытой возможность того, что демон может повторно захватить управляющий терминал. Метод двойного разветвления гарантирует, что процесс-демон не является лидером сеанса, что затем гарантирует, что вызов open, как в примере выше, не приведет к тому, что процесс-демон повторно запросит управляющий терминал.

Техника двойной вилки немного параноидальна. В этом нет необходимости, если вы знаете, что демон никогда не откроет файл терминального устройства. Кроме того, в некоторых системах это может не потребоваться, даже если демон действительно открывает файл оконечного устройства, поскольку такое поведение определяется реализацией. Однако одна вещь, которая не определяется реализацией, состоит в том, что только лидер сеанса может выделить управляющий терминал. Если процесс не является лидером сеанса, он не может выделить управляющий терминал. Поэтому, если вы хотите быть параноиком и быть уверенным, что процесс-демон не может случайно получить управляющий терминал, независимо от специфики, определенной реализацией, тогда метод двойного разветвления является существенным.

Дэн Молдинг
источник
3
+1 Жаль, что этот ответ пришел через четыре года после того, как был задан вопрос.
Тим Сегин
12
Но это все еще не объясняет, почему это так ужасно важно, что демон не может повторно
захватить
7
Ключевое слово «случайно» приобретает управляющий терминал. Если процесс откроет терминал, и он станет терминалом, управляющим процессами, то если кто-то из этого терминала выдаст ^ C, он может завершить процесс. Поэтому было бы неплохо защитить процесс от непреднамеренного случившегося с ним. Лично я буду придерживаться одной форка и setsid () для кода, который я пишу, и который, я знаю, не будет открывать терминалы.
BobDoolittle
1
@BobDoolittle, как это могло произойти "случайно"? Процесс не будет просто невольным открытием терминалов, если он не написан для этого. Возможно, двойная разветвленность будет полезна, если вы, программист, не знаете код и не знаете, может ли он открыть tty.
Мариус
10
@Marius Представьте себе , что может произойти , если вы добавить строчку в файл конфигурации вашего демона: LogFile=/dev/console. Программы не всегда имеют контроль во время компиляции над тем, какие файлы они могут открывать;)
Дэн Молдинг
11

Взято из Bad CTK :

«В некоторых разновидностях Unix вы вынуждены делать двойную форк при запуске, чтобы перейти в режим демона. Это потому, что одноразовое разветвление не гарантирует отсоединение от управляющего терминала».

Стефан Тиберг
источник
3
Как может одна вилка не отсоединиться от управляющего терминала, а двойная вилка сделать это? На каких юниксах это происходит?
Bdonlan
12
Демон должен закрыть свои дескрипторы входных и выходных файлов (fds), в противном случае он все равно будет подключен к терминалу, в котором он был запущен. Разветвленный процесс наследует их от родительского. По-видимому, первый ребенок закрывает FDS, но это не все очищает. На втором форке fds не существует, поэтому второй дочерний элемент больше не может быть связан ни с чем.
Аарон Дигулла
4
@ Аарон: Нет, демон должным образом «отсоединяется» от своего управляющего терминала, вызывая setsidпосле начальной форка. Затем он гарантирует, что он остается отсоединенным от управляющего терминала, снова разветвляясь и имея выход лидера сеанса (вызывающий процесс setsid).
Дэн Молдинг
2
@bdonlan: Дело не в forkтом, что он отсоединяется от управляющего терминала. Это то, setsidчто делает это. Но setsidпотерпит неудачу, если он будет вызван руководителем группы процессов. Поэтому сначала forkнеобходимо выполнить инициализацию, setsidчтобы обеспечить setsidвызов из процесса, который не является лидером группы процессов. Второй forkгарантирует, что конечный процесс (тот, который будет демоном) не является лидером сеанса. Только лидеры сеансов могут получить управляющий терминал, поэтому этот второй ответвление гарантирует, что демон не получит непреднамеренный запрос управляющего терминала. Это верно для любой POSIX OS.
Дэн Молдинг
@DanMoulding Это не гарантирует, что второй дочерний элемент не получит управляющий терминал, потому что он может вызвать setsid и стать лидером сеанса, а затем получить управляющий терминал.
Трисмегистос
7

Согласно «Расширенному программированию в среде Unix» Стивенса и Раго, второй ответвление является скорее рекомендацией, и оно сделано для того, чтобы гарантировать, что демон не получит управляющий терминал в системах на основе System V.

Паоло Тедеско
источник
3

Одна из причин заключается в том, что родительский процесс может немедленно поднять wait_pid () для потомка, а затем забыть об этом. Когда потом умирает внучка, его родителем является init, и он будет ждать () этого - и выводит его из состояния зомби.

В результате родительский процесс не должен знать о разветвленных дочерних элементах, а также позволяет разветвлять долго выполняющиеся процессы из библиотек и т. Д.

KarlP
источник
2

Вызов daemon () имеет родительский вызов _exit () в случае успеха. Первоначальная мотивация, возможно, заключалась в том, чтобы позволить родителю выполнять дополнительную работу, пока ребенок демонизирует.

Это также может быть основано на ошибочном убеждении, что это необходимо для того, чтобы у демона не было родительского процесса и он был переименован в init - но это произойдет в любом случае, когда родитель умирает в случае с одним форком.

Так что я полагаю, что в конце концов все сводится к традиции - достаточно одной развилки, если родитель все равно умрет в коротком порядке.

bdonlan
источник
2

Достойное обсуждение этого, кажется, находится на http://www.developerweb.net/forum/showthread.php?t=3025

Цитирую Млампкина оттуда:

... думать о вызове setsid () как о "новом" способе выполнения вещи (отсоединении от терминала) и о вызове [second] fork () после него как о избыточности для работы с SVr4 ...

Stobor
источник
-1

Это может быть легче понять следующим образом:

  • Первый форк и setsid создадут новый сеанс (но идентификатор процесса == идентификатор сеанса).
  • Вторая ветвь удостоверяется, что идентификатор процесса! = Идентификатор сессии.
pandy.song
источник