OS/2 signal handling
                       ----------------------

OS/2 has a set of interprocess messages known generically as signals. They
are the simplest form of inter-process communication under OS/2 and are
invoked, for example, whenever you type Control+C to terminate a program.

OS/2 itself predefines some signals and their default signal handler, but
allows this default behaviour to be modified and also supports a few
user-defined signal types.

The commonest example of an application modifying the default signal handler
is to prevent Control+C aborting a program; and I am partly writing this
article because I have recently met two programs (one IBM, one MicroSoft)
which do NOT handle Control+C properly, so it seems likely that a brief
overview would be of use to some OS/2 programmers!

The basics.
-----------

There are 7 defined signals: four are system signals and three are application
signals.

The system signals are:- SIG_BROKENPIPE, SIG_KILLPROCESS, SIG_CTRLC and
SIG_CTRLBREAK.

The first signal, SIG_BROKENPIPE, is used to inform that a connection to a
pipe was broken.  It will not be discussed further in this article.

The second signal, SIG_KILLPROCESS, is sent when DosKillProcess() is invoked,
and the default signal handler exits the program.

The last two signals, SIG_CTRLC and SIG_CTRLBREAK, are sent by the keyboard
handler when Control+C or Control+BREAK are pressed.  The actual signal sent
depends upon the keyboard mode - in ASCII mode (the default) SIG_CTRLC is
sent by both Control+C and Control+BREAK, and in binary mode SIG_CTRLBREAK
is sent by Control+BREAK.
These signals can also be sent by using the DosSendSignal() function - note
though that the target process must be a CHILD process of the signaller!
As for SIG_KILLPROCESS the default signal handler exits the program.


The application signals are:- SIG_PFLG_A, SIG_PFLG_B and SIG_PFLG_C.
These signals are sent by the DosFlagProcess() function, and an argument may
also be supplied.  The default signal handler ignores these signals.


When a signal is issued thread 1 of the process is INTERRUPTED and the signal
handler is called.
Once the signal handler has returned (if it does) execution of the interrupted
thread will resume from the point of interruption.
Note - thread 1 is the initial thread of the process, as opposed to any other
threads created during execution with DosCreateThread.

OS/2 signal handling functions
------------------------------

OS/2 provides two functions for modifying the default behaviour of signals:
DosHoldSignal and DosSetSigHandler.


DosHoldSignal() is used to prevent signals while a critical section of code
is being processed.  This is typically used in conjuction with a semaphore
to prevent concurrent access by other threads in the process.  DosHoldSignal
is used as well to ensure that thread 1 does not get interrupted and thus
destroy the consistency of the operation. NOTE: signals will override even
DosEnterCritSec() unless this function is used.


DosSetSigHandler() is used to change the handling for individual signals.
Various flavours are available:

  o to set up an application signal handler
  o to ignore a signal
  o to return an error for a specific signal
  o to reset the signal handler to the default action

In addition, if you write a signal handler, it must call DosSetSigHandler
when it is called to acknowledge the signal - this tells OS/2 that this
signal can be sent again.


Complications of signal handling
--------------------------------

Thus far it seems easy - you just write a simple procedure to handle a specific
signal, call DosSetSigHandler() and away you go.

Unfortunately there are various possible problems.


1) Deletion of thread 1

OS/2 will only use the first thread for signal handling.  If you terminate
this thread then signals won't work any longer.


2) Access to shared resources

The first problem, which I touched on above, is that signals can occur at
ANY time.  This means being extremely careful about such things as semaphores
in order to prevent programs hanging.  For example, suppose a procedure
printit() uses a semaphore to guarantee orderly access to the screen.  If
the signal handler tries to use this function problems may well occur:

        printit()
            DosSemRequest(hsem, -1);
            .
            .
signal --->
            printit()
                DosSemRequest(hsem, -1);

The semaphore request in procedure printit() will lock because the thread
has already locked it.

This is a particular problem with the C runtime, especially the multithreaded
DLL, which uses semaphores to serialise access.

The safest rule is ... keep the signal handler REALLY SIMPLE.

One solution is to create a SEPARATE thread, which waits for a semaphore to
be cleared by the signal handler.


3) Poor documentation of ERROR_INTERRUPT

The first signal handler I wrote was simple: it acknowledged the Ctrl+C signal,
wrote out a string with VioWrtTTY and returned.  I tried testing it with
a simple program which used getch(), and found that after pressing
Ctrl+C and getting my message printed the program was exiting.  I traced
this to getch() returning -1, which was treated as end of file and hence
of the program!

The reason for this behaviour is actually very simple: OS/2 was returning
ERROR_INTERRUPT to the C runtime which it treated as an error.

Unfortunately as far as I can tell, ERROR_INTERRUPT is not documented - at least
not in the manuals I have access to.  So here is a brief explanation of
what this error return means, and why it is used.

Firstly, remember that thread 1 is interrupted by OS/2 in order to call
the signal handler.  If thread 1 is actually blocked inside OS/2 (waiting for
a semaphore to clear, perhaps) then OS/2 has a potentially difficult job
once the signal handler has completed in attempting to return the thread to
its original state - perhaps the semaphore which was being waited on has now
been cleared.  Rather than try and return to the same wait state internally,
OS/2 returns a 'fake' error from the function - ERROR_INTERRUPT.

This means that if your program has a signal handler then you must be prepared
for ANY OS/2 calls in thread 1 to return ERROR_INTERRUPT and to process this
correctly.  Again, be very careful if you call any of the C runtime functions
since they may not handle the error code in the way you want.


The safest course is usually to reserve thread 1 for signal handling alone.

Another approach is to use this feature to work with you rather than against
you.  For example the OS/2 debugger interface DosPTrace() is a BLOCKING call
and so on a GO command the return only occurs when the program being debugged
next halts.  To force a halt when the user presses Ctrl+C in the debugger's
screen group the debugger could install a minimal signal handler for Crtl+C
and do the real work, such as issuing a STOP command, on the ERROR_INTERRUPT
return.

A third approach with SINGLE threaded C programs is to use setjmp() and
longjmp().   The program sets a signal handler up and calls setjmp() to save
the current execution state.  If a signal occurs longjmp() can be called by
the signal handling procedure to return the thread to the location of the
setjmp() call.  This approach works very well when the program is doing
a lot of calculations, etc. as may then be hard to decide where in the code
to add checks for some global 'I have been interrupted' flag.  Used like
this it is nearly as good as the non-existent DosKillThread API which
everyone wants so much, except of course only for ONE thread...


4) DosDevIOCtl holding signals

If thread 1 is held by a DosDevIOCtl call, then signals are held until the
call completes.  This is a problem!  Be especially careful with 'hidden'
calls to DosDevIOCtl - I was caught out in a network server program which used
thread 1 to listen for incoming connections and spawned a thread to deal with
each one.  If Ctrl+C was pressed then the program would exit the next time
a client tried to connect but not before.

This can be solved by creating another thread, or by using a non-blocking call
and waiting with DosSemWait.


5) Signal handling and DLLs

Each signal has at most one active handler.  For this reason it is in general
NOT good practice to use signal handlers within DLLs since this involves
co-operation with any signal handling which the main program may be performing.


Uses for signals
----------------

If you manage to avoid the traps described above then there are a few useful
things signals can be used for, espcially during development.

This first, and obvious, use is to stop users killing programs in an
uncontrolled manner.  You can either stop them completely, add a prompt
for confirmation or do some of your own tidying up first.
Here is a very simple example:



#define INCL_DOS
#define INCL_DOSERRORS
#include        <os2.h>

#include        <stdio.h>
#include        <conio.h>


/*****************************************************************************/
/* sigint_handler: 'do nothing' gracefully with Ctrl+C and Ctrl+Break        */
/* DosSemWait will return ERROR_INTERRUPT                                    */
/*****************************************************************************/

VOID FAR pascal _loadds sigint_handler (USHORT sig_arg, USHORT sig_num)
   {
   /* keep compiler happy about unreferenced formal parameters */
   sig_arg = sig_arg;
   sig_num = sig_num;

   /* acknowledge signal and resume the interrupted thread */
   DosSetSigHandler( NULL, NULL, NULL,
                     SIGA_ACKNOWLEDGE,
                     SIG_CTRLC);
   }


/*****************************************************************************/
/* process: do the work                                                      */
/*****************************************************************************/

VOID process(void)
   {
   static ULONG ramsem = 0L;
   USHORT rc;

   DosSemSet(&ramsem);
   for (; ; )
      {
      rc = DosSemWait(&ramsem, -1);
      if (rc == ERROR_INTERRUPT)
         {
         printf("\nBREAK - do you want to exit ? ");
         if (getch() == 'y')
            break;
         printf("\n");
         }
      }
   }


/*****************************************************************************/
/* M A I N  P R O G R A M                                                    */
/*****************************************************************************/

int main(int argc, char **argv)
   {
   int rc;                        /* return code                             */
   PFNSIGHANDLER old_handler ;    /* receives value from DosSetSigHandler    */
   USHORT old_action ;            /* receives value from DosSetSigHandler    */


   /* keep compiler happy */
   argc = argc;
   argv = argv;

   /* set up signal handler for ctrl-c and break */
   rc = DosSetSigHandler((PFNSIGHANDLER)sigint_handler,
                         &old_handler,
                         &old_action,
                         SIGA_ACCEPT,
                         SIG_CTRLC);

   if (rc == 0)
      process();

   return rc;
   }


This can be compiled as:
        cl /AL /W3 /Zp /G2s flag.c -link os2

(I'm using Microsoft C6.0 but C5.1 or IBM C2 should take this too)

NOTE that I have marked the signal handler as _loadds.  Since this is a very
simple procedure in a very simple program it is not actually necessary but
in general it is obligatory since you will inherit the data segment which was
active at the time of the signal.  Without this keyword, which tells the
compiler to reload the expected value into the data segment on function entry,
the signal handler may well work sometimes but not always...



Another use for signals is to handle global debugging variables.  I have for
example used the user signal A with parameters 0 (off), 1 (on) and 2 (analyse)
for simple inline profiling.  Signals are ideal for this purpose because they
interrupt the signalled program, which is just what you want when you are
trying to find out why your program is looping!

The only problem with this is that you need to know the process ID for the
DosFlagProcess call.  There are three approaches:

1) A 'for' loop - send to all processes. Not recommended but can be quite
   exciting while it lasts....

2) Use a program like the user group's KBN (kill by name) to find the PID

3) Ensure the program to be flagged writes its PID somewhere - add a call
   to DosGetPID() and a print statement somewhere near the beginning of the
   program.


Conclusion
----------

Although signals are extemely simple in outline, and thus very nice to use for
quick and simple interprocess communication, there are unfortunately various
complications when it comes to actually using them in a real program.
It is worth experimenting with signal handlers, and I hope that by taking note
of these possible pitfalls your programs will properly deal with Ctrl+C