среда, 16 сентября 2009 г.

Common Lisp Daemon. Часть 1.

При развертывании Common Lisp приложения (например, сайта) на сервере (естественно, под управление GNU/Linux) возникает проблема организации lisp-демона, что связано с некоторыми техническими проблемами, которые, вероятно, могут вызвать у начинающих некоторое непонимание (или даже ужас).

Корень этих проблем в том, что обычно lisp процесс не может быть оторван от терминала (причины этого восходят к тем далёким временам, когда компьютеры были большими...) и при закрытии стандартного потока ввода немедленно завершает свою работу.

Для решения этой проблемы наибольшую известность получило решение на базе GNU Screen. Менее известен, но также используется метод основанный на использовании detachtty.

Я пробовал оба этих варианта и совершенно не удовлетворён ни одним из них (хотя и должен заметить, что решение на базе GNU Screen значительно удобней). В конце концов, это просто не нормально, что для решения проблемы демонизации необходимо использовать "не-lisp" инструменты, а итоговое решение очень сильно напоминает хак. С другой стороны, почему бы не реализовать демонизацию средствами самого Common Lisp, что здесь невозможного? Если исходить только из стандарта Common Lisp, то, очевидно, задача действительно нерешаема (ибо разработчики этого самого стандарта кажется совершенно не размышляли на данную тему), но если взять конкретную реализацию? Я попробовал реализовать pure-lisp демонизацию для SBCL и выяснил, что это довольно тривиально.

Итак, что должен делать приличный демон? Обычно это:
  • Отсоединиться от создавшего его родительского процесса (сделать fork)
  • Снизить свои привилегии до минимально возможных (ведь обычно демоны запускаются от имени root)
  • Закрыть стандартные потоки ввода/вывода/ошибок (0, 1, 2)
  • Сигнализировать об успешном запуске (либо сообщить об ошибке)
Все эти действия могут быть реализованы тривиальным образом.

Отсоединение от родительского процесса

Стандартно это делается за счёт системного вызова fork и ничего сложного из себя не представляет. Тут даже и говорить особо нечем:
(unless (= (sb-posix:fork) 0)
(quit))
Реальный код, конечно, будет немного сложнее, но так тоже работает.

Снижение привилегий

Работать от root демону совершенно ни к чему, поэтому:
(defun change-user (name &optional group)
(let ((gid)
(uid))
(when group
(setf gid
(sb-posix:group-gid (sb-posix:getgrnam group))))
(let ((passwd (sb-posix:getpwnam name)))
(unless group
(setf gid
(sb-posix:passwd-gid passwd))
(setf uid
(sb-posix:passwd-uid passwd))))
(sb-posix:setgid gid)
(alien-funcall (extern-alien "initgroups" (function int c-string int)) name gid)
(sb-posix:setuid uid)))

;;; устанавливаем владельцем процесса пользователя rulisp
(change-user "rulisp")
Данный код, правда, содержит очевидный изъян (если вспомнить о saved user id), но для демонстрации принципиальной возможности это не критично.

Переключение на псевдо-терминал

Просто так закрыть стандартный ввод нельзя (это приведёт к завершению процесса), но его можно переключить на псевдо-терминал. При использовании GNU Screen происходит переключение (на псевдо-терминал) всех стандартных потоков, но я использую GNU Screen только для того, чтобы увидеть какие ошибки произошли при запуске демона, т.е. полный сервис, предоставляемый GNU Screen является совершенно избыточным. Поэтому, я считаю достаточным связать стандартный поток ввода с псевдо-терминалом (для предотвращения угрозы завершения процесса, после чего о нём вообще можно забыть), а стандартные потоки вывода и ошибок просто направить в файл (что даст возможность быстро просмотреть как происходила загрузка демона):
(defun switch-to-slave-pseudo-terminal (out)
(flet ((c-bit-or (&rest args)
(reduce #'(lambda (x y) (boole boole-ior x y))
args)))
(let* ((fdm (sb-posix:open #P"/dev/ptmx" sb-posix:O-RDWR))
(slavename (progn
(alien-funcall (extern-alien "grantpt" (function int int)) fdm)
(alien-funcall (extern-alien "unlockpt" (function int int)) fdm)
(alien-funcall (extern-alien "ptsname"
(function c-string int)) fdm)))
(fds (sb-posix:open slavename sb-posix:O-RDONLY))
(log (sb-posix:open out
(c-bit-or sb-posix:O-WRONLY sb-posix:O-CREAT sb-posix:O-TRUNC)
(c-bit-or sb-posix:S-IREAD sb-posix:S-IWRITE sb-posix:S-IROTH))))
(sb-posix:dup2 fds 0)
(sb-posix:dup2 log 1)
(sb-posix:dup2 log 2))))

;;;; переключаем стандартный поток ввода на пседо-терминал,
;;;; а потоки вывода и ошибок направляем в /tmp/log
(switch-to-slave-pseudo-terminal #P"/tmp/log")
До сих пор мне не приходилось работать с псевдотерминалом, так что мог что-нибудь сделать не вполне корректно, но на моей системе работает без проблем.

Сигнализация об успешном запуске демона

После вызова fork получается два процесса, один из которых становится демоном, а другой завершается и, вообще говоря, должен сигнализировать об успешности запуска демона. При использовании решения на основе GNU Screen такого эффекта добиться не получается: узнать об успешности запуска демона можно только по результатам его работы. Для решения этой проблемы можно использовать демона с обратной связью, что требует возможности обработки сигналов. Послать сигнал родительскому приложению очень просто:
(sb-posix:kill (sb-posix:getppid) sb-posix:sigusr1)
Для обработки сигналов в SBCL можно воспользоваться функцией sb-sys:enable-interrupt:
(sb-sys:enable-interrupt sb-posix:sigusr1 #'usr1-signale-handler)
Итого:
Все технические проблемы, возникающие при создании демона, для SBCL решаются довольно тривиальным образом и необходимости в использовании GNU Screen и т.п. средств нет.

P.S. Это были технические детали, а в следующей части я покажу законченное решение, использующее описанные возможности (а также кое-что ещё) для запуска реального "Common Lisp Daemon".

3 комментария:

  1. Вот тут: http://icylisper.blogspot.com/2009/09/bootstrapping-lisp-environment.html схожая проблема решается при помощи detachtty

    ОтветитьУдалить
  2. С detachtty я работал и мне это очень не понравилось (я вроде сказал об этом в начале)

    ОтветитьУдалить