Sitemap
Priii

Priii

Product Security Engineer | Documenting and sharing my learnings here!

Understanding Signals in Linux

6 min readFeb 15, 2025

A signal is a standardised notification message used in Unix and POSIX-compliant operating systems.
Some of the common signals exchanged between processes and Kernel are, SIGCHILD is sent to parent when the child process terminates, SIGSEGV is raised by the kernel when process makes illegal memory access, SIGFPE is raised when hardware exceptions like divide-by-zero errors occurs.

These signals can be categorised to standard signals offered by Linux [0] and customised / real time signals (SIGUSR1 and SIGUSR2).

Let’s understand how to send a signal, change the default behaviour, various situations where blocking of signal is required, inheritance of signals on fork and threads and lastly how kernel handles these requests.

Sending a signal
A signal can be asynchronously sent to a running program to notify it of some event by either another process or kernel.

A process can send signal to another process in following ways

  1. Using kill syscall
// refer [1]
int kill(pid_t pid, int sig);

2. kill command from terminal

kill [signal] PID

3. Hardware sending signal

  • ctrl-c: sends SIGINT to interrupt them,
  • ctrl-\\: sends SIGQUIT to kill them,
  • ctrl-z: sends SIGTSTP to suspend them.

All of these signals that the system can send to processes can be checked by kill -l command on terminal.

Receiving a signal
Processes have two bit vectors with respect to signals : pending and blocked.
Upon receiving a signal, kernel simply sets the appropriate bit to 1 in the destination process’ s pending bit vector.
Note : If there is a pending signal SIGCHILD, another signal of same type cannot be sent until action on current one is taken.

When the process resumes, kernel checks the signals that are pending and not blocked.

If the set is empty, execution moves on to the program’s next instruction.
Else, kernel chooses a signal (typically the smallest) and forces the process to react to it with an action.

The action in turn can be as follows

  • Ignore the signal.
  • Terminate/stop its own execution.
    - the process immediately terminates
    - the process immediately terminates does a core dump
  • Suspends the process’s execution until it receives the SIGCONT signal.
  • Intercept the signal by executing its own handler in response.

How to change the default behaviour of a signal?
Each signal has a default action, but it can be changed to perform user defined action using signal handlers. Exception being SIGKILL and SIGSTOP, which cannot be intercepted or have their default actions modified.

Setting up of signal handlers to change default behaviour can be done via two syscalls

  • signal : not recommended due to portability issues.
  • sigaction : function to change the action associated with a signal.
#include <signal.h>

int sigaction(int sig, const struct sigaction *__restrict__ new,
struct sigaction *__restrict__ old);

int sig : number associated with the signal whose behaviour is to be changed

Get Priii’s stories in your inbox

Join Medium for free to get updates from this writer.

const struct sigaction *new : if not NULL, points to a sigaction structure [2] where the customized action upon receiveng sig can be updated.

struct sigaction *old : can store sigaction structure describing the action currently associated with sig.

Let’s understand with an example

// A simple example to change the behaviour of SIGINT using sigaction.

#include <iostream>
#include <signal.h>

using namespace std;

void set_signal_action();
void signal_handler(int);

int main(){
//install a signal handler
set_signal_action();
while(1);
return 0;
}

void set_signal_action(){
struct sigaction sa;
bzero(&sa,sizeof(sa));
//update sa_handler to customize signal behaviour
sa.sa_handler = &signal_handler;
// register a signal handler for SIGINT
sigaction(SIGINT,&sa,NULL);
}

//signal_handler, a user defined function to be called on SIGINT
void signal_handler(int SIGNAL){
if(SIGNAL == SIGINT)
cout<<"Intercepted SIGINT. Press Ctrl \\ to exit \n";
}

Executing the above program let’s a user send SIGINT to the process by pressing Ctrl+C keys. Default behaviour of SIGINT is process termination. However as we have registered a signal_handler function for SIGINT via sigaction, upon pressing Ctrl+C, the process doesn’t get terminated but action is now according to what is defined in signal_handler i.e displays “Intercepted SIGINT. Press Ctrl \\ to exit”.

Signal safe functions
Not all functions can be called at signal handlers.
For example in case of stdio functions like printf, if it’s interrupted while the stdout buffer is being updated by another signal handler which also calls printf, stdout buffer is now corrupted. This might lead to undefined behaviour. Therefore, async-signal-safe function, one that can be safely called within signal handler like the ones mentioned here should be used.

Signals sometimes also needs to be blocked. Ask why?
Blocking a signal means telling the kernel to hold it and deliver later. As signals are asynchronous, meaning they can arrive at any time during execution, signals might need to be blocked.

  • A handler can itself be interrupted by another Signal / handler.
    Suppose a signal handler for SIGINT is registered where an important operation is to be performed but it can be interrupted by other signals which needs to blocked until the signal handler for SIGINT is completed.
    - It can be done by either setting the sa.sa_mask sigaction struct. This allows the signals to be blocked for the duration of signal handler’s execution.
    - Or block signals using sigprocmask. It allows blocking and unblocking of signal at any time. Demonstrated as follows :
#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

void install_signal_handler();
void signal_handler(int sig);
void block_signal(int sig);
void unblock_signal(int sig);


int main(){
install_signal_handler();
while(1);
return 0;
}

void signal_handler(int sig){
if(sig != SIGINT)
return;
cout<<"SIGINT caught \n";
block_signal(SIGQUIT);
sleep(3); // allow user to interrupt via SIGQUIT (Ctrl\\)
unblock_signal(SIGQUIT);
}

void install_signal_handler(){
struct sigaction sa;
bzero(&sa,sizeof(sa));
sa.sa_handler = &signal_handler;
sigaction(SIGINT,&sa,NULL);
}

void block_signal(int sig){
cout<<"Blocking signal\n";
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,sig);
sigprocmask(SIG_BLOCK,&sigset,NULL);
}

void unblock_signal(int sig){
cout<<"Unblocking signal\n";
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,sig);
sigprocmask(SIG_UNBLOCK,&sigset,NULL);
}
  • Block signals while you modify global variables that are also modified by the handlers for these signals.
    Suppose the global variables are updated in various signal handlers, these signals need to be blocked in each handler to avoid undefined changes to global variable.

What happens when there is both signal blocking and signal handling?
If there is signal blocker and signal handler simultaneously registered, when such a signal arrives, it is withheld till signal unblock is called. When the signal is unblocked, default action is not taken but the user defined action for the signal registered via signal handler is processed.

#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

//signal handler for SIGINT
void signal_handler(int sig){
if(sig != SIGINT)
return;
cout<<"SIGINT caught, Press Ctrl \\ to exit\n";
}

//register signal handler for SIGINT
void install_signal_handler(){
struct sigaction sa;
bzero(&sa,sizeof(sa));
sa.sa_handler = &signal_handler;
sigaction(SIGINT,&sa,NULL);
}

//block a signal
void block_signal(int sig){
cout<<"Blocking signal\n";
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,sig);
sigprocmask(SIG_BLOCK,&sigset,NULL);
}

//unblock a signal
void unblock_signal(int sig){
cout<<"Unblocking signal\n";
sigset_t sigset;
sigemptyset(&sigset);
sigaddset(&sigset,sig);
sigprocmask(SIG_UNBLOCK,&sigset,NULL);
}

int main(){
//register a signal hadnler for SIGINT
install_signal_handler();

//block SIGINT for a while
block_signal(SIGINT);
sleep(5);
//unblock SIGINT now
unblock_signal(SIGINT);

while(1);

return 0;
}

A signal handler is first registered for SIGINT to change the default behaviour of process termination. It’s however blocked for a while using block_signal(SIGINT). If a SIGINT arrives during this period, its withheld and not delivered to program yet. Once the SIGINT is unblocked via unblock_signal(SIGINT), signal is processed but as the default behaviour is changed due to signal handler, “SIGINT caught, Press Ctrl \\ to exit” is printed. SIGINT remains unblocked from here on.

Note if no signal handler were registered, upon unblocking, process would be terminated.

Customised Signals
As signals are notification method, it can be used for communication and mutli-thread / process synchronization as well. SIGUSR1 and SIGUSR2 are provided which a programmer can use for inter-process communication instead/along with other IPC methods and define behaviour associated with them.

Signals and Multi threading
Signal inheritance in process and threads is as follows

  • During clone, if CLONE_SIGHAND is set, the signal handlers registered in parent is also duplicated in child process. Signal masks and sets of pending signals are different though. However, a new signal handler for the same signal in child overrides the inherited one.
#include <iostream>
#include <signal.h>
#include <unistd.h>

using namespace std;

void install_signal_handler();
void signal_handler(int sig);

int main(){
install_signal_handler();
int pid = fork();
if (pid < 0){
cout<<"Error creating child process\n";
exit(1);
}else if (pid == 0){
cout<<"In Child Process, pid: "<<getpid()<<"\n";
while(1); //let user try kill -2 childPID
exit(0);
}else{
int status;
wait(&status);
cout<<"Parent process exiting now\n";
}
return 0;
}

void signal_handler(int sig){
if(sig != SIGINT)
return;
cout<<"SIGINT caught, try kill -9 "<< getpid() << " to terminate\n";
}

void install_signal_handler(){
struct sigaction sa;
bzero(&sa,sizeof(sa));
sa.sa_handler = &signal_handler;
sigaction(SIGINT,&sa,NULL);
}
  • In a multi threaded process too, each thread inherits signal handlers but not signal masking.

Thank you for reading! Hope the article sent out the right signals :D

--

--

Priii
Priii

Written by Priii

Product Security Engineer | Documenting and sharing my learnings here!