How Linux Implements Signals Across Threads and Thread Groups
This article explains the POSIX requirements for signals in multithreaded Linux programs, details the kernel structures such as task_struct and signal_struct that enable shared signal handling, and walks through the key code paths—including copy_signal, copy_sighand, __send_signal, tkill/tgkill, and group exit—that manage signal delivery and thread‑group termination.
POSIX requirements for signals in multithreaded programs
The POSIX standard mandates that signal handlers be shared among all threads, each thread must have its own pending‑signal mask, kill/sigqueue target the whole thread group, a signal sent to a multithreaded process is delivered to a single thread chosen by the kernel, and a fatal signal kills the entire process.
Kernel data structures that satisfy POSIX
The Linux kernel represents each thread with a struct task_struct. Important fields include:
pid_t pid; // thread ID (unique per thread)
pid_t tgid; // thread‑group ID (the real process ID)
struct task_struct *group_leader; // points to the first thread in the group
struct list_head thread_group; // list of all threads in the groupIn a multithreaded program, getpid() returns tgid for every thread, while a direct gettid() (accessed via syscall) returns the per‑thread pid.
The group_leader field points to the main thread (the thread that started the process). All threads created with pthread_create belong to the same thread group, and their group_leader points to the main thread’s task_struct. The thread_group list allows the kernel to iterate over all threads in the group.
Signal handling structures shared by the thread group
All threads in a group share a single struct signal_struct (pointed to by each thread’s signal pointer) and a single struct sighand_struct. Consequently, pending signals, blocked masks, and signal actions are common to the whole group.
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked, real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;Creating a new thread: copy_signal and copy_sighand
When clone() creates a thread (flags include CLONE_THREAD), the kernel executes copy_signal() and copy_sighand(). For threads, these functions simply increment reference counts and reuse the existing structures, ensuring that signal handling remains shared.
static int copy_signal(unsigned long clone_flags, struct task_struct *tsk) {
if (clone_flags & CLONE_THREAD)
return 0; // share signal_struct
// allocate new signal_struct for processes
}
static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk) {
if (clone_flags & CLONE_SIGHAND) {
atomic_inc(¤t->sighand->count);
return 0; // share sighand_struct
}
// allocate new sighand_struct for processes
}Sending signals: kill/sigqueue vs tkill/tgkill
For a thread group, kill() and sigqueue() record the signal in signal->shared_pending, which is visible to all threads. To target a specific thread, the kernel provides tkill() (tid, sig) and tgkill() (tgid, tid, sig), which store the signal in the individual thread’s pending field.
int tkill(int tid, int sig);
int tgkill(int tgid, int tid, int sig);Core delivery routine __send_signal
The function __send_signal() selects the appropriate pending structure based on the group flag:
pending = group ? &t->signal->shared_pending : &t->pending;If group is true (kill/sigqueue), the signal is added to the shared pending list; otherwise (tkill/tgkill) it is added to the thread’s private pending list. The routine then queues real‑time signals, updates bitmap with sigaddset(&pending->signal, sig), and notifies via signalfd_notify().
Thread‑group termination
When a fatal signal requires the whole process to exit, the kernel invokes do_group_exit(). This function sets the SIGNAL_GROUP_EXIT flag, records the exit code in sig->group_exit_code, and calls zap_other_threads() to deliver SIGKILL to every other thread in the group.
void do_group_exit(int exit_code) {
struct signal_struct *sig = current->signal;
if (signal_group_exit(sig))
exit_code = sig->group_exit_code;
else if (!thread_group_empty(current)) {
// set flags and zap other threads
}
do_exit(exit_code);
}
int zap_other_threads(struct task_struct *p) {
struct task_struct *t = p;
while_each_thread(p, t) {
if (t->exit_state)
continue;
sigaddset(&t->pending.signal, SIGKILL);
signal_wake_up(t, 1);
}
return count;
}Blocking signals
Each thread has its own blocked‑signal bitmap stored in task_struct->blocked. The glibc function sigprocmask() manipulates this bitmap, allowing a thread to prevent certain signals from being delivered.
In summary, Linux satisfies POSIX signal semantics by sharing a single signal_struct among all threads in a group, using reference‑counted structures for signal handlers, and providing both group‑wide and per‑thread APIs to deliver and block signals, while ensuring orderly termination of the entire thread group when required.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
ITPUB
Official ITPUB account sharing technical insights, community news, and exciting events.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
