OS/2 signal handling

From EDM2
Jump to: navigation, search

By Roger Orr

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 conjunction 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:

  • to set up an application signal handler
  • to ignore a signal
  • to return an error for a specific signal
  • 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);

  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.
  2. 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, especially 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 C/2 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 extremely 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.