Reading:
man 7 signal
man 2 signal
man 2 sigaction
man 2 kill
Signals are a form of inter-process communications in Unix system. Historically a process only knew that it received a signal, no other information other than the signal number was available, however it is now possible to communicate additional information (a siginfo_t structure), however we will not likely require that information to build a simple shell.
siginfo_t
The standard Linux signals are:
* = Of most interest to use at this time.
A signals disposition is the default action (from the table above) that the signal causes in the process that receives it. The following are the possible default dispositions:
Default action is to: Term Terminate the process. Ign Ignore the signal. Core Terminate the process and dump core (see core(5)). Stop Stop the process. Cont Continue the process if it is currently stopped.
Using the system calls signal() or sigaction() allows a process to change the disposition or install a handler for the signal. A signal handler is a programmer defined function that is invoked upon receiving a signal that is to be handled.
signal()
sigaction()
A processes signal dispositions and handlers are "per process", so threads all use the same dispositions and child processes inherit the dispositions of the parent. Dispositions are reset on an execve() to their defaults except for those signals that are set to be ignored (this allows things like nohup to work.)
execve()
nohup
There are two signals, SIGKILL and SIGSTOP, that cannot be caught (i.e. a handler cannot be installed for them,) blocked or ignored. They will always terminate or stop the process respectively.
When a signal is delivered to a process that has installed handler, a function to be called to handle the signal, whatever the process was in the middle of is suspended or interrupted, possibly including a system-call, which may cause the system call to fail with a EINTR error (in errno.) Also some care should be taken to do as little as possible with the state of the program in the handler, such as I/O, writing to globals, messing around with the stack (although a handler may use an alternate stack (man 2 sigaltstack), etc.
man 2 sigaltstack
The preferred modern way of installing a signal handler is via sigaction() which is the preferred, most portable method of changing a signals disposition and has the following prototype:
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); }; void handler(int sig, siginfo_t *info, void *ucontext) { ... }
To define for example a signal handler for the SIGCHLD signal (the signal that is sent to a parent process when a child process has finished,) we would make code such as:
void my_handler(int sig) { int status; wait(&status); } int main(void) { struct sigaction sa; sa.sa_handler = my_handler; sa.sa_sigaction = NULL; sa.sa_mask = 0; sa.sa_flags = SA_RESTART; // We don't wish to save the old signal disposition, so use NULL for the 3rd argument: sigaction(SIGCHLD, &sa, NULL); ... }
If sa_handler is set to SIG_DFL, then the default disposition is restored, or the signal is ignored if set to SIG_IGN.
sa_handler
SIG_DFL
SIG_IGN
All of the above can be accomplished with the older, less portable signal() system call as well:
signal(SIGCHLD, my_handler);
Though the this method prevents setting more advanced options, such as controlling which signals are blocked while in the signal handler. Also when setting a handler on SysV Unix (not BSD or Linux), the signal disposition is reset back to the default (i.e. the signal handler has one-shot semantics and does not prevent the signal from being delivered while inside of the signal handler, equivalent to using the SA_RESETHAND & SA_NODEFER options with sigaction.) For this reason to make properly portable code, use sigaction.
#include <sys/types.h> #include <signal.h> int kill(pid_t pid, int sig);
Sends the signal sig to a process or processes depending on the value of the pid given:
sig
pid
pid signal sig is sent to: > 0 The process with the ID specified by pid. = 0 Every process in the process group of the calling process. = -1 Every process for which the calling process has permission to send signals, except for process 1 (init) < -1 Every process in the process group whose ID is -pid.
There is a C wrapper function called killpg(int pgrp, int sig) to send a signal to a process group, but is just a wrapper function for:
killpg(int pgrp, int sig)
kill(pgrp == 0? -getpgrp(): -pgrp, sig);
A process group is typically the process ID of the last command in a pipeline (i.e the process that is most likely to control the terminal in a pipeline,) where it usually desirable for a signal sent to the process group leader to be sent to all the processes in the pipeline to terminate them simultaneously.
Example:
// Sends the STOP signal to process 1234: kill(1234, SIGSTOP);
waitpid()
Normally when a child changes its state, i.e. it is either suspended, un-suspended, terminated or exits normally, a SIGCHLD signal is sent to the parent process. This can be avoided if we set the disposition for SIGCHLD to SIG_IGN (ignore the signal), in which case a child process will be reaped by the init process (process 1.) When using a sigaction style handler the childs state can be retrieved via the siginfo_t structure, but not when using an old style handler. To get the child's state, we call wait() or waitpid() to get the child's current state.
SIGCHLD
wait()
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *wstatus); pid_t waitpid(pid_t pid, int *wstatus, int options);
The system call will return the status of a process (not necessarily the one that generated SIGCHLD signal) that has changed its state. The wstatus variable is an encoded integer that can be inspected with a number of macro functions:
Macro What it reports (returns) WIFEXITED(wstatus) True if the child terminated normally, that is, by calling exit(3) or _exit(2), or by returning from main(). WEXITSTATUS(wstatus) The exit status of the child. This consists of the least significant 8 bits of the status argument that the child specified in a call to exit(3) or _exit(2) or as the argument for a return statement in main(). This macro should be employed only if WIFEXITED returned true. WIFSIGNALED(wstatus) True if the child process was terminated by a signal. WTERMSIG(wstatus) The number of the signal that caused the child process to terminate. This macro should be employed only if WIFSIGNALED returned true. WCOREDUMP(wstatus) True if the child produced a core dump (see core(5)). This macro should be employed only if WIFSIGNALED returned true. WIFSTOPPED(wstatus) True if the child process was stopped by delivery of a signal; this is possible only if the call was done using WUNTRACED or when the child is being traced (see ptrace(2)). WSTOPSIG(wstatus) The number of the signal which caused the child to stop. This macro should be employed only if WIFSTOPPED returned true. WIFCONTINUED(wstatus) (since Linux 2.6.10) True if the child process was resumed by delivery of SIGCONT.
WIFEXITED(wstatus)
WEXITSTATUS(wstatus)
WIFEXITED
WIFSIGNALED(wstatus)
WTERMSIG(wstatus)
WIFSIGNALED
WCOREDUMP(wstatus)
WIFSTOPPED(wstatus)
WUNTRACED
WSTOPSIG(wstatus)
WIFSTOPPED
WIFCONTINUED(wstatus)
SIGCONT
The wait() system call is essentially waitpid(-1, &wstatus, 0), where -1 for the pid indicates that waitpid should wait on any child process and 0 indicates no additional options. Normally when a child process is suspended or resumed the parent won't receive a signal, if we want to properly do job control we will need to know when a process has stopped (such as say via Ctrl-Z) so the shell can re-assert control of the terminal. To get all events that occur on a child we'd use code such as:
waitpid(-1, &wstatus, 0)
int status; pid_t pid; // This will loop until all children have been waited on. while((pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) { // do something with status for the process with PID in 'pid' here }
Waitpid options WNOHANG Return immediately if no child has exited. WUNTRACED Also return if a child has stopped (but not traced via ptrace(2)). WCONTINUED (since Linux 2.6.10) Also return if a stopped child has been resumed by delivery of SIGCONT.
When WNOHANG is specified waitpid will return 0 when no more children processes have had a status change and -1 on error, otherwise the pid of the process that has been waited on will be returned.