我们来深入学习 rt_sigqueueinfo
系统调用,在 Linux 中,进程间通信(IPC)有多种方式,信号(Signal)是其中一种轻量级的通知机制。通常,我们使用 kill()
来发送一个简单的信号,或者使用 sigqueue()
来发送一个信号并附带一小段数据(一个整数或指针)。
1. 函数介绍
在 Linux 中,进程间通信(IPC)有多种方式,信号(Signal)是其中一种轻量级的通知机制。通常,我们使用 kill()
来发送一个简单的信号,或者使用 sigqueue()
来发送一个信号并附带一小段数据(一个整数或指针)。
rt_sigqueueinfo
是一个更底层、更强大但也更危险的系统调用。它允许你向另一个进程发送一个信号,并且可以完全自定义随信号一起传递的 siginfo_t
结构体中的所有信息。这包括发送者的 PID、信号产生的原因代码(si_code
)、以及附带的数据等。
为什么危险?
因为它强大的自定义能力意味着发送者可以伪造信号的来源和原因。例如,一个普通用户进程可以伪造自己是内核(si_code
为 SI_KERNEL
)或其他进程发送的信号。因此,这个系统调用通常受到严格的权限检查,普通应用程序一般不应该直接使用它。
什么时候会用到?
主要是在实现更高级别的信号发送函数(如 sigqueue
)时,由 C 库(glibc)内部调用,或者在一些非常特殊的、需要精确控制信号信息的系统级编程中。
对于 Linux 编程小白:你更可能使用 sigqueue()
函数,它更安全、更易用,并且能满足绝大多数需要发送带数据信号的场景。
2. 函数原型
// 这是底层系统调用,直接在用户空间调用比较复杂且需要特殊权限
#include <sys/syscall.h> // 包含系统调用号
#include <unistd.h> // 包含 syscall 函数
#include <signal.h> // 包含 siginfo_t 定义
long syscall(SYS_rt_sigqueueinfo, pid_t pid, int sig, siginfo_t *uinfo);
// 注意:用户空间标准 C 库通常不直接提供 rt_sigqueueinfo 的包装函数
用户空间更常用、更安全的替代函数:
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
3. 功能
向指定进程 ID (pid
) 发送指定信号 (sig
),并允许发送者完全指定 siginfo_t
结构体 (uinfo
) 中包含的详细信息。
4. 参数
pid
:pid_t
类型。- 目标进程的进程 ID (PID)。信号将被发送给这个进程。
sig
:int
类型。- 要发送的信号编号,例如
SIGUSR1
,SIGRTMIN
等。注意,不能发送SIGKILL
和SIGSTOP
。
uinfo
:siginfo_t *
类型。- 一个指向
siginfo_t
结构体的指针。这个结构体包含了你想随信号一起传递给目标进程的所有详细信息。调用者需要自己填充这个结构体的各个字段。
5. 返回值
- 成功: 返回 0。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因:EAGAIN
: (对于实时信号) 已达到接收者排队信号的最大数量限制 (RLIMIT_SIGPENDING
)。EPERM
: 调用者没有权限发送信号给目标进程(例如,普通用户不能向 root 进程发送任意伪造的信号)。EINVAL
:sig
是无效的信号号,或者uinfo
中的si_code
是无效的或不允许由用户设置的。ESRCH
: 找不到pid
指定的进程或进程组。
6. 相似函数或关联函数
sigqueue
: 用户空间更安全、更常用的发送信号和数据的方法。它只允许设置siginfo_t
中的si_value
字段,其他字段由内核自动填充。kill
: 发送一个不带附加数据的信号。siginfo_t
: 包含信号详细信息的结构体。主要字段包括:si_signo
: 信号编号(内核会设置)。si_errno
: 如果非零,表示伴随信号的错误代码(内核会设置)。si_code
: 信号产生的原因代码(例如SI_USER
,SI_QUEUE
,SI_TIMER
等)。这是rt_sigqueueinfo
允许用户自定义的关键字段,但也因此危险。si_pid
: 发送信号的进程 ID(通常由内核设置,但rt_sigqueueinfo
可能允许伪造)。si_uid
: 发送信号的用户 ID(通常由内核设置)。si_value
: 伴随信号的用户数据(union sigval
,包含sival_int
或sival_ptr
)。- … 还有其他针对特定信号类型的字段。
7. 示例代码
由于直接使用 rt_sigqueueinfo
需要特殊权限且容易误用,下面的示例将演示如何通过 syscall
调用它,并说明其风险。同时,也会提供一个使用标准 sigqueue
的对比示例。
警告:直接使用 rt_sigqueueinfo
可能会因为权限问题而失败,尤其是在没有 root 权限的情况下。
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/syscall.h> // 包含 syscall 和 SYS_rt_sigqueueinfo
#include <errno.h>
#include <sys/types.h> // 包含 pid_t
// 用于接收信号的处理函数 (使用 SA_SIGINFO 获取详细信息)
void signal_handler(int sig, siginfo_t *info, void *context) {
printf("Received signal %d\n", 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_KERNEL: printf(" (SI_KERNEL: sent by kernel)\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);
printf(" si_uid: %d\n", info->si_uid);
if (info->si_code == SI_QUEUE) {
printf(" si_value.sival_int: %d\n", info->si_value.sival_int);
printf(" si_value.sival_ptr: %p\n", info->si_value.sival_ptr);
}
// 注意:这里为了演示打印了信息,但实际信号处理函数应只调用异步信号安全函数
}
int main() {
pid_t my_pid = getpid();
struct sigaction sa;
siginfo_t si_to_send;
union sigval data_to_send = {.sival_int = 999};
printf("My PID is: %d\n", my_pid);
// 1. 设置 SIGUSR1 的处理函数,使用 SA_SIGINFO 获取详细信息
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = signal_handler; // 注意是 sa_sigaction
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO; // 必须设置此标志
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
// --- 方法 1: 使用标准的 sigqueue (推荐) ---
printf("\n--- Using sigqueue (Recommended) ---\n");
printf("Sending SIGUSR1 with data %d using sigqueue()...\n", data_to_send.sival_int);
if (sigqueue(my_pid, SIGUSR1, data_to_send) == -1) {
perror("sigqueue");
// sigqueue 失败通常是因为资源限制或信号无效
}
sleep(1); // 给点时间处理信号
// --- 方法 2: 使用 rt_sigqueueinfo (不推荐,仅供演示) ---
printf("\n--- Using rt_sigqueueinfo (Not Recommended) ---\n");
// 2. 准备 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_pid; // 伪造 PID
si_to_send.si_uid = getuid(); // 伪造 UID
si_to_send.si_value = data_to_send; // 附带数据
printf("Attempting to send SIGUSR1 with forged info using rt_sigqueueinfo()...\n");
printf("(This will likely fail with EPERM unless run with special privileges)\n");
// 3. 调用底层系统调用
long result = syscall(SYS_rt_sigqueueinfo, my_pid, SIGUSR1, &si_to_send);
if (result == -1) {
perror("rt_sigqueueinfo");
printf("Error: rt_sigqueueinfo failed. This is expected for unprivileged processes.\n");
printf("Errno: %d\n", errno);
if (errno == EPERM) {
printf("Reason: EPERM - Operation not permitted (insufficient privileges to forge signal info).\n");
}
} else {
printf("rt_sigqueueinfo succeeded (unexpected for unprivileged user).\n");
}
sleep(1); // 给点时间处理可能的信号
// --- 方法 3: 使用 rt_sigqueueinfo 伪造为内核发送 (非常不推荐) ---
printf("\n--- Forging signal as from Kernel (Highly Not Recommended) ---\n");
si_to_send.si_code = SI_KERNEL; // 尝试伪造为内核发送
printf("Attempting to forge signal as sent by the KERNEL...\n");
result = syscall(SYS_rt_sigqueueinfo, my_pid, SIGUSR1, &si_to_send);
if (result == -1) {
perror("rt_sigqueueinfo (forged as kernel)");
printf("Error: Forging kernel signal failed (as expected).\n");
} else {
printf("rt_sigqueueinfo (forged as kernel) succeeded (highly unexpected!).\n");
}
sleep(1);
printf("\nProgram finished.\n");
return 0;
}
使用 sigqueue
的简单对比示例 (推荐方式):
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
void signal_handler(int sig, siginfo_t *info, void *context) {
if (info->si_code == SI_QUEUE) {
printf("Received signal %d with value: %d\n", sig, info->si_value.sival_int);
} else {
printf("Received signal %d (not from sigqueue)\n", sig);
}
}
int main() {
pid_t pid;
struct sigaction sa;
union sigval value1 = {.sival_int = 100};
union sigval value2 = {.sival_int = 200};
// 设置信号处理函数
memset(&sa, 0, sizeof(sa));
sa.sa_sigaction = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_SIGINFO;
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// Child: 发送信号给父进程
sleep(1);
printf("Child: Sending SIGUSR1 with value %d\n", value1.sival_int);
if (sigqueue(getppid(), SIGUSR1, value1) == -1) {
perror("sigqueue 1");
}
sleep(1);
printf("Child: Sending SIGUSR1 with value %d\n", value2.sival_int);
if (sigqueue(getppid(), SIGUSR1, value2) == -1) {
perror("sigqueue 2");
}
exit(EXIT_SUCCESS);
} else {
// Parent: 等待信号
printf("Parent: Waiting for signals...\n");
sleep(5); // 等待子进程发送信号并处理
wait(NULL); // 等待子进程结束
printf("Parent: Finished.\n");
}
return 0;
}
编译和运行:
# 假设代码保存在 rt_sigqueueinfo_example.c 和 sigqueue_example.c 中
gcc -o rt_sigqueueinfo_example rt_sigqueueinfo_example.c
gcc -o sigqueue_example sigqueue_example.c
# 运行第一个示例 (会展示 rt_sigqueueinfo 的权限限制)
./rt_sigqueueinfo_example
# 运行第二个示例 (推荐的 sigqueue 用法)
./sigqueue_example
总结:
对于 Linux 编程新手,请优先学习和使用 sigqueue()
。rt_sigqueueinfo
是一个底层工具,功能强大但使用不当有安全风险,通常由系统库内部使用。