
Вот как по мнению Стивенс вы должны демонизировать:
void daemonize(const char *cmd) {
if ((pid = fork()) < 0)
err(1, "fork fail");
else if (pid != 0) /* parent */
exit(0)
/* child A */
setsid()
if ((pid = fork()) < 0)
err(2, "fork fail");
else if (pid != 0) /* child A * /
exit(0)
/* child B (grandchild) */
do_daemon_stuff();
}
У большинства людей возникает два вопроса к этому коду:
Почему
setsid()
находится в первом дочернем элементе?Зачем делать
fork()
еще раз?
Почему setsid() находится в первом дочернем элементе?
Согласно Стивенсу, setsid()
делает три важные вещи:
Процесс становится лидером сеанса нового сеанса, который содержит только вызывающий процесс. (PID = SID)
Процесс становится лидером группы процессов новой группы. (PID = SID = PGID)
У процесса не будет управляющего терминала. Если он у него был до
setsid()
, то связь будет разорвана.
Кроме того, setsid()
не может завершиться успехом, если вызывающий процесс уже является лидером группы процессов (PID = PGID), поэтому необходимо сначала вызвать fork()
, который гарантирует, что вновь созданный процесс не является лидером группы процессов (он наследует идентификатор группы от родителя).
Вызов setsid()
важен, потому что у демонов не должно быть управляющих терминалов. Если демон запускается из командной оболочки, он подвергается воздействию сигналов от управляющего терминала пользователя, которые могут спровоцировать его неожиданное завершение.
Зачем делать fork() еще раз?
На это есть две причины:
Для того, чтобы процесс был переподчинен
init
, его родительский процесс должен завершиться. Большинство благоразумных демонов Unix делают это, поэтому им не нужно снова вызыватьfork()
. Но если родительский процесс НЕ собирается завершаться, тогда, если вы хотите, чтобы дочерний процесс был переподчиненinit
, скажем, потому что вы не собираетесь вызывать для негоwaitpid()
, тогда вы ДОЛЖНЫ вызватьfork()
во второй раз. Если вы не вызоветеfork()
второй раз и не вызоветеwaitpid()
для дочернего элемента, то он станет зомби. Когда родитель, наконец, умирает, его ребенок-зомби может быть переподчиненinit
, но он навсегда останется зомби, потому чтоinit
никогда не получит SIGCLD, потому что ребенок уже мертв, и поэтомуinit
никогда не вызоветwaitpid()
.В операционных системах на основе System V второй вызов
fork()
не позволяет демону когда-либо снова получить управляющий терминал. Единственные широко используемые сегодня варианты System V: AIX, Solaris и HP-UX, так что это не повод использоватьfork()
второй раз, если вы не ожидаете увидеть одну из этих систем.
Так почему я создаю зомби?
Когда процесс завершается, система предполагает, что другой процесс может захотеть вызвать для него wait/waitpid()
, чтобы получить его статус выхода (как будто кому-то это небезразлично!). Если никто не вызывает wait/waitpid()
для pid
мертвого процесса, то запись сохраняется в ядре на неопределенный срок. Так на свет появляется зомби.
Даже если вы правильно демонизируете программу, в некоторых системах можно создать зомби из-за багов программы инициализации. Например, busybox - ошибка 2005 года, из-за которой плодились зомби, потому что init
не избавлялся от них. В этом случае, возможно, будет лучше вызывать waitpid
на дочерних процессах, а не форкать дважды.
Как создать зомби
Вот как вы можете сделать зомби:
Сделайте ребенка
Дайте ему умереть, пока вы живы
Выйдите, не вызывая
wait/waitpid
для ребенка
При этом я должен отметить, что во всех системах, которые я видел, это не создаст настоящего зомби по следующим причинам:
В некоторых системах есть kernel reaper поток, который ожидает мертвых процессов и выдает их родительский SIGCHLD (OpenBSD).
Некоторые системы автоматически переопределяют процесс для инициализации при
exit()
, а затем дают родителю SIGCHLD, вызывая зомби, от которого нужно избавиться (XNU/OSX)
Как сделать фоновую задачу
Для долгоработающих имплантов, обычно нет необходимости во втором форке. Если вы можете настроить обработчик сигналов для SIGCHLD, просто вызовите метод fork
после вызова setsid
и выполните задачу. Имплант получит сигнал SIGCHLD, когда ребенок завершится, после чего может быть вызван wait
.
static void signal_handler(int sig) {
int stat;
wait(&stat);
}
void main(void) {
struct sigaction sigact;
sigact.sa_handler = signal_handler;
sigemptyset(&sigact.sa_mask);
sigact.sa_flags = 0;
sigaction(SIGCHLD, &sigact, (struct sigaction *)NULL);
if ((pid = fork()) < 0)
err(1, "fork fail");
else if (pid != 0)
do_parent_stuff();
else {
setsid()
do_child_stuff();
}
}
Перевод статьи подготовлен в преддверии старта курса «Программист C».
Также приглашаем всех желающих на демо-урок «Жизненный цикл программы на C под Windows». На этом вебинаре мы рассмотрим полный жизненный цикл программы на языке C под ОС Windows, начиная от исходного кода и заканчивая загрузкой готового exe-файла. По ходу дела посмотрим "под капот" различным низкоуровневым механизмам операционной системы и тулчейна компиляции и познакомимся с инструментами для анализа программ.
- Узнать подробнее о курсе «Программист C».
- Смотреть вебинар «Жизненный цикл программы на C под Windows».