Pull to refresh

Golang daemon

Reading time 4 min
Views 33K
Около года назад мне понадобилось написать 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
Tags:
Hubs:
+26
Comments 36
Comments Comments 36

Articles