Golang daemon

Около года назад мне понадобилось написать linux демона, реализующего небольшой сетевой сервис. В то время я активно изучал Go и мне очень нравился этот язык, поэтому взвесив все за и против я решил реализовать задачу на нем. К тому же, Go уже был стабильным и имел версию 1.0.1.

О том, с какими подводными камнями мне пришлось столкнуться, читайте под катом, но сразу оговорюсь: я буду описывать только тонкости реализации демона на Go. Если вы слабо представляете что такое «демон» или как демонизируется процесс, сначала стоит об этом почитать, поискав в гугле или на хабре «linux daemon» или пройдясь по списку ссылок в конце статьи.

Но вернемся к демонам. Сначала я решил действовать классически:
  • Порождение дочернего процесса и завершение родительского (системный вызов fork);
  • Далее в дочернем процессе:
    • Установка маски для прав доступа на вновь создаваемые файлы (системный вызов umask);
    • Создание нового сеанса, отключение от терминала (системный вызов setsid);
    • Смена рабочей директории на корневую (системный вызов chdir);
    • Перенаправление дескрипторов потоков стандартного ввода/вывода на /dev/null.

Отсутствие в стандартном пакете syscall чистого fork меня не остановило и даже не вызвало никаких подозрений. Я просто сделал примерно так (упрощено):
ret, _, err := syscall.Syscall(syscall.SYS_FORK, 0, 0, 0)
if err != 0 {
	os.Exit(2)
}
if ret > 0 {
	// родительский процесс
	os.Exit(0)
}

Реализовав все пункты, запустив демона и полюбовавшись выводом команд ps -eafw и
lsof -p , я подумал, что пора бы переходить к реализации обработки системных сигналов.

Добавление обработки сигналов поначалу мне казалось пустяковой вещью, ведь в Go есть стандартный пакет os/signal. Но когда я проделал это работу, мой демон наотрез отказывался получать эти самые сигналы. Причем если я убирал fork, обработка сигналов работала отлично. Сей факт меня весьма огорчил. Тогда я начал искать информацию в сети и, почитав code.google.com/p/go/issues/detail?id=227, огорчился еще больше. Собственно вывод был прост: В Go нельзя использовать fork, т.к. дочерний процесс не наследует потоки, а это означает, что все горутины (goroutines), заблокированные системными вызовами в потоках, отличных от текущего, отваливаются.

Тогда я оставил в покое обработку сигналов и начал экспериментировать с горутинами. Оказалось, что после вызова fork они прекрасно запускаются и работают в дочернем процессе. Открыв и почитав исходный код пакета os/signal, я понял, что все дело в этом коде:
func init() { signal_enable(0) // first call - initialize go loop() }

Здесь, в функции инициализации пакета, функция loop() запускается в качестве отдельной горутины. Это происходит еще до вызова функции main(). Функция loop() в цикле запрашивает очередной системный вызов и передает его назначенным обработчикам. Получается, что при вызове fork, перестает функционировать loop(). Но, горутины прекрасно запускаются и работаю после вызова fork. Значит надо делать вызов этой функции init() после вызова fork, решил я.

Я полностью скопировал код пакета os/signal, элементарно переименовал функцию init() в Init() и добавил ее вызов после fork. После чего обработка сигналов заработала ценой отказа от стандартной библиотеки и путем создания велосипеда.

Спустя какое-то время я пришел к выводу, что мой демон состоит из: костыль - одна штука и велосипед - одна штука. А костыль от того, что если еще какому-то пакету захочется создать горутину в функции инициализации, то пакет откажется корректно работать в демоне. Поэтому я решил поискать немного другой путь, и копание в стандартной библиотеке натолкнуло меня на мысль использовать функцию StartProcess. Поковыряв исходники, я понял, что эта функция последовательно делает системные вызовы fork и exec безопасным образом. По сути мы ничего не теряем, только дочерний процесс как бы перезапускается заново, а значит, надо как-то сообщать ему об этом. Чтобы он мог спокойно закончить демонизацию, проведя системные вызовы далее по списку. Сначала я использовал передачу аргументов командной строки, а потом решил для уведомления дочернего процесса передавать переменную окружения _GO_DAEMON=1.

В результате я написал примерно такой код:
const (
	envVarName  = "_GO_DAEMON"
	envVarValue = "1"
)
func Reborn(umask uint32, workDir string) (err error) {
	if !WasReborn() {
		var path string
		if path, err = filepath.Abs(os.Args[0]); err != nil {
			return
		}
		cmd := exec.Command(path, os.Args[1:]...)
		envVar := fmt.Sprintf("%s=%s", envVarName, envVarValue)
		cmd.Env = append(os.Environ(), envVar)
		if err = cmd.Start(); err != nil {
			return
		}
		os.Exit(0)
	}
	syscall.Umask(int(umask))
	if len(workDir) == 0 {
		if err = os.Chdir(workDir); err != nil {
			return
		}
	}
	_, err = syscall.Setsid()
	return
}
func WasReborn() bool {
	return os.Getenv(envVarName) == envVarValue
}

Приведенный код прекрасно работает со стандартной библиотекой. Правда здесь используется пакет os/exec - высокоуровневая обертка над StartProcess.

Надо четко понимать, что здесь, в отличии от классического метода демонизации, весь ваш код, выполненный до вызова Reborn(), также будет выполнен в дочернем процессе. Если вы не хотите этого - следует использовать функцию WasReborn(). А так же дочерний процесс не наследует дескрипторы файлов (возможно, я добавлю это позже), поэтому родительский процесс должен закрывать все файлы до вызова Reborn(), а дочерний должен после вызова Reborn() перенаправлять стандартные потоки вывода в лог (также это позволит узнать что же произошло при неожиданном panic()), а ввода - на /dev/null.

После того, как мне пришлось написать еще пару демонов, я решил оформить функции демонизации в виде пакета и выложить на github: go-daemon. Так же в пакете доступны функции создания и блокировки pid-файлов и перенаправления потоков. Там же находится пример реализации простейшего демона на Go. Надеюсь этот материал будет кому-то полезен.

Ссылки:
Демон - Wikipedia
Пишем собственный linux демон
golang.org
Поделиться публикацией
Комментарии 34
    +1
    Он поддерживает start/stop/status?
      0
        0
        Нет, я имею ввиду поддержку именно в вашем коде. Это важно. То, что делается через системы инициализации, это обычно обертки на mydaemon start/stop/status
      0
      два return в IsWasReborn
        0
        Один на «is», другой на «was».
          0
          Благодарю, исправил
          0
          Если я вас правильно понял и вы имеете ввиду service <daemon> {start|stop|status}, то обычно это реализуется с помощью init.d скрипта и механизмов вроде start-stop-daemon, и моя библиотека не поддерживает этого в чистом виде.
          Извините, промахнулся, это ответ на коментарий habrahabr.ru/post/187668/#comment_6523268
            0
            При беглом просомтре не увидел дропа привелегий.
              0
              сори не заметил. =)
                0
                Это можно легко реализовать, после вызова Reborn(), примерно так:
                if syscall.Getuid() == 0 {
                    if err := syscall.Setgid(GID); err != nil {
                        log.Println(err)
                    } else {
                        if err := syscall.Setuid(UID); err != nil {
                            log.Println(err)
                        }
                    }
                }
                
                +4
                Любопытно… Какую задачу вы решали?
                Мы взяли за правило запускать все критичные приложения, либо monit'ом либо с помощью systemd. Это даёт возможность без проблем контролировать состояние приложения и спокойно рестартануть его, если с ним возникнут проблемы. При таком подходе потребности в штатном механизме демонов на Go ни разу не возникло…
                  0
                  и upstart, да
                    +1
                    Upstart для хипстеров :-)
                      0
                      Убунту мы не используем… )
                        0
                        мы на debian живем
                          0
                          Ну, тогда вы тоже скоро познакомитесь с systemd… Думаю не пожалеете… )
                            0
                            хм. когда выбирал — под дебиан еще не было порта, надо посмотреть
                              0
                              В wheezy уже есть… Был опрос, больше половины разработчиков Debian положительно вызказалось в отношении systemd. Так что вопрос времени.
                      0
                      Благодарю, хороший вопрос.
                      В статье я не упоминал механизмы мониторинга и перезапуска демона в случае сбоя. Да, для этой задачи надо использовать инструменты вроде monit или fscd во FreeBSD.
                      А задачи разные, на тот момент я писал сервер для gsm трекера. И использовать systemd или подобные решения я не мог, так как требования диктует заказчик, он бы не обрадовался дополнительным зависимостям и экспериментами на боевом сервере. Если я точно знал, что написав демона, System V init скрипт и скрипт инсталляции, и они прекрасно заработают на любом дистрибутиве (с небольшой корректировкой скриптов и перекомпиляцией демона, при необходимости), то не факт, что заказчик захочет или сможет настроить systemd.
                      К тому же, хотя это и парадокс, доступ к своему серверу мне давать никто не собирался (хотя мои скрипты и ПО они на нем запускали без вопросов). Да и иногда требуют поддержки нескольких дистрибутивов, например недавно нужно было написать сервис, который бы работал на Debian и FreeBSD. Насколько мне известно не существует порта systemd для FreeBSD, поправьте если это не так.
                      Да и по моему скромному мнению, киллер фича systemd — скорость загрузки, что в первую очередь важно для десктопных систем, а не для серверов, которые работают без перезагрузки по нескольку месяцев.
                        0
                        Для десктопов эта «киллер фича» тоже не сдалась, когда есть suspend и hibernate.
                          0
                          По большому счёту не суть важно что используется для запуска приложения. Это никак не влияет на код. Во FreeBSD мы cобственно используем monit. Можно использовать банальный nohup или screen/tmux, если иногда нужно видеть вывод… Способов много. Для наших задач нормальная конкуррентность Gо важней и востребованней, чем нормальная демонизация… Ваш пример примечателен тем, что показывает, что Go достаточно сильный язык для того, чтобы делать вещи для которых он изначально не предназначался. За это спасибо…

                          Что же касается systemd, то скорость загрузки — конечно не последняя киллерфича, но и вовсе не первая… У него много прелестей, хотя мне олдскульному юниксоиду он по началу тоже сильно не понравился. Слишком много функционала во одном флаконе. Но тем не менее он свою работу делает и делает её качественно. За это мы его и ценим…
                          0
                          А что вы пишете на Go?
                            +1
                            Сетевые сервисы и сложную обработку данных хранящихся в mongodb…
                              0
                              Посылаю вам лучи зависти.
                                0
                                Зачем мне ваши лучи зависти? Лучше пишите код на Go… )))
                                  0
                                  Тут вокруг за это не платят :(
                          0
                          Технически, Google как раз позиционирует Go как системный язык (к вопросу «делать вещи для которых он изначально не предназначался»). Поэтому я даже немного удивился, что в языке, заточенном под написание многопоточных (ок, пусть много-гороутинных ;) ) демонов возникли такие проблемы собственно с демонизацией. Скорее всего, этот момент просто упустили при проектировке, возможно исправят в какой-то версии языка, если девелоперам Go указать на эту проблему.
                            0
                            Нет, не упустили. Либо демонизация, либо конкурентность. Одновременно это нормально работать не может. Точнее это можно заставить работать при каких-то конкретных условиях как в данной статье. fork'и не совместимы с идеологией принятой в Go.
                              0
                              Честно говоря, не понимаю, чем демонизация мешает конкурентности и наоборот.
                              В гугле ведь (да и не только) все конкурентные сервисы только в виде демонов и работают, поди.

                              fork() изначально и не задумывался как средство обеспечения конкурентности при обработке запросов, другое дело, что он худо-бедно удовлетворяет потребность в этом. Но как ни крути — без fork-а ОС не обойтись, и то, что на платформе Go есть проблемы с fork() — это конкретный недосмотр (отнюдь не умаляю преимуществ Go, сам обожаю этот язык).
                                +1
                                Вы просто не очень хорошо понимаете разницу между форком и процессом…
                                Если совсем грубо, то разница в том, что процесс создает собственный контекст и имеет возможность изменять контекст родителя, а форк клонирует контекст родителя и все изменения в контексте форка никак не отражаются на родителе.

                                Проблема в том, что процессы не являются частью контекста родителя и при создании форка склонироваться по понятным причинам не могут. И если процессы изменяют контекст родителя, который перед этим сделал форк, то форк никогда об этом не узнает, и взаимодействовать с процессами запущенными ранее форк не сможет. Из этого следует целый ряд эффектов и проблем, которые способны превратить написание многопоточных, конкурентных приложений в настоящий ад, рядом с которым асинхронное программирование это детский лепет. Это как раз то, чего пытались избежать cоздавая язык Go.
                                  0
                                  Прошу прощения, но то, что Вы написали выше, не соответствует действительности.
                                  Во первых, fork() — это операция, а процесс — примитив (обьект, сущность) ОС — сравнивать их и искать разницу просто не корректно.
                                  Во вторых, никакой процесс не имеет возможности повлиять напрямую на контекст родителя — для связи процессов можно использовать только IPC.

                                  Если вы под «процесс» имеете ввиду «тред» — то так и пишите, будет понятно о чем дискуссия.

                                  Исходная проблема возникла откуда — из того факта, что до фактически запуска кода программы платформа Go запустила (неявно) goroutine по обработке сигналов. Если бы не эта особенность, то и этой статьи не было бы за ненадобностью.
                                    0
                                    Если вы под «процесс» имеете ввиду «тред» — то так и пишите, будет понятно о чем дискуссия.

                                    Если вы не поняли где я взял слово процессы, то http://golang.org/doc/faq#csp… Тут речь идет о программировании, если вы заметили. Ваше понимание терминов имеет отношение скорее к администрированию…

                                    Исходная проблема возникла откуда — из того факта, что до фактически запуска кода программы платформа Go запустила (неявно) goroutine по обработке сигналов. Если бы не эта особенность, то и этой статьи не было бы за ненадобностью.

                                    И? Заканчивайте мысль… Почему запущенный goroutine мешает сделать fork()?

                                    Никакой дискуссии нет. Я просто по-доброму попытался объяснить вам почему fork() не может работать в рамках концепции CSP принятой в Go. Сам я в своё время с этим вопросом в полной мере разобрался далеко не сразу… Но вы же похоже восприняли это как оскорбление и обвинение в некомпетентности…
                                      0
                                      По ссылке http://golang.org/doc/faq#csp слово «process» встречается только в названии модели конкурентного исполнения кода
                                      One of the most successful models for providing high-level linguistic support for concurrency comes from Hoare's Communicating Sequential Processes, or CSP

                                      В платформе Go используется термин goroutine. Общепринятое значения термина «процесс» в программировании я Вам не буду обьяснять за отсутствием необходимости (искренне надеюсь на это).

                                      fork() может работать на платформе Go и работает успешно, за исключением того, что goroutine-ы не могут быть перезапущены в потомке, если они заблокированы в другом треде в родителе. Технически, для шедулера Go нет проблемы перезапустить треды для горутин и возобновить в них исполнение запущеных горутин, за исключением случая, упомянутого выше. Если бы os/signal не требовал бы горутины для своей работы, либо давал возможность перезапустить её явно, то все было бы в порядке.
                                  +1
                                  Как раз Google ушёл от использования в своём коде fork'ов. С ними немало проблем.

                                  Это собственно и отразилось в отстутствии форков в языке Go. С ходу, к сожалению, не смог найти ссылку… Об этом было толи в faq то ли в группе golang-nuts.

                                  fork'и это довольно специфичная для unix/posix вещь и ничего в них жизненно необходимого для операционной системы нет. В родном API Windows их например нет, и не похоже, чтобы windows плохо себя чувствовала из-за этого. И тем более нет проблемы c fork'ами в Go. Они там, по большому счету не нужны… Для того, чтобы отвязать проложение от консоли и дать возможность выполняться в системе без активного терминала, как я уже говорил, есть масса способов.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое