发送 `struct siginfo.si_int` 是否需要使用 SI_QUEUE 从内核发送实时信号?
Is sending real-time signal from kernel with SI_QUEUE is required for sending `struct siginfo.si_int`?
简短的问题
- 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
应该是检查 siginfo
的 si_int
/si_ptr
.
的标志
完整描述
- 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
.
这是其中的一些
- Sending signal from kernel to user space
- Sending realtime signal from a kernel module to user space fails
- How to send signal from kernel to user space
- triggering user space with kernel
在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_QUEUE
或 SI_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+5
、si_errno == 0
和 si_code == -1 == SI_QUEUE
是为所有信号定义的。
其余的字段实际上在各种联合中,这意味着我们可以访问的字段子集取决于si_code
字段(根据man 2 sigaction)。
当 si_code == SI_QUEUE
时,我们有 si_pid
(执行 sigqueue() 的进程的 pid,如果来自内核则为 0)、si_int == si_value.sival_int
和 si_ptr == si_value.sival_ptr
。其余字段本质上是这些字段的并集,因此通过访问它们,我们只是对内容进行类型双关,得到垃圾。
当si_code == SI_KERNEL
时,用户空间不知道填充了哪个联合。也就是说,我们不知道 si_pid
和 si_int
或 si_ptr
是否有效,或者内核是否打算让我们检查 si_addr
(类似于 SIGBUS)或某些其他领域。
这意味着为了让用户空间正确理解内核发送的包含 si_int
或 si_ptr
中相关数据的信号,合乎逻辑且最不意外的选项是 si_code == SI_QUEUE
和 si_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_signo
、si_errno
,以及 si_code
。 (尽管 si_errno
确实是为 errno 代码保留的。)
现有用户空间代码——使用man 2 sigaction
的指导——知道仅在si_code == SI_QUEUE
时检查si_ptr
/si_int
。因此,内核使用 si_pid == 0
和 si_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 == 0
和 si_code == SI_QUEUE
。 C 库没有理由消耗或丢弃此类信号。而且,这种和普通用户空间排队信号之间的唯一区别是 si_pid
为零(这不是有效的进程 ID)。
在这一点上,我们可以声称所述问题的答案是 “好吧,不,不是真的;但是你想使用 SI_QUEUE 所以 C 库 and/or 用户空间进程不会混淆。不过,这不是权威答案,只是个人意见。
简短的问题
- 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
应该是检查 siginfo
的 si_int
/si_ptr
.
完整描述
- 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
.
这是其中的一些
- Sending signal from kernel to user space
- Sending realtime signal from a kernel module to user space fails
- How to send signal from kernel to user space
- triggering user space with kernel
在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_QUEUE
或 SI_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+5
、si_errno == 0
和 si_code == -1 == SI_QUEUE
是为所有信号定义的。
其余的字段实际上在各种联合中,这意味着我们可以访问的字段子集取决于si_code
字段(根据man 2 sigaction)。
当 si_code == SI_QUEUE
时,我们有 si_pid
(执行 sigqueue() 的进程的 pid,如果来自内核则为 0)、si_int == si_value.sival_int
和 si_ptr == si_value.sival_ptr
。其余字段本质上是这些字段的并集,因此通过访问它们,我们只是对内容进行类型双关,得到垃圾。
当si_code == SI_KERNEL
时,用户空间不知道填充了哪个联合。也就是说,我们不知道 si_pid
和 si_int
或 si_ptr
是否有效,或者内核是否打算让我们检查 si_addr
(类似于 SIGBUS)或某些其他领域。
这意味着为了让用户空间正确理解内核发送的包含 si_int
或 si_ptr
中相关数据的信号,合乎逻辑且最不意外的选项是 si_code == SI_QUEUE
和 si_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_signo
、si_errno
,以及 si_code
。 (尽管 si_errno
确实是为 errno 代码保留的。)
现有用户空间代码——使用man 2 sigaction
的指导——知道仅在si_code == SI_QUEUE
时检查si_ptr
/si_int
。因此,内核使用 si_pid == 0
和 si_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 == 0
和 si_code == SI_QUEUE
。 C 库没有理由消耗或丢弃此类信号。而且,这种和普通用户空间排队信号之间的唯一区别是 si_pid
为零(这不是有效的进程 ID)。
在这一点上,我们可以声称所述问题的答案是 “好吧,不,不是真的;但是你想使用 SI_QUEUE 所以 C 库 and/or 用户空间进程不会混淆。不过,这不是权威答案,只是个人意见。