[Small-cl-src] prefork-example-simple.lisp

;;;; prefork-example-simple.lisp ;;;; ;;;; This is a simple example of writing a preforking Unix server using SBCL. ;;;; This file was written to demonstrate the basic techniques of writing a ;;;; preforking server for educational purposes. For a more nuanced example, ;;;; see prefork-example-realistic.lisp ;;;; ;;;; Because it uses fork(2) and waitpid(2), this file requires a ;;;; patched version of the sb-posix contrib module for SBCL. A patch ;;;; against SBCL-0.8.8 is available separately. ;;;; ;;;; We also assumes a BSD-like system, due to its use of flock(2) for ;;;; serialization. ;;;; Copyright (C) 2004, Thomas F. Burdick ;;;; ;;;; Permission is hereby granted, free of charge, to any person obtaining a ;;;; copy of this software and associated documentation files (the "Software"), ;;;; to deal in the Software without restriction, including without limitation ;;;; the rights to use, copy, modify, merge, publish, distribute, sublicense, ;;;; and/or sell copies of the Software, and to permit persons to whom the ;;;; Software is furnished to do so, subject to the following conditions: ;;;; ;;;; The above copyright notice and this permission notice shall be included in ;;;; all copies or substantial portions of the Software. ;;;; ;;;; THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR ;;;; IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, ;;;; FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE ;;;; AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER ;;;; LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING ;;;; FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS ;;;; IN THE SOFTWARE. (eval-when (:compile-toplevel :load-toplevel :execute) (require :sb-posix) (require :sb-bsd-sockets)) (defpackage :prefork-example-simple (:use :cl :sb-bsd-sockets)) (in-package :prefork-example-simple) ;;; In this simple example, the parent sets up a server socket, forks ;;; +NCHILDREN+ child processes, then just sits there, and periodically reaps ;;; and reforks any child processes that die. ;;; ;;; The child processes wait for incoming connections. Not all Unixes serialize ;;; `accept's in the kernel, so we need to handle this ourselves. We do this by ;;; acquiring an exclusive file lock around the call to accept(2). We use ;;; flock(2) to do this, but we could just as well use fcntl(2) or SysV ;;; semaphores. ;;; ;;; The Parent ;;; (defconstant +nchildren+ 8) (defconstant +backlog+ 16) (declaim (type (simple-array (or integer null)) *children*)) (defvar *children* (make-array +nchildren+ :initial-element nil) "the PIDs of our children") (defvar *childp* nil "True in child processes.") (defvar *server-socket* nil) (defvar *lock* nil) (defun start-server () "The main loop of the parent process." (setup) (unwind-protect (progn (fork-children) (loop ;; We call SERVE-EVENT here to let other Lisp applications ;; run, eg, SLIME. (sb-sys:serve-event 60) ;; Periodically reap any dead processes. (reap-children))) (unless *childp* (stop-server)))) (defun stop-server () (kill-children) (teardown-lock) (socket-close *server-socket*)) (defun setup () "Set up sockets and locks so the parent can begin forking." (setup-lock) (setup-parent-signal-handlers) (when *server-socket* (socket-close *server-socket*)) (setf *server-socket* (make-instance 'inet-socket :type :stream :protocol :tcp)) (setf (sockopt-reuse-address *server-socket*) t) (socket-bind *server-socket* (make-inet-address "127.0.0.1") 1978) (socket-listen *server-socket* +backlog+)) ;;; ;;; The child ;;; (defun child-main () "Get in the queue to accept, serve the request, loop." (setup-child-signal-handlers) (loop for socket = (serial-accept *server-socket*) for stream = (socket-make-stream socket :input t :output t) for client-id = (read-line stream) for message = (read stream) do (format stream "Hello ~A, this is PID ~D~%~S" client-id (sb-posix:getpid) message) (finish-output stream) (socket-close socket))) ;;; ;;; Forking and reaping ;;; (defun fork-children () (dotimes (i +nchildren+) (fork-one-child i))) (defun fork-one-child (index) (labels ((child () (unwind-protect (progn (setf *children* (vector) *childp* t) (child-main)) (sb-ext:quit))) (parent (pid) (setf (aref *children* index) pid))) (let ((pid (sb-posix:fork))) (if (zerop pid) (child) (parent pid))))) (defun reap-children () "Unix is sick." (when (every #'null *children*) (return-from reap-children)) (loop for pid = (ignore-errors (sb-posix:waitpid 0 sb-posix::wnohang)) while pid do (let ((index (position pid *children*))) (when index (warn "Reaping child process ~D" pid) (fork-one-child index))))) (defun kill-children () "Unix is sick." (unless *childp* (loop for pid across *children* for index upfrom 0 when pid do (handler-case (sb-posix:kill pid sb-posix::sigterm) (sb-posix:syscall-error (error) (warn "Could not kill PID ~D: ~A" pid error) (setf (aref *children* index) nil)))) (loop for pid across *children* for index upfrom 0 when pid do (ignore-errors (sb-posix:waitpid pid 0)) (setf (aref *children* index) nil)))) ;;; ;;; Signals ;;; (defun setup-child-signal-handlers () (sb-sys:enable-interrupt sb-unix:sigterm (signal-handler 'sb-ext:quit)) (sb-sys:ignore-interrupt sb-unix:sigint)) (defun setup-parent-signal-handlers () (sb-sys:enable-interrupt sb-unix:sigterm (signal-handler 'terminate-gracefully))) (defun terminate-gracefully () (kill-children) (sb-ext:quit)) (defun signal-handler (function) "Return a signal hander function that will funcall FUNCTION." (check-type function (or function symbol)) (lambda (signal code scp) (declare (ignore signal code scp)) (funcall function))) ;;; Locking ;;; ;;; We serialize the child processes access to the server socket by acquiring an ;;; exclusive file lock around the call to accept. The advantage of this is ;;; it's really simple and the kernel takes care of everything for us. The ;;; disadvantage is that the parent process doesn't know what's happening, so it ;;; can't intervene to add or remove child processes. (defmacro with-lock (fd-spec &body forms) (let ((=fd (gensym))) `(let ((,=fd ,fd-spec)) (unwind-protect (progn (flock ,=fd :exclusive) ,@forms) (flock ,=fd :unlock))))) (defun serial-accept (socket) (with-lock *lock* (socket-accept socket))) (defun setup-lock () (let ((lock-file (format nil "/tmp/~D.lock" (sb-posix:getpid)))) (unless *lock* (setf *lock* (open lock-file :if-does-not-exist :create))))) (defun teardown-lock () (when *lock* (delete-file *lock*) (close *lock*) (setf *lock* nil))) ;; I didn't patch SB-POSIX to add flock(2) because it isn't POSIX, it's just a ;; convenient BSD function. (defmacro defconstant-once (name form &optional doc) `(defconstant ,name (if (boundp ',name) (symbol-value ',name) ,form) ,doc)) (defconstant-once +flock-table+ #((:shared 1) (:exclusive 2) (:nonblocking 4) (:unlock 8))) (defun flock (fd-spec &rest options) "Perform flock(2) on the FD or FILE-STREAM given as FD-SPEC. OPTIONS are taken from +FLOCK-TABLE+" (let* ((options (reduce #'logior options :key (lambda (option) (or (second (find option +flock-table+ :key #'first)) (error "Unknown flock option: ~S" option))) :initial-value 0)) (fd (etypecase fd-spec (sb-alien:int fd-spec) (file-stream (sb-sys:fd-stream-fd fd-spec))))) (sb-alien:alien-funcall (sb-alien:extern-alien "flock" (function sb-alien:int sb-alien:int sb-alien:int)) fd options)))
participants (1)
-
Thomas F. Burdick