пятница, 25 сентября 2009 г.

Перевод Practical Common Lisp в формате PDF

Перевод Practical Common Lisp теперь доступен в формате PDF: http://lisper.ru/pcl/pcl.pdf. Ещё не всё реализовано, в частности, не показываются таблицы, но читать можно :) Вся работа по генерации PDF выполняется pure-lisp кодом, каждый час, после синхронизации с основной вики.

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

Переменные, локальные относительно контекста

Gnu Emacs поддерживает Buffer-Local Variables, т.е. переменные, которые являются локальными относительного конкретного буфера, а в документации также указано, что в будущем возможно появление переменных, локальных относительно фрэйма или окна. Т.е. вырисовывается достаточно чёткая концепция переменных, локальных относительно некоторого контекста. Это находит применение в Gnu Emacs, но может оказаться полезным и для решения других задач, по крайней мере, я обратил внимание на Buffer-Local Variables уже после того, как сам пришёл к подобной идеи в процессе размышлений над структурой сервера приложений.

Реализация этой идеи на Common Lisp весьма тривиальна:

(defun make-preserve-context ()
(make-hash-table))

(defun context-add-variable (context symbol)
(setf (gethash symbol context)
(symbol-value symbol)))

(defun context-remove-variable (context symbol)
(remhash symbol context))

(defun context-symbol-value (context symbol)
(gethash symbol context))

(defun (setf context-symbol-value) (newval context symbol)
(setf (gethash symbol context)
newval))

(defmacro with-context (context &body body)
`(let ((cntx ,context))
(if cntx
(let ((symbols)
(values))
(iter (for (s v) in-hashtable cntx)
(push s symbols)
(push v values))
(progv symbols values
(unwind-protect
(progn ,@body)
(iter (for s in symbols)
(setf (gethash s cntx)
(symbol-value s))))))
(progn ,@body))))


Теперь можно создать "защищённый контекст", добавить в него несколько ранее объявленных глобальных переменных и изолировать с помощью макроса with-context код, оперирующий этими переменными, от остального мира.

понедельник, 21 сентября 2009 г.

ClozureCL и lisper.ru

Немного подправил код lisper.ru и смог запустить его на Clozure CL. Радость была, однако, недолгой... Попробовал провести нагрузочное тестирование с помощью ab, но оказалось, что если указывать для ab параметр -c больше 1 (в этом случае тестирование ведётся в несколько потоков), то hunchentoot валится без вариантов. При тестировании в одном потоке (-с 1) SBCL отдаёт примерно в 8 раз больше запросов в секунду, чем Clozure CL. Печально...

воскресенье, 20 сентября 2009 г.

Common Lisp Daemon. Часть 2.

В предыдущей части я рассказал о преодолении основных технических трудностей, возникающих при создании lisp-демонов. Теперь я готов опубликовать код законченного lisp-демона. Полный код можно посмотреть здесь: http://lisper.ru/apps/format/15. Это реальный код, который используется для запуска сайта lisper.ru. Как мне кажется, код достаточно тривиален и по структуре в основном соответствует структуре демонов, написанных на языке C (информацию о принципах разработки демонов на языке С легко найти в сети). Отдельно хотел бы отметить, что данный демон позволяет запускать hunchentoot на 80-ом порту уже после отказа от root-привилегий, что достигается за счёт использования библиотеки libcap (версии 2). При использования данного демона нет необходимости в вспомогательных средствах: GNU Screen, detachtty и т.п. (которые традиционно рекомендуются для решения данной проблемы). Ну и данный код, в том числе, можно рассматривать как пример системного программирования на Common Lisp :) К сожалению, в код пришлось добавить пару платформо-зависимых констант, но я надеюсь при случае написать патч для SBCL, после чего необходимость в этом отпадёт.

Изменив всего несколько строк можно приспособить данный код для запуска другого демона. Но подобная техника, конечно, не есть "хорошо", поэтому я сейчас раздумываю об написании библиотеки, которая позволит создавать демонов в несколько строк кода.

среда, 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".