c语言 - 是否从内核发送实时信号,发送"结构siginfo.si_int"需要SI_QUEUE



简短的问题

signal(7)

  1. 如果使用sigqueue(2)发送信号,则附带值 (整数或指针)可以随信号一起发送。

结构 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()的 prgram 时应用? 我已经从内核的send_sig_info()中追踪到,但没有找到与SI_QUEUE有关的任何内容。试图查看glibc,但我不知道如何阅读这个..

2020/11/24更新:

格拉博的回答很有道理。

由于内核代码服务于用户空间,因此siginfo携带的数据应该服务于用户空间程序。其中si_code == SI_QUEUE应该是检查siginfosi_int/si_ptr的标志。

完整描述

signal(7)

  1. 如果使用sigqueue(2)发送信号,则附带值 (整数或指针)可以随信号一起发送。

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

以下是其中的一些

  • 将信号从内核发送到用户空间
  • 从内核模块向用户空间发送实时信号失败
  • 如何将信号从内核发送到用户空间
  • 使用内核触发用户空间

在如何将信号从内核发送到用户空间中,有一个有趣的棘手评论。

这有点诡计:SI_QUEUE通常由用户空间的 sigqueue 使用,内核空间应该使用 SI_KERNEL。但是,如果使用SI_KERNEL,则real_time数据不会传递到用户空间信号处理程序函数。

此注释特别指出是否必须将设置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,包括SIGRTMIN+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:    %dn", info.si_signo);
printf("    si_errno:    %dn", info.si_errno);
printf("    si_code:     %dn", info.si_code);
printf("    si_pid:      %dn", (int)info.si_pid);
printf("    si_uid:      %dn", (int)info.si_uid);
printf("    si_status:   %dn", info.si_status);
printf("    si_utime:    %.3fn", (double)info.si_utime / (double)CLOCKS_PER_SEC);
printf("    si_stime:    %.3fn", (double)info.si_stime / (double)CLOCKS_PER_SEC);
printf("    si_value.sival_int: %dn", info.si_value.sival_int);
printf("    si_value.sival_ptr: %pn", info.si_value.sival_ptr);
printf("    si_int:      %dn", info.si_int);
printf("    si_ptr:      %pn", info.si_ptr);
printf("    si_overrun:  %dn", info.si_overrun);
printf("    si_timerid:  %dn", info.si_timerid);
printf("    si_addr:     %pn", info.si_addr);
printf("    si_band:     %ld (0x%lx)n", info.si_band, (unsigned long)(info.si_band));
printf("    si_fd:       %dn", info.si_fd);
printf("    si_addr_lsb: %dn", (int)info.si_addr_lsb);
printf("    si_lower:    %pn", info.si_lower);
printf("    si_upper:    %pn", info.si_upper);
}
}

编译它,例如gcc -Wall -Wextra -O2 catcher.c -o catcher,并在终端窗口中运行它(./catcher)。 (它不需要命令行参数。

它会告诉您其进程 ID,并一直运行到您按Ctrl+C或向其发送 INT、HUP 或 TERM 信号。

为了这个例子,我假设它稍后会作为进程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 == '';
}
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 VALUEn", 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)

(由于字节顺序和长/指针大小差异,其他硬件体系结构可能略有不同。

在这些字段中,只有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_errnosi_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库和/或用户空间进程就不会混淆"。然而,这不是一个权威的答案,只是一个意见。

最新更新