[ Home | Resume | Programming | Engineering Philosophy | Family ]

Here's a UNIX signal handling dilemma.

For the sake of simplicity, let's assume that the only graceful termination signal is SIGINT. (Extending this to also include SIGHUP, SIGPIPE and SIGTERM, along with signals indicating synchronous actions other than termination, is allegedly straightforward.)

Consider the following program (ex1.c):

	#include <signal.h>
	#include <errno.h>
	int Interrupt=0;
	struct resource {
		char *name;
		struct resource *next;
	} *Resources=0;
	extern int ConnectToServer();
	extern struct resource *NewResource(char *);
	extern void FreeResources();
	void remember(int signo) {
		Interrupt=1;
	}
	void handle() {
		FreeResources();
		sigset(SIGINT, SIG_DFL);
		kill(getpid(), SIGINT);
	}
	void poll() {
		while(Interrupt) {
			Interrupt=0;
			handle();
		}
	}
	ssize_t my_write(int fildes, char *buf, ssize_t nbyte) {
		poll();
		/* What if SIGINT is received right here? */
		return write(fildes, buf, nbyte);
	}
	void AllocateResource(int server, char *name) {
		int done=0;
		do {
			char buf[1000];
			sprintf(buf, "Create %s for me, please\n", name);
			if(my_write(server, name, strlen(name))==-1) {
				if(errno!=EINTR) {
					FreeResources();
					sigset(SIGINT, SIG_DFL);
					poll();
					exit(2);
				}
			}
			else {
				Resources=NewResource(name);
				done=1;
			}
		} while(!done);
	}
	int main() {
		int server;
		sigset(SIGINT, remember);
		server=ConnectToServer();
		AllocateResource(server,"bob");
		AllocateResource(server,"mary");
		FreeResources();
		return(0);
	}

If SIGINT is received after poll returns, but before the write() is initiated, then it is handled by setting Interrupt, but no other action is taken before the write(), which might block for quite a while. Since the interrupt doesn't actually happen during the write(), it will continue blocking and not return with errno==EINT.

Stevens (p. 308) declares this problem insoluble, which isn't particularly satisfying if you've got your heart set on writing a reliable application.

To deal with this, the following is proposed (ex2.c):

	int Hot=0;
	void handle() {
		Hot=0; /* Because FreeResources() isn't reentrant */
		FreeResources();
		sigset(SIGINT, SIG_DFL);
		kill(getpid(), SIGINT);
	}
	void remember(int signo) {
		if(Hot) {
			handle();
		}
		else {
			Interrupt=1;
		}
	}
	ssize_t my_write(int fildes, char *buf, ssize_t nbyte) {
		ssize_t result;
		Hot=1;
		poll();
		result=write(fildes, buf, nbyte);
		/* What if SIGINT is received right here? */
		Hot=0;
		return result;
	}

This is even worse, because SIGINT will cause resources to be leaked if it is received after the write() succeeds, but before resetting Hot. One possible approach is not to use any system calls that both might block for a noticeable amount of time and might allocate resources, and to set Hot around only the calls that might block. That's not really a general solution, though.

Instead, we can deal with problem using alarm(2) (ex3.c):

	int Wake=0;
	void remember(int signo) {
		alarm(1);
		Interrupt=1;
	}
	void wake() {
		alarm(1);
		Wake=1;
	}
	void poll() {
		while(Wake || Interrupt) {
			Wake=0;
			alarm(0);
			if(Interrupt) {
				/* Wake needed in case SIGINT right here */
				Interrupt=0;
				handle();
			}
		}
	}
	int main() {
		int server;
		sigset(SIGALRM, wake);
		/* From original main(): */
		sigset(SIGINT, remember);
		server=ConnectToServer();
		AllocateResource(server,"bob");
		AllocateResource(server,"mary");
		FreeResources();
		return(0);
	}

Whenever Interrupt is set, there is always an alarm pending. If the write() blocks for more than a second, then it will be interrupted by SIGALRM so that we can poll() for SIGINT. For granularity of less than a second, use setitimer(2).

The drawback is that it doesn't work if the process is using SIGALRM for another purpose. This can be dealt with by fork()ing a new parent process that does nothing but periodically send the child a benign signal while it has another signal pending (ex4.c):

	#include <wait.h>
	int Wake=0;
	pid_t Child=0;
	int Pipe[2];
	sigset_t AllSigs;
	void to_parent(char *buf) {
		int done=0;
		do {
			if(write(Pipe[0], buf, 1)==1) {
				done=1;
			}
			else if(errno!=EINTR) {
				done=1;
				perror(0);
			}
		} while(!done);
	}
	void remember(int signo) {
		to_parent("i");
		Interrupt=1;
	}
	void wake() {
		to_parent("i");
		Wake=1;
	}
	void poll() {
		while(Wake || Interrupt) {
			Wake=0;
			to_parent("p");
			if(Interrupt) {
				/* Wake needed in case SIGINT right here */
				Interrupt=0;
				handle();
			}
		}
	}
	void p_poll() {
		if(Interrupt) {
			if(Child && Child!=-1) {
				kill(Child, SIGINT);
			}
			else {
				sigset(SIGINT, SIG_DFL);
				kill(getpid(), SIGINT);
				/* unreachable */
			}
		}
	}
	void p_handler(int signo) {
		/* Might get called after fork(), but before setting Child */
		switch(signo) {
		case SIGALRM:
			if(Child) {
				/* It's OK if Child is a zombie */
				kill(Child, SIGUSR1);
			}
			break;
		default:
			if(Child && Child!=-1) {
				kill(Child, signo);
			}
			else {
				Interrupt=1;
			}
			break;
		}
	}
	void p_chld_handler() {
		int result;
		if(waitpid(-1, &result, WNOHANG)) {
			if(WIFEXITED(result) || WIFSIGNALED(result)) {
				Child=0;
				/* SIGQUIT gets translated into SIGINT, which
				 * is what we want. */
				sigset(SIGTSTP, SIG_DFL);
				sigset(SIGCONT, SIG_DFL);
				sigprocmask(SIG_UNBLOCK, &AllSigs, 0);
				if(WIFSIGNALED(result)) {
					kill(getpid(), WTERMSIG(result));
					sigset(SIGINT, SIG_DFL);
					p_poll();
					exit(WTERMSIG(result) | 0x80);
				}
				else {
					sigset(SIGINT, SIG_DFL);
					p_poll();
					exit(WEXITSTATUS(result));
				}
				/* unreachable */
			}
		}
	}
	int main() {
		int server;
		struct sigaction sa;
		sigfillset(&AllSigs);
		if(pipe(Pipe)==-1) {
			perror(0);
			exit(2);
		}
		sigset(SIGINT, p_handler);
		sigset(SIGALRM, p_handler);
		sa.sa_handler=p_chld_handler;
		sa.sa_mask=AllSigs;
		sa.sa_flags=0;
		sigaction(SIGCHLD, &sa, 0); /* block all signals */
		if((Child=fork())!=0) {
			close(Pipe[0]);
			if(Child==-1) {
				perror(0);
				sigset(SIGINT, SIG_DFL);
				p_poll();
				exit(2);
			}
			/* If SIGQUIT happens right here, then you'll
			 * get an unwanted second core dump. */
			sigset(SIGQUIT, p_handler);
			sigset(SIGTSTP, p_handler);
			sigset(SIGCONT, p_handler);
			/* Sending SIGSTOP to parent doesn't affect the child
			 * until the pipe gets clogged. */
			p_poll();
			while(1) {
				char c[1];
				ssize_t nbytes;
				if((nbytes=read(Pipe[1],c,1))==1) {
					switch(*c) {
					case 'i':
						alarm(1);
						break;
					case 'p':
						alarm(0);
						break;
					}
				}
				else if(nbytes==0 || errno!=EINTR) {
					if(nbytes!=0) { 
						perror(0);
					}
					pause();
				}
			}
		}
		else {
			close(Pipe[1]);
			sigset(SIGCHLD, SIG_DFL);
			sigset(SIGALRM, SIG_DFL);
			sigset(SIGINT, SIG_DFL);
			p_poll();
			sigset(SIGUSR1, wake);
			/* From original main(): */
			sigset(SIGINT, remember);
			server=ConnectToServer();
			AllocateResource(server,"bob");
			AllocateResource(server,"mary");
			FreeResources();
			return(0);
		}
	}

This is an awful lot of complex and brittle code for accomplishing such a simple and commonplace goal. If you've got any better ideas, I'm all ears.

It seems that what's called for is a change to the system call interface. Here's a concrete proposal:

int setintrflagp(int *flagp)

Set the current value of intrflagp to flagp. Intrflagp comprises a new part of each process's context. If it is NULL (the default), then everything behaves as it would otherwise. If it is not NULL, then every system call that is capable of returning EINTR initially checks *intrflagp. If *intrflagp is zero, then the system call continues normally. If *intrflagp is positive, then the system call returns immediately with errno set to EINTR. If *intrflagp is negative, then the behavior is undefined (to allow for future functionality).

Anders Johnson, last modified $Date: 2002/02/05 $

[ Home | Resume | Programming | Engineering Philosophy | Family ]