8.5.Signal

\(8.5.\)Signal

1.Signal Terminology

  A signal is a small message that notifies a process that an event of some type has occurred in the system.

  The transfer of a signal to a destination process contains two distinct steps:

  • Sending a signal: The kernel sends a signal to a destination process by updating some state in the context of the destination process.

  The signal is delivered for one of two reasons:

  • The kernel has detected a system event such as a divide-by-zero error or the termination of a child process.

  • A process has invoked the kill function.

  • Receiving a signal: A destination process receives a signal when it is forced by the kernel to react in some way to the delivery of the signal.

    • The process can either ignore the signal, terminate, or catch the signal by executing a user-level function called a signal handler.

  • A signal that has been sent but not yet received is called a pending signal.

  • At any point in time, there can be at most one pending signal of a particular type. If a process has a pending signal of type \(k\), then any subsequent signals of type \(k\) sent to that process will be ignored and simply discarded.

  • A process can selectively block the receipt of certain signals. When a signal is blocked, it can be delivered, but the resulting pending signal will not be received until the process unblocks the signal.

  • For each process, the kernel maintains the set of pending signals in the pending bit vector, and the set of blocked signals in the blocked bit vector.

    • The kernel sets bit \(k\) in pending whenever a signal of type \(k\) is delivered and clears bit \(k\) in pending whenever a signal of type \(k\) is received.

2.Sending Signals

  \(a.\)Process groups

  Every process belongs to exactly one process group, which is identified by a positive integer process group ID.

  The getpgrp function returns the process group ID of the current process.

1
2
3
4
5
#include <unistd.h>

pid_t getpgrp(void);

/* Returns: process group ID of calling process */
  • By default, a child process belongs to the same process group as its parent.

  We can change the process group of a process buyt using the setpgid function:

1
2
3
4
5
#include <unistd.h>

int setpgid(pid_t pid, pid_t pgid);

/* Returns: 0 on success, −1 on error */
  • If pid is zero, the PID of the current process is used.

  • If pgid is zero, the PID of the process specified by pid is used for the process group ID.

  \(b.\)Sending signals from the keyboard

  • Unix shells use the abstraction of a job to represent the processes that are created as a result of evaluating a single command line.

  • At any point in time, there is at most one foreground job and zero or more background jobs.

  \(c.\)Sending signals with the kill function

  Processes send signals to other processes(including themselves) by calling the kill function:

1
2
3
4
5
6
#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

/* Returns: 0 if OK, −1 on error */
  • If pid is greater than zero, then the kill function sends signal number sig to process pid.

  • If pid is equal to zero, then kill sends signal sig to every process in the process group of the calling process, including the calling process itself.

  • If pid is less than zero, then kill sends signal sig to every process in process group |pid|(the absolute value of pid).

  \(d.\)Sending signals with the alarm function

  A process can send SIGALRM signals to itself by calling the alarm function:

1
2
3
4
5
#include <unistd.h>

unsigned int alarm(unsigned int secs);

/* Returns: remaining seconds of previous alarm, or 0 if no previous alarm */

3.Receiving Signals

  When the kernel switches a process \(p\) from kernel mode to user mode, it checks the set of unblocked pending signals(pending & ~blocked) for \(p\).

  • If this set is empty, the kernel passes control to the next instruction in the logical control flow of \(p\).

  • If the set is nonempty, the kernel chooses some signal \(k\) in the set(typically the smallest \(k\)) and forces \(p\) to receive signal \(k\).

    • Once the process completes the action, control passes back to the next instruction.

  A process can modify the default action associated with a signal by using the signal function:

1
2
3
4
5
6
#include <signal.h>
typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

/* Returns: pointer to previous handler if OK, SIG_ERR on error (does not set errno)*/

  The signal function can change the action associated with a signal signum in one of three ways:

  • If handler is SIG_IGN, then signals of type signum are ignored.

  • If handler is SIG_DFL, then the action for signals of type signum reverts to the default action.

  • Otherwise, handler is the address of a user-defined function, called a signal handler, that will be called whenever the process receives a signal of type signum.

  Signal handlers can be interrupted by other handlers:

4.Blocking and Unblocking Signals

  • Implicit blocking mechanism: By default, the kernel blocks any pending signals of the type currently being processed by a handler.

  • Explicit blocking mechanism: It can be done by using the sigprocmask function and its helpers:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);

/* Returns: 0 if OK, −1 on error */

int sigismember(const sigset_t *set, int signum);

/* Returns: 1 if member, 0 if not, −1 on error */

  The sigprocmask function changes the set of currently blocked signals. The specific behavior depends on the value of how:

  • SIG_BLOCK: Add the signals in set to blocked (blocked = blocked | set).

  • SIG_UNBLOCK: Remove the signals in set from blocked (blocked = blocked & ~set).

  • SIG_SETMASK: blocked = set.

  \(a.\)Correct signal handling

  Because the pending bit vector contains exactly one bit for each type of signal, there can be at most one pending signal of any particular type. Thus, if two signals of type \(k\) are sent to a destination process while signal \(k\) is blocked because the destination process is currently executing a handler for signal \(k\), then the second signal is simply discarded.

  This characteristic of signal handling may lead to some mistakes. Consider the following program, for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/* WARNING: This code is buggy! */

void handler1(int sig)
{
int olderrno = errno;

if ((waitpid(-1, NULL, 0)) < 0)
sio_error("waitpid error");
Sio_puts("Handler reaped child\n");
Sleep(1);
errno = olderrno;
}

int main()
{
int i, n;
char buf[MAXBUF];

if (signal(SIGCHLD, handler1) == SIG_ERR)
unix_error("signal error");

/* Parent creates children */
for (i = 0; i < 3; i++) {
if (Fork() == 0) {
printf("Hello from child %d\n", (int)getpid());
exit(0);
}
}

/* Parent waits for terminal input and then processes it */
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
unix_error("read");

printf("Parent processing input\n");
while (1)
;

exit(0);
}

  When executing, we get the following result:

1
2
3
4
5
6
7
8
linux> ./signal1
Hello from child 14073
Hello from child 14074
Hello from child 14075
Handler reaped child
Handler reaped child
CR
Parent processing input

  We first do some analysis for the program:

  1. The parent process first create three child processes through invoking Fork().

  2. For each child, they will do the conditional exit(0), terminate and send the signal.

  3. The handler fetch the signal, reap the corresponding child process, and send the message.

  The result, however, shows that only two child processes are reaped. The reason is that:

  1. Our handler handle one signal per time.

  2. While the handler is still processing the first signal, the second signal is delivered and added to the set of pending signals.

  3. Since the first SIGCHLD signals are blocked by the handler, the second signals are not received.

  4. While the handler is still processing the first signal, the third signal arrives. Since there is already a pending SIGCHLD, this third SIGCHLD signal is discarded, and the information of the third signal has been lost.

  To fix the problem, we must modify the SIGCHLD handler to reap as many zombie children as possible each time it is invoked. We do this by using the while loop:

1
2
3
4
5
6
7
8
9
10
11
12
void handler2(int sig)
{
int olderrno = errno;

while (waitpid(-1, NULL, 0) > 0) {
Sio_puts("Handler reaped child\n");
}
if (errno != ECHILD)
Sio_error("waitpid error");
Sleep(1);
errno = olderrno;
}

  \(b.\)Portable signal handling

  The standard sigaction function for specifying the signal-handling semantics they want when they install a handler is as below:

1
2
3
4
5
#include <signal.h>
int sigaction(int signum, struct sigaction *act,
struct sigaction *oldact);

/* Returns: 0 if OK, −1 on error */

  The Signal wrapper is as below:

1
2
3
4
5
6
7
8
9
10
11
handler_t *Signal(int signum, handler_t *handler) {
struct sigaction action, old_action;

action.sa_handler = handler;
sigemptyset(&action.sa_mask); /* Block sigs of type being handled */
action.sa_flags = SA_RESTART; /* Restart syscalls if possible */

if (sigaction(signum, &action, &old_action) < 0)
unix_error("Signal error");
return (old_action.sa_handler);
}

5.Blocking Signals to Avoid Concurrency Bugs

  Take the following program as an example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* WARNING: This code is buggy! */
void handler(int sig) {
int olderrno = errno;
sigset_t mask_all, prev_all;
pid_t pid;

Sigfillset(&mask_all);
while ((pid = waitpid(-1, NULL, 0)) > 0) { /* Reap a zombie child */
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all);
deletejob(pid); /* Delete the child from the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
if (errno != ECHILD)
Sio_error("waitpid error");
errno = olderrno;
}

int main(int argc, char **argv) {
int pid;
sigset_t mask_all, prev_all;

Sigfillset(&mask_all);
Signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */

while (1) {
if ((pid = Fork()) == 0) { /* Child process */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, &prev_all); /* Parent process */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_all, NULL);
}
exit(0);
}

  There consists two concurrent procedure:

  1. The addjob and deletejob of the Unix shell job list.

  2. The reaping of the child process.

  and we may meet the situation that the child process has been reaped, and the deletejob function tries to delete it, which is nonexisted.

  To prevent this, we blocking SIGCHLD signals before the call to fork and then unblocking them only after we have called addjob:

1
2
3
4
5
6
7
8
9
10
while (1) {
Sigprocmask(SIG_BLOCK, &mask_one, &prev_one); /* Block SIGCHLD */
if ((pid = Fork()) == 0) { /* Child process */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
Execve("/bin/date", argv, NULL);
}
Sigprocmask(SIG_BLOCK, &mask_all, NULL); /* Parent process */
addjob(pid); /* Add the child to the job list */
Sigprocmask(SIG_SETMASK, &prev_one, NULL); /* Unblock SIGCHLD */
}

Notice that children inherit the blocked set of their parents, so we must be careful to unblock the SIGCHLD signal in the child.

6.Explicitly Waiting for Signals

  In the previous code, we use while(1) loop to stall the process. This is wasteful of processor resourses. We can use sigsuspend instead:

1
2
3
4
5
#include <signal.h>

int sigsuspend(const sigset_t *mask);

/* Returns: −1 */

  The sigsuspend function temporarily replaces the current blocked set with mask and then suspends the process until the receipt of a signal whose action is either to run a handler or to terminate the process.

  This code is equal to the atomic version of the following:

1
2
3
sigprocmask(SIG_BLOCK, &mask, &prev);
pause();
sigprocmask(SIG_SETMASK, &prev, NULL);