Как стать автором
Обновить
351.24
Рейтинг
OTUS
Цифровые навыки от ведущих экспертов

Чем сложна демонизация POSIX

Блог компании OTUSПрограммированиеC
Перевод
Автор оригинала: wikileaks.org

Вот как по мнению Стивенс вы должны демонизировать:

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();
}

У большинства людей возникает два вопроса к этому коду:

  1. Почему setsid() находится в первом дочернем элементе?

  2. Зачем делать fork() еще раз?

Почему setsid() находится в первом дочернем элементе?

Согласно Стивенсу, setsid() делает три важные вещи:

  1. Процесс становится лидером сеанса нового сеанса, который содержит только вызывающий процесс. (PID = SID)

  2. Процесс становится лидером группы процессов новой группы. (PID = SID = PGID)

  3. У процесса не будет управляющего терминала. Если он у него был до setsid(), то связь будет разорвана.

Кроме того, setsid() не может завершиться успехом, если вызывающий процесс уже является лидером группы процессов (PID = PGID), поэтому необходимо сначала вызвать fork(), который гарантирует, что вновь созданный процесс не является лидером группы процессов (он наследует идентификатор группы от родителя).

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

Зачем делать fork() еще раз?

На это есть две причины:

  1. Для того, чтобы процесс был переподчинен init, его родительский процесс должен завершиться. Большинство благоразумных демонов Unix делают это, поэтому им не нужно снова вызывать fork(). Но если родительский процесс НЕ собирается завершаться, тогда, если вы хотите, чтобы дочерний процесс был переподчинен init, скажем, потому что вы не собираетесь вызывать для него waitpid(), тогда вы ДОЛЖНЫ вызвать fork() во второй раз. Если вы не вызовете fork() второй раз и не вызовете waitpid() для дочернего элемента, то он станет зомби. Когда родитель, наконец, умирает, его ребенок-зомби может быть переподчинен init, но он навсегда останется зомби, потому что init никогда не получит SIGCLD, потому что ребенок уже мертв, и поэтому init никогда не вызовет waitpid().

  2. В операционных системах на основе System V второй вызов fork() не позволяет демону когда-либо снова получить управляющий терминал. Единственные широко используемые сегодня варианты System V: AIX, Solaris и HP-UX, так что это не повод использовать fork() второй раз, если вы не ожидаете увидеть одну из этих систем.

Так почему я создаю зомби?

Когда процесс завершается, система предполагает, что другой процесс может захотеть вызвать для него wait/waitpid(), чтобы получить его статус выхода (как будто кому-то это небезразлично!). Если никто не вызывает wait/waitpid() для pid мертвого процесса, то запись сохраняется в ядре на неопределенный срок. Так на свет появляется зомби.

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

Как создать зомби

Вот как вы можете сделать зомби:

  1. Сделайте ребенка

  2. Дайте ему умереть, пока вы живы

  3. Выйдите, не вызывая 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».

Теги:posixязык cжизненный цикл подемонизациязомби
Хабы: Блог компании OTUS Программирование C
Всего голосов 13: ↑8 и ↓5+3
Просмотры2K

Похожие публикации

Лучшие публикации за сутки

Информация

Дата основания
Местоположение
Россия
Сайт
otus.ru
Численность
51–100 человек
Дата регистрации
Представитель
OTUS

Блог на Хабре