Signal and Signal Management Calls
In the previous section we noted that a process can handle a signal by doing nothing (thus allowing the default action to occur), ignoring the signal, or catching the signal. Both the ignoring and catching of a signal entail the association of a signal-catching routine with a signal. In brief, when this is done the process automatically invokes the signal-catching routine when the stipulated signal is received. There are two basic system calls that can be used to modify what a process will do when a signal has been received: signal and sigaction . The signal system call has been present in all versions of UNIX and is now categorized as the ANSI C version signal-handling routine (Table 4.19). The sigaction system call (Table 4.20) is somewhat more recent and is one of a group of POSIX signal management calls.
Table 4.19. Summary of the signal System Call.
Include File(s) |
Manual Section |
2 |
||
Summary |
void (*signal(int signum, void (*sighandler)(int)))(int); |
|||
Return |
Success |
Failure |
Sets errno |
|
Signal's previous disposition |
SIG_ERR (defined as -1) |
Yes |
Table 4.20. Summary of the sigaction System Call.
Include File(s) |
Manual Section |
2 |
||
Summary |
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); |
|||
Return |
Success |
Failure |
Sets errno |
|
-1 |
Yes |
The most difficult part of using signal is deciphering its prototype. In essence, the prototype declares signal to be a function that accepts two argumentsan integer signum value and a pointer to a functionwhich are called when the signal is received. If the invocation of signal is successful, it returns a pointer to a function that returns nothing ( void ). This is the previous disposition for the signal. The mysterious ( int ) , found at the far right of the prototype, indicates the referenced function has an integer argument. This argument is automatically filled by the system and contains the signal number. Either system call fails and returns the value -1, setting the value in errno to EINTR (4), if it is interrupted or to EINVAL (22) if the value given for signum is not valid or is set to SIGKILL or SIGSTOP. Further, sigaction returns EFAULT (14) if the act or oldact arguments reference an invalid address space.
While both signal and sigaction deal with signal handling, the functionality of each is slightly different. Let's begin with the signal system call.
The first argument to the signal system call is the signal that we intend to associate with a new action. The signal value can be an integer or a symbolic signal name . This value cannot be SIGKILL or SIGSTOP. The second argument to signal is the address of the signal-catching function. The signal-catching function can be a user -defined function or one of the defined constants SIG_DFL or SIG_IGN. Specifying SIG_DFL for a signal resets the action to be taken to its default action when the signal is received. Indicating SIG_IGN for a signal means the process will ignore the receipt of the indicated signal.
An examination of the signal header files shows that SIG_DFL and SIG_IGN are defined as integer values that have been appropriately cast to address locations that are invalid (such as -1, etc.). The declaration most commonly found for SIG_DFL and SIG_IGN is shown below. With these definitions is another defined constant that can be usedSIG_ERR. This constant is the value that is returned by signal if it fails. See Figure 4.8.
Figure 4.8 Defined constants used by signal and sigset .
/* Fake signal functions. */ #define SIG_ERR ((__sighandler_t) -1) /* Error return. */ #define SIG_DFL ((__sighandler_t) 0) /* Default action. */ #define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. */
Program 4.4 uses the signal system call to demonstrate how a signal can be ignored.
Program 4.4 Pseudo nohup ignoring a signal.
File : p4.4.cxx /* Using the signal system call to ignore a hangup signal */ #include + #include #include #include #include #include using namespace std; 10 const char *file_out = "nohup.out"; int main(int argc, char *argv[]){ int new_stdout; if (argc < 2) { + cerr << "Usage: " << *argv << " command [arguments]" << endl; return 1; } if (isatty(1)) { cerr << "Sending output to " << file_out << endl; 20 close(1); if ((new_stdout = open(file_out, O_WRONLY O_CREAT O_APPEND, 0644)) == -1) { perror(file_out); return 2; + } } if (signal(SIGHUP, SIG_IGN) == SIG_ERR) { perror("SIGHUP"); return 3; 30 } ++argv; execvp(*argv, argv); perror(*argv); // Should not get here unless return 4; // the exec call fails. + }
Program 4.4 is a limited version of the /usr/bin/nohup command found on most UNIX-based systems. The nohup command can be used to run commands so they will be immune to the receipt of SIGHUP signals. If the standard output for the current process is associated with a terminal, the output from nohup will be sent to the file nohup.out . The nohup command is often used with the command-line background specifier & to allow a command to continue its execution in the background even after the user has logged out.
Like the real nohup , our pseudo nohup program (Program 4.4) will execute the command (with optional arguments) that is passed to it on the command line. After checking the number of command-line arguments, the file descriptor associated with stdout is evaluated. The assumption here is that the file descriptor associated with stdout is 1. However, if needed, there is a standard I/O function named fileno that can be used to find the integer file descriptor for a given argument stream. The library function isatty (Table 4.21) is used to determine if the descriptor is associated with a terminal device.
Table 4.21. Summary of the isatty Library Function.
Include File(s) |
Manual Section |
3 |
||
Summary |
Int isatty(int desc); |
|||
Return |
Success |
Failure |
Sets errno |
|
1 |
The isattty library function takes a single integer desc argument. If desc is associated with a terminal device, isatty returns a 1; otherwise , it returns a 0. In the program, if the isatty function returns a 1, an informational message is displayed to standard error to tell the user where the output from the command passed to the pseudo nohup program can be found. Next, the file descriptor for stdout is closed. The open statement that follows the close returns the first free file descriptor. As we have just closed stdout , the descriptor returned by the open will be that of stdout . Once this reassignment has been done, any information written to stdout ( cout ) by the program will in turn be appended to the file nohup.out . Notice that the call to signal to ignore the SIGHUP signal is done within an if statement. Should the signal system call fail (return a SIG_ERR), a message would be displayed to standard error and the program would exit. If the signal call is successful, the argv pointer is incremented to step past the name of the current program. The remainder of the command line is then passed to the execvp system call. Should the execvp call fail, perror will be invoked and a message displayed. If execvp is successful, the current process will be overlaid by the program/command passed from the command line.
The output in Figure 4.9 shows what happens when the pseudo nohup program is run on a local system and passed a command that takes a long time to execute. In the example the long-running command is a small Korn shell script called count that counts from 1 to 100, sleeping one second after the display of each value. As written, the output from the script would normally be displayed on the screen.
Figure 4.9 Output of Program 4.4 when passed a command that takes a long time to execute.
linux$ cat count #! /bin/ksh c=1 while (($c <= 100)) <-- 1 do echo "$c" sleep 1 ((c = c + 1)) done linux$ ./p4.4 ./count & <-- 2 Sending output to nohup.out [1] 19481 linux$ jobs <-- 3 [1] + Running p4.4 count linux$ kill -HUP %1 <-- 4 linux$ jobs [1] + Running p4.4 count linux$ kill -KILL %1 linux$ [1] Killed p4.4 count linux$ jobs linux$
(1) The script count from 1 to 100, sleeping one second in between the display of each number. If run on the command line, it will take approximately 100 seconds to count from 1 to 100.
(2) Pass the count script to our pseudo nohup programplace it in the background.
(3) The operating system returns the PID of the background process.
(4) Sending a hangup signal to the process does not cause it to terminate.
When the program was placed in the background, the system reported the job number (in this case [ 1 ]) and the PID (19481). The jobs command confirms that the process is still running. As can be seen, the kill -HUP %1 command (which sends a hangup signal to the first job in the background) did not cause the program to terminate. This is not unexpected, as the SIGHUP signal was being ignored. The command kill KILL %1 was used to terminate the process by sending it a SIGKILL signal.
As noted, if a signal-catching function name is supplied to the signal system call, the process will automatically call this function when the process receives the signal. However, prior to calling the function, if the signal is not SIG KILL, SIGPWR, or SIGTRAP, the system will reset the signal's disposition to its default. This means that if two of the same signals are received successively, it is entirely possible that before the signal-catching routine is executed, the second signal may cause the process to terminate (if that is the default action for the signal). This behavior reduces the reliability of using signals as a communication device. It is possible to reduce, but not entirely eliminate, this window of opportunity for failure by resetting the disposition for the signal in the catching routine. Program 4.5 catches signals and attempts to reduce this window of opportunity.
Program 4.5 Catching SIGINT and SIGQUIT signals.
File : p4.5.cxx /* Catching a signal */ #include + #include #include #include #include using namespace std; int 10 main() { void signal_catcher(int); if (signal(SIGINT , signal_catcher) == SIG_ERR) { perror("SIGINT"); return 1; + } if (signal(SIGQUIT , signal_catcher) == SIG_ERR) { perror("SIGQUIT"); return 2; } 20 for (int i=0; ; ++i) { // Forever ... cout << i << endl; // display a number sleep(1); } return 0; + } void signal_catcher(int the_sig){ signal(the_sig, signal_catcher); // reset immediately cout << endl << "Signal " << the_sig << " received." << endl; 30 if (the_sig == SIGQUIT) exit(3); }
In an attempt to avoid taking the default action (which in this case is to terminate) for either of the two caught signals, the first statement (line 28) in the program function signal_catcher is a call to signal . This call reestablishes the association between the signal being caught and the signal-catching routine.
Figure 4.10 shows the output of the program when run on a local system.
Figure 4.10 Output of Program 4.5.
linux$ p4.5 0 1 2 <-- 1 Signal 2 received. 3 4 <-- 2 Signal 2 received. 5 <-- 2 Signal 2 received. 6 Signal 3 received. linux$
(1) The user types CTRL+C. The terminal program displays a funny graphics character, ².
(2) Here the signals are generated in rapid succession.
From this output we can see that each time CTRL+C was pressed, it was echoed back to the terminal as ² . If CTRL+C was struck twice in quick succession, the program responded with the Signal 2 received message for each keyboard sequence. On this system it appears as if some of the signals were queued if they were received in rapid succession. However, this is somewhat misleading, as the mechanics of terminal I/O come into play. Say we were (via a background process) to deliver to the process, in very rapid succession, multiple copies of the same signal. In this setting we would find most often that only one copy of the signal would be delivered to the process, while the others are discarded. Most systems do not queue the signals 1 through 31. When a SIGQUIT signal was generated, a message was displayed and the program exited.
The sigaction system call, like the signal system call, can be used to associate an alternate action with the receipt of a signal. This system call has three arguments. The first is an integer value that specifies the signal. As with the signal system call, this argument can be any valid signal except SIGKILL or SIGSTOP. The second and third arguments are references to a sigaction structure. Respectively these structures store the new and previous action for the signal. The full definition of the sigaction structure is found in the file sigaction.h . This file is automatically included by signal.h. Basically, the sigaction structure is
struct sigaction { void (*sa_handler)(int); // 1 void (*sa_sigaction)(int, siginfo_t *, void *); // 2 sigset_t sa_mask; // 3 int sa_flags; // 4 void (*sa_restorer)(void); // 5 }
Both sa_handler and sa_sigaction can be used to reference a signal handling function. Only one of these should be specified at any given time, as on most systems this data is often stored in a union within the sigaction structure. By definition, a union can hold only one of its members at a time. Our discussion centers on using the sa_handler member. The sa_mask member specifies the signals, which should be blocked when the signal handler is executing. Each signal is represented by a bit. If the bit in the mask is on, the signal is blocked. By default the signal that triggered the handler is blocked. The sa_flags member is used to set flags that modify the behavior of the signal-handling process. Flag constants, shown in Table 4.22, can be combined using a bitwise OR .
Table 4.22. sa_flags Constants.
Flag |
Action |
---|---|
SA_NOCLDSTOP |
If the signal is SIGCHILD, then the calling process will not receive a SIGCHILD signal when its child processes exit. |
SA_ONESHOT or SA_RESETHAND |
Restore the default action after the signal handler has been called once (similar to the default of the signal call). |
SA_RESTART |
Use BSD signal semantics (certain interrupted system calls are restarted after the signal has been caught). |
SA_NOMASK or SA_NODEFER |
Undo the default whereby the signal triggering the handler is automatically blocked. |
SA_SIGINFO |
The signal handler has three argumentsuse sa_sigaction , not sa_handler . |
The remaining structure member, sa_restorer , is obsolete and should not be used.
Unlike signal , a sigaction installed signal-catching routine remains installed even after it has been invoked. Program 4.6, which is similar to Program 4.5, shows the use of the sigaction system call.
Again, notice that in the program function signal_catcher , it is no longer necessary to reset the association for the signal caught to the signal-catching routine.
Program 4.6 Using the sigaction system call.
File : p4.6.cxx /* Catching a signal using sigaction */ #define_GNU_SOURCE + #include #include #include #include #include using namespace std; 10 int main() { void signal_catcher(int); struct sigaction new_action; <-- 1 new_action.sa_handler = signal_catcher; + new_action.sa_flags = 0; <-- 2 if (sigaction(SIGINT, &new_action, NULL) == -1) { perror("SIGINT"); <-- 3 return 1; 20 } <-- 3 if (sigaction(SIGQUIT, &new_action, NULL) == -1) { perror("SIGQUIT"); return 2; } + for (int i=0; ; ++i) { // Forever ... cout << i << endl; // display a number sleep(1); } return 0; 30 } void signal_catcher(int the_sig){ cout << endl << "Signal " << the_sig << " received." << endl; if (the_sig == SIGQUIT) + exit(3); }
(1) A sigaction structure is allocated.
(2) The signal catching function is assigned and the sa_flags member set to 0.
(3) A new action is associated with each signal.
Three other POSIX signal- related system calls that can be used for signal management are shown in Table 4.23.
Table 4.23. Summary of the sigprocmask , sigpending , and sigsuspend System Call.
Include File(s) |
Manual Section |
2 |
||
Summary |
int sigprocmask (int how, const sigset_t *set, sigset_t *oldset); int sigpending(sigset_t *set); int sigsuspend(const sigset_t *mask);; |
|||
Return |
Success |
Failure |
Sets errno |
|
-1 |
Yes |
Each function returns a 0 if it is successful; otherwise, it returns a -1 and sets the value in errno (Table 4.24).
Table 4.24. sigprocmask , sigpending , and sigsuspend Error Messages.
# |
Constant |
perror Message |
Explanation |
---|---|---|---|
4 |
EINTR |
Interrupted system call |
A signal was caught during the system call. |
14 |
EFAULT |
Bad address |
set or oldset references an invalid address space. |
The process's signal mask can be manipulated with the sigprocmask system call. The first argument, how , indicates how the list of signals (referenced by the second argument, set ) should be treated. The action that sigprocmask will take, based on the value of how , is summarized in Table 4.25.
Table 4.25. Defined how Constants.
Signal |
Action |
---|---|
SIG_BLOCK |
Block the signals specified by the union of the current set of signals with those specified by the set argument. |
SIG_UNBLOCK |
Unblock the signals specified by the set argument. |
SIG_SETMASK |
Block just those signals specified by the set argument. |
If the third argument, oldset , is non-null, the previous value of the signal mask is stored in the location referenced by oldset .
The use of the sigprocmask system call is shown in Program 4.7.
Program 4.7 Using sigprocmask .
File : p4.7.cxx /* Demonstration of the sigprocmask call */ #define_GNU_SOURCE #include #include + #include #include using namespace std; sigset_t new_signals; int 10 main() { void signal_catcher(int); <-- 1 struct sigaction new_action; <-- 2 sigemptyset(&new_signals); + sigaddset(&new_signals,SIGUSR1); sigprocmask(SIG_BLOCK, &new_signals, NULL); new_action.sa_handler = signal_catcher; new_action.sa_flags = 0; 20 if (sigaction(SIGUSR2, &new_action, NULL) == -1) { perror("SIGUSR2"); return 1; } cout << "Waiting for signal" << endl; + pause(); cout << "Done" << endl; return 0; } void 30 signal_catcher(int n) { cout << "Received signal " << n << " will release SIGUSR1" << endl; sigprocmask(SIG_UNBLOCK, &new_signals, NULL); cout << "SIGUSR1 released!" << endl; }
(1) Empty (clear) the set of signals.
(2) Add the SIGUSR1 signal to this set.
The example makes use of the SIGUSR1 and SIGUSR2 signals. These are two user-defined signals whose default action is termination of the process. In lines 14 and 15 of the example are two signal-mask manipulation library functions ( sigemptyset and sigaddset ) that are used to clear and then add a signal to the new signal mask. A signal mask is essentially a string of bitseach set bit represents a signal. The signal-mask manipulation library functions are covered in detail in Chapter 11, "Threads." In Program 4.7, the sigprocmask system call in line 17 holds (blocks) incoming SIGUSR1 signals. The sigaction system call (line 20) is used to associate the receipt of SIGUSR2 with the signal-catching routine. Following this, an informational message is displayed, and a call to pause is made. In the program function signal_catcher , the sigprocmask system call is used to release the pending SIGUSR1 signal. Notice that a cout statement was placed before and after the sigprocmask call. A sample of this program run locally is shown in Figure 4.11.
When run, the program is placed in background so the user can continue to issue commands from the keyboard. The system displays the job number for the process and the PID. The program begins by displaying the Waiting for signal message. The user, via the kill command, sends the process a SIGUSR1 signal. This signal, while received by the process, is not acted upon, as the process has been directed to block this signal. When the SIGUSR2 signal is sent to the process, the process catches the signal, and the program function signal_catcher is called. The initial cout statement in the signal-catching routine is executed, and its message about receiving signals is displayed. The following sigprocmask call then unblocks the pending SIGUSR1 signal that was issued earlier. As the default action for SIGUSR1 is termination, the process terminates and the system produces the trailing information indicating the process was terminated via user signal 1. As the process terminates abnormally, the second cout statement in the signal-catching routine and the cout in the main of the program are not executed.
Figure 4.11 Output of Program 4.7.
linux$ ./p4.7 & Waiting for signal [1] 21895 linux$ kill -USR1 21895 <-- 1 linux$ kill -USR2 21895 <-- 2 Received signal 12 will release SIGUSR1 linux$ [1] User signal 1 ./p4.7
(1) SIGUSR1 would normally cause the process to exitbut it has been blocked.
(2) SIGUSR2 has been mapped to the signal-catching routine. In this routine, SIGUSR1 is unblocked; consequently, the process exits without executing the second cout statement in the signal catcher .
The sigsuspend system call is used to pause (suspend) a process. It replaces the current signal mask with the one passed as an argument. The process suspends until a signal is delivered whose action is to execute a signal-catching function or terminate the process. Program 4.8 demonstrates the use of the sigsuspend system call.
Program 4.8 Using sigsuspend .
File : p4.8.cxx /* Pausing with sigsuspend */ #define_GNU_SOURCE #include #include + #include #include using namespace std; int main(){ 10 void signal_catcher(int); struct sigaction new_action; sigset_t no_sigs, blocked_sigs, all_sigs; sigfillset (&all_sigs); // turn all bits on + sigemptyset(&no_sigs); // turn all bits off sigemptyset(&blocked_sigs); // Associate with catcher new_action.sa_handler = signal_catcher; new_action.sa_mask = all_sigs; 20 new_action.sa_flags = 0; if (sigaction(SIGUSR1, &new_action, NULL) == -1) { perror("SIGUSR1"); return 1; } + sigaddset(&blocked_sigs, SIGUSR1); sigprocmask(SIG_SETMASK, &blocked_sigs, NULL); while (1) { cout << "Waiting for SIGUSR1 signal" << endl; sigsuspend(&no_sigs); // Wait 30 } cout << "Done." << endl; return 0; } void + signal_catcher(int n){ cout << "Beginning important stuff" << endl; sleep(10); // Simulate work .... cout << "Ending important stuff" << endl; }
In main , the signal-catching function is established. Lines 14 to 16 create three signal masks. The sigfillset call turns all bits on, while the sigemptyset turns all bits off. The filled set (all bits on, denoting all signals) becomes the signal mask for the signal-catching routine. Thus specified, this directs the signal-catching routine to block all signals. In line 21 the receipt of signal SIGUSR1 is associated with the signal-catching function signal_catcher . In lines 25 and 26 the process is directed to block any SIGUSR1 signals. While at first glance this might seem superfluous, as receipt of this signal has been mapped to signal_catcher , it allows duplicate SIGUSR1 signals to be pending rather than discarded. Then, in an endless loop, the program pauses when the sigsuspend statement is reached, waiting for the receipt of the SIGUSR1 signal. Once the SIGUSR1 signal is received (caught), the signal-catching function is executed. While in the signal-catching function, all signals that can be blocked are held. A set of messages indicating the beginning and end of an important section of code are displayed. When the signal-catching routine is exited, any blocked signals are released. In summary, the program defers the execution of an interrupt-protected section of code until it receives a SIGUSR1 signal. A run of the program produces the output shown in Figure 4.12.
Figure 4.12 Output of Program 4.8.
linux$ p4.8 & Waiting for SIGUSR1 signal [1] 6277 linux$ kill -USR1 %1 Beginning important stuff linux$ kill -INT %1 linux$ jobs [1] + Running p4.8 linux$ Ending important stuff [1] Interrupt p4.8
The process was first sent a SIGUSR1 signal that caused it to begin the program function signal_catcher . While it was in the signal_catcher function, an interrupt signal was sent to the process. This signal did not cause the process to immediately terminate, as the process had indicated that all signals were to be blocked (held). The jobs command confirms that the process is still active after the interrupt command was sent. However, once the blocked signals are released (when the signal-catching routine is exited), the pending SIGINT signal is acted upon and the process terminates.