Chapter 4 FreeBSD System Programming |
||
---|---|---|
Prev | Next |
So far we have covered process creation and other system calls. There will be times that you need to
communicate between multiple processes, gain more fine grain process control, or even allow programs or
operators to notify your program of an event. For example, you might want your program to re-read its
configuration file. Or, your database program may need to terminate gracefully by writing out transactions
prior to exiting. These are just a few possibilities for using signals. Although there are many ways to do
similar tasks, including using sockets, fifos, pipes, and semaphores, we'll focus on signals and other
process control mechanisms, which offer most of the features and power you'd need for use in the real world.
4.2 Signals
Signals are similar to hardware interrupts. Just as when a device wants service it tells the CPU so by generating a hardware interrupt, a process that wants to notify another process can use a signal.
Most Unix system administrators will be familiar with the SIGHUP signal. Most server daemons will re-read their configuration files or restart when you send them a SIGHUP signal via the kill command. Some of these signals have direct correlation to the hardware like SIGFPE, "floating point exception" or SIGILL, "illegal instruction"; others are software related like SIGSYS, "non-existent system call invoked".
What the process does once a signal is received depends on the signal and what the process wants to do with it. Some signals can be blocked, ignored, or caught, while others cannot. If a process wants to catch a signal and perform some actions based on it, you can set the process with a signal handler for that specific signal. A signal handler is just a function that is called once that signal has been received. Or rather, a signal handler is just a function call that you can specify.
There are defaults performed by the operating system if no signal handler has been specified. These default actions vary from termination to full core dumps. Note that there are two signals that cannot be caught or ignored: SIGSTOP and SIGKILL, explained below.
There are many signals defined on a BSD system; we discuss the standard signals defined in the /usr/include/sys/signals.h. Note that NetBSD has a few more signals that are not included here so if you need a specific that is not found below, check your header files.
#define SIGHUP 1 /* hangup */
The SIGHUP is a very common signal for Unix system administrators. Many server daemons will re-read their configuration files once they receive this signal. Its original function, however, was to notify a process that its controlling terminal has disconnected. The default action is to terminate the process.
#define SIGINT 2 /* interrupt */
The SIGINT is another common signal for Unix users, and is better known as CTRL-C for most shells. The formal name is the Interrupt signal. The default action is to terminate the process.
#define SIGQUIT 3 /* quit */
The SIGQUIT signal is used by shells that accept the CTRL-/ keys. Otherwise, it acts as a signal to tell the process to quit. This is a commonly used signal for an application to be given notice to shutdown gracefully. The default action is to terminate the process and create a core dump.
#define SIGILL 4 /* illegal instr. (not reset when caught) */
The SIGILL signal will be sent to a process if it tries to execute an illegal instruction. If your program makes use of use of threads, or pointer functions, try to catch this signal if possible for aid in debugging. The default action is to terminate the process and create a core dump.
#define SIGTRAP 5 /* trace trap (not reset when caught) */
The SIGTRAP is a POSIX signal, and is used for debugging. It notifies a process being debugged that it has reached a break point. Once delivered, the process being debugged would stop and the parent process would be notified. The default action is to terminate the process and create a core dump.
#define SIGABRT 6 /* abort() */
The SIGABRT signal provides a way to abort a process and create a core dump. If this is caught and the signal handler does not return, however, the program will not terminate. The default action is to terminate the process and create a core dump.
#define SIGFPE 8 /* floating point exception */
The SIGFPE is sent to a process when a floating-point error has occurred. For programs that handle complex mathematics, it's recommended that you catch this signal. The default action is to terminate the process and create a core dump.
#define SIGKILL 9 /* kill (cannot be caught or ignored) */
The SIGKILL is the most evil signal of them all. As you can see from its comment, this signal cannot be caught or ignored. Once this signal is delivered to the process, it is terminated. There are rare cases where this is not the case and even a SIGKILL will not terminate the process, however. These rare conditions occur when the process is doing an "un-interruptible operation" such as disk I/O. Although these conditions are rare, once it happens and the process is deadlocked, the only way to terminate the process is to reboot. The default action is to terminate the process.
#define SIGBUS 10 /* bus error */
As the name implies, the SIGBUS signal is a result of the CPU detecting an error on its data bus. This error could be the result of a program trying to access an improperly aligned memory address. The default action is to terminate the process and create a core dump.
#define SIGSEGV 11 /* segmentation violation */
The SIGSEGV is another common signal for many C/C++ programmers, this is a result of the program trying to access a protected memory location that it does not have rights to or an invalid virtual memory address (dirty pointers). The default action is to terminate the process and create a core dump.
#define SIGSYS 12 /* non-existent system call invoked */
The SIGSYS signal will be delivered to a process after the program tries to execute a system call that does not exist. The operating system will deliver this signal and the process will be terminated. The default action is to terminate the process and create a core dump.
#define SIGPIPE 13 /* write on a pipe with no one to read it */
Pipes allow process to communicate with each other, as in a phone call. If a process tries to write to the pipe and there isn't a responder, however, the operating system will deliver the SIGPIPE signal to the offending process (the one who attempted the write). The default action is to terminate the process.
#define SIGALRM 14 /* alarm clock */
The SIGALRM is delivered to a process when its alarm has expired. These alarms are set with the setitimer and alarm calls, covered later in this chapter. The default action is to terminate the process.
#define SIGTERM 15 /* software termination signal from kill */
The SIGTERM signal is sent to a process to let it know it needs to clean up after its self and terminate. The SIGTERM is also the default signal sent by the Unix kill command, as well as by the operating system when shutting down. The default action is to terminate the process.
#define SIGURG 16 /* urgent condition on IO channel */
The SIGURG signal is sent to a process when certain conditions exist on an open socket, and will be discarded if not caught. The default action is to discard the signal.
#define SIGSTOP 17 /* sendable stop signal not from tty */
This signal cannot be caught or ignored. Once a process receives the SIGSTOP signal it will stop until it receives another SIGCONT signal. The default action is to stop the process until a SIGCONT signal has been received.
#define SIGTSTP 18 /* stop signal from tty */
The SIGSTP signal is similar to the SIGSTOP; however this signal can be caught or ignored. Shells will deliver this signal to a process when it receives the CTRL-Z from the keyboard. The default action is to stop the process until a SIGCONT signal has been received.
#define SIGCONT 19 /* continue a stopped process */
The SIGCONT signal is also an interesting signal. As mentioned earlier, the SIGCONT is sent to a process once it has stopped to notify it to resume. This signal is interesting because it cannot be ignored or blocked but can be caught. This makes sense, because a process probably would not want to ignore or block the SIGCONT signal. Otherwise what would it do once it receives a SIGSTOP or SIGSTP? The default action is to discard the signal.
#define SIGCHLD 20 /* to parent on child stop or exit */
The SIGCHLD was introduced by Berkeley Unix and has a better interface than SRV 4 Unix's implementation. (The BSD implementations is better in that the signal is not a retroactive process. In system V Unix if a process requests to catch this signal, the operating system will check to see if any outstanding children exist (these are children that have exited and are waiting for the parent to call wait to collect their status). If child processes exist with terminating information, the signal handler will be called. So, merely requesting to catch this signal can result in the signal handler being called, a rather messy situation.)
The SIGCHLD signal will be sent to a process once a child has changed status. As I mentioned in the previous chapter, a parent can fork but does not have to wait for a child to exit. Normally this is bad since the process could turn into a zombie once it exits. However if the parent catches the SIGCHLD signal then it can use one of the wait system calls to collect the status or determine what happened. The SIGCHLD is also sent to the parent once a SIGSTOP, SIGSTP or SIGCONT has been sent to any of the child processes as well. The default action is to discard the signal.
#define SIGTTIN 21 /* to readers pgrp upon background tty read */
The SIGTTIN signal is sent when a background process attempts a read. The process will then be stopped until a SIGCONT signal is received. The default action is to stop the process until a SIGCONT signal has been received.
#define SIGTTOU 22 /* like TTIN if (tp->t_local<OSTOP) */
The SIGTTOU signal is similar to the SIGTTIN, except the SIGTTOU signal is sent when a background process attempts to write to a tty that has set the TOSTOP attribute. If this attribute is not set on the tty, however, then the SIGTTOU is not sent. The default action is to stop the process until a SIGCONT signal has been received.
#define SIGIO 23 /* input/output possible signal */
The SIGIO signal is sent to a process that has possible I/O on a file descriptor. The process should set this using the fcntl call. The default action is to discard the signal.
#define SIGXCPU 24 /* exceeded CPU time limit */
The SIGXCPU signal is sent to a process once it exceeds the CPU limit imposed on it. The limits can be set by the setrlimit, discussed later. The default action is to terminate the process.
#define SIGXFSZ 25 /* exceeded file size limit */
The SIGXFSZ signal is sent to a process when it has exceeded its imposed file size limit, discussed later. The default action is to terminate the process.
#define SIGVTALRM 26 /* virtual time alarm */
The SIGVTALRM is sent to a process once its virtual alarm time has expired. The default action is to terminate the process.
#define SIGPROF 27 /* profiling time alarm */
The SIGPROF is another signal sent to a process that has set an alarm. The default action is to terminate the process.
#define SIGWINCH 28 /* window size changes */
The SIGWINCH signal is sent to a process that has adjusted the columns or rows of the terminal, such as to increase the size of your xterm. The default action is to discard the signal.
#define SIGUSR1 29 /* user defined signal 1 */ #define SIGUSR2 30 /* user defined signal 2 */
The SIGUSR1 and SIGUSR2 are designed to be defined by the user. They can be set to do whatever is needed.
In other words, the operating system does not have any actions associated with these signals. The defaults are
to terminate the process.
4.3 System calls
So how do you use signals? Sometimes it's not even clear whether to use them. Or, after a signal is delivered,
you may need to analyze the situation, and find out why was the signal sent or from where did it originate
before taking action. Other times you'll simply want to exit the program and create a core file after cleanup.
Look at the code samples at the end for detailed examples on each function.
The kill function
The kill function will be familiar to those who have ever killed processes from the command line. The basic syntax is:
int kill(pid_t pid, int sig);
The kill function will send the specified signal to the process with the process id PID. The signal will only be delivered if the process matches the following criteria:
Note: SIGCONT is an exception in that it can be sent to any descendent of the current process.
The action of kill function varies widely depending on its arguments. These actions are as follows:
The other version of kill is the raise function, as in:
int raise(int sig);
The raise function will send the current process the signal sig. This function is not very useful because it can only send signals to the current process. The raise function will return 0 if its successful and -1 on failure. On failure the raise function will set the errno with the respective error, as in:
void (*signal(int sig, void (*func)(int)))(int);
Now that we know how to raise and send signals, how do you handle them?
The Signal Function
The signal function call offers the easiest paradigm. It does look a bit intimidating, however, because C prototypes can look more complex than they really are. The signal function associates a given function with a specific signal. Here's the FreeBSD definition with added typedefs:
typedef void (*sig_t) (int); sig_t signal(int sig, sig_t func);
The first parameter is the target signal, and can be any one of the ones outlined above. The func parameter is a pointer to a function that will handle the signal. This function should return nothing (void) and take a single integer as its argument. The func parameter can also be set to the following values:
SIG_IGN: If the func parameter is set to SIG_IGN then the signal will be ignored.
SIG_DFL: If the func parameter is set to SIG_DFL then the signal will be set to the default action.
Sigaction
The sigaction is a more flexible conterpart to the signal function. The first parameter is the target signal. The next parameter act is the sigaction structure that will describe what to do with this signal. The final parameter oact is a pointer to a location to store the previous settings. The sigaction structure is as follows:
int sigaction(int sig, const struct sigaction *act, struct sigaction *oact);
The struct sigaction has the following members:
void (*sa_handler)(int);
This member is a pointer to a function that returns nothing (void) and takes a single integer as an argument. This is the same as the function argument to signal and can also be set to SIG_IGN and SIG_DFL, achieving the same results as a call to signal.
void (*sa_sigaction)(int, siginfo_t *, void *);
This member is a pointer to a function that again returns nothing (void) and takes three arguments. These arguments are an integer that will specify the signal sent; a pointer to a siginfo_t structure that will contain information regarding the signal; a pointer to the specific context as to where the signal was delivered.
sigset_t sa_mask;
This member is a bitwise mask of signals to block while the signal is being delivered. Attempts to block SIGKILL or SIGSTOP will be ignored. The signals will be then postponed until they are unblocked. See sigprocmask for more on global masks.
int sa_flags;
This member is a bitwise mask of the following flags:
SA_NOCLDSTOP: If the SA_NOCLDSTOP bit is set and the target signal is SIGCHLD, then a parent process will not be notified when the child stops, but only if the child exits.
SA_NOCLDWAIT: The SA_NOCLDWAIT flag will prevent children from becoming zombie processes. This is used when the target signal is SIGCHLD. If the process should set this and then call one of the wait system calls, the process will block until all of the children have terminated and return -1 with errno set to ECHILD. To use this feature you must set this bit, and the target signal should be SIGCHLD.
SA_ONSTACK: Sometimes there's a need for a signal to be handled on a specified stack. The sigaction system call provides these means. If the this bit is set, then the signal will be delivered on the stack specified.
SA_NODEFER: When the SA_NODEFER bit is set the system will not mask further deliveries of the current signal while the handler is executing.
SA_RESETHAND: If the SA_RESETHAND bit is set, then the signal handler is set to SIG_DFL once the signal is delivered.
SA_SIGINFO: When set, the function pointed to by sa_sigaction member of the sigaction structure is used.
Note: this bit should not be set when using the SIG_IGN or SIG_DFL. Upon a successful call to sigaction, the
return value is 0 and -1 otherwise, with errno set to the respective error.
4.5 Signal Masks (blocking and unblocking signals)
A process can decide to block a signal or a set of signals. Once a signal is blocked its delivery is postponed until the process unblocks it. This is useful when a program enters a certain part of code that cannot be interrupted and still wishes to receive and process sent signals that could have been missed. The ability to reliably deliver signals that weren't swallowed up by the operating system was not always present but was introduced in 4.2BSD and later adopted by SVR3.
With the advent of reliable signals, the life and delivery of signals changed. Signals before could be generated and delivered. Now, once a signal is pending, a process can then decide what to do with the signal prior to accepting it. The process may choose to handle it, set it back to the default or ignore and discard the signal.
NOTE: If multiple signals are pending the system will deliver the signals that could change the process state, such as SIGBUS first.
Sigprocmask
Any given process can block signals by using sigprocmask. The syntax is:
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
The sigprocmask function will modify or examine the current signal mask. The action sigprocmask takes when the set parameter is not null depends on the first parameter how. The actions along with their associated meanings are listed below:
SIG_BLOCK: The signals specified to be blocked in the set parameter are added to the list of blocked signals.
SIG_UNBLOCK: The signals that are specified in the set parameter are to be removed from the signal mask.
SIG_SETMASK: The set parameter will replace the current signal mask entirely. If the oset parameter is not null, then it will be set to the previous signal mask. If the set value is null, then the how parameter is ignored and the signal mask is left unchanged. So, to examine the signal mask call the sigprocmask with set null and oset non-null. Once the mask is retrieved, you probably want to examine or manipulate it. The following routines are available. Note that these are currently implemented as macros.
int sigemptyset(sigset_t *set)
When called the set parameter will be initialized to an empty set of signals.
int sigfillset(sigset_t *set)
When called the set parameter will be initalize to contain all signals.
int sigaddset(sigset_t *set, int signo)
When called the signal specified by signo will be added to the set of signals pointed to by the set parameter.
int sigaddset(sigset_t *set, int signo)
When called the signal specified by signo will be removed from the set of signals prointed to by the set parameter.
int sigismember(const sigset_t *set, int signo)
When called this will return 1 of the signal specified by signo is in the set pointed to by the set parameter. If the signal is not set then the return value will be 0.
int sigpending(sigset_t *set);
A process can use the sigpending function to discover what signals are currently pending. The sigpending
function will return a mask containing all pending signals. This mask can then be examined by the above routines.
The sigpending function will return 0 on success and -1 otherwise, with errno set accordingly.
4.6 Customizing Behavior
Sometimes a program may require that the signal handler to run on a specified stack. In order for this to happen an alternate stack must be specified with the signaltstack function. The structure used for this function is the signaltstack:
int sigaltstack(const struct sigaltstack *ss, struct sigaltstack *oss);
Its members are described below.
char *ss_sp;
This member points to an area of memory to be used as the stack. There is a minimum amount of memory needed for the signal handler to operate, defined as MINSIGSTKSZ. There is also another predefined amount that should cover the usual case SIGSTKSZ. This memory should be allocated prior to calling the signaltstack function.
size_t ss_size;
The ss_size member specifies the size of the new stack. If this value is inaccurate, the behavior of the signal handler when executing will be unpredictable - you won't have a say in how the system handles this signal.
int ss_flags;
The ss_flags member can take on a few values depending on the calling circumstances. The first is when the process wishes to disable the alternate stack; the ss_flags will be set to SS_DISABLE. In that case, the ss_sp and ss_size values are ignored and the alternate stack is disabled. Note that the alternate stack can only be disabled if currently the handler is not operating on it.
If calling the signaltstack with a non-null value for oss, then the ss_flags will contain information specifying the current state. These are:
SS_DISABLE: The alternate stack is disabled.
SS_ONSTACK: The alternate stack is currently in use and attempts to disable it will fail.
If calling signaltstack and the oss parameter is not null, it will return the current state. The return value for signaltstack is 0 for success and -1 otherwise. On failure, the errno value will be set accordingly. Because signals can be delivered at any point, they are hard to anticipate. For this reason 4.2 BSD's default behavior was to restart interrupted system calls, provided data has not been transmitted yet. This is for the most part good behavior and still the default action across BSD. There are rare cases, though, when you may want to turn this feature off. You can accomplish this using the siginterrupt function call. The schema is simple:
int siginterrupt(int sig, int flag);
Set the sig parameter value to the target signal, and set the flag to true (in this case 1). If the
flag parameter is set to false (in this case 0) then the default behavior is that of restarting the system calls.
4.7 Waiting for signals
The sigsuspend function call will temporarily change the current set of blocked signals to the new set specified by the sigmask parameter. After that, the sigsuspend will wait until a signal is delivered. Once a signal is delivered the original signal mask is restored. Because the sigsuspend always terminates due to a signal, the return value from sigsuspend will always be -1 with errno set to EINTR. Here's the syntax:
int sigsuspend(const sigset_t *sigmask);
The sigwait function will take a set of signals specified by the set parameter as a signal mask. It will then check for any pending signals contained in this specified set . If so the sigwait function will clear the pending signal, and return with the sig parameter set to the numerical value of the cleared signal. If no signals are pending then the sigwait function will wait until one of the specified signals is generated. The syntax:
int sigwait(const sigset_t *set, int *sig);
When a signal is delivered to a process and the process has a signal handler set to catch it, process execution will switch to the signal handler. This can pose a problem once the handler returns. For example, say your program listens on a port specified by a configuration file. Your program sets a signal handler to catch the SIGHUP signal to re-read the configuration file. Once the SIGHUP signal is delivered your program will execute the signal handler and re-read the configuration file. One problem is, you will have no way to know where in your execution will a signal be delivered. You could use some of the function calls listed above to narrow it down but what if you have open sockets, open connections and other things that you need to clean up before you listen on the new port? How do you then determine where to start and when to do so? If your program is currently waiting for input, the system call will be restarted if no data has been transferred, and so the return from a SIGHUP will just continue to wait.
This is one of the uses for the setjmp and longjmp functions, which allow for non-local branching. To use them you need to call one of the setjmp functions with the env parameter, as below:
jmp_buff env; int sigsetjmp(sigjmp_buf env, int savemask); void siglongjmp(sigjmp_buf env, int val); int setjmp(jmp_buf env); void longjmp(jmp_buf env, int val); int _setjmp(jmp_buf env); void _longjmp(jmp_buf env, int val); void longjmperror(void);
The return from setjmp will be 0 and the current environment will be saved into env. You can then call the corresponding longjmp from inside a signal handler. Once called, longjmp will restore the execution environment to env and the program will return to the original location in which setjmp was called. The original call to setjmp will then return the value of val that was sent as the parameter to the longjmp function.
A few notes about the setjmp and longjmp functions: first, the two are not intermixable. That is, a call to setjmp cannot pass a env variable to a call to _longjmp. Also, when a function calls setjmp returns a subsequent call to longjmp will fail.
The different calls have specific actions that they take. These actions are listed below:
setjmp and longjmp: These will save and restore the signal masks, register set, and the stack.
_setjmp and _longjmp: These will save and restore only the resister set and the stack.
sigsetjmp and siglongjmp: These will save and restore the register set, stack, and the signal masks as long as the save mask parameter value is non-zero.
If, for some reason, the env parameter is corrupted or the function that called setjmp has returned, the
longjmp functions will call the function longjmperror. If the longjmperror function then returns, the program
is aborted. You can customize the longjmperror function with a function that has the same prototype. The
default longjmperror will write "longjmp botch" to standard error and return.
4.8 Alarms
unsigned int alarm(unsigned int seconds);
The alarm function is basically a simple alarm clock, and is a useful function that allows a process to be notified once a specified amount of seconds has expired. Upon expiration, the calling process will be sent a SIGALRM signal. All subsequent calls to alarm will supersede any previous calls. Unlike the sleep function, alarm will not block.
It has a few return values that you should note. First, if the calling process has no alarms set then the return value is 0. Second, if an alarm has been set but has not expired then the remaining amount of time since the previous call is returned.
The current maximum amount of seconds that can be specified is 100,000,000 - a pretty long time.
int getitimer(int which, struct itimerval *value);
The getitimer function will retrieve the proper struct itimerval as described by the first argument (the which argument). The options for the first argument are described below:
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
The setitmer provides a more robust interface than the previous alarm call. On a BSD system each process is provided with three interval timers. These timers are described below:
#define ITIMER_REAL 0
The real timer is decremented in real time regardless of how long the process spends actually executing on the CPU (in other words, it tracks natural time). This will allow a process to set a timer based on natural real time. When real timer expires, the process is sent a SIGALRM signal.
#define ITIMER_VIRTUAL 1
The virtual timer is decremented only when the process is executing on the CPU, and allows a process to set an alarm based on CPU usage. When the virtual timer has expired the process is sent a SIGVTALRM signal.
#define ITIMER_PROF 2
The profile timer is decremented when the process is both executing in the CPU and when the system is executing system calls on behalf of the process. This is helpful for interpreters that want to perform statistical profiling. When the profile timer has expired the process is sent a SIGPROF signal. Unlike the real and virtual timers, however, the SIGPROF can be sent during a system call; the process should be prepared to restart the interrupted system call(s).
This chapter has focused on the signal libraries. These functions and the usage of signals are very important to system programming. Signals can make programs more robust by allowing a system administrator to notify the application to re-read a configuration file. Other important signals deal with pending I/O on an open file descriptor. The next chapter will show how to make use of these I/O related signals.
Prev | Next | |
Chapter 4 FreeBSD System Programming |
---|