diff options
Diffstat (limited to '')
-rw-r--r-- | src/master/master_spawn.c | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/src/master/master_spawn.c b/src/master/master_spawn.c new file mode 100644 index 0000000..c3b70f2 --- /dev/null +++ b/src/master/master_spawn.c @@ -0,0 +1,371 @@ +/*++ +/* NAME +/* master_spawn 3 +/* SUMMARY +/* Postfix master - child process birth and death +/* SYNOPSIS +/* #include "master.h" +/* +/* void master_spawn(serv) +/* MASTER_SERV *serv; +/* +/* void master_reap_child() +/* +/* void master_delete_children(serv) +/* MASTER_SERV *serv; +/* DESCRIPTION +/* This module creates and cleans up child processes, and applies +/* a process creation throttle in case of serious trouble. +/* This module is the working horse for the master_avail(3) process +/* creation policy module. +/* +/* master_spawn() spawns off a child process for the specified service, +/* making the child process available for servicing connection requests. +/* It is an error to call this function then the specified service is +/* throttled. +/* +/* master_reap_child() cleans up all dead child processes. One typically +/* runs this function at a convenient moment after receiving a SIGCHLD +/* signal. When a child process terminates abnormally after being used +/* for the first time, process creation for that service is throttled +/* for a configurable amount of time. +/* +/* master_delete_children() deletes all child processes that provide +/* the named service. Upon exit, the process creation throttle for that +/* service is released. +/* DIAGNOSTICS +/* Panic: interface violations, internal inconsistencies. +/* Fatal errors: out of memory. Warnings: throttle on/off. +/* BUGS +/* SEE ALSO +/* master_avail(3), process creation policy. +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/*--*/ + +/* System libraries. */ + +#include <sys_defs.h> +#include <sys/wait.h> +#include <stdlib.h> +#include <unistd.h> +#include <syslog.h> /* closelog() */ +#include <signal.h> +#include <stdarg.h> +#include <syslog.h> + +/* Utility libraries. */ + +#include <msg.h> +#include <binhash.h> +#include <mymalloc.h> +#include <events.h> +#include <vstring.h> +#include <argv.h> + +/* Global library. */ + +#include <mail_conf.h> + +/* Application-specific. */ + +#include "master_proto.h" +#include "master.h" + +BINHASH *master_child_table; +static void master_unthrottle(MASTER_SERV *serv); + +/* master_unthrottle_wrapper - in case (char *) != (struct *) */ + +static void master_unthrottle_wrapper(int unused_event, void *ptr) +{ + MASTER_SERV *serv = (MASTER_SERV *) ptr; + + /* + * This routine runs after expiry of the timer set in master_throttle(), + * which gets called when it appears that the world is falling apart. + */ + master_unthrottle(serv); +} + +/* master_unthrottle - enable process creation */ + +static void master_unthrottle(MASTER_SERV *serv) +{ + + /* + * Enable process creation within this class. Disable the "unthrottle" + * timer just in case we're being called directly from the cleanup + * routine, instead of from the event manager. + */ + if ((serv->flags & MASTER_FLAG_THROTTLE) != 0) { + serv->flags &= ~MASTER_FLAG_THROTTLE; + event_cancel_timer(master_unthrottle_wrapper, (void *) serv); + if (msg_verbose) + msg_info("throttle released for command %s", serv->path); + master_avail_listen(serv); + } +} + +/* master_throttle - suspend process creation */ + +static void master_throttle(MASTER_SERV *serv) +{ + + /* + * Perhaps the command to be run is defective, perhaps some configuration + * is wrong, or perhaps the system is out of resources. Disable further + * process creation attempts for a while. + */ + if ((serv->flags & MASTER_FLAG_THROTTLE) == 0) { + serv->flags |= MASTER_FLAG_THROTTLE; + event_request_timer(master_unthrottle_wrapper, (void *) serv, + serv->throttle_delay); + if (msg_verbose) + msg_info("throttling command %s", serv->path); + master_avail_listen(serv); + } +} + +/* master_spawn - spawn off new child process if we can */ + +void master_spawn(MASTER_SERV *serv) +{ + const char *myname = "master_spawn"; + MASTER_PROC *proc; + MASTER_PID pid; + int n; + static unsigned master_generation = 0; + static VSTRING *env_gen = 0; + + if (master_child_table == 0) + master_child_table = binhash_create(0); + if (env_gen == 0) + env_gen = vstring_alloc(100); + + /* + * Sanity checks. The master_avail module is supposed to know what it is + * doing. + */ + if (!MASTER_LIMIT_OK(serv->max_proc, serv->total_proc)) + msg_panic("%s: at process limit %d", myname, serv->total_proc); + if (serv->avail_proc > 0) + msg_panic("%s: processes available: %d", myname, serv->avail_proc); + if (serv->flags & MASTER_FLAG_THROTTLE) + msg_panic("%s: throttled service: %s", myname, serv->path); + + /* + * Create a child process and connect parent and child via the status + * pipe. + */ + master_generation += 1; + switch (pid = fork()) { + + /* + * Error. We're out of some essential resource. Best recourse is to + * try again later. + */ + case -1: + msg_warn("%s: fork: %m -- throttling", myname); + master_throttle(serv); + return; + + /* + * Child process. Redirect child stdin/stdout to the parent-child + * connection and run the requested command. Leave child stderr + * alone. Disable exit handlers: they should be executed by the + * parent only. + * + * When we reach the process limit on a public internet service, we + * create stress-mode processes until the process count stays below + * the limit for some amount of time. See master_avail_listen(). + */ + case 0: + msg_cleanup((void (*) (void)) 0); /* disable exit handler */ + closelog(); /* avoid filedes leak */ + + if (master_flow_pipe[0] <= MASTER_FLOW_READ) + msg_fatal("%s: flow pipe read descriptor <= %d", + myname, MASTER_FLOW_READ); + if (DUP2(master_flow_pipe[0], MASTER_FLOW_READ) < 0) + msg_fatal("%s: dup2: %m", myname); + if (close(master_flow_pipe[0]) < 0) + msg_fatal("close %d: %m", master_flow_pipe[0]); + + if (master_flow_pipe[1] <= MASTER_FLOW_WRITE) + msg_fatal("%s: flow pipe read descriptor <= %d", + myname, MASTER_FLOW_WRITE); + if (DUP2(master_flow_pipe[1], MASTER_FLOW_WRITE) < 0) + msg_fatal("%s: dup2: %m", myname); + if (close(master_flow_pipe[1]) < 0) + msg_fatal("close %d: %m", master_flow_pipe[1]); + + close(serv->status_fd[0]); /* status channel */ + if (serv->status_fd[1] <= MASTER_STATUS_FD) + msg_fatal("%s: status file descriptor collision", myname); + if (DUP2(serv->status_fd[1], MASTER_STATUS_FD) < 0) + msg_fatal("%s: dup2 status_fd: %m", myname); + (void) close(serv->status_fd[1]); + + for (n = 0; n < serv->listen_fd_count; n++) { + if (serv->listen_fd[n] <= MASTER_LISTEN_FD + n) + msg_fatal("%s: listen file descriptor collision", myname); + if (DUP2(serv->listen_fd[n], MASTER_LISTEN_FD + n) < 0) + msg_fatal("%s: dup2 listen_fd %d: %m", + myname, serv->listen_fd[n]); + (void) close(serv->listen_fd[n]); + } + vstring_sprintf(env_gen, "%s=%o", MASTER_GEN_NAME, master_generation); + if (putenv(vstring_str(env_gen)) < 0) + msg_fatal("%s: putenv: %m", myname); + if (serv->stress_param_val && serv->stress_expire_time > event_time()) + serv->stress_param_val[0] = CONFIG_BOOL_YES[0]; + + execvp(serv->path, serv->args->argv); + msg_fatal("%s: exec %s: %m", myname, serv->path); + /* NOTREACHED */ + + /* + * Parent. Fill in a process member data structure and set up links + * between child and process. Say this process has become available. + * If this service has a wakeup timer that is turned on only when the + * service is actually used, turn on the wakeup timer. + */ + default: + if (msg_verbose) + msg_info("spawn command %s; pid %d", serv->path, pid); + proc = (MASTER_PROC *) mymalloc(sizeof(MASTER_PROC)); + proc->serv = serv; + proc->pid = pid; + proc->gen = master_generation; + proc->use_count = 0; + proc->avail = 0; + binhash_enter(master_child_table, (void *) &pid, + sizeof(pid), (void *) proc); + serv->total_proc++; + master_avail_more(serv, proc); + if (serv->flags & MASTER_FLAG_CONDWAKE) { + serv->flags &= ~MASTER_FLAG_CONDWAKE; + master_wakeup_init(serv); + if (msg_verbose) + msg_info("start conditional timer for %s", serv->name); + } + return; + } +} + +/* master_delete_child - destroy child process info */ + +static void master_delete_child(MASTER_PROC *proc) +{ + MASTER_SERV *serv; + + /* + * Undo the things that master_spawn did. Stop the process if it still + * exists, and remove it from the lookup tables. Update the number of + * available processes. + */ + serv = proc->serv; + serv->total_proc--; + if (proc->avail == MASTER_STAT_AVAIL) + master_avail_less(serv, proc); + else + master_avail_listen(serv); + binhash_delete(master_child_table, (void *) &proc->pid, + sizeof(proc->pid), (void (*) (void *)) 0); + myfree((void *) proc); +} + +/* master_reap_child - reap dead children */ + +void master_reap_child(void) +{ + MASTER_SERV *serv; + MASTER_PROC *proc; + MASTER_PID pid; + WAIT_STATUS_T status; + + /* + * Pick up termination status of all dead children. When a process failed + * on its first job, assume we see the symptom of a structural problem + * (configuration problem, system running out of resources) and back off. + */ + while ((pid = waitpid((pid_t) - 1, &status, WNOHANG)) > 0) { + if (msg_verbose) + msg_info("master_reap_child: pid %d", pid); + if ((proc = (MASTER_PROC *) binhash_find(master_child_table, + (void *) &pid, sizeof(pid))) == 0) { + if (init_mode) + continue; /* non-Postfix process */ + msg_panic("master_reap: unknown pid: %d", pid); + } + serv = proc->serv; + +#define MASTER_KILL_SIGNAL SIGTERM +#define MASTER_SENT_SIGNAL(serv, status) \ + (MASTER_MARKED_FOR_DELETION(serv) \ + && WTERMSIG(status) == MASTER_KILL_SIGNAL) + + /* + * XXX The code for WIFSTOPPED() is here in case some buggy kernel + * reports WIFSTOPPED() events to a Postfix daemon's parent process + * (the master(8) daemon) instead of the tracing process (e.g., gdb). + * + * The WIFSTOPPED() test prevents master(8) from deleting its record of + * a child process that is stopped. That would cause a master(8) + * panic (unknown child) when the child terminates. + */ + if (!NORMAL_EXIT_STATUS(status)) { + if (WIFSTOPPED(status)) { + msg_warn("process %s pid %d stopped by signal %d", + serv->path, pid, WSTOPSIG(status)); + continue; + } + if (WIFEXITED(status)) + msg_warn("process %s pid %d exit status %d", + serv->path, pid, WEXITSTATUS(status)); + if (WIFSIGNALED(status) && !MASTER_SENT_SIGNAL(serv, status)) + msg_warn("process %s pid %d killed by signal %d", + serv->path, pid, WTERMSIG(status)); + /* master_delete_children() throttles first, then kills. */ + if (proc->use_count == 0 + && (serv->flags & MASTER_FLAG_THROTTLE) == 0) { + msg_warn("%s: bad command startup -- throttling", serv->path); + master_throttle(serv); + } + } + master_delete_child(proc); + } +} + +/* master_delete_children - delete all child processes of service */ + +void master_delete_children(MASTER_SERV *serv) +{ + BINHASH_INFO **list; + BINHASH_INFO **info; + MASTER_PROC *proc; + + /* + * XXX turn on the throttle so that master_reap_child() doesn't. Someone + * has to turn off the throttle in order to stop the associated timer + * request, so we might just as well do it at the end. + */ + master_throttle(serv); + for (info = list = binhash_list(master_child_table); *info; info++) { + proc = (MASTER_PROC *) info[0]->value; + if (proc->serv == serv) + (void) kill(proc->pid, MASTER_KILL_SIGNAL); + } + while (serv->total_proc > 0) + master_reap_child(); + myfree((void *) list); + master_unthrottle(serv); +} |