我们来学习一下 Linux 中与实时信号(Real-time signals)相关的系统调用,通常以 rt_sig*
命名。这些系统调用提供了对信号处理更强大和精确的控制,特别是处理实时信号(信号编号从 SIGRTMIN
到 SIGRTMAX
)。
1. 函数介绍
rt_sig*
系列系统调用是 Linux 内核提供的一组用于处理信号的底层接口。它们是标准信号处理函数(如 signal
, sigaction
, kill
, sigprocmask
等)在内核层面的实现,并且扩展了对实时信号的支持。
与传统的非实时信号相比,实时信号具有以下特点:
- 排队(Queuing): 多个相同的实时信号可以排队,确保每个信号都被传递。而非实时信号可能会丢失(合并)。
- 伴随数据(伴随数据(伴随数据(伴随数据(Associated Data)))): 发送实时信号时,可以附带一个整数值(
sigval
)或一个联合体(union),接收方可以获取这个数据。 - 优先级(Priority): 实时信号的编号范围是
SIGRTMIN
到SIGRTMAX
,编号越小优先级越高。
rt_sig*
系列调用包括:
rt_sigaction
: 类似于sigaction
,用于检查或修改信号的处理方式(处理函数、掩码、标志)。rt_sigprocmask
: 类似于sigprocmask
,用于检查或修改当前进程的信号屏蔽字(阻塞哪些信号)。rt_sigpending
: 类似于sigpending
,用于检查当前有哪些被阻塞且待处理的信号。rt_sigtimedwait
: 类似于sigsuspend
,但允许指定超时时间,并可以获取信号附带的数据。rt_sigqueueinfo
: 用于向进程发送信号并附带siginfo_t
结构的数据(比sigqueue
更底层)。rt_sigsuspend
: 类似于sigsuspend
,临时替换信号掩码并挂起进程等待信号。
这些系统调用通常由标准 C 库(glibc)封装成更易用的函数(如 sigaction
, sigqueue
等)供应用程序调用。直接使用这些底层系统调用比较复杂,主要用于库实现或特殊需求。
2. 函数原型
这些是内核系统调用的原型,用户空间程序通常不直接调用它们,而是通过 glibc 提供的封装函数。
// 注意:这些是内核系统调用的签名,通常在用户空间不可见或需要通过 syscall() 调用
// 并且参数类型和含义可能与 glibc 封装函数略有不同。
long sys_rt_sigaction(int sig, const struct sigaction *act,
struct sigaction *oact, size_t sigsetsize);
long sys_rt_sigprocmask(int how, sigset_t *nset,
sigset_t *oset, size_t sigsetsize);
long sys_rt_sigpending(sigset_t *uset, size_t sigsetsize);
long sys_rt_sigtimedwait(const sigset_t *uthese, siginfo_t *uinfo,
const struct timespec *uts, size_t sigsetsize);
long sys_rt_sigqueueinfo(pid_t pid, int sig, siginfo_t *uinfo);
long sys_rt_sigsuspend(sigset_t *unewset, size_t sigsetsize);
sigsetsize
: 这是sigset_t
类型的大小(以字节为单位),内核用它来确保用户空间和内核空间对信号集大小的理解一致。
3. 功能
rt_sigaction
: 检查或修改特定信号的处理动作(disposition),包括处理函数、信号屏蔽字、标志(如SA_RESTART
,SA_SIGINFO
等)。rt_sigprocmask
: 检查或修改当前进程的信号屏蔽字(signal mask),控制哪些信号被阻塞(暂时不递送)。rt_sigpending
: 获取当前进程中所有被阻塞且已产生但尚未递送的信号集合。rt_sigtimedwait
: 原子地临时解除对指定信号集的阻塞,并挂起进程等待其中一个信号到来,或者等待超时。可以获取信号的详细信息(siginfo_t
)。rt_sigqueueinfo
: 向指定进程发送一个信号,并允许发送者指定siginfo_t
结构中的详细信息(比kill
或sigqueue
更灵活但也更危险,因为它可以伪造信号来源)。rt_sigsuspend
: 用指定的信号集替换当前进程的信号屏蔽字,并挂起进程直到捕获一个信号。返回时恢复原来的信号屏蔽字。
4. 参数
参数因具体调用而异,但通常涉及:
sig
:int
类型。信号编号。act
/nset
/uthese
/unewset
: 指向sigaction
结构体、sigset_t
信号集、siginfo_t
结构体或timespec
结构体的指针,包含要设置或使用的数据。oact
/oset
/uinfo
/uts
: 指向用于存放旧值或获取信息的缓冲区的指针。pid
:pid_t
类型。目标进程的进程 ID。sigsetsize
:size_t
类型。sigset_t
的大小(字节),用于内核验证。how
:int
类型。指定rt_sigprocmask
的操作类型(SIG_BLOCK
,SIG_UNBLOCK
,SIG_SETMASK
)。
5. 返回值
- 成功:
rt_sigaction
,rt_sigprocmask
,rt_sigpending
,rt_sigsuspend
: 通常返回 0。rt_sigtimedwait
: 返回被捕获的信号编号。
- 失败: 返回 -1,并设置
errno
。
6. 相似函数或关联函数
- 用户空间封装函数:
sigaction
(封装rt_sigaction
)sigprocmask
(封装rt_sigprocmask
)sigpending
(封装rt_sigpending
)sigtimedwait
(封装rt_sigtimedwait
)sigwaitinfo
(封装rt_sigtimedwait
)sigqueue
(封装rt_sigqueueinfo
或类似机制)sigsuspend
(封装rt_sigsuspend
)
- 信号相关类型:
sigset_t
: 信号集类型。struct sigaction
: 定义信号处理动作。siginfo_t
: 包含信号产生的详细信息。struct timespec
: 用于指定时间。
- 实时信号常量:
SIGRTMIN
,SIGRTMAX
: 定义实时信号的编号范围。
- 相关头文件:
<signal.h>
: 包含信号处理函数和常量。<sys/ucontext.h>
: 有时与信号上下文相关。
7. 示例代码
下面的示例演示如何使用用户空间的封装函数(它们内部调用 rt_sig*
系统调用)来处理实时信号,并传递附加数据。
#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
// 实时信号处理函数,使用 SA_SIGINFO 标志
// sa_sigaction 函数指针接收额外的 siginfo_t 和 void* 参数
void rt_signal_handler(int sig, siginfo_t *info, void *context) {
printf("Received real-time signal %d\n", sig);
// 检查信号来源和附加数据
if (info->si_code == SI_QUEUE) {
// 信号是通过 sigqueue 发送的
printf(" Signal sent by sigqueue()\n");
printf(" Sender PID: %d\n", info->si_pid);
// 打印附加的整数值
printf(" Attached integer value: %d\n", info->si_value.sival_int);
// 或者如果是通过指针传递的 (较少见)
// printf(" Attached pointer value: %p\n", info->si_value.sival_ptr);
} else if (info->si_code == SI_USER) {
// 信号是通过 kill() 发送的
printf(" Signal sent by kill()\n");
printf(" Sender PID: %d\n", info->si_pid);
} else {
printf(" Signal sent by other means (code: %d)\n", info->si_code);
}
}
int main() {
pid_t pid;
struct sigaction sa;
sigset_t block_mask;
union sigval value_to_send;
int rt_sig_num;
// 1. 选择一个实时信号 (使用编号最小的,优先级最高)
rt_sig_num = SIGRTMIN;
if (rt_sig_num > SIGRTMAX) {
fprintf(stderr, "No real-time signals available.\n");
exit(EXIT_FAILURE);
}
printf("Using real-time signal number: %d\n", rt_sig_num);
// 2. 设置实时信号处理函数
memset(&sa, 0, sizeof(sa));
// 使用 sa_sigaction 而不是 sa_handler 来获取 siginfo_t
sa.sa_sigaction = rt_signal_handler;
sigemptyset(&sa.sa_mask); // 不在处理函数执行时阻塞其他信号
sa.sa_flags = SA_SIGINFO; // 必须设置此标志才能获取 siginfo_t
if (sigaction(rt_sig_num, &sa, NULL) == -1) {
perror("sigaction");
exit(EXIT_FAILURE);
}
printf("Set up handler for signal %d\n", rt_sig_num);
// 3. 创建子进程
pid = fork();
if (pid == -1) {
perror("fork");
exit(EXIT_FAILURE);
}
if (pid == 0) {
// --- 子进程 ---
printf("Child process (PID: %d) running...\n", getpid());
// 4. 子进程阻塞一段时间,等待父进程发送信号
printf("Child sleeping for 2 seconds...\n");
sleep(2);
// 5. 子进程向父进程发送带数据的实时信号
value_to_send.sival_int = 42; // 要发送的整数值
printf("Child sending real-time signal %d with value %d to parent (PID: %d)...\n",
rt_sig_num, value_to_send.sival_int, getppid());
if (sigqueue(getppid(), rt_sig_num, value_to_send) == -1) {
perror("sigqueue (child)");
exit(EXIT_FAILURE);
}
printf("Child finished sending signal.\n");
exit(EXIT_SUCCESS);
} else {
// --- 父进程 ---
int status;
printf("Parent process (PID: %d) waiting for signal...\n", getpid());
// 6. 父进程可以执行其他任务...
// 这里我们简单地等待信号处理函数被调用
// 或者使用 sigwaitinfo/sigtimedwait 等同步等待信号
// 7. 等待子进程结束
if (waitpid(pid, &status, 0) == -1) {
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
} else {
printf("Child did not exit normally.\n");
}
printf("Parent process continuing...\n");
// 8. 父进程也可以发送信号给自己或测试排队
printf("Parent sending two more instances of signal %d to itself...\n", rt_sig_num);
value_to_send.sival_int = 100;
if (sigqueue(getpid(), rt_sig_num, value_to_send) == -1) {
perror("sigqueue (parent 1)");
}
value_to_send.sival_int = 200;
if (sigqueue(getpid(), rt_sig_num, value_to_send) == -1) {
perror("sigqueue (parent 2)");
}
printf("Parent sent two signals. They should be queued and delivered one by one.\n");
// 9. 给一点时间处理信号
sleep(1);
// 10. 演示 sigtimedwait: 等待信号或超时
sigset_t wait_mask;
siginfo_t sig_info;
struct timespec timeout = {2, 0}; // 2 秒超时
sigemptyset(&wait_mask);
sigaddset(&wait_mask, rt_sig_num);
printf("Parent calling sigtimedwait for signal %d (timeout 2s)...\n", rt_sig_num);
int sig_received = sigtimedwait(&wait_mask, &sig_info, &timeout);
if (sig_received == -1) {
if (errno == EAGAIN) {
printf("sigtimedwait timed out.\n");
} else {
perror("sigtimedwait");
}
} else {
printf("sigtimedwait caught signal %d\n", sig_received);
if (sig_info.si_code == SI_QUEUE) {
printf(" Value from sigtimedwait: %d\n", sig_info.si_value.sival_int);
}
}
printf("Parent process exiting.\n");
}
return 0;
}
编译和运行:
# 假设代码保存在 rt_sig_example.c 中
gcc -o rt_sig_example rt_sig_example.c
# 运行
./rt_sig_example
这个示例展示了:
- 如何设置处理实时信号的函数 (
sigaction
+SA_SIGINFO
)。 - 如何使用
sigqueue
发送带附加数据的实时信号。 - 实时信号的排队特性(父进程给自己发送两个信号)。
- 如何使用
sigtimedwait
同步等待信号并获取其信息。
这些功能底层都依赖于 rt_sig*
系统调用。
好的,我们来深入学习 rt_sigaction
系统调用,从 Linux 编程小白的角度出发。
1. 函数介绍
在 Linux 系统中,信号(Signal)是一种软件中断机制,用于通知进程发生了某个事件(例如用户按下 Ctrl+C、子进程终止、定时器到期等)。当一个信号发送给进程时,进程需要知道如何“响应”这个信号。
rt_sigaction
(通常通过用户空间的 sigaction
函数调用)就是用来指定进程如何处理特定的信号。你可以告诉系统:“当收到 SIGINT
(通常是 Ctrl+C)信号时,请调用我写的这个函数 my_handler
来处理”,或者“请忽略 SIGPIPE
信号”,或者“收到 SIGTERM
信号时,请执行默认操作(通常是终止进程)”。
简单来说,sigaction
是你和操作系统之间关于“如何处理信号”的一个协议或约定。它比老式的 signal
函数更强大、更可靠。
2. 函数原型
#include <signal.h>
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
3. 功能
检查或修改与指定信号 signum
相关联的处理动作(disposition)。你可以设置一个新的处理动作,也可以查询当前的处理动作。
4. 参数
signum
:int
类型。- 你想要设置或查询的信号的编号。常见的信号有
SIGINT
(中断,Ctrl+C)、SIGTERM
(终止)、SIGUSR1
/SIGUSR2
(用户自定义信号)、SIGALRM
(定时器)等。
act
:const struct sigaction *
类型。- 一个指针,指向一个
struct sigaction
结构体。这个结构体定义了你希望为signum
信号设置的新处理方式。如果你只是想查询当前设置而不修改它,可以传NULL
。
oldact
:struct sigaction *
类型。- 一个指针,指向一个
struct sigaction
结构体。函数调用成功后,会把signum
信号原来的处理方式复制到这个结构体中。如果你不关心旧的设置,可以传NULL
。
5. 返回值
- 成功: 返回 0。
- 失败: 返回 -1,并设置全局变量
errno
来指示具体的错误原因(例如,signum
是无效的信号号)。
6. 相似函数或关联函数
signal
: 一个更早、更简单的设置信号处理函数的方式,但功能有限且行为在不同系统上可能不一致。struct sigaction
: 这是sigaction
函数的核心。你需要填充这个结构体来指定信号处理方式。它的主要成员包括:sa_handler
或sa_sigaction
: 指向信号处理函数的指针。可以是SIG_DFL
(默认处理)、SIG_IGN
(忽略信号)或你自定义的函数地址。sa_mask
: 类型为sigset_t
。当信号处理函数正在执行时,这个信号集中的信号会被临时阻塞(屏蔽),防止处理函数被其他信号中断。sa_flags
: 一些标志位,用于修改信号处理的行为。常用的有:SA_RESTART
: 如果一个系统调用(如read
,write
)被该信号中断,在信号处理函数返回后,系统调用会自动重新开始,而不是返回错误。SA_SIGINFO
: 如果设置了这个标志,你应该使用sa_sigaction
成员而不是sa_handler
,并且你的处理函数签名需要是void handler(int sig, siginfo_t *info, void *context)
,这样可以获取更多关于信号的信息。
7. 示例代码
下面是一个简单的例子,展示如何使用 sigaction
来处理 SIGINT
(Ctrl+C)信号。
#define _GNU_SOURCE // 启用 GNU 扩展,以便使用 SA_RESTART 等
#include <stdio.h>
#include <stdlib.h> // 包含 exit
#include <unistd.h> // 包含 sleep
#include <signal.h> // 包含 sigaction 相关
#include <string.h> // 包含 memset
// 定义一个信号处理函数
// 这个函数的签名必须是 void function_name(int signum)
void handle_sigint(int sig) {
// 注意:在信号处理函数内部,应该只调用异步信号安全(async-signal-safe)的函数
// printf 通常不是异步信号安全的,但 write 是
// 这里为了简单和可读性使用 printf,但在生产代码中推荐用 write
printf("\nCaught signal %d (SIGINT, usually Ctrl+C)\n", sig);
printf("Performing cleanup...\n");
// 这里可以做一些清理工作,比如关闭文件、释放内存等
// ...
printf("Cleanup done. Exiting gracefully.\n");
exit(EXIT_SUCCESS); // 优雅退出程序
}
int main() {
struct sigaction sa_new; // 用于设置新的信号处理动作
struct sigaction sa_old; // 用于保存旧的信号处理动作
// 1. 初始化 sigaction 结构体
// 使用 memset 将结构体清零,确保没有垃圾数据
memset(&sa_new, 0, sizeof(sa_new));
// 2. 设置处理函数
sa_new.sa_handler = handle_sigint; // 指定我们自定义的处理函数
// 3. 设置在处理函数执行期间要临时阻塞的信号
// sigemptyset 初始化信号集为空
sigemptyset(&sa_new.sa_mask);
// 如果你想在处理 SIGINT 时也阻塞 SIGTERM,可以这样添加:
// sigaddset(&sa_new.sa_mask, SIGTERM);
// 4. 设置标志
// SA_RESTART: 被信号中断的系统调用自动重启
sa_new.sa_flags = SA_RESTART;
// 5. 调用 sigaction 设置 SIGINT 的处理方式
printf("Setting signal handler for SIGINT (Ctrl+C)...\n");
if (sigaction(SIGINT, &sa_new, &sa_old) == -1) {
// 如果 sigaction 调用失败
perror("sigaction"); // perror 会打印错误信息
exit(EXIT_FAILURE); // 退出程序
}
// 6. 检查并打印旧的处理方式 (可选)
printf("Old handler for SIGINT was: ");
if (sa_old.sa_handler == SIG_DFL) {
printf("Default action (terminate)\n");
} else if (sa_old.sa_handler == SIG_IGN) {
printf("Ignored\n");
} else {
printf("A custom function (address: %p)\n", (void*)sa_old.sa_handler);
}
// 7. 程序主体逻辑:进入一个循环,等待信号
printf("Program is running. Press Ctrl+C to trigger the signal handler.\n");
printf("Or try sending SIGTERM with 'kill %d' in another terminal.\n", getpid());
while (1) {
printf("Working... (Press Ctrl+C to stop)\n");
sleep(2); // 睡眠2秒,模拟工作
// 如果在 sleep 期间按下 Ctrl+C,handle_sigint 会被调用
}
// 程序正常流程不会执行到这里,因为 Ctrl+C 会调用 exit
return 0;
}
编译和运行:
# 假设代码保存在 sigaction_example.c 中
gcc -o sigaction_example sigaction_example.c
# 运行程序
./sigaction_example
# 在程序运行时,按 Ctrl+C,观察输出
# 或者在另一个终端,使用 kill 命令发送信号
# kill -TERM <PID> (其中 <PID> 是上面程序输出的 PID)
这个例子展示了如何使用 sigaction
来捕获 SIGINT
信号,并在信号处理函数中执行清理操作后优雅地退出程序。