Fundamentals 15 min read

How Linux Threads Share Signals: Inside task_struct and Signal Handling

This article explains how Linux implements POSIX signal handling in multithreaded applications, detailing the relationship between signals and threads, the kernel structures like task_struct, signal_struct, and how signals are delivered, blocked, and cause group exits within a thread group.

ITPUB
ITPUB
ITPUB
How Linux Threads Share Signals: Inside task_struct and Signal Handling

POSIX requirements for signals in multithreaded programs

Signal handlers must be shared across all threads in a multithreaded application, while each thread has its own pending and blocked signal masks.

POSIX kill/sigqueue functions target the entire multithreaded process, not a specific thread.

Each signal sent to a multithreaded process is delivered to exactly one thread chosen by the kernel from those not blocking the signal.

If a fatal signal is sent to a multithreaded process, the kernel terminates all threads, not just the receiving thread.

Linux kernel data structures

struct task_struct {
    pid_t pid;            // thread ID (not process ID)
    pid_t tgid;           // thread‑group ID, the real process ID
    struct task_struct *group_leader;   /* thread‑group leader */
    struct list_head thread_group;      /* list of threads in the group */
    /* ... */
};

Thread IDs and group leader

In Linux, pid represents the thread ID, while tgid is the thread‑group (process) ID. All threads return the same value for getpid() (the tgid). A thread‑specific gettid() (accessed via syscall) returns the pid stored in task_struct. The group_leader field points to the first thread of the group (the main thread for a typical program). All threads created with pthread_create belong to the same thread group, sharing the same group_leader and thread_group list.

Signal handling structures shared by a thread group

struct signal_struct {
    struct sighand_struct *sighand;
    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask;    /* restored if set_restore_sigmask() was used */
    struct sigpending pending;
    /* ... */
};

struct sighand_struct {
    /* ... */
};

All threads in the same thread group point to the same signal_struct and sighand_struct, meaning signal handlers and pending signals are shared.

Copying signal structures on fork/clone

static int copy_signal(unsigned long clone_flags, struct task_struct *tsk)
{
    struct signal_struct *sig;
    if (clone_flags & CLONE_THREAD)
        return 0; // same thread group shares the structure
    sig = kmem_cache_zalloc(signal_cachep, GFP_KERNEL);
    tsk->signal = sig;
    if (!sig)
        return -ENOMEM;
    sig->nr_threads = 1;
    atomic_set(&sig->live, 1);
    atomic_set(&sig->sigcnt, 1);
    init_waitqueue_head(&sig->wait_chldexit);
    sig->curr_target = tsk;
    /* ... */
    return 0;
}

static int copy_sighand(unsigned long clone_flags, struct task_struct *tsk)
{
    struct sighand_struct *sig;
    if (clone_flags & CLONE_SIGHAND) {
        atomic_inc(&current->sighand->count);
        return 0; // share the existing sighand_struct
    }
    sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
    rcu_assign_pointer(tsk->sighand, sig);
    if (!sig)
        return -ENOMEM;
    atomic_set(&sig->count, 1);
    memcpy(sig->action, current->sighand->action, sizeof(sig->action));
    return 0;
}

The first thread gets identical pid and tgid; subsequent threads receive a unique pid while inheriting the creator's tgid. All threads are linked into thread_group for easy traversal.

Sending signals to a thread group vs. a specific thread

int tkill(int tid, int sig);
int tgkill(int tgid, int tid, int sig);
tkill

and tgkill target a particular thread within a group; the kernel records the signal in that thread's private pending bitmap. In contrast, kill and sigqueue set group = true and store the signal in the shared signal->shared_pending structure.

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
                         int group, int from_ancestor_ns)
{
    struct sigpending *pending;
    /* ... */
    pending = group ? &t->signal->shared_pending : &t->pending;
    /* queue handling, real‑time checks, etc. */
    sigaddset(&pending->signal, sig);
    complete_signal(sig, t, group);
    return 0;
}

Group exit handling

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)) {
        struct sighand_struct *const sighand = current->sighand;
        spin_lock_irq(&sighand->siglock);
        if (signal_group_exit(sig)) {
            exit_code = sig->group_exit_code;
        } else {
            sig->group_exit_code = exit_code;
            sig->flags = SIGNAL_GROUP_EXIT;
            zap_other_threads(current);
        }
        spin_unlock_irq(&sighand->siglock);
    }
    do_exit(exit_code);
}

int zap_other_threads(struct task_struct *p)
{
    struct task_struct *t = p;
    int count = 0;
    p->signal->group_stop_count = 0;
    while_each_thread(p, t) {
        task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
        count++;
        if (t->exit_state)
            continue;
        sigaddset(&t->pending.signal, SIGKILL);
        signal_wake_up(t, 1);
    }
    return count;
}

When a fatal signal (e.g., SIGSEGV) occurs, do_signal eventually calls do_group_exit, which marks the whole thread group for termination and injects SIGKILL into each remaining thread via zap_other_threads. Each thread then processes the kill signal, leading to a final do_exit.

Blocking signals and synchronous mask

The per‑thread blocked bitmap in task_struct stores signals that the thread has masked via sigprocmask. Certain signals are treated as synchronous (e.g., SIGSEGV, SIGBUS, SIGILL, SIGTRAP, SIGFPE, SIGSYS) and have higher delivery priority:

#define SYNCHRONOUS_MASK \
    (sigmask(SIGSEGV) | sigmask(SIGBUS) | sigmask(SIGILL) | \
     sigmask(SIGTRAP) | sigmask(SIGFPE) | sigmask(SIGSYS))

When dequeuing signals, the kernel first checks a thread's private pending queue; if empty, it falls back to the shared signal->shared_pending bitmap.

Conclusion

The Linux kernel satisfies POSIX signal semantics by sharing a signal_struct among all threads in a thread group, while keeping per‑thread identifiers ( pid) and blocked masks separate. Signal delivery, queuing, and group termination are all orchestrated through the structures and functions illustrated above, providing a coherent model for multithreaded signal handling.

task_struct layout
task_struct layout
signal_struct layout
signal_struct layout
signal handling code
signal handling code
event promotion
event promotion
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

LinuxThreadstask_structsignalsPOSIX
ITPUB
Written by

ITPUB

Official ITPUB account sharing technical insights, community news, and exciting events.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.