发送 `struct siginfo.si_int` 是否需要使用 SI_QUEUE 从内核发送实时信号?

Is sending real-time signal from kernel with SI_QUEUE is required for sending `struct siginfo.si_int`?

简短的问题

根据signal(7)

  1. If the signal is sent using sigqueue(2), an accompanying value (either an integer or a pointer) can be sent with the signal.

struct siginfo 有一个字段 si_int 用于携带数据。

typedef struct siginfo {
    int si_signo;
    int si_errno;
    int si_code;
    int si_int; //  This is actually a macro specifying a union value in struct siginfo

当使用 send_sig_info() 内核模块 发送信号时,上面的联机帮助页描述是否适用?或者它只是在用户空间中的程序调用系统调用 sigqueue() 时应用? 我已经从内核的 send_sig_info() 中找到任何与 SI_QUEUE 相关的内容。试图研究 glibc 但我不知道如何阅读 this..

2020/11/24更新:

有道理。

既然内核代码是为用户空间服务的,那么siginfo携带的数据应该是为用户空间程序服务的。其中 si_code == SI_QUEUE 应该是检查 siginfosi_int/si_ptr.

的标志

完整描述

根据signal(7)

  1. If the signal is sent using sigqueue(2), an accompanying value (either an integer or a pointer) can be sent with the signal.

从内核发送信号时是否适用此规则?因为我发现很多内核模块示例都在使用 SI_QUEUE.

这是其中的一些

How to send signal from kernel to user space中,有一个有趣的tricky评论。

// This is bit of a trickery: SI_QUEUE is normally used by sigqueue from user space, and kernel space should use SI_KERNEL. But if SI_KERNEL is used the real_time data is not delivered to the user space signal handler function.

此评论明确说明设置 struct siginfo.si_code 是否必须设置为 SI_QUEUE 而不是 SI_KERNEL

但是我在 Ubuntu 18.04(内核 5.4.0-53)上进行了测试。使用 SI_QUEUESI_KERNEL 都可以从内核获得 si_code

深入内核代码

试图追踪内核 src 到 __send_signal()

在 L1044 处,它通过宏参数信息切换

/* These can be the second arg to send_sig_info/send_group_sig_info.  */
#define SEND_SIG_NOINFO ((struct siginfo *) 0)
#define SEND_SIG_PRIV   ((struct siginfo *) 1)
#define SEND_SIG_FORCED ((struct siginfo *) 2)

我不确定上面的宏如何转换 0、1、2,但我假设它进入默认情况下,在我的用例中复制完整的 struct siginfo info

switch ((unsigned long) info) {  // where info is the struct siginfo parameter
case (unsigned long) SEND_SIG_NOINFO:
    q->info.si_signo = sig;
    q->info.si_errno = 0;
    q->info.si_code = SI_USER;
    q->info.si_pid = task_tgid_nr_ns(current,
                    task_active_pid_ns(t));
    q->info.si_uid = from_kuid_munged(current_user_ns(), current_uid());
    break;
case (unsigned long) SEND_SIG_PRIV:
    q->info.si_signo = sig;
    q->info.si_errno = 0;
    q->info.si_code = SI_KERNEL;
    q->info.si_pid = 0;
    q->info.si_uid = 0;
    break;
default:
    copy_siginfo(&q->info, info);
    if (from_ancestor_ns)
        q->info.si_pid = 0;
    break;
}

我可能遗漏了一些东西,但我想知道是否有任何文档或代码说明实时信号的行为。

这不是答案,而是扩展评论,因为试验有时会产生见解。从技术上讲,这只是一个意见,但有详细的意见基础。所以,“评论​​”最合适。

这是一个捕获 SIGUSR1、SIGUSR2 和所有 POSIX 实时信号(SIGRTMIN+0 到 SIGRTMAX-0,含)的简单程序; catcher.c:

#define _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>

static const char *signal_name(const int signum)
{
    static char name_buffer[16];

    switch (signum) {
    case SIGINT:  return "SIGINT";
    case SIGHUP:  return "SIGHUP";
    case SIGTERM: return "SIGTERM";
    case SIGUSR1: return "SIGUSR1";
    case SIGUSR2: return "SIGUSR2";
    }

    if (signum >= SIGRTMIN && signum <= SIGRTMAX) {
        snprintf(name_buffer, sizeof name_buffer, "SIGRTMIN+%d", signum-SIGRTMIN);
        return (const char *)name_buffer;
    }

    snprintf(name_buffer, sizeof name_buffer, "[%d]", signum);
    return (const char *)name_buffer;
}

int main(void)
{
    const int pid = (int)getpid();
    siginfo_t info;
    sigset_t  mask;
    int       i;

    sigemptyset(&mask);

    /* INT, HUP, and TERM for termination. */
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGHUP);
    sigaddset(&mask, SIGTERM);

    /* USR1 and USR2 signals, for comparison to realtime signals. */
    sigaddset(&mask, SIGUSR1);
    sigaddset(&mask, SIGUSR2);

    /* Realtime signals. */
    for (i = SIGRTMIN; i <= SIGRTMAX; i++)
        sigaddset(&mask, i);

    if (sigprocmask(SIG_BLOCK, &mask, NULL) == -1) {
        fprintf(stderr, "Cannot block signals: %s.\n", strerror(errno));
        return EXIT_FAILURE;
    }

    printf("Process %d is waiting for realtime signals (%d to %d, inclusive).\n", pid, SIGRTMIN, SIGRTMAX);
    printf("        (sigwaitinfo() is at %p, and is called from %p.)\n", (void *)sigwaitinfo, (void *)&&callsite);
    fflush(stdout);

    while (1) {
        /* Clear the signal info structure, so that we can detect nonzero data reliably. */
        memset(&info, 0, sizeof info);

callsite:
        i = sigwaitinfo(&mask, &info);
        if (i == SIGINT || i == SIGTERM || i == SIGHUP) {
           fprintf(stderr, "%d: Received %s. Exiting.\n", pid, signal_name(i));
            return EXIT_SUCCESS;
        } else
        if (i == -1) {
            fprintf(stderr, "%d: sigwaitinfo() failed: %s.\n", pid, strerror(errno));
            return EXIT_FAILURE;
        }

        printf("%d: Received %s:\n", pid, signal_name(i));

        printf("    si_signo:    %d\n", info.si_signo);
        printf("    si_errno:    %d\n", info.si_errno);
        printf("    si_code:     %d\n", info.si_code);
        printf("    si_pid:      %d\n", (int)info.si_pid);
        printf("    si_uid:      %d\n", (int)info.si_uid);
        printf("    si_status:   %d\n", info.si_status);
        printf("    si_utime:    %.3f\n", (double)info.si_utime / (double)CLOCKS_PER_SEC);
        printf("    si_stime:    %.3f\n", (double)info.si_stime / (double)CLOCKS_PER_SEC);
        printf("    si_value.sival_int: %d\n", info.si_value.sival_int);
        printf("    si_value.sival_ptr: %p\n", info.si_value.sival_ptr);
        printf("    si_int:      %d\n", info.si_int);
        printf("    si_ptr:      %p\n", info.si_ptr);
        printf("    si_overrun:  %d\n", info.si_overrun);
        printf("    si_timerid:  %d\n", info.si_timerid);
        printf("    si_addr:     %p\n", info.si_addr);
        printf("    si_band:     %ld (0x%lx)\n", info.si_band, (unsigned long)(info.si_band));
        printf("    si_fd:       %d\n", info.si_fd);
        printf("    si_addr_lsb: %d\n", (int)info.si_addr_lsb);
        printf("    si_lower:    %p\n", info.si_lower);
        printf("    si_upper:    %p\n", info.si_upper);
    }
}

使用例如编译它gcc -Wall -Wextra -O2 catcher.c -o catcher 和 运行 它在终端 window (./catcher) 中。 (它不需要命令行参数。)

它告诉你它的进程 ID,然后 运行s 直到你按下 Ctrl+C,或者给它发送一个INT、HUP 或 TERM 信号。

为了示例,我假设它是 运行ning 作为稍后的过程 12345

为了将信号排队到另一个用户空间进程,我们需要第二个程序,queue.c:

#define _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <errno.h>

static inline int at_end(const char *s)
{
    if (!s)
        return 0; /* NULL pointer is not at end of string. */

    /* Skip whitespace. */
    while (isspace((unsigned char)(*s)))
        s++;

    /* Return true/1 if at end of string, false/0 otherwise. */
    return *s == '[=11=]';
}

static int parse_pid(const char *src, pid_t *to)
{
    long        s;
    const char *end;

    if (!src || at_end(src))
        return -1;

    errno = 0;
    end = src;
    s = strtol(src, (char **)&end, 0);
    if (!errno && at_end(end) && s) {
        const pid_t p = s;
        if ((long)p == s) {
            if (to)
                *to = p;
            return 0;
        }
    }

    return -1;
}

static int parse_signum(const char *src, int *to)
{
    const unsigned int  rtmax = SIGRTMAX - SIGRTMIN;
    int                 signum = 0;
    unsigned int        u;
    char                dummy;

    if (!src || !*src)
        return -1;

    /* Skip leading whitespace. */
    while (isspace((unsigned char)(*src)))
        src++;

    /* Skip optional SIG prefix. */
    if (src[0] == 'S' && src[1] == 'I' && src[2] == 'G')
        src += 3;

    do {
        if (!strcmp(src, "USR1")) {
            signum = SIGUSR1;
            break;
        }
        if (!strcmp(src, "USR2")) {
            signum = SIGUSR2;
            break;
        }
        if (!strcmp(src, "RTMIN")) {
            signum = SIGRTMIN;
            break;
        }
        if (!strcmp(src, "RTMAX")) {
            signum = SIGRTMAX;
            break;
        }
        if (sscanf(src, "RTMIN+%u %c", &u, &dummy) == 1 && u <= rtmax) {
            signum = SIGRTMIN + u;
            break;
        }
        if (sscanf(src, "RTMAX-%u %c", &u, &dummy) == 1 && u <= rtmax) {
            signum = SIGRTMAX - u;
            break;
        }
        if (sscanf(src, "%u %c", &u, &dummy) == 1 && u > 0 && (int)u <= SIGRTMAX) {
            signum = u;
            break;
        }

        return -1;
    } while (0);
    if (to)
        *to = signum;
    return 0;
}

static int parse_sigval(const char *src, union sigval *to)
{
    unsigned long u;    /* In Linux, sizeof (unsigned long) == sizeof (void *). */
    long          s;
    int           op = 0;
    const char   *end;

    /* Skip leading whitespace. */
    if (src)
        while (isspace((unsigned char)(*src)))
            src++;

    /* Nothing to parse? */
    if (!src || !*src)
        return -1;

    /* ! or ~ unary operator? */
    if (*src == '!' || *src == '~')
        op = *(src++);

    /* Try parsing as an unsigned long first. */
    errno = 0;
    end = src;
    u = strtoul(src, (char **)&end, 0);
    if (!errno && at_end(end)) {
        if (op == '!')
            u = !u;
        else
        if (op == '~')
            u = ~u;
        if (to)
            to->sival_ptr = (void *)u;
        return 0;
    }

    /* Try parsing as a signed long. */
    errno = 0;
    end = src;
    s = strtol(src, (char **)&end, 0);
    if (!errno && at_end(end)) {
        if (op == '!')
            s = !s;
        else
        if (op == '~')
            s = ~s;
        if (to)
            to->sival_ptr = (void *)s;
        return 0;
    }

    return -1;
}

int main(int argc, char *argv[])
{
    const int     pid = (int)getpid();
    pid_t         target = 0;
    int           signum = -1;
    union sigval  value;

    if (argc != 4 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        const char *argv0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv0);
        fprintf(stderr, "       %s PID SIGNAL VALUE\n", argv0);
        fprintf(stderr, "\n");
        fprintf(stderr, "Queues signal SIGNAL to process PID, with value VALUE.\n");
        fprintf(stderr, "You can use negative PIDs for process group -PID.\n");
        fprintf(stderr, "\n");
        return (argc <= 2) ? EXIT_SUCCESS : EXIT_FAILURE;
    }

    if (parse_pid(argv[1], &target) || !target) {
        fprintf(stderr, "%s: Invalid process ID.\n", argv[1]);
        return EXIT_FAILURE;
    }

    if (parse_signum(argv[2], &signum)) {
        fprintf(stderr, "%s: Invalid signal name or number.\n", argv[2]);
        return EXIT_FAILURE;
    }

    if (parse_sigval(argv[3], &value)) {
        fprintf(stderr, "%s: Invalid value.\n", argv[3]);
        return EXIT_FAILURE;
    }

callsite:
    if (sigqueue(target, signum, value) == -1) {
        fprintf(stderr, "Process %d failed to send signal %d with value %p to process %d: %s.\n", pid, signum, value.sival_ptr, (int)target, strerror(errno));
        return EXIT_FAILURE;
    } else {
        printf("Process %d sent signal %d with value %p to process %d.\n", pid, signum, value.sival_ptr, (int)target);
        printf("        (sigqueue() is at %p, calling sigqueue() at %p.)\n", (void *)sigqueue, (void *)(&&callsite));
        return EXIT_SUCCESS;
    }
}

也可以使用例如编译它gcc -Wall -Wextra -O2 queue.c -o queue。它需要三个命令行参数; 运行 它不带参数(或仅带 -h 或 --help)以查看其用法。

如果捕手运行宁作为进程12345,我们可以运行例如./queue 12345 SIGRTMIN+5 0xcafedeadbeefbabe 将信号排队到捕手,并查看输出。

如果队列进程恰好是54321,我们可以期待x86-64架构上的输出如下:

    si_signo:    39
    si_errno:    0
    si_code:     -1
    si_pid:      54321
    si_uid:      1001
    si_status:   -1091585346
    si_utime:    0.000
    si_stime:    0.000
    si_value.sival_int: -1091585346
    si_value.sival_ptr: 0xcafedeadbeefbabe
    si_int:      -1091585346
    si_ptr:      0xcafedeadbeefbabe
    si_overrun:  1001
    si_timerid:  54321
    si_addr:     0x3e90000d431
    si_band:     4299262317617 (0x3e90000d431)
    si_fd:       -1091585346
    si_addr_lsb: -17730
    si_lower:    (nil)
    si_upper:    (nil)

(由于字节顺序和 long/pointer 大小差异,其他硬件架构可能略有不同。)

在这些字段中,只有 si_signo == SIGRTMIN+5si_errno == 0si_code == -1 == SI_QUEUE 是为所有信号定义的。

其余的字段实际上在各种联合中,这意味着我们可以访问的字段子集取决于si_code字段(根据man 2 sigaction)。

si_code == SI_QUEUE 时,我们有 si_pid(执行 sigqueue() 的进程的 pid,如果来自内核则为 0)、si_int == si_value.sival_intsi_ptr == si_value.sival_ptr。其余字段本质上是这些字段的并集,因此通过访问它们,我们只是对内容进行类型双关,得到垃圾。

si_code == SI_KERNEL时,用户空间不知道填充了哪个联合。也就是说,我们不知道 si_pidsi_intsi_ptr 是否有效,或者内核是否打算让我们检查 si_addr(类似于 SIGBUS)或某些其他领域。

这意味着为了让用户空间正确理解内核发送的包含 si_intsi_ptr 中相关数据的信号,合乎逻辑且最不意外的选项是 si_code == SI_QUEUEsi_pid == 0.

(的确,我确实记得在现实生活中看到过这个,但记不起我在哪里。如果我记得,我本可以回答这个问题,但因为我没有,所以必须保留作为扩展评论;仅报告观察到的行为。)

最后,如果我们查看 Linux 内核 5.9.9 的用户空间 API,我们可以在 include/uapi/asm-generic/siginfo.h 中看到 siginfo_t 的定义。请记住,这不是 C 库公开信息的方式;这就是 Linux 内核向用户空间传递信息的方式。结合可读性的定义,并忽略某些架构差异(如成员顺序),我们基本上有

typedef struct siginfo {
    union {
        struct {
           int si_signo;
           int si_errno;
           int si_code;

           union {

                struct {
                    __kernel_pid_t    _pid;
                    __kernel_uid32_t  _uid;
                } _kill;

                struct {
                    __kernel_timer_t  _tid;
                    int               _overrun;
                    sigval_t          _sigval;
                    int               _sys_private;  /* not to be passed to user */
                } _timer;

                struct {
                    __kernel_pid_t    _pid;
                    __kernel_uid32_t  _uid;
                    sigval_t          _sigval;
                } _rt;

                struct {
                    __kernel_pid_t    _pid;
                    __kernel_uid32_t  _uid;
                    int               _status;
                    __ARCH_SI_CLOCK_T _utime;
                    __ARCH_SI_CLOCK_T _stime;
                } _sigchld;

                struct {
                    void __user      *_addr;
                    int               _trapno;
                    union {
                        short           _addr_lsb;
                        struct {
                            char            _dummy_bnd[__ADDR_BND_PKEY_PAD];
                            void __user    *_lower;
                            void __user    *_upper;
                        } _addr_bnd;
                        struct {
                            char            _dummy_pkey[__ADDR_BND_PKEY_PAD];
                            __u32           _pkey;
                        } _addr_pkey;
                    };
                } _sigfault;

                struct {
                    __ARCH_SI_BAND_T  _band;
                    int               _fd;
                } _sigpoll;

                struct {
                    void __user      *_call_addr;
                    int               _syscall;
                    unsigned int      _arch;
                } _sigsys;


           } _sifields;
        };

        int _si_pad[SI_MAX_SIZE/sizeof(int)];
    };
} siginfo_t;

因此,本质上,内核只能提供 _rt_kill_timer_sigchld_sigfault 之一的字段_sigpoll_sigsys 结构——因为它们彼此互为别名——并且用户空间确定访问哪一个的唯一字段是常见的:si_signosi_errno,以及 si_code。 (尽管 si_errno 确实是为 errno 代码保留的。)

现有用户空间代码——使用man 2 sigaction的指导——知道仅在si_code == SI_QUEUE时检查si_ptr/si_int。因此,内核使用 si_pid == 0si_code == SI_QUEUE.

发出此类信号是合乎逻辑的

最后一个问题是 C 库。例如,GNU C 库在内部使用一个或两个 POSIX 实时信号(通常为 32 和 33;除其他外,同步诸如进程 uid 之类的东西,它们实际上是 Linux 中的每个线程属性,但 POSIX 中的每个进程属性)。因此,C 库可能会“消费”看起来很奇怪的信号,因为它可能会将它们视为自己的信号。 (不过通常不会,因为信号编号非常重要!)

更重要的是,特定 C 库使用的 siginfo_t 结构可能与 Linux 内核使用的结构完全不同(该库只是根据需要从临时副本中复制字段的结构)。因此,如果依赖于 Linux 内核如何提供 siginfo_t 的详细信息,而不是 siginfo_t 在实践中的使用方式,那么 C 库中的此类翻译层可能会被咬住。

在这里,对于来自内核的具有 si_int/si_ptr 有效负载的信号,最不令人惊讶的情况是 si_pid == 0si_code == SI_QUEUE。 C 库没有理由消耗或丢弃此类信号。而且,这种和普通用户空间排队信号之间的唯一区别是 si_pid 为零(这不是有效的进程 ID)。

在这一点上,我们可以声称所述问题的答案是 “好吧,不,不是真的;但是你想使用 SI_QUEUE 所以 C 库 and/or 用户空间进程不会混淆。不过,这不是权威答案,只是个人意见。