Logo  

CS456 - Systems Programming

Creating a Unix Shell part 2:

Reading:

  • man 7 signal
  • man 2 signal
  • man 2 sigaction
  • man 2 kill

Signals

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.

The standard Linux signals are:

Signal Action Comment
SIGABRT Core Abort signal from abort(3)
SIGALRM Term Timer signal from alarm(2)
SIGBUS Core Bus error (bad memory access)
* SIGCHLD Ign Child stopped or terminated
SIGCLD Ign A synonym for SIGCHLD
* SIGCONT Cont Continue if stopped
SIGEMT Term Emulator trap
SIGFPE Core Floating-point exception
* SIGHUP Term Hangup detected on controlling terminal or death of controlling process
SIGILL Core Illegal Instruction
SIGINFO Term A synonym for SIGPWR
* SIGINT Term Interrupt from keyboard
SIGIO Term I/O now possible (4.2BSD)
SIGIOT Core IOT trap. A synonym for SIGABRT
* SIGKILL Term Kill signal
SIGLOST Term File lock lost (unused)
* SIGPIPE Term Broken pipe: write to pipe with no readers; see pipe(7)
SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF Term Profiling timer expired
SIGPWR Term Power failure (System V)
SIGQUIT Core Quit from keyboard
SIGSEGV Core Invalid memory reference
SIGSTKFLT Term Stack fault on coprocessor (unused)
* SIGSTOP Stop Stop process
* SIGTSTP Stop Stop typed at terminal
SIGSYS Core Bad system call (SVr4); see also seccomp(2)
SIGTERM Term Termination signal
SIGTRAP Core Trace/breakpoint trap
* SIGTTIN Stop Terminal input for background process
* SIGTTOU Stop Terminal output for background process
SIGUNUSED Core Synonymous with SIGSYS
SIGURG Ign Urgent condition on socket (4.2BSD)
SIGUSR1 Term User-defined signal 1
SIGUSR2 Term User-defined signal 2
SIGVTALRM Term Virtual alarm clock (4.2BSD)
SIGXCPU Core CPU time limit exceeded (4.2BSD); see setrlimit(2)
SIGXFSZ Core File size limit exceeded (4.2BSD); see setrlimit(2)
SIGWINCH Ign Window resize signal (4.3BSD, Sun)

* = Of most interest to use at this time.

Signal dispositions

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.

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.)

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.

Signal handlers

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.

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.

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.

Sending signals

#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:

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:

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);

SIGCHLD & 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.

#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.


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:

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.