
SIGUSR1 Default Action: Why Signals Terminate Processes
SIGUSR1's default action is to terminate the process. No core dump, no stop, no continue – if you never install a handler, the first SIGUSR1 that lands kills the process dead. So if a program is dying with no error on stderr and no core file, check for an unhandled SIGUSR1 before you blame the allocator or the disk. This guide to signal handling in Linux gives you the step-by-step instructions to confirm that disposition, install a handler, and trace where a stray signal came from. Run strace -e trace=signal on the process and read the real disposition in /proc/PID/status, not a half-remembered version of the man page.
Last updated: 2026-07-21
Signals are forced attention pings from the kernel to user space. Some request a graceful stop, some force termination, some report a crash. The default action decides what happens if you do nothing, and for the two user signals that default is unforgiving.
What is the default action of SIGUSR1?
Terminate. The disposition column in signal(7) lists SIGUSR1 as Term, meaning the process exits and no core file is written. That is the whole behavior. There is no built-in meaning attached to the signal, and there is no gentle fallback if you ignore it.
People trip over this because they assume a "user" signal must be harmless by default. It is not. SIGUSR1 and SIGUSR2 share the same default action, and both kill an unprepared process on delivery.
| Signal | Default action | Core dump? | Catchable? |
|---|---|---|---|
| SIGUSR1 | Terminate | No | Yes |
| SIGUSR2 | Terminate | No | Yes |
The fix is always the same: install a handler, or set the disposition to ignore, before anything sends the signal. Do neither and you are shipping a process that dies the first time some tool tries to poke it.
Why does POSIX define SIGUSR1's default as terminate?
Because SIGUSR1 has no assigned meaning, and terminate is the honest default for that. POSIX reserves SIGUSR1 and SIGUSR2 for application-defined use. The standard deliberately refuses to give them any semantics, so the kernel has nothing sensible to do on delivery except end the process.
Ignore would have been the dangerous choice. If the default were "do nothing," a signal you meant to handle would silently vanish when your handler failed to install, and you would never notice the bug. Terminate forces the issue. You either opt in with a handler or the process dies loudly the first time it matters.
That is the pattern worth remembering. When the system has no idea what you want, it picks the option that makes your mistake visible.
Confirm the disposition on your own box
Do not trust a cached memory of the table. Read it live, then cross-check against the running process. Here is the workflow I run before touching anything in production.
- Open the manual:
man 7 signal. Find the Standard signals table and read the Action column for SIGUSR1. It saysTerm. - Check whether it is catchable in the same table. SIGUSR1 is catchable, unlike SIGKILL and SIGSTOP.
- Inspect the live process:
cat /proc/PID/statusand read theSigCgt,SigIgn, andSigBlkmasks. Those hex bitmasks tell you exactly which signals the process catches, ignores, and blocks right now. - Convert a mask bit if you need to. Find SIGUSR1's signal number in the
man 7 signaltable, then locate that same bit position in the hex mask to see whether it is set. - Confirm your handler actually installed by checking the return value of
sigaction(). A silent failure there is why your "handled" signal still kills the process.
The /proc/PID/status masks are the ground truth. The man page tells you the default; the status file tells you what this process decided to do about it.
For a wider view of what the process is doing when it dies, a walkthrough of tracing system calls pairs well with reading those masks.
How does SIGUSR1 compare to SIGINT and other terminate-by-default signals?
SIGINT also defaults to terminate. So does SIGTERM. The difference is not the default action – it is what typically sends them and whether programs are expected to catch them.
- SIGINT is the interactive interrupt, what Ctrl-C sends to the foreground process group. Its default action is to terminate the process. Most programs catch it to clean up before exit.
- SIGTERM is the polite shutdown request. Its default action is Term, and daemons usually trap it to flush logs and close sockets.
- SIGUSR1 shares that terminate default but carries no meaning at all until you give it one.
- SIGKILL is the outlier. It cannot be caught or ignored, so there is no handler to write and no cleanup path to run.
The broader map splits into four buckets. Term signals like SIGINT, SIGTERM, and the two user signals just end the process. Core signals like SIGSEGV and SIGFPE end it and drop a core file. Ign signals like SIGCHLD do nothing by default. Stop/Cont signals pause and resume a process.
| Class | Meaning | Example |
|---|---|---|
| Term | Exit, no core | SIGINT, SIGTERM, SIGUSR1 |
| Core | Exit plus core dump | SIGSEGV, SIGFPE |
| Ign | No effect unless caught | SIGCHLD |
| Stop/Cont | Pause or resume | SIGSTOP, SIGCONT |
Knowing which bucket a signal falls in tells you immediately whether to expect a clean exit, a core file, or nothing at all.
Install a handler so SIGUSR1 stops killing your process
Use sigaction(), not signal(). The manual is blunt about it: use sigaction() unless you have very compelling reasons not to. The old signal() API behaves differently across systems – System V semantics reset the handler after one delivery, BSD semantics do not – and that difference is a phantom-bug factory.
Here is the minimal shape. Set a flag in the handler and nothing else, then act on the flag from the main loop where it is safe.
#include <signal.h>
static volatile sig_atomic_t got_usr1 = 0;
static void on_usr1(int signo) {
got_usr1 = 1;
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = on_usr1;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
return 1;
}
for (;;) {
if (got_usr1) {
got_usr1 = 0;
/* do the real work here, in the main loop */
}
/* ... */
}
}
The rule that saves you: never do real work inside the handler. A handler can interrupt your program at almost any instruction, so it must do very little. Set one volatile sig_atomic_t flag and get out. Allocate, log with printf, or touch a shared data structure in there and you will corrupt state or deadlock at the worst possible moment.
To ignore SIGUSR1 instead of handling it, set sa.sa_handler = SIG_IGN. To restore the default terminate behavior, use SIG_DFL. Both go through the same sigaction() call.
What SA_RESTART actually does when SIGUSR1 interrupts a blocking call
SA_RESTART makes some interrupted system calls restart automatically instead of failing with EINTR. Without it, a signal that arrives while you sit in a blocking call kicks you out of that call with errno set to EINTR, and you have to retry by hand.
The catch nobody mentions: SA_RESTART does not cover everything. Calls like read, write, and wait restart when the flag is set. But poll, select, epoll_wait, and several timeout-bearing calls never auto-restart, no matter what you set. They always return EINTR on signal delivery. If your event loop assumes SA_RESTART protects its poll, you have a latent bug waiting for the first SIGUSR1.
So write the retry loop anyway:
ssize_t n;
do {
n = read(fd, buf, len);
} while (n == -1 && errno == EINTR);
To catch these bugs, watch for them under strace. Run strace -f -e trace=read,poll,select and look for a syscall returning -1 EINTR right after a signal line. That tells you exactly which call your handler is interrupting. If you would rather not deal with EINTR at all, signalfd() turns signals into readable file descriptors so your normal poll-based event loop handles them like any other input.
Debug a process that keeps dying from an unexpected SIGUSR1
Run this first: strace -f -e trace=signal -p PID. It prints every signal the process sends, receives, and delivers, including which thread got it. When the process dies, the last few lines show you the SIGUSR1 delivery and the default Term that followed. That alone tells you whether SIGUSR1 is the real killer or a red herring.
Then find the sender. On delivery, strace shows the si_pid in the siginfo struct – that is the PID that called kill() or tgkill(). Track that PID down and you have your culprit. A common one is a supervisor or watchdog script that repurposes SIGUSR1 for its own signaling and aims it at the wrong process.
If si_pid is 0, the signal came from the kernel, not another process. Check dmesg -T | tail and journalctl -b -p err for an out-of-memory kill or a watchdog timeout around that timestamp. Those leave a clear line in the kernel ring buffer.
The mistake I see most is guessing at the sender from application logs alone. The signal path is reproducible under strace, so read the actual delivery instead of theorizing. Understanding how a process moves through its lifecycle helps you reason about which parent could be sending it.
Does SIGUSR1's behavior change across kernels, libc, or init systems?
The default action does not. Terminate is terminate on any Linux kernel and any C library. What changes around the edges is the API you install the handler with and who else on the box already claims the signal.
signal() semantics differ between glibc and musl. glibc's signal() gives BSD-style behavior where the handler stays installed. Some other libraries and older setups reset it to default after one delivery. This is exactly why the manual pushes you toward sigaction(), where the behavior is spelled out and does not shift under you.
The bigger surprise is that SIGUSR1 is often already taken. Several daemons and runtimes use it as an operational hook – a common convention is SIGUSR1 for log reopen or rotation. Under systemd or inside a container runtime, a supervising process may send SIGUSR1 for its own purposes. Before you assign SIGUSR1 in a service, check what the init system and any wrapper expect, or your handler and theirs will fight. When you build your own long-running service, wire the signal contract deliberately, the way a hand-written Linux daemon sets up its shutdown and reload signals from the start.
Two things are fixed no matter the environment: SIGUSR1 defaults to terminate, and SIGKILL cannot be caught or ignored. Everything else is convention, so verify it on the actual host. The authoritative reference is the signal(7) manual page, and the POSIX signal concepts behind it.
FAQ
What signal number is SIGUSR1, and can it vary across architectures?
The signal numbers assigned to SIGUSR1 and SIGUSR2 are not fixed across all Linux architectures; they vary by platform. On MIPS and a few other platforms the values differ. Always pass the name in scripts and read the number from kill -l on the target, because hardcoding a signal number is how a signal-sending script breaks after a port.
Does sending SIGUSR1 to a shell script kill it the same way?
Yes, unless the script traps it. A bare bash script with no trap 'handler' USR1 inherits the terminate default and exits on delivery. Add a trap for USR1 if you want the script to react instead of die. The disposition rule is the same for shells and compiled programs.
Why does my process die silently with no message when it gets SIGUSR1?
Because a plain terminate produces no output and no core file. The process just stops. There is nothing to print because no handler ran and no fault was raised. Confirm it with strace -e trace=signal, which will show the delivery and the exit even though the process itself says nothing.
Can I make SIGUSR1 dump core for debugging instead of exiting quietly?
Not through its default action, since that never writes a core file. If you need a core, install a handler that calls abort(), which raises SIGABRT, and SIGABRT's default is terminate-plus-core. Make sure your core size limit is not zero first: check ulimit -c and raise it if needed.
Is it safe to reuse SIGUSR1 for two different things in one program?
No. Give SIGUSR1 exactly one meaning and document it. If you overload it, the handler cannot tell the two events apart, and you end up decoding intent from shared state that a signal handler should not be touching. Use SIGUSR2 for the second event, or move the second channel to a pipe or socket.
