Trailing-Edge
-
PDP-10 Archives
-
SRI_NIC_PERM_FS_1_19910112
-
c/lib5/usys/codsig.doc
There are 5 other files named codsig.doc in the archive. Click here to see a list.
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 <signal.h>) 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 <imposs, fail w/err>
(b) in-line without ERJMP Continue fail with err msg
(c) jsys() with int flag do intret <imposs, but do intret>
(d) jsys() without flag Continue <imposs, but do intret>
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 <imposs, but take ERJMP>
(b) in-line without ERJMP Restart Restart
(c) jsys() with int flag do intret <imposs, but take intret>
(d) jsys() without flag Restart <imposs, but take intret>
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 <E> JSYS <E>
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 <jsys.h> 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 <jsys.h> 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
<determine bits to give IIC>
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.