09 - Signals
Class: CSCE-313
Notes:
Outline
- What is a “signal”?
- Signal “handling”.
- UNIX signals API.
What is a signal?
Signals
- Signals are software interrupts. Handling signals is a way of reacting to "asynchronous" events.
- For e.g., a user at a terminal typing the interrupt key (usually
) to stop a program. - The term "asynchronous" refers to the fact that signals can arrive at any time during the execution of a process, without being explicitly requested by the process.
- For e.g., a user at a terminal typing the interrupt key (usually
- Every signal has a name that begins with
SIG.
Examples:
SIGABRTis the abort signal generated when a process calls theabortfunction.SIGALRMis the alarm signal generated when the timer set by thealarmfunction goes off.
Notes:
- If you define a handler for specific signals, you will jump directly to your defined handler the moment the signal comes in
- In the case of a signal, that something is actually written by you
- Asynchronous means that a signal can arrive at any time, even if you do not expect it.
- Examples:
SIGABRTgenerates a cool down for you -> a huge binary sort of like a debugging sessionSIGALARMsets an alarm to be reminded of something that you need to take care on the future.
Example
cat -
cat: reads standard input and copy that to the standard output- But you can send it a signal:
- you can do
^Cto send an INTERRUPT signal - There is a setting in your terminal that bounds the key bind
^Ctointr(interrupt)
- you can do
> stty -a
speed 9600 baud; 52 rows; 49 columns;
lflags: icanon isig iexten echo echoe -echok echoke -echonl echoctl
-echoprt -altwerase -noflsh -tostop -flusho pendin -nokerninfo
-extproc
iflags: -istrip icrnl -inlcr -igncr ixon -ixoff ixany imaxbel iutf8
-ignbrk brkint -inpck -ignpar -parmrk
oflags: opost onlcr -oxtabs -onocr -onlret
cflags: cread cs8 -parenb -parodd hupcl -clocal -cstopb -crtscts -dsrflow
-dtrflow -mdmbuf
cchars: discard = ^O; dsusp = ^Y; eof = ^D; eol = <undef>;
eol2 = <undef>; erase = ^?; intr = ^C; kill = ^U; lnext = ^V;
min = 1; quit = ^\; reprint = ^R; start = ^Q; status = ^T;
stop = ^S; susp = ^Z; time = 0; werase = ^W;
- This is programmable
- We have a way of binding control sequences to actions!
If we look at the types of signals:
> kill -l
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
- Lists the signals that the system supports
- Some interesting ones
WINCH: If you are running a program and you change the window size of that program, how can the program notice this change and manage its own view (that is through the WINCH signal)HUP: Hang Up SignalINT: Interrupt signalSTOP: quite important and it cannot be handable
Q&A
What happens when you press Ctrl-C on the terminal with a running foreground process?
It is your terminal driver that is looking at the characters that is passing to the reader. There is a program at the other end that reads characters from that file, but in between there is this terminal driver that looks at all of these characters. It is the terminal drive that will send the SIGINT signal, not to a process but to the process group.
- What the shell does is that it finds a group of processes and the UNIX API is able to send the signal to this group.
- It is the kernel that looks at what processes are in that process group and delivers that SIGINT signal to each process.
- At this point, you either handle the signal or you don't
- If you do not handle the signal, the kernel will just terminate you (most times the SIGINT signal means exit)
Notes:
- Signal is entirely a software concept, it is designed to mimic a hardware interrupt, but it is solely a UNIX creation.
How to deal with runaway processes
https://www.youtube.com/watch?v=z6sDujfLNfI
- Sometimes you can't really do anything
- You either power off your machine or you get into more tricky methods
Signals are asynchronous
func (int a1, int a2) {
int i, j = 2;
for (i = a1; i < a2; i++) {
j = j * 2; // <----
j = j / 127; // <----
// ...
}
}
- A signal can come for you at any time!
- If you go into your handler you may have invariance in mind that may be violated
- This is always a trouble and we have to figure out what to do with these problems
What is the purpose of signals?
- Allow humans to interact with programs using the terminal. For e.g.,
sends an interrupt signal SIGINT- Ctrl+Z suspends the process by sending
SIGTSTP
- Allow the kernel to enforce semantics
SIGSEGVis sent on memory exceptionsSIGILLsent on encountering illegal instructionsSIGPIPEwhen one writes to a pipe with no reader- This happens even mid-flight if readers exit
Notes:
- Generally
CTRL+Zis programmed toSIGTSTP- This is possible through the
waitsystem call - Children have not exited but have changed their status
- This is possible through the
SIGSEGV: you can't really do anything significant in the handler, since the OS will redo your instruction and again give you aSIGSEGVSIGILL: illegal instruction, yourexecsystem call is not checking is the instructions are valid or not, it just launches your program- Actually parsing a blob of binary into an instruction is not trivial
- You can convert a trap or exception into a signal and handle it that way
- Your program counter now is pointing to that
SIGILLinstruction- You can't actually ignore a signal
- If you intend to ignore it won't have the desire effect
- If you do not have an address mapped into you address space, ignoring it then won't help
Example of handler for an illegal isntruction:
void sigill_handler(int sig) {
print("Caught SIGILL (Illegal instruction)")
}
...
UNIX System signals
/CSCE-313/Lecture/Visual%20Aids/image-6.png)
- Remember
SIGPIPEhappens whenever one of the children does not close an end of a pipe
What can a process do about a signal-I?
A program can tell the kernel to do one of three things when a signal occurs. We call this the disposition of the signal, or the action associated with a signal.
- Accept the default action. All signals have a default action.
signal(SIGINT, SIG_DFL)- Ignore
- Terminate
- Terminate and dump core
- Ignore the signal. Works for most signals.
signal(SIGINT, SIG_IGN)- Note: cannot ignore SIGKILL and SIGSTOP. It is not advised to ignore hardware exception signals.
- Catch the signal. (Invoke a function). Tell the kernel to invoke a given function (signal handler) whenever the signal occurs.
signal(SIGINT, do_something)
Notes:
- You can ignore a lot of signals by just setting the handler to be the default which a lot of times will just terminate you.
What can a process do about signal-II?
- You can override the action for some signals
- For e.g., SIGINT, SIGUSRx, SIGTERM, SIGTSTP
- For others, you cannot (sometimes meaningfully) override
- SIGKILL, SIGILL, SIGFPE, SIGSEGV, SIGSTOP,...
- "Overriding" is also called "signal handling", or "catching".
- SIGCHLD is the only signal so far that is ignored by default.
- Most others will kill the process except for SIGCONT, SIGSTOP, and SIGTSTP
Notes:
- The essence of being able to catch signals is to do something useful before actually exiting, otherwise why would they be useful?
Where do signals come from?
From the user: Terminal-generated signals: for e.g.,
kill(2)function: Sends a signal to another process.kill(1)command: The command-line interface to kill(2)
From the kernel: CPU Exceptions delivered as signals
SIGFPEdivide by 0 ,SIGSEGVinvalid memory reference,SIGILLwhen an instruction with illegal opcode is found.
From processes: Software generated signals
SIGURGby out-of-band data on network connectionSIGPIPEby broken pipeSIGALRMby timer
Notes:
SIGURG: Sig Urgent- Imagine you are on an ssh connection and type Ctrl+C while on the ssh terminal, how does that affect you and the remote server?
- The only thing between you and the other guy is the network pipe
- Ctrl+C is not meaningful in a network pipe
- Your network pipe is actually a bidirectional duplex stream of data
- Even if you could get the bytes of Ctrl+C and push it into the network pipe, this Ctrl+C is living behind the blob of data.
- At the other end what is going to happen is that your server will read in such a way that it can be processed out of band.
- Analogy of Leading to the side when an ambulance is passing by, the network needs a fast route to reach the server so that signals can be delivered?
Generating Signals: kill(2) and raise(3)
#include <signal.h>
int kill(pid_t pid, int sig); /* send signal sig to process pid */
/* example: send signal SIGUSR1 to process 1234 */
if (kill(1234, SIGUSR1) == -1)
perror("Failed to send SIGUSR1 signal");
/* example: kill parent process */
if (kill(getppid(), SIGTERM) == -1)
perror("Failed to kill parent");
#include <signal.h>
int raise(int sig); /* Sends signal sig to itself. Part of ANSI C */
raisesends a signal to the executing processkillsends a signal to the specified process
Q&A
What happens if a signal arrives while read() is blocking?
char buf[100];
read(fd, buf, 100);
- Does
read()always return? - What does it return?
- When does it automatically restart? You don't know this yet, but it
Notes:
- If you did not manage to read a single byte, then you will return with -1 and the global variable ERRORNO will be set.
- If read returns with -1 (some generic value), you need to interpret why a read actually failed, and you can do that with
perror
- If read returns with -1 (some generic value), you need to interpret why a read actually failed, and you can do that with
- Read will return if it managed to read some bytes.
- You ask read to read 1000 bytes, but it read 200 bytes so far before the interruption, it will return you the 200 bytes.
- You can say please restart this system call.
- If this happens you will execute your handler and you will go back at the same exact point where you interrupted your read.
- Is useful to know that some system calls are restartable
- Every time read returns you need to check if it got interrupted
- Now every read turns in sort of like a little while loop when you have signals enabled
- In some sense you do not have to worry about if read is interrupted.
Signal “handling”
Registering a handler for a signal
#include "apue.h"
void sighandler (int signum) {
printf ("\nSignal caught!\n");
}
int main () {
signal(SIGINT, sighandler);
for(int i = 0; i < 5; i++) {
printf("Sleeping...\n");
sleep(5);
printf("Awake\n");
}
}
davidkebo@linux:~/code/week8$ ./a.out
Sleeping...
Awake
Sleeping...
Awake
Sleeping...
^C
Signal caught!
Awake
Sleeping...
Awake
Sleeping...
^C
Notes:
- You just give the signal number, and say "register this handler"
- The prototype is that it takes an integer as an argument and does not return anything
- Whenever this signal occurs is delivered to this process and just like a software interrupt, to wherever you got interrupted you will go back.
- When you are
sleep(5), if this program receives an interrupt,sleep()will prematurely return depending on where it got the signal from - When this program is sleeping, we just need to press
^Cto see the magic.- Every time we press
{ #C}
, the signal handler gets invoked and then the program continues from where it was.
- Every time we press
One handler for multiple signals
void sig_usr(int signo) { /* argument is signal number */
if (signo == SIGUSR1)
printf ("received SIGUSR1\n");
else if (signo == SIGUSR2)
printf ("received SIGUSR2\n");
else
printf ("received signal %d\n", signo);
return;
}
int main(void) {
signal(SIGUSR1, sig_usr);
signal(SIGUSR2, sig_usr);
for (;;) {
pause ();
}
}
Notes:
- There is no reason why you can't register the same handler for different signals
- In the body of your handler you can basically discriminate and find out which specific signal interrupted you, and then you go from that.
- An application can use signals for various things!
Q&A
What happens if a signal arrives while your signal handler is already running?
When a signal arrives while its signal handler is already running, the default behavior in POSIX systems is to block (defer) the new signal until the current handler finishes. The subsequent signal will then be delivered (and its handler called) after the first handler returns.
Notes:
- What actually happens is that when the kernel is executing your handler, the kernel automatically blocks that signal
- That means that if you type another
^Cit is not going to recursively invoke that function - The problem is that if you are not a pure function, then you have a problem and need to synchronize so you need to be very careful about writing your handler
- One of the benefits of automatically blocking you signal is that the kernel will do it for you
- If you did receive the signal of that same type, it is not loss, but it is pending, and the kernel knows it.
- Signal handlers are kind of like atomic
- What happens with ordinary signals is that signals are coallessed
- If you got two signals of the same type, the kernel will deliver only one to you
- This is why using signals as a mean of counting is actually problematic
- POSIX later added real time signals that actually correctly count but we do not worry about this
Reaping child processes
void my_handler (int sig) {
pid_t pid = wait (0);
printf("Chd proc %d exited.\n", pid);
}
int main () {
signal (SIGCHLD, my_handler);
// ith proc sleeps for i sec and dies
for (int i = 1; i <= 5; i++) {
int pid = fork ();
if (pid == 0) {
sleep (i);
return 0;
}
}
// parent in an infinite busy loop,
while (1) {
printf ("Relaxing\n");
sleep (1);
}
}
- When a child process terminates, the kernel sends a
SIGCHLDsignal to its parent process. - The parent can handle this signal to reap the child process.
- This operation is asynchronous, i.e., the parent process can do other things instead of continually waiting.
Notes:
- In our handler we are going to
wait(0)which means we will wait for any children to be reaped - What could go wrong with this strategy?
- When you use a handler like this, and you have 5 children you want to call
wait()5 times. So in some sense you are using the handler for counting. - But what it forgets is that pending signals can be coallessed
- It is possible that your first child exited, and while you were in there all the remaining child exited as well, they all generated SIGCHLD but they were all coallessed into one. This makes all of this processes become zombie.
Reaping Child Processes With Simultaneous Termination
void my_handler (int sig) {
pid_t pid;
while ((pid = waitpid(-1, 0, WNOHANG)) > 0) {
printf("Child process %d exited.\n", pid);
}
}
int main () {
signal (SIGCHLD, my_handler);
// create 5 child processes.
for (int i = 1; i <= 5; i++) {
int pid = fork ();
if (pid == 0) {
sleep (5);
return 0;
}
}
// parent in an infinite loop, busy doing something else
while (1) {
printf ("Relaxing\n");
sleep (1);
}
}
Notes:
- The way you get around this is to reap your children in a while loop
WNOHANbasically says "don't hang me and you will return"- Your while loop will catch all of them
- But if they exit outside your while loop you will have a pending signal.
Nested signals
int s = 0;
static void sig_quit(int signo) {
printf("In sig_quit, s=%d. Now sleeping...\n", ++s);
sleep(5);
printf("sig_quit, s=%d: exiting\n", s);
}
static void sig_int(int signo) {
printf("Now in sig_int, s=%d. Returning immediately.\n", ++s);
}
int main(void) {
printf("\n=> Establishing initial signal handler via signal.\n");
signal(SIGQUIT, sig_quit);
signal(SIGINT, sig_int);
sleep(5);
printf("Now exiting.\n");
exit(EXIT_SUCCESS);
}
Notes:
- While you are sleeping, you get another signal!
- Does the second signal overwrite the signal? No!
- What is going to happen is that it will nest, it will look like it is a nested call
- It will nest a call to the
sig_inthandler, once it finishes, it goes back to the other signal, and once that finishes, it can go back to the point it was interrupted
=> Establishing initial signal hander via signal.
^\In sig_quit, s=1. Now sleeping...
^CNow in sig_int, s=%2. Returning immediately.
sig_quit, s=2: exiting
Now exiting.
- We went to
sig_quit, then we were in sleep and when we type
{ #C}
...
The problem with asynchrony
#include <signal.h>
typedef struct {
int x;
int y;
} computation_state_t;
computation_state_t state;
int main() {
void handler (int);
signal(SIGINT, handler);
long_running_procedure();
}
long_running_procedure() {
while(1) {
update_state(&state);
compute_more();
}
}
void handler(int sig) {
display (&state);
}
Notes:
- You have no idea when you can be interrupted!
- This means you can be in an inconsistent state.
update_stateis not atomic, it is inconsistent- This manifests in other ways
- How to fix it? We will get to that later
UNIX signals API
The signal function
The simplest interface to the signal features of the UNIX System is the signal function.
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
// Returns: previous disposition of signal if OK, SIG_ERR on error
signum is the name of the signal.
handler is either (1) the constant SIG_IGN, (2) the constant SIG_DFL, or (3) the address of a function to call when the signal occurs.
SIG_IGNtells the system to ignore the signal.SIG_DFLsets the action associated with the signal to its default value.- When we specify the address of a function to be called when the signal occurs, we're catching the signal. This function is called the signal handler.
Notes:
- If you wanted to ignore the signal, you can do that in a meaningful way, but you can also just say
SIG_IGN(ignore the signal) otherwise the default action would be to terminate the process, then you can saySIG_DFL
kill and raise functions
The term kill in the UNIX System is a misnomer.
The kill() function sends a signal to a process or a group of processes.
The raise() function allows a process to send a signal to itself.
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
Note: The call raise(signo) is equivalent to kill(getpid(), signo);
kill function
The kill() function sends a signal to a process or a group of processes.
#include <signal.h>
int kill(pid_t pid, int signo);
There are four different conditions for the pid argument to kill.
- pid
: The signal is sent to the process whose process ID is pid. - pid == 0 : The signal is sent to all processes whose process group ID equals the process group ID of the caller and for which the sender has permission to send the signal.
- pid < 0 : The signal is sent to all processes whose process group ID equals the absolute value of pid and for which the sender has permission to send the signal.
- pid == -1 : The signal is sent to all processes on the system for which the sender has permission to send the signal.
Note:
- If you provide an argument > 0 then you are sending a signal
- This value is overloaded with various things
- For us, it is just being able to send the signal to a PID
alarm
The alarm function sets a timer that will expire at a specified time SIGALRM signal is generated.
If we don't catch this signal, its default action is to terminate the process.
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// Returns: 0 or number of seconds until previously set alarm
- The
secondsvalue is the number of clock seconds in the future when the signal should be generated. - When that time occurs, the signal is generated by the kernel, although additional time could elapse before the process gets control to handle the signal, because of process scheduling delays.
There is only one alarm per process.
- When the argument
- Replaces any previous alarm.
- Returns # of seconds left in previous alarm, or 0.
- When the argument is 0
- Cancels any previous alarm.
- Returns # of seconds left in the previous alarm, or 0 .
pause function
The pause function suspends the calling process until a signal is caught.
#include <unistd.h>
int pause(void); // Returns: -1 with errno set to EINTR
The only time pause returns is if a signal handler is executed and that handler returns.
Notes:
- The handler is involved if the handle returns
- useful function to basically wait for a signal to come
Signal Sets
POSIX. 1 defines the data type sigset_t to contain a signal set and the following five functions to manipulate signal sets (multiple signals).
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); //ret 1 if true, 0 if false, - 1 on error
sigemptysetinitializes the signal set pointed to by set so that all signals are excluded.sigfillsetinitializes the signal set so that all signals are included. All applications have to call eithersigemptysetorsigfillsetonce for each signal set, before using the signal set.sigaddsetadds a single signal to an existing set.sigdelsetremoves a single signal from a set.
In all the functions that take a signal set as an argument, we always pass the address of the signal set as the argument.
typedef unsigned long sigset_t; (/usr/include/x86_64-linux-gnu/asm/signal.h)
Notes:
- A very handy way of indicating types
- Think of this as just being 64 bits (some number of bits) and you just have this function to turn on bits and turn off bits
- An abstraction to be able to send and set signals
- This is basically how you do OOP in C.
- In C++ this would be implicitly
this, but in C you basically pass a pointer to a struct that serves asthis- In Golang, this is basically how it does OOP
sigset_twill turn on all the signals, and then you can do things with it- A nice encapsulation for signal
Automatic and manual signal blocking
Blocking is automatic for a signal when the signal handler is invoked
- First, if SIGINT is delivered, the process's SIGINT handler is invoked
- Simultaneously, SIGINT is blocked for the process.
- I.e., if another SIGINT occurs during the handler execution, it is recorded in the pending bit vector, but not delivered
- When handler returns, signals of that type can start to be delivered again.
However, a process can also manually block/unblock the delivery of a signal, using sigprocmask()
Notes:
- Atomic in the sense that you won't see signals between a block and an unblock
- For that we need this thing called
sigprocmask
sigprocmask
The signal mask of a process is the set of signals currently blocked from delivery to that process. Think of it as a bit vector.
A process can examine its signal mask, change its signal mask, or perform both operations in one step by calling the following function.
int sigprocmask(int option, const sigset_t *new_set, sigset_t *old_set)
option: Indicates the way in which the existing set of blocked signals should be changed. (SIG_BLOCK, SIG_UNBLOCK, SIG_SETMASK)new_set: Points to a signal set giving the new signals that should be blocked or unblockedold_set: Points to a memory location wheresigprocmask()can store a signal set.
| SIG_BLOCK | The new signal mask for the process is the union of its current signal mask and the signal set pointed to by set. That is, set contains the additional signals that we want to block. |
|---|---|
| SIG_UNBLOCK | The new signal mask for the process is the intersection of its current signal mask and the complement of the signal set pointed to by set. That is, set contains the signals that we want to unblock. |
| SIG_SETMASK | The new signal mask for the process is replaced by the value of the signal set pointed to by set. |
| Notes: |
- Block, unblock or set mask
- Imagine that signals are basically a bit mask
- With
sigprocmask, in addition to what you are blocking you will also block whatever signal you specify, the way you specify it, is to create a structure ofsigset_t(a set of signals)
Asynchrony revisited
computation_state_t state;
sigset_t set;
int main() {
void handler (int);
sigemptyset(&set);
sigaddset(&set, SIGINT);
signal(SIGINT, handler);
long_running_procedure();
}
long_running_procedure() {
while(1) {
sigset t old set;
sigprocmask(SIG_BLOCK, &set, &old_set);
update_state(&state);
sigorocmask(SIG_SETMASK, &old_set, 0);
compute_more();
}
}
void handler(int sig) {
display(&state);
}
Notes:
- If you can't afford to receive signals, you block signals so that the handler does not get called, so that you do not worry about consistency within your signals
sigproccan either block or unblock, it is actually doing both
sigaction function
The sigaction() function examines, changes, or both examines and changes the action associated with a specific signal.
#include <signal.h>
int sigaction(int signo, struct sigaction *act, struct sigaction *oact);
sig: (Input) A signal from the list defined in Control Signals Table.
*act: (Input) A pointer to the sigaction structure that describes the action to be taken for the signal.
- If act is a NULL pointer, signal handling is unchanged.
- If act is not NULL, the action specified in the sigaction structure becomes the new action associated with sig.
*oact: (Output) A pointer to a storage location where sigaction() can store a sigaction structure. This structure contains the action currently associated with sig.
- If oact is a NULL pointer, sigaction() does not store this return information.