rt_sigaction系统调用及示例
data-ad-format="fluid"
data-ad-layout-key="-7k+ex-4a-9w+4a">
我们来学习一下 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 提供的封装函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // 注意:这些是内核系统调用的签名,通常在用户空间不可见或需要通过 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)
信号相关类型:
实时信号常量:
SIGRTMIN, SIGRTMAX: 定义实时信号的编号范围。
相关头文件:
7. 示例代码 下面的示例演示如何使用用户空间的封装函数(它们内部调用 rt_sig* 系统调用)来处理实时信号,并传递附加数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 #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; }
编译和运行:
1 2 3 4 5 6 # 假设代码保存在 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. 函数原型 1 2 3 4 #include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
3. 功能 检查或修改与指定信号 signum 相关联的处理动作(disposition)。你可以设置一个新的处理动作,也可以查询当前的处理动作。
4. 参数 signum:
act:
oldact:
5. 返回值
6. 相似函数或关联函数
signal: 一个更早、更简单的设置信号处理函数的方式,但功能有限且行为在不同系统上可能不一致。
struct sigaction: 这是 sigaction 函数的核心。你需要填充这个结构体来指定信号处理方式。它的主要成员包括:
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)信号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 #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; }
编译和运行:
1 2 3 4 5 6 7 8 9 10 # 假设代码保存在 sigaction_example.c 中 gcc -o sigaction_example sigaction_example.c # 运行程序 ./sigaction_example # 在程序运行时,按 Ctrl+C,观察输出 # 或者在另一个终端,使用 kill 命令发送信号 # kill -TERM <PID> (其中 <PID> 是上面程序输出的 PID)
这个例子展示了如何使用 sigaction 来捕获 SIGINT 信号,并在信号处理函数中执行清理操作后优雅地退出程序。