Около года назад мне понадобилось написать 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 <pid>
, я подумал, что пора бы переходить к реализации обработки системных сигналов.
Добавление обработки сигналов поначалу мне казалось пустяковой вещью, ведь в 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 !IsWasReborn() {
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 IsWasReborn() bool {
return return os.Getenv(envVarName) == envVarValue
}
Приведенный код прекрасно работает со стандартной библиотекой. Правда здесь используется пакет os/exec — высокоуровневая обертка над StartProcess.
Надо четко понимать, что здесь, в отличии от классического метода демонизации, весь ваш код, выполненный до вызова Reborn(), также будет выполнен в дочернем процессе. Если вы не хотите этого — следует использовать функцию IsWasReborn(). А так же дочерний процесс не наследует дескрипторы файлов (возможно, я добавлю это позже), поэтому родительский процесс должен закрывать все файлы до вызова Reborn(), а дочерний должен после вызова Reborn() перенаправлять стандартные потоки вывода в лог (также это позволит узнать что же произошло при неожиданном panic()), а ввода — на /dev/null.
После того, как мне пришлось написать еще пару демонов, я решил оформить функции демонизации в виде пакета и выложить на github: go-daemon. Так же в пакете доступны функции создания и блокировки pid-файлов и перенаправления потоков. Там же находится пример реализации простейшего демона на Go. Надеюсь этот материал будет кому-то полезен.
Ссылки:
Демон — Wikipedia
Пишем собственный linux демон
golang.org
Автор: Sevlyar