Signal Handling

Under the hood there isn't much that's different about signal handling in Charcoal, compared to C. However, there is a pattern that works out much nicer with activities than with threads.

First, I want to point out that signal handling in C plays two very different roles that are smooshed together into one feature. The conventional names for the roles are synchronous and asynchronous, but I don't think those names sufficiently emphasize how different the two roles are. I think better names are "low-level exception handling" and "reacting to the outside world".

Synchronous signals are delivered in direct response to something that the executing process did (divide by zero, execute an invalid instruction, access an illegal memory location, etc). In most modern programming languages these kinds of actions would raise an exception (or "throw" it depending on your preferred exception terminology). In C you get a signal delivered.

Asynchronous signals are delivered because of something external to the executing process (keyboard press, network packet arrival, timer expiration, etc). Asynchronous signals can be delivered at essentially any time, which makes them extremely dangerous. The official rules about what exception handlers are allowed to do are extremely restrictive, because if they do anything moderately interesting terrible breakage is likely to ensue. This example is about asynchronous signal handling. There's not much interesting to say about the synchronous side.

Here's a true story from the code mines. I once worked on a project that would experience deadlocks once in a blue moon with debug builds only. It turns out that the problem was:

So if a signal happened to arrive right in the middle of a malloc call, the handler would deadlock on trying to acquire the mutex. We fixed this by reorganizing the handler to not call malloc, but that required some unpleasant contortions.

In multithreaded applications, you can do something more pleasant for asynchronous signals, which is have some thread block waiting for a signal to arrive. This looks like the following:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
int sig_hand_thread_entry( void *args )
{
    
int sig;
    
sigset_t sigset;
    
/* initialize sigset */
    
while( true )
    
{
        
if( 0 != sigwait( &sigset, &sig ) )
        
{ /* handle error */ }
        
switch( sig )
        
{ /* handle signal */ }
    
}
}

void start_signal_handler( thrd_t *thr )
{
    
thrd_t _t;
    
if( !thr ) thr = &_t;
    
int err_code = thrd_create(
        
thr, sig_hand_thread_entry, NULL );
}

Using this pattern would have taken care of our deadlock in malloc problem, but as I argue elsewhere in these pages I am not generally enthusiastic about using threads for much of anything. You can implement the exact same pattern with activities:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
  
activity_t start_signal_handler( void )
{
    
return activate
    
{
        
int sig;
        
sigset_t sigset;
        
/* initialize sigset */
        
while( true )
        
{
            
if( 0 != sigwait( &sigset, &sig ) )
            
{ /* handle error */ }
            
switch( sig )
            
{ /* handle signal */ }
        
}
    
}
}

Beyond the syntactic differences, there is one substantive difference between using threads and activites to implement the blocking signal handler pattern. With threads, the delivery of a signal might interrupt whatever thread is running to context switch over to the thread that is sigwaiting. This kind of rude interruption is what makes multithreaded programming hard to get right.

With activities the delivery of a signal that is being waited on causes a flag to be set such that the next time the running activity yields, the program will context switch to the waiting activity. On the plus side, this gentler approach to interruption makes it easier to avoid concurrency bugs. On the minus side, a modest amount of time might expire before the current activity yields. This will add some latency to the handling of the signal. Well-behaved Charcoal code should be yielding at least once every millisecond, so this latency shouldn't be a killer for most applications. If you're doing something more real-time, a millisecond may be far too long to wait. I'll address that situation in a different example.


Creative Commons License
Charcoal Programming Language by Benjamin Ylvisaker is licensed under a Creative Commons Attribution 4.0 International License