UN*X SIGNAL SIMULATION - IMPLEMENTATION NOTES This file documents some of the unholy and intimate details of how the KCC USYS library routines simulate the Un*x signal mechanism. It does not explain everything. To seriously understand what is going on, you need to read the exhaustive comments in the source files (SIGVEC.C and ) as well as the user-oriented file SIGNAL.DOC. KCC Implementation: We try to implement 4.3BSD, since with that the older, cruder mechanisms can be emulated also. ITS has a very powerful software interrupt system and can readily handle the BSD scheme. T20 has more trouble. T10 as far as known is simply out of it altogether. This document is primarily about the T20 implementation (the only one currently existing). First, a note on what "a/synchronous" means: A SYNCHRONOUS interrupt is one that happens at a specific PC due to the instruction at that location. Typical examples: illegal instruction interrupts (which can include JSYS calls), floating-point exceptions. For these types of interrupts the PC is significant and it or the contents it points to may need to be checked to determine what to do, because simply continuing the process at that PC will very likely just generate another such interrupt. An ASYNCHRONOUS interrupt is one that may happen at ANY time, regardless of place; these are generated by events external to the program. Typical examples: TTY input interrupts, timer interrupts. For these, the PC is unimportant except that it should be preserved and restored if the interrupt handler wishes to continue whatever was interrupted. No UN*X C signal handler has the capability of returning from handling a synchronous interrupt. In fact there is no mechanism provided for a signal handler to find out what its return PC is. (it's possible, with trickery, but I've never seen an example). 4.3BSD (as opposed to 4.2 or any other Un*x) now makes this possible by providing the handler with a pointer to a saved-context structure! Note that some signal handlers return to normal code by means of longjmp(); this is particularly true for alarm() handlers. ANSI specifies that longjmp should restore the environment properly even from within a signal handler, but is not required to do anything meaningful if called from a nested signal handler. KCC supports this use of longjmp(). T20 Problems: (1) T20 PSIs have 3 priority levels. While interrupting at one level, no other interrupts at that level can be serviced (although they will be deferred). This is unlike UN*X where signals and their handlers are all completely independent of each other. (2) The T20 return address may be a "monitor address" which indicates that a JSYS was interrupted before it completed. This can only be continued by the first DEBRK at that interrupt level; anything else aborts that JSYS, with unknown consequences. To clarify: if the return PC does not have the user-mode bit set, then it was interrupted out of a JSYS. The PC address however refers to the user address space, and points to the JSYS return location (that is, one greater than the location of the JSYS call.) As far as I can tell from examination of the monitor code, a JSYS which is interrupted just as it is about to return will have already changed the PC to user mode, so the fact of a monitor-mode PC should always imply that there is still something left which the JSYS hasn't yet finished doing. Unfortunately, the fact that IIC% itself interrupts with a monitor-mode PC implies that this analysis is bogus and that the PSI system is even more losing than could possibly be expected. Doing a DEBRK back to this place will complete the JSYS. Doing a DEBRK anywhere else (or turning on the user-mode bit) will abort it and the monitor forgets the saved context. It isn't clear whether re-starting the JSYS, by backing up the PC by one, will do the right things; the ACs may not have been properly adjusted. Given the usual history of T20/10X, they probably haven't. This is the biggest problem, but can be coped with if user code is careful. I think. IIC% should NOT be restarted! SIN% and probably SOUT% appear to update their ACs and can be restarted. It is possible to execute a signal handler entirely within an interrupt level even if the handler uses longjmp(), if longjmp conspires to do a DEBRK when jumping back to the previous context. So the main problem with handling interrupts in this way is the lock-out of other interrupts. FINAL PLAN: Key aspect: all T20 PSI interrupts are handled IMMEDIATELY and DEBRK'd as soon as possible. The time spent at interrupt level is absolutely minimal, and no signal handlers will be called at interrupt level unless specifically (and non-portably) requested. The UN*X signal mechanism is primarily implemented by the PSI handling code, including the BSD signal block mask, rather than by trying to use the monitor's PSI Jsys calls. No USYS (Un*x System call simulation) routine should be interrupted. If an internal JSYS is interrupted then that system call should return -1 with errno set to EINTR. Otherwise things run through to completion and the signal takes effect just before returning a normal return value. ************************************************************************ NOTE!!!!! THE DETAILS OUTLINED IN THE REST OF THIS FILE ARE NOT NECESSARILY COMPLETE!!! Read the sources also, namely: SIGVEC.C - main source, PSI handlers SIGDAT.C - additional important source JSYS.C - jsys() function ************************************************************************ Global variables: int _sigusys; /* Positive if within critical USYS code */ unsigned int _sigpendmask; /* 36-bit mask of pending signals */ /* PSI can add to this during _sigusys. */ unsigned int _sigblockmask; /* 36-bit mask saying which signals to block */ /* PSI cannot change this during _sigusys. */ BSD signal block mask: This is implemented softwarily, by having the PSI code check the mask itself and so forth, thus implementing whatever notion of blocking is needed, rather than trying to defer the actual PSIs themselves. This doesn't actually turn off the PSI channels, and it is in theory possible there might be runaway interrupts, but this seems unlikely. It cannot happen with synchronous interrupts, and the asynchronous ones all seem fairly sporadic. Installing the default action or no handler will still result in turning off the relevant PSI channel. If a PSI is triggered for a signal which is currently handled by SIG_DFL (default) or SIG_IGN (ignored) then the actions are straightforward. Actually no PSI should ever arrive for a SIG_DFL handler. If a PSI comes in for which a user signal handler is defined, there are four cases that must be checked for: (1) USYS code, user-mode PC - Simple deferred case. (2) USYS code, JSYS-mode PC - Complex but all planned for. (3) User code, user-mode PC - Simple immediate case. (4) User code, JSYS-mode PC - Complex and unpredictable; worst!! In general, user-mode PCs are simple. For JSYS-mode PCs things are more complex. In fact, much of the hair in jsys() is due to the need to coordinate its activities with those of the PSI handling system. There is one special case: an illegal instruction interrupt (.ICILI) is ALWAYS delivered with the PC claiming JSYS mode, regardless of whether the offending instruction was a failing JSYS or a bad word value. To handle this screw-up, the PSI handler checks .ICILI interrupts specially and turns back on the user-mode PC bit if the bad instruction was not a JSYS. This simplifies things for the rest of the code. (1) USYS code, user-mode PC - Simple deferred case. If we are in USYS code we cannot call a handler. What we do is set the signal's bit in _sigpendmask (for pending ints) and do a simple DEBRK right back to the place interrupted from, so the USYS code can run on to completion. Special checking is required for synchronous interrupts, since those usually cannot be continued. If the interrupt is .ICAOV or .ICFOV we can simply continue. Otherwise we must fail with an error message. (2) USYS code, JSYS-mode PC - Complex but all planned for. Again, synchronous interrupts are a problem. For a JSYS-mode synchronous interrupt we can only continue if the PC was within the jsys() routine, in which case we can cause the jsys() call to abort (whether or not it was marked as interruptible). Otherwise, our only recourse is to halt the process with an error message. The important thing about asynchronous interrupts is that because the code interrupted from was USYS code, we can always be assured that whatever form the JSYS invocation took, both the USYS code and the PSI code agree on how the interrupt will be handled. The pending signal bit is always set; no handler is ever invoked from within inside USYS code. Since we always do an immediate DEBRK%, we always have the option of either fully continuing a JSYS or of aborting it. The USYS code has four different ways of invoking a JSYS: Asynch Synch (a) in-line with ERJMP Continue (b) in-line without ERJMP Continue fail with err msg (c) jsys() with int flag do intret (d) jsys() without flag Continue The interesting case here is (c) where an asynchronous interrupt causes the PSI code to turn on the user-mode bit and then DEBRK% to a specific interrupt return location within jsys(). Otherwise, execution merely continues. A synchronous interrupt within jsys() is very unlikely, because jsys() always has an ERJMP following a jsys invocation; also, any reasonable USYS assembler code should have ERJMP to handle expected errors. But if one happens, then: b: halt process with error message. a: should not happen, but halt with error msg anyway. c,d: fail with an "interrupted" return value. This is also not supposed to happen, but a reading of the T20 page fault code seems to indicate it is possible for a PSI to happen regardless of whether an ERJMP exists. (3) User code, user-mode PC - Simple immediate case. This is the simple, straightforward case. We simply DEBRK to the signal handler, after saving all stuff on stack such that if the handler returns, control returns to interrupted point. Note that for synchronous interrupts that have the CF_OPC flag, the saved PC is that of the guilty instruction, so that continuation will simply generate another interrupt if the handler failed to remedy the problem! (4) User code, JSYS-mode PC This is the biggest problem. Would be nice to only DEBRK when signal handler returns, cuz then the JSYS could be continued. But having the priority level stuck will prevent other signals from being handled, and longjmp() would have to test for needing to hack DEBRK% itself. If at all possible we want to avoid running signal handlers at interrupt level. So we evidently have to do the same thing as for a user-mode PC, and just call the signal handler with the DEBRK%. Users can use the interruptable flag bit for jsys() calls, and this will work. Otherwise, if there was any way of knowing whether it was safe to re-start a JSYS then we could back up the PC to do this, but as far as is known there isn't any guaranteed method. So "random" JSYS calls that don't go through the jsys() facility are subject to being aborted without any error indication. Continuation actions for user code, JSYS-mode PC: Asynch Synch (a) in-line with ERJMP Restart (b) in-line without ERJMP Restart Restart (c) jsys() with int flag do intret (d) jsys() without flag Restart There is one exception to the "Restart" action: if the JSYS was IIC% then we simply "continue" (at the next instruction). USYS interrupt handling: We need some macros or functions, one to suspend interrupts at the start of critical USYS code, and another to reactivate them once the critical code is done. May also need one which checks for pending interrupts. Hard part is how to handle an interruptible invocation of jsys() such that no signal gets missed between the test for pending signals and actual execution of the JSYS. The best way seems to be for the PSI code to know about the address range of critical code in jsys(). USYS_BEG(); - Suspends all signals; used by USYS code to tell PSI stuff that a usys call is being executed. #define USYS_BEG() (++_sigusys) USYS_END(); - Reactivates all signals; used by USYS code to tell PSI stuff that it's OK to handle signals now, and checks to see if there are any pending signals that should be handled. NOTE: the signal suspension is lifted BEFORE we test for pending signals; if a signal happens after the lift but before the _sigpendmask test, the worst that happens is we call _sigtrigpend() unnecessarily. If the ordering was reversed then we might get a signal after the _sigpendmask test but before lifting the suspension; that signal would never be serviced. #define USYS_END() { if (--_sigusys <= 0 && _sigpendmask) \ _sigtrigpend(); } USYS_RET(val); - Reactivates all signals; used by USYS code to tell PSI stuff that a usys call is finished and about to return. Also must check for pending signals and dispatch to handler if any. NOTE: the return value is computed into a temporary location so it cannot change regardless of what a signal handler does. #define USYS_RET(val) { int tmpret = (val); \ USYS_END(); \ return tmpret; } USYS_INTRET(-1); - same as above, when USYS code knows that a jsys() has been interrupted and is returning for this reason. No test of _sigpendmask is needed. We set errno last in an attempt to avoid having its value changed by the signal handler; of course another signal could happen anytime after errno is set and before it is checked by the user program, but that is a problem with this stupid mechanism on UN*X too. #define USYS_INTRET(val) { --_sigusys; _sigtrigpend(); \ errno = EINTR; \ return val; } /* Should always be -1 */ USYS_SIGTST() - Tests for pending signals. Needed? #define USYS_SIGTST() (_sigpendmask) USYS code cautions: The USYS routines need to be careful to always invoke the signal macros appropriately. But a more important (and more subtle) problem is that they also have to be careful of calling on other library routines! UN*X code assumes, of course, that a system call is executed entirely within the kernel, and has no effect on anything in the process address space other than things explicitly requested by the call. This means that our USYS code must have the same non-effect; the routines can only call those library functions which are fully re-entrant and change no static data. The problem is worse for those USYS calls invoked by a signal handler. In particular, if any USYS routine calls malloc() then it runs the risk of enormous unexpected screwage if the main user code happened to be in the middle of a malloc call! Of course the user's signal handler itself could call malloc() with similar bad consequences, but in this case the user has control over what is going on, and presumably knows what is going on. PSI code: All signal PSIs happen at level 2. If we are so unfortunate as to get a panic PSI within the PSI code, this will cause process termination, but the PSI code will be extremely minimal and should not cause any problems. Levels 1 and 3 can be reserved for the user, thus giving flexibility of using a level either higher or lower than the Un*x signal facility; some mechanism would have to be figured out, though (see next page). When a PSI interrupt happens: Monitor saves PC in predefined table, jumps to PSI handler. Save an AC or two for checking. If SIG_DFL (default) was in effect, we shouldn't be getting this PSI at all. If SIG_IGN was in effect and PSI was enabled for this signal then the PSI handler will be a do-nothing that just DEBRK%s immediately. Otherwise, we must have a handler of some type. "psisig" is the location of the PSI handler for such signals. It distinguishes between interrupts from USYS code and user code, as well as whether the PC is a monitor (JSYS) or user-mode PC. If the handler is invoked, this is done with a DEBRK%. More on extensions to sigvec(): For additional flexibility, the "sigvec" structure can be extended to include additional parameters. Existence of a new bit in sv_flags would indicate that the additional structure members are significant. The things we'd like to be able to specify, independently of each other: SV_XINTLEV: ON If handler should run at interrupt level. SV_XASMHAN: ON If handler is special assembler routine (ACs not saved, no args given). Otherwise, normal C handler. SV_XOS: ON If the OS structure should be checked for: (1) Exact PSI channel # to use for this signal (0 = existing). (2) What PSI level to use (0 = existing). (3) .TICxx code (plus 1) to ATI% to this channel (0 = none). Some of those are interdependent: Specification of a positive .TIC code always replaces any existing code by the new one, and use of -1 always clears any existing code. If the value is 0, however, then the meaning depends on whether a channel # was specified. If the channel # was given, 0 is the same as -1. Otherwise, if no channel # was given, then 0 means leave any existing code alone. If the handler is an assembly routine, then it MUST run at interrupt level. Thus, it is an error to specify SV_XASMHAN without SV_XINTLEV. Currently only SV_XINTLEV is implemented. It should work to use longjmp() within handlers called with this flag! Fixing up jsys(): The jsys() function has been fixed up so that it understands which calls take what kinds of returns, and can guarantee the following return values: -1: interrupted 0: error (return error code in acs[0]) 1,2,3: success, returned at that location. The calls SIBE, SOBE, SOBF are weird. Not clear how to distinguish an error return from a "normal" +1 or +2 return. Same applies to SKPIR. GFRKH and GFRKS have both +1, +2, and Illeg-Instr PSIs. Last remaining ambiguities are for the +3 returns, of which only 3 exist: ERSTR, GACTF, and STDIR (10X only). STDIR and GACTF are simple as only the successes need to be distinguished. ERSTR is hard as there are two different error returns. STDIR (10X only) - all 3 returns counted as success. Error happens if illegal instruction interrupt. Can distinguish success returns by value 1,2,3. GACTF - Returns 2 or 3 for success, 0 for failure (+1 return). ERSTR - We'll have to fudge this one: Return 0 for interrupt or +1 failure return. Return 2 for the +2 failure return. Return 3 for the +3 full winnage return. So, for those three calls, the caller has to check the exact return value and cannot just assume a positive return value means winnage. The jsys() code needs to distinguish skipping from non-skipping calls in order to return the right stuff. Normally skips Non-skip JSYS JSYS ERJMP ret0 ERJMP ret0 JRST ret2 JRST ret1 JRST ret3 For 10X the normally-skipped ERJMP would have to be replaced by a ERJMPA ret0, and the PSI handler made to know about this. Different code sequences may be needed for the weirdos like SIBE/SOBE/SOBF. Rather than keeping a global table indexed by JSYS value, it would be possible to have the jsys-number argument to jsys() carry some flags with it in the high-order bits which indicated what sort of jsys it was, or what actions jsys() should carry out. The jsys-name macro definitions in can include those flags. This also makes it easy for user programs to examine the flags as well, and they can be used in #if preprocessor tests. This would involve putting the table into UNV.C or whatever program is used to generate the type files from MONSYM.UNV. JSYS-related PSI handling: Some special handling is needed for interrupts from JSYS calls and from within jsys(). This is discussed in more detail in a previous part of this documentation. _SIGTRIGPEND discussion: Once _sigusys is turned off, any signals which arrived during the call execution (but were suspended because _sigusys was on) need to be triggered, and this is the function of _sigtrigpend(). If user handlers never expect to run at interrupt level then we could "trigger" these signals entirely within our own software, without fiddling with the T20 PSI system. However, to be most general it is best if actual PSIs are triggered -- this can be done with IIC%. While we are checking the pending signal mask, we have to be careful, because if an asynchronous signal happens just after the code has decided to trigger that signal, but before doing so, then the handler could be called twice. We need to ensure that incoming PSIs/signals are not serviced for the signals in _sigpendmask, even though _sigusys must be off. One straightforward method of solving this is to use DIR% to temporarily disable the PSI system: DIR% ; Suspend PSIs IIC% ; Initiate PSIs on chans EIR% ; Allow PSIs to take effect However, the overhead of additional monitor calls is a little annoying. We could also solve this in our software by having the PSI code NOT service an interrupt if _sigpendmask already indicates a signal instance is outstanding. Thus, _sigtrigpend() can proceed to trigger signals on just those bits in _sigpendmask without fear that an asynchronous interrupt will wrest away control in the middle; if a PSI does happen for one of those bits, it will see that it is already set in the mask and will simply DEBRK% right back. Note that the only place that _sigpendmask bits are turned off is within the PSI code just before jumping to a handler. There's a BUG in this scheme, though: how does the signal handler know when an interrupt on an already outstanding signal should be ignored, and when it should be handled? _sigtrigpend() is doing an IIC% which causes an interrupt indistinguishable from the real thing! So, to patch this up, the PSI code needs to check the PC and see whether it's at the IIC% in _sigtrigpend(); if it is, then it's OK to handle the signal. A third solution might be possible such that IIC% is unnecessary too. If the handler for a signal happens to want interrupt-level processing (as this would be a special feature, most handlers won't) then IIC% could be used anyway. This seems unlikely however since at SOME point PSIs need to be turned off so that bits can be tested and set without fear of having them changed out from under, and normally it's the PSI code itself that has this security because it is running at interrupt level. After all, we need to turn the bit in _sigpendmask off just before jumping to the handler. Another idea: don't worry about calling it twice, after all the interrupt did happen at least twice? Bad thought: not good to permit interrupts during sigtrigpend because some of them might want to change things about the signal system, by calling sigsetmask(), sigvec(), etc! Much safer to just prohibit any handler calls whatsoever.