ss_sig *ss_asap();
ss_sig *ss_signal(signo);
ss_sig *ss_sigread(fd);
ss_sig *ss_sigwrite(fd);
ss_sig *ss_sigexcept(fd);
int ss_addsig(signo);
int ss_schedvwait(sig,t,flagi,i,p,wait);
int ss_schedwait(sig,t,i,wait);
int ss_sched(sig,t,i);
int ss_schedonce(sig,t,i);
int ss_unschedv(sig,t,flagi,i,p);
int ss_unsched(sig,t,i);
void ss_externsetsig(sig,x);
int ss_exec();
void ss_forcewait();
void ss_unforcewait();
ss_thread *t;
ss_sig *sig;
ss_extern *x;
int flagi;
int signo;
int fd;
ss_id i;
ss_idptr p;
int wait;
sigsched supports far more flexible signals than C normally provides under UNIX. ``File descriptor 2 is writable'' is a signal, for example. Furthermore, threads do not have to be written to handle a signal at any moment, so code written to use sigsched can be fully optimized. In contrast, preemptive thread models (including UNIX's usual signal handling) prevent optimizations involving global variables.
In general, a ``signal'' is any persistent condition. The ``file descriptor 2 is writable'' signal starts when the pipe is created, persists at least until the next I/O, finishes when the pipe is written to capacity, restarts when the pipe is read, and so on. UNIX signals are examples of thread-lowered signals. For example, SIGINT starts (is raised) when some process executes kill(pid,SIGINT), and finishes (is lowered) just before process pid calls the appropriate signal handler (thread). Note that if another process calls kill(pid,SIGINT) before the first one is delivered, the signal merely persists. It is not delivered twice, as after the first delivery the signal condition has been turned off and can't be redelivered. Any number of kill()s may be absorbed into one delivery in this way.
With sigsched, the program can schedule a thread to execute upon receipt of a signal. ss_schedvwait() and ss_unschedv() schedule and unschedule threads. ss_exec() then executes one scheduled thread after another, as described below. It exits when there are no ``important'' threads left to execute.
ss_schedvwait(sig,t,flagi,i,p,wait) schedules the thread t to execute with integer identifier i or pointer identifier p as soon as condition sig exists. This is an ``important'' thread if wait is nonzero. sig is of type ss_sig *; various functions produce signals of this type. t is of type ss_thread *, defined as a function returning void; it is called as t(i) if flagi is nonzero, or t(p) if flagi is zero. i is an integer, which must be zero if flagi is; p is a character pointer, which must be a null pointer if flagi is nonzero. <sigsched.h> defines the ss_sig and ss_thread types; it also abbreviates int as ss_id and char * as ss_idptr. ss_schedvwait normally returns 0, but will return -1 in case of a memory allocation failure.
ss_unschedv(sig,t,flagi,i,p) unschedules the thread t previously scheduled to execute with identifier i or p as soon as condition sig existed. flagi, i, and p must follow the same rules as above. ss_unschedv returns 0 if the unschedule was successful, 1 if there was no such thread. The effects are currently undefined if a thread is scheduled more than once for the same signal with the same identifier.
ss_exec() executes one thread after another, with no interruptions. It calls t(id) only if, for some signal sig, (1) t(id) is scheduled to execute upon sig; (2) condition sig has existed sometime between the end of the last call of a thread scheduled upon sig and the beginning of this call to t(id). If a thread has just finished executing and ss_exec can call one or more threads under the above restrictions, it will choose one and call that, unless every scheduled thread has a wait value of 0 (i.e., there are no important threads scheduled). In the latter case (including, for example, when there are no threads scheduled at all), ss_exec() immediately returns 0. It returns -1 in case of a memory allocation failure; in that case its internal structures may be permanently corrupted, and ss_exec may not be called again in the same program.
If no threads can execute at a given moment, but if some thread is scheduled with a non-zero wait value, ss_exec has to wait for a signal to arrive. As an optimization, it will block the process until some thread can execute, rather than actively polling the set of signals.
Note that if several threads are scheduled to execute upon one signal, and the signal suddenly exists, one of the threads will execute. If the signal turns off before the end of that thread, the other threads scheduled upon the signal will not execute. This is always true for thread-lowered signals. This behavior stands in marked contrast to the behavior of interrupts---upon an interrupt, all the scheduled threads would be executed.
Each signal provides its own scheduling guarantees. For instance, under this implementation, any (waiting) thread scheduled on the signal ss_asap() will in fact execute at some point, provided that no thread blocks or loops forever. There is no way to keep pushing ss_asap() farther and farther into the future by scheduling other threads. On the other hand, ss_asap() will never flush out the other builtin signals.
sigsched provides several builtin signals: ss_asap() returns a (pointer to a) signal which always exists. ss_signal(signo) returns a thread-lowered signal which is true when UNIX signal signo is received. ss_sigread(fd) returns a signal which is true when fd is open and readable, as defined by select(); similarly for ss_sigwrite and ss_sigexcept.
In order for sigsched to handle UNIX signal signo, you must call ss_addsig(signo) before calling ss_exec(). ss_addsig will discard the old signal handler; later, ss_exec will not restore the handler upon exiting, and may leave the signal blocked or unblocked. ss_addsig will return 0 normally, -1 if signo is not in the right range for signals. If another library makes use of ss_signal with sigsched, it should provide a mandatory initialization routine which calls ss_addsig.
ss_schedvwait and ss_unschedv can be abbreviated in common cases. ss_schedwait(sig,t,i,wait) is the same as ss_schedvwait(sig,t,1,i,(ss_idptr)0,wait). ss_sched(sig,t,i) is the same with wait set to 0; it is commonly used for handling user signals. ss_unsched(sig,t,i) is the same as ss_unschedv(sig,t,1,i,(ss_idptr)0).
ss_schedonce(sig,t,i) is similar to ss_sched but is in fact implemented on top of ss_schedvwait with an independent mechanism. Each call to ss_schedonce schedules t upon a new signal which starts when sig does and exists only until t(i) is executed. After the first execution the new signal disappears. The new signal cannot be unscheduled.
ss_forcewait() tells sigsched that something important is going on outside sigsched and that ss_exec should not exit. ss_unforcewait() negates a previous ss_forcewait(). ss_forcewait() and ss_unforcewait() control a counter, not a flag, so independent libraries can use them, but each library should be careful to use as many of one call as of the other. These functions must not be used outside ss_exec().
ss_externsetsig(sig,x) creates a new signal in the ss_sig pointed to by sig. x points to an ss_extern, which is defined as follows in <sigsched.h>:
typedef struct {
int (*sched)();
int (*unsched)();
union { int n; char *c; } u;
} ss_extern;
sched must be filled in with a scheduling function, which is called as (*sched)(x,t,flagi,i,p,wait) whenever ss_schedvwait(sig,t,flagi,i,p,wait) is called; similarly for unsched. Use of u is up to the caller. sched and unsched must observe the same rules as ss_schedvwait and ss_unschedv on any other signals: i.e., they must schedule threads upon a persistent condition, make sure that ss_exec does not exit if any important threads are scheduled, etc. Note that ss_externsetsig records x in sig, so x must point either to static memory or to memory which remains allocated as long as any thread is scheduled or executing upon sig. Memory management of the sig structure itself is up to the caller.
It is recommended that library foo define a foo_sig structure, which contains ss_sig sig, ss_extern x, and any other necessary information for the signals defined by foo. Then foo_setsig(&fsig,otherinfo), where fsig is a foo_sig, should set up the otherinfo, set fsig.x.u.c to &fsig, set fsig.x.sched and fsig.x.unsched appropriately, and finish with ss_externsetsig(&fsig.sig,&fsig.x). That way the user can use &fsig.sig as the signal argument to sigsched functions, and when foo's scheduling routines are passed &fsig.x as a first argument, they can get to otherinfo through fsig.x.u.c.
sigsched uses ralloc for all allocation.