rt_tgsigqueueinfo系统调用及示例

我们来深入学习 rt_tgsigqueueinfo 系统调用,在 Linux 系统中,信号(Signal)是进程间通信的一种方式。kill() 系统调用可以向一个进程发送信号,而 sigqueue() 则可以发送信号并附带一些数据(union sigval)。rt_tgsigqueueinfo 是一个更底层、更具体、但也更复杂的系统调用。

1. 函数介绍

在 Linux 系统中,信号(Signal)是进程间通信的一种方式。kill() 系统调用可以向一个进程发送信号,而 sigqueue() 则可以发送信号并附带一些数据(union sigval)。

rt_tgsigqueueinfo 是一个更底层、更具体、但也更复杂的系统调用。它的名字可以拆解为:

  • rt_: 表示这是一个与实时信号相关的系统调用。
  • tg: 表示 Thread Group(线程组)。在 Linux 中,一个进程(Process)可以包含多个线程(Threads),这些线程共享同一个 PID,但每个线程有自己的唯一 Thread ID (TID)。所有共享同一个 PID 的线程构成了一个线程组。
  • sigqueueinfo: 表示它像 sigqueue 一样发送信号和数据,但允许发送者提供更详细的信号信息(通过 siginfo_t 结构体)。

所以,rt_tgsigqueueinfo 的作用是:向指定进程(线程组)中的指定线程(由 TID 指定)发送一个信号,并允许发送者完全指定该信号的 siginfo_t 信息

与 sigqueue 的区别:

  • sigqueue(pid_t pid, ...): 向进程 pid 发送信号。内核会选择该进程(线程组)中的某个线程来接收信号。
  • rt_tgsigqueueinfo(pid_t tgid, pid_t tid, ...)tgid 是线程组 ID(通常就是进程的 PID),tid 是线程组内具体线程的 ID(TID)。它允许你指定哪个线程应该接收这个信号。

为什么复杂/危险?
与 rt_sigqueueinfo 类似,它允许发送者完全伪造 siginfo_t 中的信息(如发送者 PID、si_code 等),这可能带来安全风险。因此,它的使用受到严格的权限检查。

对于 Linux 编程小白:你通常不需要直接使用 rt_tgsigqueueinfo。在需要向特定线程发送信号时,更常见的做法是在线程内部使用 pthread_kill()(POSIX 线程库函数)。rt_tgsigqueueinfo 主要供系统级程序或 C 库实现使用。

2. 函数原型

// 这是底层系统调用,用户空间标准 C 库通常不直接提供包装函数
#include <sys/syscall.h> // 包含系统调用号
#include <unistd.h>      // 包含 syscall 函数
#include <signal.h>      // 包含 siginfo_t 定义

long syscall(SYS_rt_tgsigqueueinfo, pid_t tgid, pid_t tid, int sig, siginfo_t *uinfo);

用户空间更常用的相关函数:

#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value); // 向进程发送信号和数据

#include <pthread.h> // (需要链接 -lpthread)
int pthread_kill(pthread_t thread, int sig); // 向特定 POSIX 线程发送信号

3. 功能

向指定线程组 ID (tgid) 和线程 ID (tid) 的线程发送指定信号 (sig),并允许发送者完全指定 siginfo_t 结构体 (uinfo) 中包含的详细信息。

4. 参数

  • tgid:
    • pid_t 类型。
    • 目标线程所属的线程组 ID (Thread Group ID)。对于单线程进程,这通常就是它的 PID。对于多线程程序,所有线程共享同一个 tgid(即主线程的 PID)。
  • tid:
    • pid_t 类型。
    • 目标线程在内核中的唯一 ID (Thread ID)。在用户空间的 pthread_create 返回的 pthread_t 和内核的 tid 之间需要转换(可以使用 /proc/self/task/ 目录或特定的系统调用来获取)。
  • sig:
    • int 类型。
    • 要发送的信号编号,例如 SIGUSR1SIGRTMIN 等。注意,不能发送 SIGKILL 和 SIGSTOP
  • uinfo:
    • siginfo_t * 类型。
    • 一个指向 siginfo_t 结构体的指针。这个结构体包含了你想随信号一起传递给目标线程的所有详细信息。调用者需要自己填充这个结构体的各个字段。

5. 返回值

  • 成功: 返回 0。
  • 失败: 返回 -1,并设置全局变量 errno 来指示具体的错误原因:
    • EAGAIN: (对于实时信号) 已达到接收者排队信号的最大数量限制 (RLIMIT_SIGPENDING)。
    • EPERM: 调用者没有权限发送信号给目标线程(例如,权限不足,或试图伪造某些 si_code)。
    • EINVALsig 是无效的信号号,tgid 或 tid 无效,或者 uinfo 中的 si_code 是无效的或不允许由用户设置的。
    • ESRCH: 找不到 tgid 指定的线程组或 tid 指定的线程。

6. 相似函数或关联函数

  • sigqueue: 向进程(线程组)发送信号和数据,由内核选择线程接收。
  • pthread_kill: POSIX 标准函数,用于向指定的 pthread_t 线程发送信号,更易于使用。
  • tgkill: 一个更安全的系统调用,用于向指定线程组和线程发送信号(但不带 siginfo_t 数据),通常由 pthread_kill 内部调用。
  • kill: 向进程发送信号。
  • siginfo_t: 包含信号详细信息的结构体。
  • getpid: 获取当前进程的 PID (也是线程组 ID tgid)。
  • gettid: 获取当前线程的内核 TID。注意:这不是标准 C 库函数,需要通过 syscall(SYS_gettid) 调用。
  • pthread_self: 获取当前线程的 POSIX 线程 ID (pthread_t)。

7. 示例代码

由于直接使用 rt_tgsigqueueinfo 需要获取内核线程 ID (tid) 并且容易因权限或错误的 siginfo_t 而失败,下面的示例将演示如何获取 tid 并尝试调用 rt_tgsigqueueinfo,同时提供一个使用推荐的 pthread_kill 的对比示例。

警告:直接使用 rt_tgsigqueueinfo 可能会因为权限问题或 siginfo_t 构造不当而失败。

#define _GNU_SOURCE // 启用 GNU 扩展,获取 gettid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/syscall.h> // 包含 syscall 和 SYS_rt_tgsigqueueinfo, SYS_gettid
#include <errno.h>
#include <sys/types.h> // 包含 pid_t
#include <pthread.h>   // 包含 pthread_t, pthread_create 等
#include <stdatomic.h> // 用于线程间安全通信

// 全局变量用于线程间通信
_Atomic int signal_count = 0;

// 信号处理函数 (使用 SA_SIGINFO 获取详细信息)
void signal_handler(int sig, siginfo_t *info, void *context) {
    atomic_fetch_add(&signal_count, 1); // 原子增加计数
    printf("Thread %ld: Received signal %d\n", syscall(SYS_gettid), sig);
    printf("  si_signo: %d\n", info->si_signo);
    printf("  si_code:  %d", info->si_code);
    switch(info->si_code) {
        case SI_USER: printf(" (SI_USER: kill, sigsend or raise)\n"); break;
        case SI_QUEUE: printf(" (SI_QUEUE: sigqueue)\n"); break;
        case SI_TKILL: printf(" (SI_TKILL: tkill or tgkill)\n"); break;
        default: printf(" (Other code)\n"); break;
    }
    printf("  si_pid:   %d\n", info->si_pid);
    if (info->si_code == SI_QUEUE) {
        printf("  si_value.sival_int: %d\n", info->si_value.sival_int);
    }
}

// 线程函数
void* worker_thread(void *arg) {
    sigset_t set;
    int sig;
    siginfo_t info;

    printf("Worker thread started. TID: %ld, PID (TGID): %d\n", syscall(SYS_gettid), getpid());

    // 为这个线程设置信号掩码,只等待 SIGUSR1
    sigemptyset(&set);
    sigaddset(&set, SIGUSR1);

    // 阻塞 SIGUSR1,准备用 sigwaitinfo 接收
    if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
        perror("sigprocmask (worker)");
        return NULL;
    }

    while (atomic_load(&signal_count) < 3) { // 等待接收3次信号
        printf("Worker thread (TID %ld) waiting for SIGUSR1...\n", syscall(SYS_gettid));
        sig = sigwaitinfo(&set, &info); // 同步等待 SIGUSR1
        if (sig == -1) {
            perror("sigwaitinfo (worker)");
            break;
        }
        // sigwaitinfo 成功返回后,信号处理函数 signal_handler 会被调用
        // 因为我们阻塞了信号,sigwaitinfo 会接收它,但处理函数仍然会执行
        // 这里我们主要依靠 signal_handler 中的计数
    }
    printf("Worker thread (TID %ld) finished.\n", syscall(SYS_gettid));
    return NULL;
}


int main() {
    pthread_t thread;
    pid_t my_tgid, my_tid, worker_tid;
    struct sigaction sa;
    siginfo_t si_to_send;
    union sigval data_to_send = {.sival_int = 54321};

    my_tgid = getpid();
    my_tid = syscall(SYS_gettid); // 获取主线程的内核 TID
    printf("Main thread: PID (TGID): %d, TID: %ld\n", my_tgid, my_tid);

    // 1. 设置 SIGUSR1 的处理函数
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = signal_handler; // 使用 sa_sigaction 获取 info
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_SIGINFO; // 必须设置
    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // 2. 创建一个工作线程
    if (pthread_create(&thread, NULL, worker_thread, NULL) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    // 简单等待一下,让子线程启动并打印其 TID
    sleep(1);
    // 在真实的程序中,获取另一个线程的 TID 比较复杂,
    // 通常需要线程自己报告。这里我们简化处理,
    // 假设我们 somehow 知道了 worker 线程的 TID。
    // 为了演示,我们还是向主线程自己发送。
    worker_tid = my_tid; // 简化:发送给自己
    printf("\n--- Attempting to use rt_tgsigqueueinfo ---\n");
    printf("Targeting TGID: %d, TID: %ld\n", my_tgid, worker_tid);

    // 3. 准备 siginfo_t 结构体
    memset(&si_to_send, 0, sizeof(si_to_send));
    si_to_send.si_signo = SIGUSR1;
    si_to_send.si_code = SI_QUEUE; // 伪造为 sigqueue 发送
    si_to_send.si_pid = my_tgid;   // 伪造 PID
    si_to_send.si_uid = getuid();  // 伪造 UID
    si_to_send.si_value = data_to_send; // 附带数据

    printf("Sending SIGUSR1 with data %d using rt_tgsigqueueinfo()...\n", data_to_send.sival_int);
    printf("(This may fail with EPERM unless run with special privileges or on some systems)\n");

    // 4. 调用底层系统调用
    long result = syscall(SYS_rt_tgsigqueueinfo, my_tgid, worker_tid, SIGUSR1, &si_to_send);

    if (result == -1) {
        perror("rt_tgsigqueueinfo");
        printf("Error: rt_tgsigqueueinfo failed.\n");
        printf("Errno: %d\n", errno);
        if (errno == EPERM) {
            printf("Reason: EPERM - Operation not permitted (insufficient privileges or invalid si_code).\n");
        }
    } else {
        printf("rt_tgsigqueueinfo succeeded.\n");
    }

    sleep(2); // 等待信号处理

    // 5. 再发送一次,使用标准的 sigqueue (推荐方式)
    printf("\n--- Using sigqueue (Recommended) ---\n");
    printf("Sending SIGUSR1 with data %d using sigqueue()...\n", data_to_send.sival_int);
    if (sigqueue(my_tgid, SIGUSR1, data_to_send) == -1) {
        perror("sigqueue");
    }
    sleep(2);

    // 6. 等待线程结束
    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
    }

    printf("\nFinal signal count: %d\n", atomic_load(&signal_count));
    printf("Main program finished.\n");
    return 0;
}

使用 pthread_kill 的推荐示例:

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <pthread.h>
#include <stdatomic.h>

_Atomic int signal_received = 0;

void thread_signal_handler(int sig) {
    atomic_store(&signal_received, 1);
    printf("Signal %d received by thread %lu\n", sig, pthread_self());
}

void* target_thread(void *arg) {
    // 设置本线程的信号处理掩码,解除对 SIGUSR1 的阻塞
    // (假设主线程已经阻塞了它)
    sigset_t set;
    sigemptyset(&set);
    // sigaddset(&set, SIGUSR1); // 如果需要在这里处理
    pthread_sigmask(SIG_UNBLOCK, &set, NULL);

    printf("Target thread (pthread_t: %lu) running...\n", pthread_self());
    // 简单循环等待信号
    while(!atomic_load(&signal_received)) {
        sleep(1);
    }
    printf("Target thread received signal and is exiting.\n");
    return NULL;
}

int main() {
    pthread_t thread;
    struct sigaction sa;

    // 设置信号处理函数
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = thread_signal_handler;
    sigemptyset(&sa.sa_mask);
    if (sigaction(SIGUSR1, &sa, NULL) == -1) {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }

    // 阻塞 SIGUSR1 在主线程
    sigset_t block_set;
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGUSR1);
    sigprocmask(SIG_BLOCK, &block_set, NULL);

    if (pthread_create(&thread, NULL, target_thread, NULL) != 0) {
        perror("pthread_create");
        exit(EXIT_FAILURE);
    }

    sleep(1); // 让子线程启动

    printf("Main thread sending SIGUSR1 to target thread using pthread_kill()...\n");
    if (pthread_kill(thread, SIGUSR1) != 0) { // 推荐方式
        perror("pthread_kill");
    }

    if (pthread_join(thread, NULL) != 0) {
        perror("pthread_join");
    }

    printf("Main thread finished.\n");
    return 0;
}

编译和运行:

# 假设代码保存在 rt_tgsigqueueinfo_example.c 和 pthread_kill_example.c 中
# 需要链接 pthread 库
gcc -o rt_tgsigqueueinfo_example rt_tgsigqueueinfo_example.c -lpthread
gcc -o pthread_kill_example pthread_kill_example.c -lpthread

# 运行示例 (rt_tgsigqueueinfo 可能会因权限失败)
./rt_tgsigqueueinfo_example

# 运行推荐示例 (pthread_kill)
./pthread_kill_example

总结:
对于 Linux 编程新手,处理多线程信号时,请优先学习和使用 pthread_kill()rt_tgsigqueueinfo 是一个底层、强大的工具,但使用不当会有安全风险且复杂,通常由系统库内部或高级系统编程使用。

rt_tgsigqueueinfo系统调用及示例-CSDN博客

此条目发表在linux文章分类目录。将固定链接加入收藏夹。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注