dyx
2021-03-29 +0800
When UNIX is mentioned in this document it means macOS or Linux as they are the mainly used UNIX at this moment. When shell is mentioned it means Bash or Zsh. Most demos are written in C for macOS with Apple libc and Linux with glibc.
signal
is a function defined in libc. sigaction
is a function defined by POSIX. sigaction
is able to specify many detailed behaviors of a signal while signal
can’t. As a libc function, signal
behaves differently across systems thus don’t use it if the portability is required and sigaction
is available.
This document narrows the environment down to macOS with Apple libc and Linux with glibc. The behavior of signal
is determined then. Thus signal
is used to demonstrate for conciseness.
A process has two bitmap — pending
and blocked
. Sending a signal to a process is just setting the corresponding bit in pending
to 1. Blocking a signal is setting the corresponding bit in blocked
to 1.
The process checks pending & ~blocked
to find which signals are ready to be delivered. Making a signal delivered is resetting the corresponding bit in pending
to 0 and:
The loop keeps running even it’s in process of running a handler.
The above text describes most behaviors of signals installed by the signal
function in modern UNIX. Some of them can be modified by calling sigaction
instead of signal
. For example, sigaction
can specify which extra signals to block while handling it by the sa_mask
variable and whether to restart automatically an interrupted system call by the SA_RESTART
flag. See the manual page of sigaction
for detail1 2.
If a signal with the same signal number are sent to a process twice within an enough short duration that the second signal arrives before the first is delivered, the second arrival will be ignored because the corresponding bit is already 1. If the second signal arrives while handling the first, it will be delivered after the first one is delivered.
A handler will be interrupted by handling another unblocked signal because the loop keeps running while handling a signal. After the new handler returns, the old one resumes.
Signals can be ignored by a process — e.g. signal(SIGALRM, SIG_IGN)
ignores SIGALRM
. What does ignoring mean exactly? Is it setting an empty function as the handler or it is a special flag? This varies across systems.
On macOS, ignoring is more like a special flag. It clears the corresponding bit in the current pending
bitmap and prevents subsequent arrivals of the signal to update pending
3. Thus if a signal arrives at a process blocks and ignores it, the arrival will not be handled even later the process unblocks and installs a handler for it.
On Linux, ignoring a signal is more like setting an empty function as the handler. It also clears the corresponding bit in the current pending
bitmap but not prevents subsequent arrivals to update pending
. The subsequent arrivals are handled normally by doing nothing4. So if the signal is blocked and ignored, the arrival will be handled after the process unblocks and installs a handler.
The following C program verifies the different behaviors.
/* Print `14 delivered` on Linux
* Print nothing on macOS
*/
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
void handler(int sig)
{
printf("%d delivered\n", sig);
}
int main(void)
{
sigset_t sigalrm;
sigemptyset(&sigalrm);
sigaddset(&sigalrm, SIGALRM);
sigprocmask(SIG_BLOCK, &sigalrm, NULL);
signal(SIGALRM, SIG_IGN);
alarm(1);
sleep(3);
signal(SIGALRM, handler);
sigprocmask(SIG_UNBLOCK, &sigalrm, NULL);
sleep(3);
return 0;
}
It prints 14 delivered
on Ubuntu 18.04 but prints nothing on macOS .
Some system calls may be interrupted and terminated with the error EINTR
by invoking a signal handler. Setting the SA_RESTART
flag on the handler with sigaction
makes the system call automatically restarts.
Handlers installed with signal
has the SA_RESTART
flag set by default. The following C code verifies the behaviors.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void nop(int sig);
int main(void)
{
signal(SIGALRM, nop);
alarm(1);
char buf[0xff];
if (read(0, buf, 1) < 0) {
perror("read");
return 1;
}
puts("DONE");
return 0;
}
void nop(int sig) { }
On both macOS and Linux the program waits until a character is entered.
On Linux, there are some system calls can’t be restarted even the SA_RESTART
flag is set. See signal(7)5.
A notable fact is that the document of glibc version 2.326 conflicts with the man-page5. It states that handlers installed by signal
of glibc doesn’t restart system calls. The experiment above shows that the man-page is correct. Maybe there are some misunderstanding here so the paragraph of the glibc document is quoted below.
WARNING The following quote doesn’t state the fact if there’s no misunderstanding.
When you don’t specify with
sigaction
orsiginterrupt
what a particular handler should do, it uses a default choice. The default choice in the GNU C Library is to make primitives fail withEINTR
.
A forked process inherits the signal disposition from the parent process but pending
is discarded.
If a process calls an exec*
function, signal handlers are reset to SIG_DFL
except the ignored. The ignored signals keep ignored. The blocked
mask is also kept.
sa_mask
and sa_flags
are kept after reloading on macOS but not on Linux. However, keeping sa_mask
and sa_flags
or not is meaningless. Because the signal is set to either SIG_DFN
or SIG_IGN
after reloading, there’s no place for sa_mask
or sa_flags
to play in. If a handler is later installed in the reloaded process, sa_mask
and sa_flags
will be overwritten.
The following C code demonstrates the behaviors.
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/errno.h>
#include <sys/wait.h>
/* Print the error message set by errno and exit */
void unixerr(char *msg);
/* Return "yes" if n is non-zero else "no" */
char *yesno(int n);
/* Print the signal state of SIGALRM */
void psigst(void);
/* Do nothing */
void nop();
int main(int argc, char **argv)
{
if (argc > 1) {
/* reloaded with execl */
printf("RELOADED\n------\n");
psigst();
return 0;
}
sigset_t sigalrm, sigterm;
sigemptyset(&sigalrm);
sigemptyset(&sigterm);
sigaddset(&sigalrm, SIGALRM);
sigaddset(&sigterm, SIGTERM);
sigprocmask(SIG_BLOCK, &sigalrm, NULL);
struct sigaction sa = {
.sa_handler = nop,
.sa_mask = sigterm,
.sa_flags = SA_NODEFER
};
sigaction(SIGALRM, &sa, NULL);
alarm(1);
sleep(2);
printf("PARENT\n------\n");
psigst();
printf("\n");
pid_t cpid = fork();
if (cpid == -1)
unixerr("fail to fork");
if (!cpid) {
printf("FORKED\n------\n");
psigst();
printf("\n");
fflush(stdout);
execl(argv[0], argv[0], "exec", NULL);
unixerr("fail to reload");
}
else {
if (wait(NULL) == -1)
unixerr("wait failed");
return 0;
}
}
void unixerr(char *msg)
{
perror(msg);
exit(1);
}
char *yesno(int n)
{
return n ? "yes" : "no";
}
void psigst(void)
{
sigset_t pending, blocked;
struct sigaction sa;
sigpending(&pending);
sigprocmask(0, NULL, &blocked);
sigaction(SIGALRM, NULL, &sa);
printf("SIGALRM pending: %s\n", yesno(sigismember(&pending, SIGALRM)));
printf("SIGALRM blocked: %s\n", yesno(sigismember(&blocked, SIGALRM)));
printf(
"SIGALRM sa_mask: %s\n",
sigismember(&sa.sa_mask, SIGTERM) ? "SIGTERM" : "{}"
);
printf("SIGALRM sa_flags: %d\n", sa.sa_flags);
}
void nop() { }
Output on macOS
PARENT
------
SIGALRM pending: yes
SIGALRM blocked: yes
SIGALRM sa_mask: SIGTERM
SIGALRM sa_flags: 16
FORKED
------
SIGALRM pending: no
SIGALRM blocked: yes
SIGALRM sa_mask: SIGTERM
SIGALRM sa_flags: 16
RELOADED
------
SIGALRM pending: no
SIGALRM blocked: yes
SIGALRM sa_mask: SIGTERM
SIGALRM sa_flags: 16
Output on Linux:
PARENT
------
SIGALRM pending: yes
SIGALRM blocked: yes
SIGALRM sa_mask: SIGTERM
SIGALRM sa_flags: 1140850688
FORKED
------
SIGALRM pending: no
SIGALRM blocked: yes
SIGALRM sa_mask: SIGTERM
SIGALRM sa_flags: 1140850688
RELOADED
------
SIGALRM pending: no
SIGALRM blocked: yes
SIGALRM sa_mask: {}
SIGALRM sa_flags: 0
A process can’t ignore or reset a handler for SIGSTOP
. When SIGSTOP
is sent to a process, the process will be stopped (suspended). Pressing CTRL + Z
in terminal sends SIGTSTP
to the foreground processes. The default behavior of SIGTSTP
is also suspending the process but it can be reset by the process.
A stopped process resumes after SIGCONT
is sent to it. The behavior of sending a signal other than SIGCONT
to a stopped process differs across systems.
On macOS, any signal resumes a stopped process even the signal is blocked.
On Linux, (except SIGCONT
) SIGTERM
resumes a stopped process (even blocked) but other signals don’t.
On both systems, SIGKILL
terminates a process no matter it’s stopped or not.
While a child process is terminated or suspended or resumed, SIGCHLD
will be sent to the parent process. If a handler of SIGCHLD
is installed in the parent process with the SA_NOCLDSTOP
flag, the signal will be sent only when a child process is terminated.
Normally, a process becomes a zombie and waits for the parent to reap it. However, if the parent installs a handler of SIGCHLD
with the SA_NOCLDWAIT
flag, children won’t become zombies anymore and they’re reaped automatically.
If the parent explicitly specifies SIG_IGN
as the action for the signal SIGCHLD
, zombies will not be created even the SA_NOCLDWAIT
flag is removed from sa_flags
3 2. The following C code verifies the behavior.
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
int main(void)
{
sigset_t empty;
sigemptyset(&empty);
sigaction(SIGCHLD, &(struct sigaction) {
.sa_handler=SIG_IGN,
.sa_mask=empty,
.sa_flags=SA_RESTART /* No SA_NOCLDWAIT here */
}, NULL);
pid_t cpid = fork();
if (cpid < 0) {
perror("fork");
return 1;
}
if (cpid) {
sleep(3);
if (kill(cpid, 0) == -1 && errno == ESRCH)
puts("the child is reaped");
else
puts("the child is NOT reaped");
}
else
return 0;
return 0;
}
The program prints both in macOS and Linux:
the child is reaped
When CTRL + C
is pressed, the shell sends SIGINT
to all processes in the foreground process group.
However, if background processes are launched in a non-interactive shell, SIGINT
and SIGQUIT
are by default ignored for the background processes7. Moreover, if a background process is a shell, the POSIX standard forbids it to restore the handler of SIGINT
and SIGQUIT
8. Zsh doesn’t respect the weird requirement but Bash does.
Consider the following 3 shell programs. parent.sh launches a.sh and b.sh as background processes and waits for their terminations.
# parent.sh
./a.sh &
./b.sh &
wait
# a.sh
while true
do
echo message from a
sleep 2
done
# b.sh
while true
do
echo message from b
sleep 2
done
If CTRL + C
is pressed while parent.sh is running, parent.sh will be terminated but a.sh and b.sh keep running. Restoring SIGINT
handlers in a.sh and b.sh could make them terminated in Zsh but not in Bash.
A solution for most simple cases is to send a signal other than SIGINT
and SIGQUIT
— e.g. SIGTERM
— to child processes while the parent process exits.
#!/bin/bash
# parent.sh
function onexit() {
kill -- `pgrep -P $$`
}
trap onexit EXIT
./a.sh &
./b.sh &
wait
When the terminal disconnects — e.g. close Terminal.app on macOS — SIGHUP
will be sent to the session leader which is usually a shell9. SIGHUP
is also sent to the foreground process group if the session leader terminates.
After SIGHUP
is delivered to the shell, the shell may exit immediately or send SIGHUP
to background processes before it exits. In any case SIGHUP
will be sent to the foreground process group.
Bash has an option called huponexit
. It could be checked by shopt | grep huponexit
. If huponexit
is on and it’s in an interactive login shell, SIGHUP
will be sent to background processes10.
On both macOS and Linux, being interactive means it can’t be running a script file.
On Linux, being a login shell means an SSH shell or a true TTY. A shell open by the local terminal emulator is not a login shell11.
On macOS, a shell open by Terminal.app does also be a login shell12. However if a subshell is entered, the subshell is a non-login shell.
In Zsh if the job control is enable which is by default in an interactive shell and the nohup
option is off which is by default in most systems, SIGHUP
will be sent to background processes. Run setopt
in Zsh; If the option is on it would appear in the output13. Zsh provides an extension syntax command &!
to run command
in background and disown from SIGHUP
14.
There were barely chances to write an init program until Docker becomes popular. Docker is Linux-based so this section discusses Linux only and demos are for Docker.
The only signals that can be sent to process ID 1, the init process, are those for which init has explicitly installed signal handlers15. Thus even SIGKILL
can’t be sent to PID 1.
The following init.sh
and Dockerfile
build a container whose init program is init.sh
.
init.sh
#!/bin/bash
# init.sh
while true
do
sleep 1
done
Dockerfile
# Dockerfile
FROM ubuntu:18.04
COPY init.sh /
ENTRYPOINT ["/init.sh"]
Running docker build -t init .
to build the image and running docker run --name init --rm -it init
to start a container and attach it to the terminal. Open another terminal and run docker exec -it init bash
to attach to the container in the new terminal and make bash be the shell.
In the new terminal run ps -ax
and it will print the following.
root@784455b692bd:/# ps -ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss+ 0:00 /bin/bash /init.sh
10 pts/1 Ss 0:00 bash
21 pts/0 S+ 0:00 sleep 1
22 pts/1 R+ 0:00 ps -ax
It confirms that init.sh
is run with PID 1.
Execute kill -TERM 1
then ps -ax
. The result shows that init.sh
is still there. SIGTERM
is ignored though init.sh
never sets it. The result is the same if kill -TERM 1
is replaced with kill -KILL 1
.
Also it can’t be terminated by press CTRL + C
in the previous terminal because SIGINT
is also ignored.
Modify init.sh
to the following.
#!/bin/bash
# init.sh
trap exit SIGTERM
trap exit SIGINT
while true
do
sleep 1
done
CTRL + C
and kill -TERM 1
work properly now.
Stevens, W. R., & Rago, S. A. (2008). Advanced programming in the UNIX environment. Addison-Wesley. ↩
Difference between Login Shell and Non-Login Shell? - Unix & Linux on Stack Exchange ↩
What is the difference between .bash_profile and .bashrc? - Ask Different on Stack Exchange ↩