mirror of
https://github.com/PDP-10/its.git
synced 2026-01-13 07:19:57 +00:00
Binary-only compiler and library, plus documentation and include files for compiling new programs.
470 lines
23 KiB
Plaintext
Executable File
470 lines
23 KiB
Plaintext
Executable File
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.
|