Linux系统--信号--信号屏蔽(阻塞)核心函数
信号屏蔽(阻塞)管理
- 核心功能: 控制哪些信号当前被阻塞(屏蔽),即暂时阻止其递送给进程。被阻塞的信号会保持挂起状态,直到解除阻塞。
- 关键函数:
sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
- 设置或修改进程的信号屏蔽字。用于进程级别。
pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset)
(POSIX 线程)- 设置或修改调用线程的信号屏蔽字。用于线程级别。
sigpending(sigset_t *set)
- 获取当前被阻塞且已到达(挂起)的信号集合。
sigfillset(sigset_t *set)
/sigemptyset(sigset_t *set)
/sigaddset(sigset_t *set, int signum)
/sigdelset(sigset_t *set, int signum)
/sigismember(const sigset_t *set, int signum)
- 信号集操作函数: 创建、修改和查询
sigset_t
类型的信号集。这些集合是sigprocmask
,pthread_sigmask
,sigaction
,sigpending
,sigsuspend
,sigwait
等函数操作的基础。
- 信号集操作函数: 创建、修改和查询
sigprocmask
函数
Linux 系统中的 sigprocmask()
函数是一个用于管理进程信号屏蔽掩码(Signal Mask)的核心系统调用,对于控制信号的接收和处理至关重要。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-
作用:
sigprocmask()
用于检查或修改调用进程的信号屏蔽掩码(Signal Mask)。- 信号屏蔽掩码是什么? 它是一个进程级别的位掩码(bitmask),其中每一位对应一个信号(如
SIGINT
,SIGTERM
)。如果某个信号对应的位在掩码中被置位(1),则表示该信号当前被阻塞(Blocked)。 - 阻塞(Blocked)的含义: 当一个信号被阻塞时:
- 如果该信号发送给进程,它不会被立即递送(Delivered) 给进程。
- 该信号会保持为未决(Pending) 状态(内核会记录它已经到达)。
- 只有当该信号在信号屏蔽掩码中对应的位被清除(0)(即解除阻塞)后,它才会被递送给进程(如果进程没有终止或忽略它)。
- 核心目的:
- 防止关键代码段被信号中断: 在执行一段不能被中断的敏感代码(如修改共享数据结构)之前,阻塞相关信号。执行完后再解除阻塞,确保代码的原子性。
- 控制信号处理时机: 决定进程在何时处理特定的信号。
- 获取当前屏蔽状态: 可以查询当前哪些信号被阻塞。
- 信号屏蔽掩码是什么? 它是一个进程级别的位掩码(bitmask),其中每一位对应一个信号(如
2. 参数详解
int how
: 指定如何修改当前的信号屏蔽掩码。它可以是以下三个值之一:SIG_BLOCK
: 添加阻塞。 将set
指向的信号集添加到当前的信号屏蔽掩码中。即:new_mask = current_mask | *set
SIG_UNBLOCK
: 解除阻塞。 将set
指向的信号集移除出当前的信号屏蔽掩码。即:new_mask = current_mask & ~(*set)
SIG_SETMASK
: 直接设置。 将当前的信号屏蔽掩码替换为set
指向的信号集。即:new_mask = *set
const sigset_t *set
: 指向一个sigset_t
类型变量的指针。这个变量包含了你想要根据how
参数进行操作的信号集合。- 如果
set
是NULL
,则how
参数会被忽略,函数仅用于获取当前的屏蔽掩码(通过oldset
返回)。
- 如果
sigset_t *oldset
: 指向一个sigset_t
类型变量的指针。如果这个参数不是NULL
,函数会在修改信号屏蔽掩码之前,将旧的(当前的) 信号屏蔽掩码存储到这个变量中。- 这非常有用,通常用于在修改后恢复之前的屏蔽状态。
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因(常见的错误是EINVAL
,表示无效的how
参数或无效的信号)。
4. 使用步骤与示例
使用 sigprocmask()
通常涉及以下步骤:
- 初始化信号集: 使用
sigemptyset()
,sigfillset()
,sigaddset()
,sigdelset()
,sigismember()
等函数操作sigset_t
类型的变量。 - (可选)获取旧屏蔽掩码: 如果你计划之后恢复旧的屏蔽状态,需要提供一个
sigset_t
变量给oldset
。 - 调用
sigprocmask()
: 指定how
和set
来修改屏蔽掩码。 - 执行关键代码: 在信号被阻塞的期间,执行不希望被中断的代码。
- (可选)恢复旧屏蔽掩码: 使用
SIG_SETMASK
和保存的oldset
恢复之前的屏蔽状态。
示例 1:阻塞 SIGINT 执行关键代码
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t new_set, old_set;// 步骤 1: 初始化一个空的信号集if (sigemptyset(&new_set) == -1) {perror("sigemptyset");exit(EXIT_FAILURE);}// 步骤 1: 将 SIGINT 添加到 new_set 中if (sigaddset(&new_set, SIGINT) == -1) {perror("sigaddset");exit(EXIT_FAILURE);}// 步骤 2 & 3: 阻塞 SIGINT (添加到当前屏蔽掩码),并保存旧掩码if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}// 步骤 4: 关键代码段 - 此时按 Ctrl+C (SIGINT) 会被阻塞printf("Critical section started. Pressing Ctrl+C now will NOT interrupt immediately.\n");for (int i = 0; i < 5; i++) {printf("Working... %d\n", i);sleep(1); // 模拟工作}printf("Critical section finished.\n");// 步骤 5: 恢复旧的信号屏蔽掩码 (解除对 SIGINT 的阻塞)if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask (restore)");exit(EXIT_FAILURE);}printf("SIGINT unblocked. Press Ctrl+C now will terminate the process.\n");// 等待一个信号来终止进程,以便演示解除阻塞后 SIGINT 生效pause();return 0;
}
示例 2:完全替换信号屏蔽掩码(仅阻塞 SIGUSR1)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t new_set, old_set;// 步骤 1: 初始化一个空的信号集if (sigemptyset(&new_set) == -1) {perror("sigemptyset");exit(EXIT_FAILURE);}// 步骤 1: 将 SIGUSR1 添加到 new_set 中 (我们只关心这个)if (sigaddset(&new_set, SIGUSR1) == -1) {perror("sigaddset");exit(EXIT_FAILURE);}// 步骤 2 & 3: 用 new_set 完全替换当前屏蔽掩码 (现在只阻塞 SIGUSR1),并保存旧掩码if (sigprocmask(SIG_SETMASK, &new_set, &old_set) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("Now only SIGUSR1 is blocked. Send SIGUSR1 (kill -USR1 %d) won't be delivered yet.\n", getpid());printf("Waiting for 10 seconds...\n");sleep(10); // 等待期间发送 SIGUSR1 会被阻塞// 步骤 5: 恢复旧的信号屏蔽掩码 (解除对 SIGUSR1 的阻塞)if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask (restore)");exit(EXIT_FAILURE);}printf("Old mask restored. If SIGUSR1 was sent during sleep, it will be delivered NOW!\n");// 短暂等待,让可能 pending 的 SIGUSR1 被递送sleep(1);printf("Exiting.\n");return 0;
}
5. 重要注意事项与使用细节
- 阻塞 vs. 忽略:
- 阻塞(Blocking): 信号被暂时阻止递送,内核将其标记为未决(Pending)。一旦解除阻塞,信号会被递送(如果进程没有终止或忽略它)。
- 忽略(Ignoring): 使用
signal()
或sigaction()
将信号的处理动作设置为SIG_IGN
。被忽略的信号永远不会被递送,内核直接丢弃它,不会将其标记为未决。 - 关键区别: 阻塞是临时的延迟递送;忽略是永久性的丢弃。
sigprocmask()
只负责阻塞/解除阻塞,不负责设置信号处理程序(那是signal()
或sigaction()
的工作)。
- 未决信号(Pending Signals):
- 被阻塞的信号如果发生,会成为未决信号。使用
sigpending()
函数可以获取当前进程的未决信号集。 - 当对一个阻塞信号的屏蔽被解除时,该信号会立即被递送给进程(只要它处于未决状态–>存在于pending位图中)。
- 标准信号(Regular Signals)(编号 1-31)不排队。如果在信号被阻塞期间,同一个标准信号发生了多次,解除阻塞后通常只会递送一次(后续发生的可能会被覆盖)。实时信号(编号 34-64)支持排队。
- 被阻塞的信号如果发生,会成为未决信号。使用
- 继承规则:
fork()
: 子进程继承父进程的信号屏蔽掩码。execve()
: 新程序继承旧程序的信号屏蔽掩码。因为exec
替换了进程映像,但内核状态(包括信号屏蔽)被保留。注意: 通过sigaction()
设置的信号处理程序会被重置为SIG_DFL
(默认动作),但信号屏蔽掩码本身保留。- 多线程(
pthreads
):sigprocmask()
的行为是未定义的。在多线程程序中,必须使用pthread_sigmask()
函数,它操作的是调用线程的信号屏蔽掩码。进程级别的信号屏蔽概念在线程模型中不再适用。
- 原子性:
sigprocmask()
调用本身是原子的。内核保证检查和修改信号掩码的操作是不可分割的。- 在阻塞信号期间执行关键代码,是为了保护这段代码内部的操作不被信号处理程序中断,从而保证其原子性。
SIGKILL
和SIGSTOP
:- 这两个信号不能被阻塞、捕获或忽略。任何试图将它们添加到信号屏蔽掩码的操作都会被系统静默忽略。它们是操作系统用来终止或停止进程的终极手段。
- 恢复旧屏蔽掩码的重要性:
- 示例中展示了保存和恢复
oldset
的标准做法。这非常重要,因为:- 你通常只想在特定关键区域阻塞信号,之后应恢复进程的原始状态。
- 你不知道调用你的代码之前哪些信号已经被阻塞了。盲目地解除所有阻塞(例如用
sigemptyset
作为SIG_SETMASK
的参数)可能会让进程暴露在不希望接收的信号下。同样,盲目地阻塞信号也可能干扰调用者期望的信号行为。
- 示例中展示了保存和恢复
set
为 NULL:- 如果
set
是NULL
,how
参数会被忽略。函数不会修改当前屏蔽掩码,但会将当前屏蔽掩码复制到oldset
指向的变量中(如果oldset
不是NULL
)。这是查询当前屏蔽状态的唯一方法。
- 如果
- 错误处理:
- 务必检查
sigprocmask()
的返回值!常见的错误是EINVAL
(无效的how
或set
中包含无效信号)。处理错误对于编写健壮的程序至关重要。
- 务必检查
总结:
sigprocmask()
是 Linux/Unix 信号处理机制中的基石函数,用于精细控制进程何时接收信号。它的核心是操作信号屏蔽掩码,实现信号的临时阻塞。理解阻塞与忽略的区别、未决信号的概念、继承规则(特别是多线程环境下的 pthread_sigmask
)以及恢复旧掩码的重要性,是正确使用该函数的关键。它主要用于保护关键代码段不被中断,是编写可靠、异步安全的系统程序的重要工具。
pthread_sigmask
函数
我们来详细了解一下 Linux 系统中用于多线程环境的 pthread_sigmask()
函数。它是 sigprocmask()
在多线程编程中的对应物,用于管理线程级别的信号屏蔽。
1. 函数声明与作用
-
声明:
#include <signal.h> int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
-
作用:
pthread_sigmask()
用于检查或修改调用线程的信号屏蔽掩码(Signal Mask)。- 核心概念: 在多线程程序中:
- 信号可以被发送到整个进程(例如通过
kill()
)或发送到特定线程(例如通过pthread_kill()
)。 - 每个线程都拥有自己独立的信号屏蔽掩码。
- 信号的处理程序(handler)是在进程级别设置的(通过
signal()
或sigaction()
)。也就是说,所有线程共享同一个信号处理函数定义。但是,哪个线程执行这个处理函数取决于信号的递送规则。
- 信号可以被发送到整个进程(例如通过
- 阻塞(Blocked)的含义(与
sigprocmask
相同): 如果某个信号在线程的信号屏蔽掩码中被置位(1),那么:- 发送给该线程的信号(
pthread_kill()
)会被阻塞。 - 发送给整个进程的信号(
kill()
)会由内核递送给进程中任意一个其信号屏蔽掩码中未阻塞该信号的线程。如果所有线程都阻塞了该信号,它将成为进程的未决信号,直到某个线程解除阻塞。
- 发送给该线程的信号(
- 核心目的:
- 线程特定的信号控制: 允许不同的线程阻塞或解除阻塞不同的信号集,实现精细化的信号处理策略。
- 防止关键线程代码段被中断: 在线程执行不能被中断的关键代码(如操作线程私有数据或特定共享数据结构)前,阻塞相关信号。
- 创建“专用信号处理线程”: 一种常见模式是让一个特定线程负责处理所有(或大部分)信号。其他工作线程则阻塞所有信号(或特定信号),确保只有这个专用线程能接收并处理信号。这需要结合
sigwait()
或sigwaitinfo()
使用。 - 获取线程的当前屏蔽状态。
- 核心概念: 在多线程程序中:
2. 参数详解
int how
: 指定如何修改调用线程的信号屏蔽掩码。取值与sigprocmask()
相同:SIG_BLOCK
: 添加阻塞。 将set
指向的信号集添加到线程当前的信号屏蔽掩码中。即:new_mask = current_mask | *set
SIG_UNBLOCK
: 解除阻塞。 将set
指向的信号集移除出线程当前的信号屏蔽掩码。即:new_mask = current_mask & ~(*set)
SIG_SETMASK
: 直接设置。 将线程当前的信号屏蔽掩码替换为set
指向的信号集。即:new_mask = *set
const sigset_t *set
: 指向一个sigset_t
类型变量的指针。这个变量包含了你想要根据how
参数进行操作的信号集合。- 如果
set
是NULL
,则how
参数会被忽略,函数仅用于获取线程当前的屏蔽掩码(通过oldset
返回)。
- 如果
sigset_t *oldset
: 指向一个sigset_t
类型变量的指针。如果这个参数不是NULL
,函数会在修改线程的信号屏蔽掩码之前,将旧的(当前的) 信号屏蔽掩码存储到这个变量中。- 用于在修改后恢复线程之前的屏蔽状态。
3. 返回值
- 成功: 返回
0
。 - 失败: 返回一个错误号(是
errno
的值,而不是-1
)。常见错误:EINVAL
:how
参数无效或set
中包含无效信号。
4. 使用步骤与示例
使用 pthread_sigmask()
的步骤与 sigprocmask()
类似,但需注意线程上下文:
- 初始化信号集: 使用
sigemptyset()
,sigfillset()
,sigaddset()
,sigdelset()
,sigismember()
操作sigset_t
变量。 - (可选)获取旧屏蔽掩码: 提供
sigset_t
变量给oldset
以保存旧状态。 - 调用
pthread_sigmask()
: 在需要控制信号的线程内部调用此函数。 - 执行代码: 在信号被阻塞的期间,执行线程不希望被中断的代码。
- (可选)恢复旧屏蔽掩码: 使用
SIG_SETMASK
和保存的oldset
恢复线程之前的屏蔽状态。
示例 1:工作线程阻塞 SIGINT,主线程不阻塞
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>void *worker_thread(void *arg) {sigset_t new_set, old_set;// 初始化信号集并添加 SIGINTsigemptyset(&new_set);sigaddset(&new_set, SIGINT);// 阻塞 SIGINT (仅在这个工作线程内)if (pthread_sigmask(SIG_BLOCK, &new_set, &old_set) != 0) {perror("pthread_sigmask (worker block)");pthread_exit(NULL);}printf("Worker Thread [%lu]: SIGINT blocked. Pressing Ctrl+C here won't interrupt me immediately.\n", pthread_self());for (int i = 0; i < 5; i++) {printf("Worker [%lu] working... %d\n", pthread_self(), i);sleep(1);}printf("Worker Thread [%lu] finished critical work.\n", pthread_self());// 恢复旧的屏蔽掩码 (解除 SIGINT 阻塞)if (pthread_sigmask(SIG_SETMASK, &old_set, NULL) != 0) {perror("pthread_sigmask (worker restore)");}printf("Worker Thread [%lu]: SIGINT unblocked.\n", pthread_self());// 等待一会,让主线程有机会发信号sleep(2);pthread_exit(NULL);
}int main() {pthread_t tid;// 主线程不阻塞 SIGINT,所以 Ctrl+C 会中断主线程printf("Main Thread [%lu]: SIGINT NOT blocked. Press Ctrl+C here will terminate the main thread (and likely the whole process).\n", pthread_self());if (pthread_create(&tid, NULL, worker_thread, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}// 主线程等待工作线程结束pthread_join(tid, NULL);printf("Main Thread exiting.\n");return 0;
}
示例 2:创建专用信号处理线程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>void *signal_handler_thread(void *arg) {sigset_t set;int sig;// 这个线程等待的信号集 (例如,我们处理 SIGINT 和 SIGTERM)sigemptyset(&set);sigaddset(&set, SIGINT);sigaddset(&set, SIGTERM);printf("Signal Handler Thread [%lu] ready.\n", pthread_self());while (1) {// 等待信号到来。sigwait 会原子地解除对 set 中信号的阻塞并等待。if (sigwait(&set, &sig) != 0) {perror("sigwait");break;}switch (sig) {case SIGINT:printf("Handler [%lu]: Received SIGINT. Doing cleanup...\n", pthread_self());// 执行 SIGINT 相关的清理操作break;case SIGTERM:printf("Handler [%lu]: Received SIGTERM. Terminating gracefully...\n", pthread_self());// 执行 SIGTERM 相关的终止操作,例如通知其他线程退出exit(EXIT_SUCCESS); // 示例中直接退出进程break;default:printf("Handler [%lu]: Received unexpected signal %d\n", pthread_self(), sig);}}return NULL;
}int main() {pthread_t sig_tid, worker_tid;sigset_t all_signals;// **关键步骤 1: 在主线程中阻塞所有信号**// 这样,后续创建的线程将继承这个屏蔽掩码sigfillset(&all_signals);if (pthread_sigmask(SIG_SETMASK, &all_signals, NULL) != 0) {perror("pthread_sigmask (main block all)");exit(EXIT_FAILURE);}printf("Main Thread [%lu]: Blocked ALL signals. New threads will inherit this mask.\n", pthread_self());// **关键步骤 2: 创建专用信号处理线程**if (pthread_create(&sig_tid, NULL, signal_handler_thread, NULL) != 0) {perror("pthread_create (handler)");exit(EXIT_FAILURE);}// **关键步骤 3: 在信号处理线程内部,它会调用 sigwait() 等待它关心的信号。// (这部分逻辑在 signal_handler_thread 函数里实现)// 创建一些工作线程 (它们继承了主线程的屏蔽掩码,即阻塞所有信号)// void *worker_thread(void *arg) { ... (工作逻辑,不处理信号) ... }// if (pthread_create(&worker_tid, NULL, worker_thread, NULL) != 0) { ... }printf("Main Thread [%lu]: Process running. Send SIGINT (Ctrl+C) or SIGTERM (kill) to test.\n", pthread_self());// 主线程和工作线程通常做它们的事情,不会被信号中断// 因为信号都被阻塞了,最终会被递送给正在 sigwait() 的专用线程pause(); // 主线程简单等待 (实际程序可能有其他逻辑)// ... 清理工作线程等 ...pthread_join(sig_tid, NULL);return 0;
}
5. 重要注意事项与使用细节
- 线程继承:
- 新创建的线程会继承创建它的那个线程的信号屏蔽掩码。这是实现“专用信号处理线程”模式的基础(主线程先阻塞所有信号,然后创建线程)。
- 进程启动时,主线程的信号屏蔽掩码通常继承自其父进程(可能为空,即不阻塞任何信号)。
- 信号递送(Delivery):
- 进程定向信号 (
kill()
): 内核选择进程中任意一个其信号屏蔽掩码中未阻塞该信号的线程来递送信号。选择哪个线程是不确定的。 - 线程定向信号 (
pthread_kill()
): 信号被发送给指定的线程。只有当该线程的信号屏蔽掩码未阻塞该信号时,才会递送给它。 - 硬件/异常产生的信号 (e.g., SIGSEGV, SIGFPE): 通常递送给导致异常的线程本身,与该线程的信号屏蔽掩码无关(即使阻塞了,也会被递送,因为这是同步信号)。
- 进程定向信号 (
- 信号处理程序(Handler)执行:
- 信号处理程序是在进程级别定义的(
signal()
/sigaction()
)。 - 当信号被递送给一个线程时,该线程负责执行对应的信号处理函数。
- 执行处理程序会中断线程的正常控制流。处理程序执行完毕后,线程恢复被中断处的执行(除非处理程序终止了进程或线程)。
- 信号处理程序是在进程级别定义的(
pthread_sigmask()
vssigprocmask()
:pthread_sigmask()
: 操作调用线程的信号屏蔽掩码。这是多线程程序中应该使用的函数。sigprocmask()
: 操作进程的信号屏蔽掩码。其行为在 POSIX 多线程程序中是未定义(Undefined) 的。不要在多线程程序中使用它。在 Linux 的 NPTL 线程实现中,sigprocmask()
可能等同于pthread_sigmask()
(作用于调用线程),但这不可移植,依赖于具体实现。
- 专用信号处理线程模式:
- 这是处理信号的一种健壮且推荐的方式,尤其在复杂多线程程序中。
- 步骤:
- 主线程在创建任何其他线程之前,使用
pthread_sigmask(SIG_SETMASK, &full_set, NULL)
阻塞所有信号。 - 创建专用信号处理线程。
- 在专用线程中:
- 使用
pthread_sigmask(SIG_SETMASK, &wait_set, NULL)
或直接在sigwait()
中设置它等待的信号集(sigwait
内部会处理屏蔽)。 - 调用
sigwait()
或sigwaitinfo()
同步等待信号到来。 - 在
sigwait()
返回后,根据收到的信号执行相应的同步处理逻辑(不再是异步处理程序)。这避免了异步信号处理程序带来的复杂性(如可重入性问题)。
- 使用
- 其他工作线程继承了主线程的屏蔽掩码(阻塞所有信号),因此它们永远不会异步执行信号处理程序,也不会被信号中断。它们可以安全地进行任何操作(包括调用非异步安全的函数)。
- 主线程在创建任何其他线程之前,使用
SIGKILL
和SIGSTOP
:- 与
sigprocmask()
一样,这两个信号不能被任何线程阻塞、捕获或忽略。pthread_sigmask()
对它们的操作会被静默忽略。
- 与
- 错误处理:
- 务必检查返回值!它是错误号,不是
-1
。使用strerror()
或perror()
输出错误信息。
- 务必检查返回值!它是错误号,不是
- 原子性与可重入性:
pthread_sigmask()
调用本身是原子的。- 在线程阻塞信号期间执行的代码段受到保护,不会被这些特定信号的异步处理程序中断。但需要注意,如果代码段包含可能被其他信号中断的操作,或者涉及共享资源,仍需其他同步机制(如互斥锁)。
总结:
pthread_sigmask()
是多线程 Linux/Unix 程序中管理信号的核心函数。它赋予开发者对线程级别信号屏蔽的精细控制能力。理解线程的信号屏蔽继承规则、进程/线程定向信号的递送机制、以及信号处理程序的执行上下文至关重要。“专用信号处理线程”结合 sigwait()
的模式是处理异步信号的一种强大且安全的方法,能显著简化多线程程序的信号处理逻辑并提高可靠性。务必记住在多线程环境中始终使用 pthread_sigmask()
而非 sigprocmask()
。
sigpending
函数
我们来详细了解一下 Linux 系统中的 sigpending()
函数。这个函数是理解信号阻塞(Blocking)和未决(Pending)状态的关键工具。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigpending(sigset_t *set);
-
作用:
sigpending()
用于获取调用进程(或调用线程)当前未决(Pending)的信号集。- 未决(Pending)信号是什么? 当一个信号被发送给进程,但该信号当前在进程(或线程)的信号屏蔽掩码(Signal Mask)中被阻塞(Blocked) 时,该信号不会立即被递送(Delivered)。内核会将该信号标记为未决(Pending),并记录在进程的未决信号集中。
sigpending()
就是用来查询这个未决信号集。
- 未决(Pending)信号是什么? 当一个信号被发送给进程,但该信号当前在进程(或线程)的信号屏蔽掩码(Signal Mask)中被阻塞(Blocked) 时,该信号不会立即被递送(Delivered)。内核会将该信号标记为未决(Pending),并记录在进程的未决信号集中。
2. 参数详解
sigset_t *set
: 这是一个输出参数。函数会将当前未决的信号集填充到这个sigset_t
类型的变量中。- 你需要预先声明一个
sigset_t
变量,并将其地址传递给sigpending()
。 - 函数执行成功后,这个变量将包含所有当前处于未决状态的信号。
- 你需要预先声明一个
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因(通常没有特别常见的错误,但理论上可能有权限问题等)。
4. 核心概念:阻塞、递送与未决
要透彻理解 sigpending()
,必须清晰掌握信号生命周期的几个关键状态:
- 生成(Generation): 事件发生(如用户按下 Ctrl+C、硬件异常、
kill()
调用等),内核或进程为接收进程生成一个信号。 - 递送(Delivery): 内核将信号传递给目标进程(或线程),并触发与该信号关联的动作(执行默认动作、忽略或调用信号处理程序)。这是信号的最终处理阶段。
- 未决(Pending): 信号已经生成,但尚未递送。信号处于等待递送的状态。
- 阻塞(Blocking): 进程(或线程)可以设置其信号屏蔽掩码来阻止特定信号的递送。
- 如果一个信号被阻塞,当它生成时,它不会被立即递送,而是被放入该进程(或线程)的未决信号集中。
- 只有当该信号解除阻塞后,内核才会将其从未决信号集中取出并执行递送(如果它仍然处于未决状态)。
sigpending()
的角色: 它提供了一种机制,让进程(或线程)可以主动查询当前有哪些信号因为被阻塞而处于“等待处理”的未决状态。
5. 使用步骤与示例
使用 sigpending()
的典型流程:
- 阻塞信号: 使用
sigprocmask()
(进程) 或pthread_sigmask()
(线程) 阻塞你关心的信号。 - 执行操作: 在信号被阻塞期间,执行某些操作(可能触发信号)。
- 检查未决信号: 调用
sigpending()
获取当前未决的信号集。 - 处理未决信号 (可选):
- 可以检查哪些信号未决。
- 可以选择在解除阻塞前进行一些预处理。
- 通常,最终需要解除阻塞这些信号,让内核递送它们(执行默认动作或处理程序)。
- 解除阻塞: 使用
sigprocmask()
或pthread_sigmask()
解除对信号的阻塞。如果信号仍然未决,内核会立即递送它们。
示例 1:基本用法 - 阻塞 SIGINT 并检查未决状态
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set, pending_set;// 1. 初始化信号集并添加 SIGINTsigemptyset(&block_set);sigaddset(&block_set, SIGINT);// 2. 阻塞 SIGINTif (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("SIGINT is blocked. Press Ctrl+C now. It will be pending.\n");printf("Waiting for 5 seconds...\n");sleep(5); // 在这5秒内按下 Ctrl+C (SIGINT)// 3. 检查未决信号if (sigpending(&pending_set) == -1) {perror("sigpending");exit(EXIT_FAILURE);}// 4. 检查 SIGINT 是否在未决信号集中if (sigismember(&pending_set, SIGINT)) {printf("SIGINT is pending!\n");} else {printf("No SIGINT is pending.\n");}// 5. 解除对 SIGINT 的阻塞 (这将导致 SIGINT 被递送,通常终止进程)printf("Unblocking SIGINT. It will be delivered now!\n");sigset_t unblock_set;sigemptyset(&unblock_set);sigaddset(&unblock_set, SIGINT);if (sigprocmask(SIG_UNBLOCK, &unblock_set, NULL) == -1) {perror("sigprocmask (unblock)");exit(EXIT_FAILURE);}// 解除阻塞后,SIGINT 被递送,程序通常在此终止// 如果信号处理程序捕获了 SIGINT,程序会继续printf("This line may not be printed if SIGINT terminates the process.\n");sleep(1); // 短暂等待,确保信号有机会被处理return 0;
}
示例 2:区分标准信号与实时信号 (排队)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>void handler(int sig) {printf("Process %d: Received signal %d\n", getpid(), sig);
}int main() {// 设置 SIGUSR1 和 SIGRTMIN 的处理程序struct sigaction sa;sa.sa_handler = handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0;if (sigaction(SIGUSR1, &sa, NULL) == -1 ||sigaction(SIGRTMIN, &sa, NULL) == -1) {perror("sigaction");exit(EXIT_FAILURE);}sigset_t block_set, pending_set;// 阻塞 SIGUSR1 和 SIGRTMINsigemptyset(&block_set);sigaddset(&block_set, SIGUSR1);sigaddset(&block_set, SIGRTMIN);if (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}pid_t pid = getpid();printf("Parent PID: %d. Signals blocked. Sending multiple signals...\n", pid);// 向自己发送多个 SIGUSR1 (标准信号) 和 SIGRTMIN (实时信号)kill(pid, SIGUSR1); // 1st SIGUSR1kill(pid, SIGUSR1); // 2nd SIGUSR1 (可能覆盖第一个)kill(pid, SIGRTMIN); // 1st SIGRTMINkill(pid, SIGRTMIN); // 2nd SIGRTMINkill(pid, SIGRTMIN); // 3rd SIGRTMIN// 检查未决信号if (sigpending(&pending_set) == -1) {perror("sigpending");exit(EXIT_FAILURE);}// 打印未决信号状态printf("Pending signals:\n");printf(" SIGUSR1 (%d) pending? %s\n", SIGUSR1,sigismember(&pending_set, SIGUSR1) ? "Yes" : "No");printf(" SIGRTMIN (%d) pending? %s\n", SIGRTMIN,sigismember(&pending_set, SIGRTMIN) ? "Yes" : "No");// 注意:sigpending 只告诉你至少有一个实例未决,不告诉你有多少个printf("Unblocking signals...\n");// 解除阻塞if (sigprocmask(SIG_UNBLOCK, &block_set, NULL) == -1) {perror("sigprocmask (unblock)");exit(EXIT_FAILURE);}// 观察处理程序输出,看有多少个 SIGUSR1 和 SIGRTMIN 被递送printf("Parent waiting to see how many signals are delivered...\n");sleep(2); // 给处理程序执行时间return 0;
}
运行示例 2 的可能输出:
Parent PID: 12345. Signals blocked. Sending multiple signals...
Pending signals:SIGUSR1 (10) pending? YesSIGRTMIN (34) pending? Yes
Unblocking signals...
Process 12345: Received signal 34 // SIGRTMIN 1
Process 12345: Received signal 34 // SIGRTMIN 2
Process 12345: Received signal 34 // SIGRTMIN 3
Process 12345: Received signal 10 // SIGUSR1 (Only one!)
Parent waiting to see how many signals are delivered...
6. 重要注意事项与使用细节 (深度解析)
- 未决信号的来源:
- 被阻塞的信号发送到进程(
kill()
)。 - 被阻塞的信号发送到特定线程(
pthread_kill()
)。 - 硬件异常产生的信号(如
SIGSEGV
,SIGFPE
)如果发生在信号被阻塞的上下文中,也会成为未决信号。但请注意,这些同步信号通常由导致异常的线程接收,且其递送通常与屏蔽无关(见第7点)。
- 被阻塞的信号发送到进程(
- 标准信号 vs. 实时信号 (排队):
- 标准信号 (1-31, e.g., SIGINT, SIGTERM, SIGUSR1):
- 不排队: 如果在信号被阻塞期间,同一个标准信号发生了多次,内核只记录一次该信号处于未决状态。解除阻塞后,该信号只递送一次。后续发生的信号可能会被丢失(覆盖)。
sigpending()
只能告诉你该信号至少有一个实例未决,无法知道实际发生了多少次。
- 实时信号 (34-64, SIGRTMIN to SIGRTMAX):
- 排队: 如果在信号被阻塞期间,同一个实时信号发生了多次,内核会为每一次发生都记录一个未决实例(直到达到系统限制)。解除阻塞后,这些信号会按发送顺序依次递送。
sigpending()
只能告诉你该信号至少有一个实例未决。它不能告诉你具体有多少个实例在排队。需要使用sigqueue()
发送信号并携带额外数据时,排队行为才更明显。
- 标准信号 (1-31, e.g., SIGINT, SIGTERM, SIGUSR1):
sigpending()
的范围:- 进程 vs. 线程: POSIX 标准规定
sigpending()
返回的是调用进程的未决信号集。然而,在 Linux NPTL 线程实现中:- 对于进程定向信号 (
kill()
):sigpending()
返回的是整个进程的未决信号集(即所有线程共享的未决信号)。 - 对于线程定向信号 (
pthread_kill()
): 该信号只对目标线程是未决的。sigpending()
在目标线程中调用才会看到这个信号。在其他线程中调用sigpending()
看不到其他线程专属的未决信号。
- 对于进程定向信号 (
- 简而言之,在 Linux 多线程环境中,
sigpending()
主要反映的是进程级别的未决信号(kill()
发送的),以及调用线程自身专属的未决信号(pthread_kill()
发送给它的)。要精确获取某个特定线程的完整未决信号集是复杂的。通常,“专用信号处理线程”模式结合sigwait()
是更清晰的处理方式。
- 进程 vs. 线程: POSIX 标准规定
SIGKILL
和SIGSTOP
:- 这两个信号不能被阻塞、捕获或忽略。
- 因此,它们永远不会出现在
sigpending()
返回的未决信号集中。一旦生成,内核会立即递送它们(终止或停止进程)。
- 同步信号 (硬件/异常) 的特殊性:
- 由硬件异常(如非法内存访问
SIGSEGV
、除零SIGFPE
)或特定指令(如SIGTRAP
)产生的信号称为同步信号。 - 同步信号是定向到导致异常的特定线程的。
- 关键点:同步信号的递送通常不受信号屏蔽的影响! 即使线程阻塞了
SIGSEGV
,当它发生非法内存访问时,内核仍然会递送SIGSEGV
给该线程(通常导致进程终止)。因此,同步信号很少有机会真正进入未决状态并被sigpending()
检测到。它们倾向于立即递送。
- 由硬件异常(如非法内存访问
- 解除阻塞后的递送:
- 当使用
sigprocmask()
或pthread_sigmask()
解除对一个或多个信号的阻塞时:- 如果这些信号中有处于未决状态的,内核会立即将它们递送给进程(或线程)。
- 递送的顺序通常是:先递送标准信号(顺序未指定),然后按编号从小到大递送实时信号。
- 在解除阻塞和信号实际递送(执行处理程序)之间,
sigpending()
的状态会相应更新。
- 当使用
sigpending()
的使用场景:- 诊断与调试: 检查是否有预期之外的信号被阻塞并堆积。
- 自定义信号处理逻辑: 在解除阻塞前,根据哪些信号未决来决定处理策略(虽然较少见,因为通常依赖处理程序)。
- 实现安全机制: 在关键操作完成后,检查是否有中断请求(如
SIGINT
)在操作期间被阻塞,以便在安全点响应。 - 结合
sigsuspend()
或sigwait()
: 有时在设置等待信号前,需要先检查是否有信号已经未决。但在“专用信号处理线程”模式中,sigwait()
本身就能处理未决信号,通常不需要额外调用sigpending()
。
- 原子性考虑:
sigpending()
调用本身是原子的,它返回调用时刻的未决信号集快照。- 但是,在调用
sigpending()
之后、程序检查其返回结果并做出决策之前,新的信号可能到达并被阻塞(从而成为新的未决信号),或者未决信号可能被递送(如果在此期间解除了阻塞)。这意味着返回的set
反映的只是一个瞬间状态。
7. 总结
sigpending()
是 Linux/Unix 信号机制中一个关键的“观察窗”,它允许进程或线程探查那些因被阻塞而暂时搁置、等待处理的信号(未决信号)。深入理解其行为需要掌握信号阻塞、递送、未决状态的关系,以及标准信号与实时信号在排队行为上的根本区别。在多线程环境中,需要特别注意 Linux 实现中关于进程信号和线程信号未决状态的细节。虽然 sigpending()
本身不改变信号状态,但它为开发者提供了信号流控的重要信息,对于调试复杂的信号交互、实现特定的信号处理策略或在安全点响应中断请求具有重要价值。然而,在现代多线程编程中,“专用信号处理线程 + sigwait()
”的模式往往能更清晰、更安全地管理信号,减少对 sigpending()
直接操作未决状态的需求。
sigemptyset
函数
我们来详细了解一下 Linux 系统中的 sigemptyset()
函数。它是操作信号集 (sigset_t
) 的基础函数,是构建任何信号屏蔽、等待或检查操作的起点。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigemptyset(sigset_t *set);
-
作用:
sigemptyset()
用于初始化一个sigset_t
类型的信号集,将其设置为空集,即该信号集不包含任何信号。- 核心概念:
sigset_t
是一个不透明的数据类型(通常实现为一个位掩码数组),用于表示一组信号。在使用任何sigset_t
变量之前,必须对其进行初始化。sigemptyset()
就是进行这种初始化的标准方式之一(另一种是sigfillset()
)。 - 为什么需要初始化? 未初始化的
sigset_t
变量包含的是内存中的随机数据(垃圾值)。如果直接将其传递给sigprocmask()
,pthread_sigmask()
,sigaction()
,sigpending()
,sigaddset()
等函数,会导致不可预测的行为(可能阻塞/操作了意料之外的信号集),通常引发严重错误。
- 核心概念:
2. 参数详解
sigset_t *set
: 这是一个输入输出参数。指向需要被初始化为空集的sigset_t
变量。- 你需要预先声明一个
sigset_t
变量。 - 调用
sigemptyset(&your_sigset_var)
后,your_sigset_var
的内容将被清空,表示不包含任何信号。
- 你需要预先声明一个
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因。- 在实践中,
sigemptyset()
失败非常罕见。最常见的理论错误是EINVAL
(如果set
是一个无效指针),但这通常意味着程序逻辑有严重错误(如未分配内存)。
- 在实践中,
4. 深入理解:sigset_t
的本质与初始化的重要性
sigset_t
是什么? 它是一个抽象数据类型,其内部结构对应用程序是隐藏的(#include <signal.h>
后即可使用,但具体实现依赖操作系统和 C 库)。在 Linux 上,它通常定义为一个结构体,内部包含足够多的位(bit)来表示所有支持的信号(例如,一个或多个unsigned long
数组)。每一位代表一个信号。如果某位被置为 1,表示该信号在集合中;置为 0,表示不在集合中。- 内存中的随机性: 当你在函数中声明一个局部
sigset_t
变量(如sigset_t myset;
)时,变量myset
占据一块内存空间。这块内存之前可能被其他数据使用过,其内容是未定义的(Undefined),充满了随机比特(0 和 1 的随机组合)。 - 灾难性的未初始化使用: 想象一下,你声明了
sigset_t block_set;
但没有初始化它,然后直接调用sigprocmask(SIG_BLOCK, &block_set, NULL)
。block_set
包含的随机比特会被解释为一个信号集。这可能导致:- 阻塞了大量(甚至所有) 你本不想阻塞的信号,导致程序对关键信号(如
SIGTERM
)无响应。 - 阻塞了无效的信号编号(如果随机比特设置了不存在的信号位),可能导致
sigprocmask
返回EINVAL
错误。 - 行为完全不可预测,难以调试。
- 阻塞了大量(甚至所有) 你本不想阻塞的信号,导致程序对关键信号(如
sigemptyset()
的职责: 它遍历sigset_t
内部结构的所有位,将它们全部设置为 0。这确保了信号集从一个已知的、安全的、不包含任何信号的状态开始。之后,你可以使用sigaddset()
,sigdelset()
,sigfillset()
等函数精确地添加或移除特定的信号。
5. 使用步骤与示例
使用 sigemptyset()
是操作信号集的第一步:
- 声明信号集变量:
sigset_t my_signal_set;
- 初始化为空集:
if (sigemptyset(&my_signal_set) == -1) { /* 错误处理 */ }
- (可选) 添加信号: 使用
sigaddset(&my_signal_set, signo)
将特定信号signo
(如SIGINT
) 添加到集合中。 - (可选) 移除信号: 使用
sigdelset(&my_signal_set, signo)
从集合中移除特定信号。 - (可选) 填充所有信号: 使用
sigfillset(&my_signal_set)
将集合设置为包含所有有效信号(除了SIGKILL
和SIGSTOP
)。 - 使用信号集: 将初始化并配置好的
my_signal_set
传递给其他信号函数,如:sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
- 设置进程信号屏蔽pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset)
- 设置线程信号屏蔽sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
- 设置信号处理程序时指定阻塞信号集 (act->sa_mask
)sigpending(sigset_t *set)
- (输出参数,但通常不需要先sigemptyset
,因为函数会覆盖它)sigsuspend(const sigset_t *mask)
- 临时替换信号屏蔽并挂起sigwait(const sigset_t *set, int *sig)
- 同步等待集合中的信号
示例 1:基础用法 - 阻塞 SIGINT
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set;// *** 关键步骤 1: 初始化信号集为空 ***if (sigemptyset(&block_set) == -1) {perror("sigemptyset");exit(EXIT_FAILURE);}// *** 关键步骤 2: 添加 SIGINT 到信号集 ***if (sigaddset(&block_set, SIGINT) == -1) {perror("sigaddset");exit(EXIT_FAILURE);}// 使用信号集阻塞 SIGINTif (sigprocmask(SIG_BLOCK, &block_set, NULL) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("SIGINT is blocked. Press Ctrl+C won't work immediately.\n");printf("Sleeping for 5 seconds...\n");sleep(5);printf("Unblocking SIGINT. Press Ctrl+C now should work.\n");// 解除阻塞 SIGINT (使用相同的信号集和 SIG_UNBLOCK)if (sigprocmask(SIG_UNBLOCK, &block_set, NULL) == -1) {perror("sigprocmask (unblock)");exit(EXIT_FAILURE);}// 等待信号pause(); // 通常会被 SIGINT 中断return 0;
}
示例 2:设置信号处理程序的阻塞掩码 (sa_mask)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void handler(int sig) {write(STDOUT_FILENO, "Handler called!\n", 17);// 模拟一个耗时操作sleep(3);write(STDOUT_FILENO, "Handler finished.\n", 19);
}int main() {struct sigaction sa;// 设置处理函数sa.sa_handler = handler;// *** 关键步骤 1: 初始化 sa_mask 为空 ***if (sigemptyset(&sa.sa_mask) == -1) {perror("sigemptyset (sa_mask)");exit(EXIT_FAILURE);}// *** 关键步骤 2 (可选但推荐): 在处理程序执行期间阻塞 SIGINT ***// 防止处理程序被同一个信号再次中断if (sigaddset(&sa.sa_mask, SIGINT) == -1) {perror("sigaddset (sa_mask)");exit(EXIT_FAILURE);}sa.sa_flags = 0; // 无特殊标志// 注册 SIGUSR1 的处理程序if (sigaction(SIGUSR1, &sa, NULL) == -1) {perror("sigaction");exit(EXIT_FAILURE);}printf("Process PID: %d. Send SIGUSR1 (kill -USR1 %d) to test.\n", getpid(), getpid());printf("Note: The handler blocks SIGINT during its execution.\n");while (1) {pause(); // 等待信号}return 0;
}
6. 重要注意事项与使用细节 (深度解析)
- 初始化是强制性的: 这是
sigemptyset()
最核心的要点。绝对不要使用未初始化的sigset_t
变量。sigemptyset()
或sigfillset()
必须是你在操作一个sigset_t
变量时调用的第一个函数。 sigemptyset()
vssigfillset()
:sigemptyset(sigset_t *set)
: 初始化set
为空集(不含任何信号)。sigfillset(sigset_t *set)
: 初始化set
为满集(包含所有当前系统支持的信号,除了SIGKILL
和SIGSTOP
)。- 选择依据:
- 如果你打算构建一个特定子集的信号(例如,只阻塞
SIGINT
和SIGQUIT
),总是先调用sigemptyset()
,然后使用sigaddset()
添加你需要的信号。 - 如果你打算阻塞除少数信号外的所有信号,或者想先拥有所有信号再移除不需要的,可以先调用
sigfillset()
,然后使用sigdelset()
移除你不想阻塞的信号(如SIGTERM
以便程序能被终止)。这在创建“专用信号处理线程”时很常见(主线程先阻塞所有信号)。
- 如果你打算构建一个特定子集的信号(例如,只阻塞
- 线程安全性与可重入性:
sigemptyset()
函数本身通常是线程安全的。它只操作传入的set
指向的内存,不依赖全局状态(标准库实现应保证这点)。sigemptyset()
是异步信号安全的吗? POSIX 标准没有明确要求sigemptyset()
是异步信号安全的(即可以在信号处理程序中安全调用)。然而,在主流 Linux 实现(如 glibc)中,sigemptyset()
的实现通常非常简单(例如,一个循环清零内存),很可能是异步信号安全的。但为了严格的可移植性,避免在信号处理程序中调用sigemptyset()
(以及sigaddset
,sigdelset
,sigfillset
)。信号处理程序应尽可能简单,通常只设置一个标志、调用write
或_exit
,或者使用sigqueue
发送另一个信号。如果你必须在处理程序中操作信号集,请查阅你的系统文档或谨慎测试。
sigset_t
的生命周期与内存管理:sigset_t
通常是一个固定大小的结构体(大小由sizeof(sigset_t)
确定)。它不涉及动态内存分配。- 当你声明一个
sigset_t
变量(如局部变量、全局变量、结构体成员)时,你拥有它的内存。 sigemptyset()
只操作你提供的这块内存。它不会分配或释放内存。- 局部
sigset_t
变量在函数返回后失效。如果你需要在函数间传递配置好的信号集,可以:- 传递
sigset_t
的值或指针(按值传递会复制整个结构体)。 - 将其存储在全局变量或结构体中。
- 传递
- 错误处理:
- 虽然
sigemptyset()
失败概率极低,但良好的编程实践要求检查其返回值。这体现了防御性编程的思想。 - 处理错误通常意味着记录日志并终止程序(如示例中的
perror()
+exit()
),因为如果连初始化信号集都失败,程序很可能处于严重错误状态。
- 虽然
- 与实时信号的关系:
sigemptyset()
对标准信号 (1-31) 和实时信号 (34-64,SIGRTMIN
-SIGRTMAX
) 的处理方式完全相同。它只是将所有信号对应的位清零。- 使用
sigaddset()
添加实时信号时,需要指定具体的信号编号(如SIGRTMIN + 5
)。sigemptyset()
为添加它们做好了准备。
- 最佳实践:
- 立即初始化: 在声明
sigset_t
变量的下一行就调用sigemptyset()
或sigfillset()
进行初始化。 - 检查返回值: 总是检查
sigemptyset()
的返回值。 - 命名清晰: 给
sigset_t
变量起有意义的名字(如block_set
,wait_set
,old_mask
),提高代码可读性。 - 作用域最小化: 尽量在需要使用信号集的局部作用域内声明和初始化它。
- 配合其他函数: 理解
sigemptyset()
是信号集操作的基础,它需要与sigaddset()
,sigdelset()
,sigismember()
,sigprocmask()
,sigaction()
等函数配合使用才能发挥完整作用。
- 立即初始化: 在声明
7. 总结
sigemptyset()
是 Linux/Unix 信号编程中一个看似简单但至关重要的函数。它的核心职责是确保 sigset_t
信号集从一个已知的、安全的空状态开始。忘记调用 sigemptyset()
(或 sigfillset()
)是信号处理代码中常见且危险的错误源头,会导致程序行为诡异且难以调试。深刻理解 sigset_t
的位掩码本质和内存中随机数据的危害,是认识到 sigemptyset()
必要性的关键。遵循“声明即初始化”和“检查返回值”的最佳实践,是编写健壮、可靠的信号处理代码的基础。虽然它本身功能单一,但它是构建复杂信号控制逻辑不可或缺的第一块基石。
sigfillset
函数
我们来详细了解一下 Linux 系统中的 sigfillset()
函数。它与 sigemptyset()
相对应,是构建和操作信号集 (sigset_t
) 的另一个基础且关键的函数。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigfillset(sigset_t *set);
-
作用:
sigfillset()
用于初始化一个sigset_t
类型的信号集,将其设置为满集,即该信号集包含当前系统支持的所有信号(除了两个特殊的不可操作信号SIGKILL
和SIGSTOP
)。- 核心概念: 与
sigemptyset()
初始化一个空集相反,sigfillset()
初始化一个包含所有可能信号的集合。这是信号集操作的另一个起点。 - 为什么需要它? 在需要操作绝大多数信号的场景下,使用
sigfillset()
作为起点比逐个添加所有信号更高效、更不容易出错。常见的场景包括:- 创建一个阻塞几乎所有信号的屏蔽掩码(例如,在创建“专用信号处理线程”之前)。
- 创建一个等待几乎所有信号的集合(虽然不常见)。
- 创建一个表示“所有信号”的集合用于比较或其他操作。
- 核心概念: 与
2. 参数详解
sigset_t *set
: 这是一个输入输出参数。指向需要被初始化为满集的sigset_t
变量。- 你需要预先声明一个
sigset_t
变量。 - 调用
sigfillset(&your_sigset_var)
后,your_sigset_var
的内容将被设置为包含所有支持的信号(SIGKILL
和SIGSTOP
除外)。
- 你需要预先声明一个
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因。- 与
sigemptyset()
类似,失败非常罕见,通常只会在set
是无效指针时发生EINVAL
。
- 与
4. 深入理解:满集的意义与 SIGKILL
/SIGSTOP
的特殊性
sigset_t
的位掩码本质: 如前所述,sigset_t
内部是一个位掩码,每一位对应一个信号编号。sigfillset()
的操作: 该函数遍历系统支持的所有有效信号编号(从 1 到NSIG-1
,NSIG
定义在<signal.h>
中,表示信号总数+1),并将sigset_t
中对应这些信号编号的位全部设置为 1。SIGKILL
(信号 9) 和SIGSTOP
(信号 19) 的例外:- 这两个信号是操作系统内核用来强制终止 (
SIGKILL
) 或停止 (SIGSTOP
) 进程的终极手段。 - POSIX 标准规定,任何进程(或线程)都不能阻塞、捕获或忽略
SIGKILL
和SIGSTOP
。 - 因此,
sigfillset()
(以及sigaddset()
,sigdelset()
) 在操作信号集时,会静默忽略对SIGKILL
和SIGSTOP
的设置请求。sigfillset()
返回的满集不包含SIGKILL
和SIGSTOP
位。 - 同样,
sigprocmask()
,pthread_sigmask()
,sigaction()
等函数也会忽略对这两个信号的阻塞或捕获设置。
- 这两个信号是操作系统内核用来强制终止 (
- 为什么满集有用?
- 效率: 一次性设置所有信号比循环调用
sigaddset()
添加几十个信号高效得多。 - 健壮性: 确保包含了所有当前和未来可能添加的信号(只要它们不是
SIGKILL
/SIGSTOP
)。避免因遗漏信号导致逻辑不完整。 - 表达清晰: 代码意图明确表示“所有信号”。
- 效率: 一次性设置所有信号比循环调用
5. 使用步骤与示例
sigfillset()
通常用于需要操作大部分信号的场景:
- 声明信号集变量:
sigset_t all_signals;
- 初始化为满集:
if (sigfillset(&all_signals) == -1) { /* 错误处理 */ }
- (可选) 移除特定信号: 使用
sigdelset(&all_signals, signo)
将不需要操作的特定信号signo
(如允许程序被SIGTERM
终止) 从集合中移除。 - 使用信号集: 将初始化并可能修改后的
all_signals
传递给其他信号函数。
示例 1:基础用法 - 阻塞所有信号(除 SIGTERM)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set, old_set;// *** 关键步骤 1: 初始化信号集为包含所有信号 (除了 SIGKILL/SIGSTOP) ***if (sigfillset(&block_set) == -1) {perror("sigfillset");exit(EXIT_FAILURE);}// *** 关键步骤 2 (可选但推荐): 从阻塞集中移除 SIGTERM ***// 这样进程仍然可以被 kill -TERM 正常终止if (sigdelset(&block_set, SIGTERM) == -1) {perror("sigdelset");exit(EXIT_FAILURE);}// 通常也会移除 SIGINT (Ctrl+C) 方便本地测试,这里为了演示只移除了 SIGTERM// 使用信号集阻塞 block_set 中的所有信号if (sigprocmask(SIG_BLOCK, &block_set, &old_set) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("All signals (except SIGTERM) are blocked.\n");printf("Try sending signals (e.g., kill -USR1 %d). They won't be delivered.\n", getpid());printf("But kill -TERM %d will still terminate the process.\n", getpid());printf("Sleeping for 30 seconds...\n");sleep(30);printf("Unblocking signals...\n");// 恢复旧的信号屏蔽掩码 (解除所有阻塞)if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask (restore)");exit(EXIT_FAILURE);}printf("Signals unblocked. Waiting for exit signal.\n");pause(); // 等待信号return 0;
}
示例 2:创建专用信号处理线程 (主线程阻塞所有信号)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>void *signal_handler_thread(void *arg) {sigset_t wait_set;int sig;// 这个线程只关心 SIGINT 和 SIGTERMsigemptyset(&wait_set);sigaddset(&wait_set, SIGINT);sigaddset(&wait_set, SIGTERM);printf("Signal Handler Thread [%lu] ready.\n", (unsigned long)pthread_self());while (1) {if (sigwait(&wait_set, &sig) != 0) {perror("sigwait");break;}switch (sig) {case SIGINT:printf("Handler [%lu]: Received SIGINT. Cleaning up...\n", (unsigned long)pthread_self());// ... 清理逻辑 ...break;case SIGTERM:printf("Handler [%lu]: Received SIGTERM. Exiting gracefully...\n", (unsigned long)pthread_self());// ... 清理并通知其他线程退出 ...exit(EXIT_SUCCESS);break;default: // 理论上不会发生,因为 wait_set 只包含 SIGINT/SIGTERMprintf("Handler [%lu]: Received unexpected signal %d\n", (unsigned long)pthread_self(), sig);}}return NULL;
}int main() {pthread_t sig_tid;sigset_t all_signals;// *** 关键步骤 1: 在主线程中初始化信号集为包含所有信号 ***if (sigfillset(&all_signals) == -1) {perror("sigfillset");exit(EXIT_FAILURE);}// 注意:sigfillset 已经排除了 SIGKILL/SIGSTOP,我们不需要(也不能)移除它们// *** 关键步骤 2: 在主线程中阻塞所有信号 (除了 SIGKILL/SIGSTOP,它们无法阻塞) ***if (pthread_sigmask(SIG_SETMASK, &all_signals, NULL) != 0) {perror("pthread_sigmask");exit(EXIT_FAILURE);}printf("Main Thread [%lu]: Blocked ALL signals (except SIGKILL/SIGSTOP). New threads will inherit this mask.\n", (unsigned long)pthread_self());// *** 关键步骤 3: 创建专用信号处理线程 ***if (pthread_create(&sig_tid, NULL, signal_handler_thread, NULL) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}// 创建其他工作线程 (它们继承了主线程的屏蔽掩码,即阻塞所有信号)// void *worker_thread(void *arg) { ... (工作逻辑,不会被信号中断) ... }// pthread_t worker_tid;// if (pthread_create(&worker_tid, NULL, worker_thread, NULL) != 0) { ... }printf("Main Thread [%lu]: Process running. Send SIGINT (Ctrl+C) or SIGTERM (kill) to test.\n", (unsigned long)pthread_self());printf("Note: SIGKILL (kill -9) will still terminate the process immediately.\n");// 主线程和工作线程做它们的事情,不会被普通信号中断// 所有发送给进程的信号 (kill()) 会被阻塞,最终由专用线程通过 sigwait() 处理pthread_join(sig_tid, NULL); // 等待信号处理线程结束 (通常由 SIGTERM 触发 exit)// ... 清理其他工作线程 ...return 0;
}
6. 重要注意事项与使用细节 (深度解析)
SIGKILL
和SIGSTOP
的绝对性:- 这是
sigfillset()
行为中最重要的细节。它返回的集合明确不包含SIGKILL
和SIGSTOP
。 - 任何试图通过
sigaddset()
将它们添加到集合的操作都会被静默忽略。 - 任何试图通过
sigprocmask()
或pthread_sigmask()
阻塞它们的操作也会被静默忽略。 - 它们不能被
signal()
或sigaction()
捕获或忽略。 - 理解这一点对于系统安全和程序健壮性至关重要。
SIGKILL
是确保管理员总能终止失控进程的最后手段。
- 这是
- 初始化是强制性的 (与
sigemptyset
同理): 虽然sigfillset()
初始化的是满集,但初始化sigset_t
变量的原则不变:绝对不要使用未初始化的sigset_t
变量。选择sigfillset()
还是sigemptyset()
取决于你的初始需求是“所有信号”还是“没有信号”。 sigfillset()
vssigemptyset()
:选择策略:sigfillset()
+sigdelset()
: 当你需要操作绝大多数信号,只排除少数几个信号时,这是最高效、最清晰的方式。例如:- 阻塞所有信号(除了允许终止的
SIGTERM
)。 - 设置信号处理程序的
sa_mask
为阻塞所有信号(除了处理程序本身处理的信号,通常由内核自动添加)。
- 阻塞所有信号(除了允许终止的
sigemptyset()
+sigaddset()
: 当你只需要操作少数几个特定信号时,这是更直接的方式。例如:- 只阻塞
SIGINT
和SIGQUIT
。 - 设置
sa_mask
只阻塞SIGUSR1
(防止它在处理SIGUSR2
时中断)。
- 只阻塞
- 线程安全性与可重入性:
- 与
sigemptyset()
类似,sigfillset()
的实现通常是线程安全的。 - 异步信号安全性: POSIX 标准没有要求
sigfillset()
是异步信号安全的。在主流 Linux 实现 (glibc) 中,它可能是安全的(因为它通常只是遍历一个预定义的信号列表并设置位)。然而,强烈建议避免在信号处理程序中调用sigfillset()
(以及sigemptyset
,sigaddset
,sigdelset
)。信号处理程序应保持极简。
- 与
- 信号集的范围 (标准信号 vs 实时信号):
sigfillset()
会包含系统支持的所有标准信号 (1-31) 和所有实时信号 (34-64,SIGRTMIN
-SIGRTMAX
)。- 实时信号的数量 (
SIGRTMAX - SIGRTMIN + 1
) 是系统定义的(可通过sysconf(_SC_RTSIG_MAX)
查询)。sigfillset()
会包含所有这些实时信号。
- 错误处理:
- 虽然失败概率极低,但良好的编程实践要求检查其返回值。这体现了代码的严谨性。处理错误通常意味着终止或回退到安全状态。
- 与
sigpending()
的关系:- 如果你使用
sigfillset()
创建了一个阻塞所有信号的屏蔽掩码,那么发送给进程的任何信号(SIGKILL
/SIGSTOP
除外)都会进入未决状态。 - 你可以使用
sigpending()
来查看哪些信号是未决的。但是,在“专用信号处理线程”模式中,通常使用sigwait()
来同步处理这些未决信号,而不是依赖sigpending()
。
- 如果你使用
- 最佳实践:
- 明确意图: 当你的逻辑需要“所有信号”时,果断使用
sigfillset()
,它比手动添加所有信号更清晰、更健壮。 - 配合
sigdelset()
: 理解sigfillset()
通常不是终点,后续往往需要用sigdelset()
剔除你不想操作或需要特殊处理的信号(如SIGTERM
)。 - 理解
SIGKILL
/SIGSTOP
: 时刻牢记这两个信号的绝对特权,它们不受任何信号集操作的影响。 - 应用于正确场景:
sigfillset()
最经典的场景就是在创建“专用信号处理线程”之前,由主线程(或任何负责创建该线程的线程)阻塞所有信号。
- 明确意图: 当你的逻辑需要“所有信号”时,果断使用
7. 总结
sigfillset()
是 Linux/Unix 信号编程中用于快速初始化包含所有可用信号的集合的关键函数。它与 sigemptyset()
构成了信号集操作的两个基础起点。其核心价值在于高效地表示“所有信号”这一概念,特别适用于需要批量操作绝大多数信号的场景,如全局信号屏蔽的设置。深刻理解它对 SIGKILL
和 SIGSTOP
的特殊处理(排除在外)是正确使用的前提。通过结合 sigdelset()
进行精细化调整,sigfillset()
为构建健壮、清晰的信号控制逻辑(尤其是“专用信号处理线程”模式)提供了强大的基础。遵循初始化原则、检查返回值并理解其与不可屏蔽信号的关系,是有效运用 sigfillset()
的关键。
sigaddset
函数
我们来详细了解一下 Linux 系统中的 sigaddset()
函数。它是构建和操作信号集 (sigset_t
) 的核心函数之一,用于精确控制哪些信号被包含在集合中。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigaddset(sigset_t *set, int signum);
-
作用:
sigaddset()
用于将指定的信号signum
添加到信号集set
中。- 核心概念:
sigset_t
是一个表示信号集合的数据结构(通常是位掩码)。sigaddset()
操作这个数据结构,将代表信号signum
的位置为 1,表示该信号现在属于集合set
。 - 为什么需要它? 在初始化一个信号集(使用
sigemptyset()
或sigfillset()
)后,sigaddset()
是向该集合添加特定信号的主要方法。你需要使用它来构建自定义的信号集,用于:- 设置信号屏蔽掩码 (
sigprocmask()
,pthread_sigmask
):指定要阻塞或解除阻塞的信号。 - 设置信号处理程序的阻塞掩码 (
sigaction.sa_mask
):指定在执行该处理程序期间需要阻塞的信号。 - 定义等待的信号集 (
sigsuspend()
,sigwait()
,sigwaitinfo()
):指定要等待哪些信号。 - 检查信号是否在未决集中 (
sigismember()
withsigpending()
):虽然sigpending()
填充集合,但sigismember()
需要检查具体信号。
- 设置信号屏蔽掩码 (
- 核心概念:
2. 参数详解
sigset_t *set
: 这是一个输入输出参数。指向需要修改的sigset_t
变量。该变量必须已经通过sigemptyset()
或sigfillset()
进行了初始化。int signum
: 要添加到信号集set
中的信号编号。可以是标准信号(如SIGINT
,SIGTERM
)或实时信号(如SIGRTMIN
,SIGRTMIN + 1
)。信号编号定义在<signal.h>
中(通常使用符号常量)。
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因。EINVAL
: 这是最常见的错误,表示signum
不是一个有效的信号编号(例如,小于 1 或大于系统支持的最大信号编号NSIG-1
)。也可能是set
指向无效内存(但更可能导致段错误)。
4. 深入理解:信号集的位操作与 signum
的有效性
sigset_t
的位掩码本质: 如前所述,sigset_t
内部结构通常是一个位数组(bit array)。每个信号编号N
对应数组中的一个特定位(bit)。位的位置P
通常由N - 1
或类似公式计算得出(因为信号编号从 1 开始)。sigaddset()
的内部操作:- 验证
signum
: 检查signum
是否在有效范围内(1 <= signum < NSIG
)。如果无效,返回EINVAL
。 - 处理
SIGKILL
/SIGSTOP
(可选但常见): 如果signum
是SIGKILL
(9) 或SIGSTOP
(19),大多数实现会静默忽略该操作并返回成功 (0)。因为这两个信号不能被阻塞或捕获,将它们添加到阻塞集是无效操作,但库函数通常选择忽略而非报错。 - 计算位位置: 根据
signum
的值,计算出在sigset_t
内部位数组中对应的位索引P
。 - 设置位: 将
sigset_t
位数组中索引为P
的位设置为 1。
- 验证
signum
的有效性至关重要: 传递无效的signum
会导致EINVAL
错误。使用符号常量(如SIGINT
)而非硬编码数字(如2
)可以大大提高代码的可读性和可移植性。实时信号应使用SIGRTMIN + offset
的形式。
5. 使用步骤与示例
sigaddset()
总是在初始化信号集之后使用:
- 声明并初始化信号集:
sigset_t my_set; if (sigemptyset(&my_set) == -1) { ... }
(或sigfillset
). - 添加信号:
if (sigaddset(&my_set, SIGUSR1) == -1) { ... }
- (可选) 添加更多信号:
if (sigaddset(&my_set, SIGUSR2) == -1) { ... }
- 使用信号集: 将
my_set
传递给sigprocmask
,pthread_sigmask
,sigaction
,sigsuspend
,sigwait
等函数。
示例 1:基础用法 - 阻塞 SIGUSR1 和 SIGUSR2
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set, old_set;// *** 步骤 1: 初始化空信号集 ***if (sigemptyset(&block_set) == -1) {perror("sigemptyset");exit(EXIT_FAILURE);}// *** 步骤 2: 添加 SIGUSR1 到信号集 ***if (sigaddset(&block_set, SIGUSR1) == -1) {perror("sigaddset (SIGUSR1)");exit(EXIT_FAILURE);}// *** 步骤 3: 添加 SIGUSR2 到信号集 ***if (sigaddset(&block_set, SIGUSR2) == -1) {perror("sigaddset (SIGUSR2)");exit(EXIT_FAILURE);}// 使用信号集阻塞 SIGUSR1 和 SIGUSR2if (sigprocmask(SIG_BLOCK, &block_set, &old_set) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("SIGUSR1 and SIGUSR2 are blocked. Send them (kill -USR1 %d; kill -USR2 %d).\n", getpid(), getpid());printf("They will be pending until unblocked. Sleeping for 10 seconds...\n");sleep(10);// 检查未决信号 (演示 sigpending 和 sigismember)sigset_t pending_set;if (sigpending(&pending_set) == -1) {perror("sigpending");exit(EXIT_FAILURE);}if (sigismember(&pending_set, SIGUSR1)) {printf("SIGUSR1 is pending!\n");}if (sigismember(&pending_set, SIGUSR2)) {printf("SIGUSR2 is pending!\n");}printf("Unblocking SIGUSR1 and SIGUSR2...\n");// 解除阻塞 (使用相同的信号集和 SIG_UNBLOCK)if (sigprocmask(SIG_UNBLOCK, &block_set, NULL) == -1) {perror("sigprocmask (unblock)");exit(EXIT_FAILURE);}printf("Signals unblocked. If pending, they should be delivered now.\n");sleep(1); // 给信号处理程序执行时间 (假设有处理程序)return 0;
}
示例 2:设置信号处理程序的阻塞掩码 (防止处理程序被自身信号中断)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h> // for strsignalvoid handler(int sig) {// 获取信号名称const char *sig_name = strsignal(sig);printf("Handler: Received %s (%d). Starting work...\n", sig_name, sig);// 模拟一个耗时操作 (在此期间,我们希望防止同一个信号再次中断)for (int i = 0; i < 5; i++) {printf("Handler: Working (%d/%d)...\n", i+1, 5);sleep(1);}printf("Handler: Finished processing %s.\n", sig_name);
}int main() {struct sigaction sa;// 设置处理函数sa.sa_handler = handler;sa.sa_flags = 0; // 无特殊标志// *** 关键步骤 1: 初始化 sa_mask 为空 ***if (sigemptyset(&sa.sa_mask) == -1) {perror("sigemptyset (sa_mask)");exit(EXIT_FAILURE);}// *** 关键步骤 2: 在处理程序执行期间阻塞同一个信号 ***// 防止处理程序被同一个信号的再次递送中断 (实现非重入处理)if (sigaddset(&sa.sa_mask, SIGUSR1) == -1) { // 阻塞 SIGUSR1 自身perror("sigaddset (sa_mask SIGUSR1)");exit(EXIT_FAILURE);}// *** 关键步骤 3 (可选): 也可以阻塞其他相关信号 ***if (sigaddset(&sa.sa_mask, SIGUSR2) == -1) { // 也阻塞 SIGUSR2perror("sigaddset (sa_mask SIGUSR2)");exit(EXIT_FAILURE);}// 注册 SIGUSR1 的处理程序if (sigaction(SIGUSR1, &sa, NULL) == -1) {perror("sigaction (SIGUSR1)");exit(EXIT_FAILURE);}printf("Process PID: %d\n", getpid());printf("Send multiple SIGUSR1 signals quickly (e.g., kill -USR1 %d; kill -USR1 %d; ...).\n", getpid(), getpid());printf("The handler will block SIGUSR1 (and SIGUSR2) during its execution, preventing re-entrancy.\n");printf("The second SIGUSR1 will be pending until the first handler finishes.\n");while (1) {pause(); // 等待信号}return 0;
}
示例 3:添加实时信号
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t wait_set;// 初始化空信号集if (sigemptyset(&wait_set) == -1) {perror("sigemptyset");exit(EXIT_FAILURE);}// 添加实时信号 SIGRTMIN 和 SIGRTMIN+1if (sigaddset(&wait_set, SIGRTMIN) == -1) {perror("sigaddset (SIGRTMIN)");exit(EXIT_FAILURE);}if (sigaddset(&wait_set, SIGRTMIN + 1) == -1) {perror("sigaddset (SIGRTMIN+1)");exit(EXIT_FAILURE);}printf("Waiting for signals %d (SIGRTMIN) or %d (SIGRTMIN+1)...\n", SIGRTMIN, SIGRTMIN + 1);printf("Send using: kill -%d %d or kill -%d %d\n", SIGRTMIN, getpid(), SIGRTMIN + 1, getpid());int sig;while (1) {// 同步等待集合中的信号 (sigwait 会原子地解除阻塞 wait_set 中的信号并等待)if (sigwait(&wait_set, &sig) != 0) {perror("sigwait");continue; // 或退出}if (sig == SIGRTMIN) {printf("Received SIGRTMIN\n");} else if (sig == SIGRTMIN + 1) {printf("Received SIGRTMIN+1\n");} else {printf("Received unexpected signal: %d\n", sig); // 理论上不会发生}}return 0;
}
6. 重要注意事项与使用细节 (深度解析)
- 初始化是前提: 这是
sigaddset()
使用中最关键的细节。传递给sigaddset()
的set
参数必须指向一个已经通过sigemptyset()
或sigfillset()
初始化过的sigset_t
变量。向一个包含随机垃圾数据的未初始化sigset_t
添加信号会导致完全不可预测和危险的后果(可能阻塞/操作了错误的信号集)。 SIGKILL
和SIGSTOP
的特殊性:- 如前所述,
sigaddset()
(以及sigdelset()
,sigfillset()
) 在操作SIGKILL
(9) 和SIGSTOP
(19) 时,大多数实现会静默忽略操作并返回成功 (0)。 - 为什么? 因为这两个信号不能被阻塞、捕获或忽略。将它们添加到阻塞集是无效操作。库函数选择忽略而非报错 (
EINVAL
) 可能是为了兼容性或简化使用(避免用户代码需要特殊处理这两个信号)。 - 关键启示: 你不能依赖
sigaddset()
来真正将SIGKILL
或SIGSTOP
添加到集合中。它们永远不会被实际阻塞。
- 如前所述,
- 错误处理 (
EINVAL
):- 最常见的错误是传递了无效的
signum
。务必检查sigaddset()
的返回值! - 使用符号常量 (
SIGXXX
) 而非魔数 (2
,15
等) 可以显著减少EINVAL
错误的风险。 - 对于实时信号,确保
signum
在SIGRTMIN
到SIGRTMAX
的范围内。SIGRTMIN + N
的值需要计算后检查是否有效。
- 最常见的错误是传递了无效的
- 线程安全性与可重入性:
sigaddset()
函数本身通常是线程安全的。它只操作传入的set
指向的内存,不依赖全局状态(标准库实现应保证这点)。- 异步信号安全性: POSIX 标准没有要求
sigaddset()
是异步信号安全的(即可以在信号处理程序中安全调用)。虽然其实现(设置一个位)在大多数系统上可能是安全的,但强烈建议避免在信号处理程序中调用sigaddset()
(以及sigdelset
,sigemptyset
,sigfillset
)。信号处理程序应保持极简,只设置原子标志、调用异步信号安全函数(如write
)或_exit
。在信号处理程序中修改全局信号集变量是危险且不必要的。
sigset_t
的生命周期与作用域:sigaddset()
修改的是你提供的sigset_t
变量本身的内存。- 如果你在函数内声明了一个局部
sigset_t
变量并调用sigaddset()
,修改只在该变量作用域内有效。 - 如果你需要跨函数使用配置好的信号集,可以:
- 传递
sigset_t
的值(按值传递会复制整个结构体)。 - 传递指向
sigset_t
的指针。 - 将其存储在全局变量或结构体成员中。
- 传递
- 与
sigprocmask()
/pthread_sigmask()
的关系:sigaddset()
只是构建信号集。它本身不改变进程或线程当前的信号屏蔽状态。- 要实际应用信号集(如阻塞信号),你需要将构建好的
sigset_t
变量传递给sigprocmask()
(进程) 或pthread_sigmask()
(线程),并指定操作方式 (SIG_BLOCK
,SIG_UNBLOCK
,SIG_SETMASK
)。
- 实时信号 (
SIGRTMIN
-SIGRTMAX
):sigaddset()
可以像添加标准信号一样添加实时信号。- 使用
SIGRTMIN + offset
的形式指定具体的实时信号编号(offset
从 0 开始)。 - 确保
offset
使得SIGRTMIN + offset <= SIGRTMAX
。 - 实时信号支持排队,将它们添加到阻塞集或等待集的行为与标准信号相同,但内核会为多次发生的同一实时信号维护多个未决实例。
- 最佳实践:
- 初始化先行: 在声明
sigset_t
后立即调用sigemptyset()
或sigfillset()
。 - 检查返回值: 总是检查
sigaddset()
的返回值,特别是当signum
是变量或计算得出时(如实时信号)。 - 使用符号常量: 始终使用
<signal.h>
中定义的信号常量 (SIGINT
,SIGTERM
,SIGUSR1
,SIGRTMIN
等)。 - 避免信号处理程序: 不要在异步信号处理程序中调用
sigaddset()
。 - 明确意图: 给信号集变量起有意义的名字(如
block_set
,wait_set
,handler_mask
)。
- 初始化先行: 在声明
7. 总结
sigaddset()
是 Linux/Unix 信号编程中用于精确构建信号集的基础函数。它的核心作用是将指定的信号添加到 sigset_t
集合中。理解并严格遵守其前提条件(必须初始化 sigset_t
)和错误处理(检查 EINVAL
)是避免灾难性错误的关键。认识到它对 SIGKILL
/SIGSTOP
的特殊处理(静默忽略)和避免在信号处理程序中使用它,是编写健壮代码的重要方面。通过结合 sigemptyset()
或 sigfillset()
以及 sigdelset()
, sigaddset()
为定义进程和线程的信号屏蔽、信号处理程序的执行环境以及等待的信号集提供了精细的控制能力,是信号处理机制中不可或缺的工具。
sigdelset
函数
我们来详细了解一下 Linux 系统中的 sigdelset()
函数。它是 sigaddset()
的对应操作,用于从信号集 (sigset_t
) 中精确移除特定的信号,是构建精细化信号控制逻辑的关键组成部分。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigdelset(sigset_t *set, int signum);
-
作用:
sigdelset()
用于将指定的信号signum
移除出信号集set
。- 核心概念:
sigset_t
是一个表示信号集合的数据结构(通常是位掩码)。sigdelset()
操作这个数据结构,将代表信号signum
的位清零(设置为 0),表示该信号不再属于集合set
。 - 为什么需要它? 在初始化一个信号集(使用
sigemptyset()
或sigfillset()
)后,sigdelset()
是从该集合中排除特定信号的主要方法。你需要使用它来定制信号集,用于:- 在基于
sigfillset()
创建的满集中排除不需要操作的信号(例如,允许SIGTERM
终止进程)。 - 修改一个已有的信号集(例如,动态调整线程的阻塞信号集)。
- 确保某些信号不被阻塞或不被等待。
- 在基于
- 核心概念:
2. 参数详解
sigset_t *set
: 这是一个输入输出参数。指向需要修改的sigset_t
变量。该变量必须已经通过sigemptyset()
或sigfillset()
进行了初始化。int signum
: 要从信号集set
中移除的信号编号。可以是标准信号(如SIGINT
,SIGTERM
)或实时信号(如SIGRTMIN
,SIGRTMIN + 1
)。信号编号定义在<signal.h>
中(使用符号常量)。
3. 返回值
- 成功: 返回
0
。 - 失败: 返回
-1
,并设置errno
以指示错误原因。EINVAL
: 这是最常见的错误,表示signum
不是一个有效的信号编号(例如,小于 1 或大于系统支持的最大信号编号NSIG-1
)。也可能是set
指向无效内存(但更可能导致段错误)。
4. 深入理解:信号集的位操作与 signum
的有效性
sigset_t
的位掩码本质: 如前所述,sigset_t
内部结构通常是一个位数组(bit array)。每个信号编号N
对应数组中的一个特定位(bit)。位的位置P
通常由N - 1
或类似公式计算得出。sigdelset()
的内部操作:- 验证
signum
: 检查signum
是否在有效范围内(1 <= signum < NSIG
)。如果无效,返回EINVAL
。 - 处理
SIGKILL
/SIGSTOP
(可选但常见): 如果signum
是SIGKILL
(9) 或SIGSTOP
(19),大多数实现会静默忽略该操作并返回成功 (0)。因为这两个信号不能被阻塞或捕获,将它们从阻塞集移除是无效操作(它们本来也不在有效阻塞集中),库函数通常选择忽略而非报错。 - 计算位位置: 根据
signum
的值,计算出在sigset_t
内部位数组中对应的位索引P
。 - 清除位: 将
sigset_t
位数组中索引为P
的位设置为 0。
- 验证
signum
的有效性至关重要: 传递无效的signum
会导致EINVAL
错误。使用符号常量(如SIGINT
)而非硬编码数字(如2
)是避免此错误的最佳实践。
5. 使用步骤与示例
sigdelset()
总是在初始化信号集之后使用,通常与 sigfillset()
配合:
- 声明并初始化信号集:
sigset_t my_set; if (sigfillset(&my_set) == -1) { ... }
(或sigemptyset
,但sigdelset
更常用于满集后剔除)。 - 移除信号:
if (sigdelset(&my_set, SIGTERM) == -1) { ... }
- (可选) 移除更多信号:
if (sigdelset(&my_set, SIGINT) == -1) { ... }
- 使用信号集: 将
my_set
传递给sigprocmask
,pthread_sigmask
,sigaction
,sigsuspend
,sigwait
等函数。
示例 1:基础用法 - 阻塞所有信号(除 SIGTERM 和 SIGINT)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set, old_set;// *** 步骤 1: 初始化信号集为包含所有信号 (除了 SIGKILL/SIGSTOP) ***if (sigfillset(&block_set) == -1) {perror("sigfillset");exit(EXIT_FAILURE);}// *** 步骤 2: 移除 SIGTERM (允许正常终止) ***if (sigdelset(&block_set, SIGTERM) == -1) {perror("sigdelset (SIGTERM)");exit(EXIT_FAILURE);}// *** 步骤 3: 移除 SIGINT (允许 Ctrl+C 本地测试) ***if (sigdelset(&block_set, SIGINT) == -1) {perror("sigdelset (SIGINT)");exit(EXIT_FAILURE);}// 使用信号集阻塞 block_set 中的所有信号 (即除了 SIGTERM 和 SIGINT)if (sigprocmask(SIG_BLOCK, &block_set, &old_set) == -1) {perror("sigprocmask");exit(EXIT_FAILURE);}printf("All signals blocked EXCEPT SIGTERM and SIGINT.\n");printf("Try sending signals (e.g., kill -USR1 %d). They won't be delivered.\n", getpid());printf("But kill -TERM %d or Ctrl+C will still work.\n", getpid());printf("Sleeping for 30 seconds...\n");sleep(30);printf("Unblocking signals...\n");// 恢复旧的信号屏蔽掩码 (解除所有阻塞)if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {perror("sigprocmask (restore)");exit(EXIT_FAILURE);}printf("Signals unblocked. Waiting for exit signal.\n");pause(); // 等待信号return 0;
}
示例 2:动态调整线程信号屏蔽 (移除阻塞)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>sigset_t global_block_set; // 全局信号集 (需线程安全访问,这里简化)void *worker_thread(void *arg) {int thread_id = *(int *)arg;printf("Worker Thread %d [%lu] started. Initially blocking SIGUSR1/SIGUSR2.\n",thread_id, (unsigned long)pthread_self());// 应用全局阻塞集 (假设它包含 SIGUSR1 和 SIGUSR2)if (pthread_sigmask(SIG_SETMASK, &global_block_set, NULL) != 0) {perror("pthread_sigmask (worker set)");pthread_exit(NULL);}// 模拟第一阶段工作 (需要阻塞 SIGUSR1/SIGUSR2)for (int i = 0; i < 3; i++) {printf("Worker %d [%lu]: Phase 1 - Working (%d/3)...\n",thread_id, (unsigned long)pthread_self(), i+1);sleep(1);}// *** 关键步骤: 动态移除对 SIGUSR1 的阻塞 ***sigset_t unblock_set;if (sigemptyset(&unblock_set) == -1) { // 初始化空集perror("sigemptyset (unblock_set)");pthread_exit(NULL);}if (sigdelset(&unblock_set, SIGUSR1) == -1) { // 准备移除 SIGUSR1perror("sigdelset (SIGUSR1)");pthread_exit(NULL);}// 从当前屏蔽掩码中移除 SIGUSR1 (解除阻塞)if (pthread_sigmask(SIG_UNBLOCK, &unblock_set, NULL) != 0) {perror("pthread_sigmask (worker unblock SIGUSR1)");pthread_exit(NULL);}printf("Worker %d [%lu]: SIGUSR1 UNBLOCKED now. SIGUSR2 still blocked.\n",thread_id, (unsigned long)pthread_self());// 模拟第二阶段工作 (可以接收 SIGUSR1)for (int i = 0; i < 3; i++) {printf("Worker %d [%lu]: Phase 2 - Working (%d/3)...\n",thread_id, (unsigned long)pthread_self(), i+1);sleep(1);}printf("Worker %d [%lu] finished.\n", thread_id, (unsigned long)pthread_self());pthread_exit(NULL);
}int main() {pthread_t tid1, tid2;int id1 = 1, id2 = 2;// *** 初始化全局阻塞集 (包含 SIGUSR1 和 SIGUSR2) ***if (sigemptyset(&global_block_set) == -1) {perror("sigemptyset (global_block_set)");exit(EXIT_FAILURE);}if (sigaddset(&global_block_set, SIGUSR1) == -1 ||sigaddset(&global_block_set, SIGUSR2) == -1) {perror("sigaddset");exit(EXIT_FAILURE);}// 主线程也阻塞这些信号 (可选)if (pthread_sigmask(SIG_BLOCK, &global_block_set, NULL) != 0) {perror("pthread_sigmask (main)");exit(EXIT_FAILURE);}// 创建两个工作线程 (它们会继承主线程的屏蔽掩码或稍后设置 global_block_set)if (pthread_create(&tid1, NULL, worker_thread, &id1) != 0 ||pthread_create(&tid2, NULL, worker_thread, &id2) != 0) {perror("pthread_create");exit(EXIT_FAILURE);}printf("Main Thread [%lu]: Workers running. Send SIGUSR1 or SIGUSR2 to test.\n",(unsigned long)pthread_self());printf("Note: Workers unblock SIGUSR1 during their second phase.\n");pthread_join(tid1, NULL);pthread_join(tid2, NULL);printf("Main Thread exiting.\n");return 0;
}
示例 3:从等待信号集中排除特定信号
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t wait_set;int sig;// *** 步骤 1: 初始化信号集为包含所有信号 (除了 SIGKILL/SIGSTOP) ***if (sigfillset(&wait_set) == -1) {perror("sigfillset");exit(EXIT_FAILURE);}// *** 步骤 2: 移除我们不关心的信号 (例如 SIGHUP, SIGPIPE) ***if (sigdelset(&wait_set, SIGHUP) == -1) { // 终端挂起perror("sigdelset (SIGHUP)");exit(EXIT_FAILURE);}if (sigdelset(&wait_set, SIGPIPE) == -1) { // 管道破裂perror("sigdelset (SIGPIPE)");exit(EXIT_FAILURE);}// 也可以移除 SIGALRM 如果程序使用定时器但不希望在此处等待它printf("Waiting for ANY signal EXCEPT SIGHUP and SIGPIPE...\n");printf("Send signals to PID %d (e.g., kill -USR1 %d, kill -TERM %d, Ctrl+C)\n", getpid(), getpid(), getpid());while (1) {// 同步等待集合中的信号 (sigwait 会原子地解除阻塞 wait_set 中的信号并等待)if (sigwait(&wait_set, &sig) != 0) {perror("sigwait");continue; // 或退出}printf("Received signal: %d (%s)\n", sig, strsignal(sig));if (sig == SIGINT || sig == SIGTERM) {printf("Exiting due to termination signal.\n");break;}// 处理其他信号...}return 0;
}
6. 重要注意事项与使用细节 (深度解析)
- 初始化是绝对前提: 这是
sigdelset()
使用中最关键的细节。传递给sigdelset()
的set
参数必须指向一个已经通过sigemptyset()
或sigfillset()
初始化过的sigset_t
变量。从一个包含随机垃圾数据的未初始化sigset_t
中“移除”信号会导致完全不可预测和危险的后果(可能错误地修改了信号屏蔽)。 SIGKILL
和SIGSTOP
的特殊性:- 如前所述,
sigdelset()
(以及sigaddset()
,sigfillset()
) 在操作SIGKILL
(9) 和SIGSTOP
(19) 时,大多数实现会静默忽略操作并返回成功 (0)。 - 为什么? 因为这两个信号不能被阻塞或捕获。将它们从阻塞集移除是无效操作(它们本来也不在有效阻塞集中),库函数选择忽略而非报错 (
EINVAL
)。 - 关键启示: 你不能依赖
sigdelset()
来实际影响SIGKILL
或SIGSTOP
的状态。它们永远不受信号屏蔽控制。
- 如前所述,
- 错误处理 (
EINVAL
):- 最常见的错误是传递了无效的
signum
。务必检查sigdelset()
的返回值! - 使用符号常量 (
SIGXXX
) 而非魔数 (2
,15
等) 是避免EINVAL
错误的最佳实践。 - 对于实时信号,确保
signum
在SIGRTMIN
到SIGRTMAX
的范围内。
- 最常见的错误是传递了无效的
- 线程安全性与可重入性:
sigdelset()
函数本身通常是线程安全的。它只操作传入的set
指向的内存。- 异步信号安全性: POSIX 标准没有要求
sigdelset()
是异步信号安全的(即可以在信号处理程序中安全调用)。虽然其实现(清除一个位)在大多数系统上可能是安全的,但强烈建议避免在信号处理程序中调用sigdelset()
(以及sigaddset
,sigemptyset
,sigfillset
)。信号处理程序应保持极简。在信号处理程序中修改全局信号集变量是危险且不必要的。
sigset_t
的生命周期与作用域:sigdelset()
修改的是你提供的sigset_t
变量本身的内存。- 修改的作用域取决于变量的作用域(局部变量在函数返回后失效,全局变量一直有效)。
- 如果你需要跨函数使用修改后的信号集,需要传递变量本身或指针。
- 与
sigprocmask()
/pthread_sigmask()
的关系:sigdelset()
只是修改信号集。它本身不改变进程或线程当前的信号屏蔽状态。- 要实际应用修改后的信号集(如解除对特定信号的阻塞),你需要将修改好的
sigset_t
变量传递给sigprocmask()
(进程) 或pthread_sigmask()
(线程),并指定操作方式 (SIG_UNBLOCK
或SIG_SETMASK
)。示例 2 展示了动态解除阻塞。
- 实时信号 (
SIGRTMIN
-SIGRTMAX
):sigdelset()
可以像移除标准信号一样移除实时信号。- 使用
SIGRTMIN + offset
的形式指定具体的实时信号编号。 - 确保
offset
使得SIGRTMIN + offset <= SIGRTMAX
。
- 典型使用模式:
sigfillset()
+sigdelset()
: 这是sigdelset()
最经典的模式。先获得所有信号的集合,然后剔除少数需要特殊处理的信号(如允许终止的信号SIGTERM
、允许调试中断的信号SIGINT
)。这是实现“阻塞除少数信号外的所有信号”的高效方式。- 动态调整: 如示例 2 所示,线程可以在运行时根据其执行阶段,使用
sigdelset()
构建一个要解除阻塞的信号集,然后调用pthread_sigmask(SIG_UNBLOCK, &unblock_set, NULL)
来动态解除对特定信号的阻塞。 - 精细化等待: 如示例 3 所示,在设置
sigsuspend()
或sigwait()
的等待集时,使用sigfillset()
+sigdelset()
可以排除掉那些你不想在该等待点处理的信号(如后台作业忽略SIGHUP
,网络服务器忽略SIGPIPE
)。
- 最佳实践:
- 初始化先行: 在声明
sigset_t
后立即调用sigemptyset()
或sigfillset()
。 - 检查返回值: 总是检查
sigdelset()
的返回值,特别是当signum
是变量或计算得出时。 - 使用符号常量: 始终使用
<signal.h>
中定义的信号常量。 - 避免信号处理程序: 不要在异步信号处理程序中调用
sigdelset()
。 - 明确意图: 给信号集变量起有意义的名字(如
all_except_term_set
,dynamic_unblock_set
)。 - 理解模式: 深刻理解
sigfillset()
+sigdelset()
组合模式的强大之处,它是构建清晰、健壮信号屏蔽逻辑的基石。
- 初始化先行: 在声明
7. 总结
sigdelset()
是 Linux/Unix 信号编程中用于精确裁剪信号集的基础函数。它的核心作用是将指定的信号从 sigset_t
集合中移除。严格遵守其前提条件(必须初始化 sigset_t
)和错误处理(检查 EINVAL
)是编写可靠代码的基础。认识到它对 SIGKILL
/SIGSTOP
的特殊处理(静默忽略)和避免在信号处理程序中使用它,是重要的注意事项。通过与 sigfillset()
的经典组合,sigdelset()
为定义“阻塞除…之外的所有信号”或“等待除…之外的所有信号”这类精细化控制提供了高效、清晰的解决方案。它也支持在运行时动态调整线程的信号屏蔽状态,为复杂的多线程信号处理提供了灵活性。理解并善用 sigdelset()
是掌握 Linux 信号处理机制的关键一环。
sigismember
函数
我们来详细了解一下 Linux 系统中的 sigismember()
函数。它是信号集 (sigset_t
) 操作中的查询函数,用于判断特定信号是否属于某个信号集合,是信号控制逻辑中进行条件判断的基础。
1. 函数声明与作用
-
声明:
#include <signal.h> int sigismember(const sigset_t *set, int signum);
-
作用:
sigismember()
用于查询指定的信号signum
是否是信号集set
的成员。- 核心概念:
sigset_t
是一个表示信号集合的数据结构(位掩码)。sigismember()
检查代表信号signum
的位在set
中是否被置位(1)。 - 返回值含义:
1
:信号signum
是集合set
的成员(位被置位)。0
:信号signum
不是集合set
的成员(位被清零)。-1
:发生错误(通常是无效的signum
)。
- 为什么需要它? 在信号处理中,经常需要根据信号的状态做出决策:
- 检查一个信号是否在未决信号集 (
sigpending()
) 中。 - 检查一个信号是否在当前的信号屏蔽掩码(阻塞集)中。
- 检查一个信号是否在某个自定义的信号集(如用于
sigwait()
的等待集)中。 - 在修改信号集或处理信号前进行条件判断。
- 检查一个信号是否在未决信号集 (
- 核心概念:
2. 参数详解
const sigset_t *set
: 这是一个输入参数。指向需要查询的sigset_t
变量。该变量必须已经通过sigemptyset()
,sigfillset()
,sigaddset()
,sigdelset()
等函数进行了初始化并填充。int signum
: 需要查询的信号编号。可以是标准信号(如SIGINT
,SIGTERM
)或实时信号(如SIGRTMIN
,SIGRTMIN + 1
)。信号编号定义在<signal.h>
中(使用符号常量)。
3. 返回值
- 成员: 返回
1
。 - 非成员: 返回
0
。 - 错误: 返回
-1
,并设置errno
以指示错误原因。EINVAL
:signum
不是一个有效的信号编号(例如,小于 1 或大于系统支持的最大信号编号NSIG-1
)。EFAULT
:set
指向了一个无效的地址(理论上可能,但实践中如果set
是局部变量或有效全局变量,很少见)。
4. 深入理解:查询的本质与 signum
的有效性
sigset_t
的位掩码本质:sigset_t
内部结构通常是一个位数组(bit array)。每个信号编号N
对应数组中的一个特定位(bit)。位的位置P
通常由N - 1
或类似公式计算得出。sigismember()
的内部操作:- 验证
signum
: 检查signum
是否在有效范围内(1 <= signum < NSIG
)。如果无效,返回-1
并设置errno = EINVAL
。 - 计算位位置: 根据
signum
的值,计算出在sigset_t
内部位数组中对应的位索引P
。 - 检查位状态: 读取
sigset_t
位数组中索引为P
的位的值。- 如果该位是
1
,返回1
(是成员)。 - 如果该位是
0
,返回0
(不是成员)。
- 如果该位是
- 验证
SIGKILL
和SIGSTOP
的特殊性:- 即使
signum
是SIGKILL
(9) 或SIGSTOP
(19),sigismember()
也会正常执行查询。 - 如果
set
是通过sigfillset()
初始化的,那么sigismember(set, SIGKILL)
和sigismember(set, SIGSTOP)
通常会返回0
。因为sigfillset()
的实现会排除这两个信号(它们不能被阻塞)。 - 如果你手动使用
sigaddset()
尝试将它们添加到集合中,sigaddset()
会静默忽略,所以后续sigismember()
查询仍然会返回0
。 - 简而言之:
sigismember()
忠实地报告set
中该信号位是否被置位,但sigaddset()
不会真正设置SIGKILL/SIGSTOP
的位。
- 即使
5. 使用步骤与示例
sigismember()
通常在获取或构建信号集后用于查询:
- 获取或构建信号集:
- 使用
sigpending(&pending_set)
获取未决信号集。 - 使用
sigprocmask(SIG_BLOCK, NULL, ¤t_mask)
获取当前阻塞集(NULL
和oldset
配合)。 - 使用
sigemptyset()
/sigfillset()
/sigaddset()
/sigdelset()
构建自定义信号集。
- 使用
- 查询特定信号:
int is_member = sigismember(&the_set, the_signal);
- 检查错误和处理结果: 如果
is_member == -1
,处理错误;否则根据is_member
是1
或0
执行相应逻辑。
示例 1:基础用法 - 检查信号是否未决
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>int main() {sigset_t block_set, pending_set;// 阻塞 SIGINT 以便产生未决信号sigemptyset(&block_set);sigaddset(&block_set, SIGINT);sigprocmask(SIG_BLOCK, &block_set, NULL);printf("SIGINT blocked. Press Ctrl+C now to generate a pending SIGINT.\n");printf("Waiting for 3 seconds...\n");sleep(3);// 获取未决信号集if (sigpending(&pending_set) == -1) {perror("sigpending");exit(EXIT_FAILURE);}// *** 关键查询: 检查 SIGINT 是否在未决集中 ***int is_pending = sigismember(&pending_set, SIGINT);if (is_pending == -1) {perror("sigismember");exit(EXIT_FAILURE);}if (is_pending) {printf("SIGINT is pending!\n");} else {printf("No SIGINT is pending.\n");}// 解除阻塞 SIGINT (它会被递送)sigprocmask(SIG_UNBLOCK, &block_set, NULL);printf("SIGINT unblocked. It should be delivered now.\n");sleep(1); // 给信号处理程序时间return 0;
}
示例 2:检查信号是否被阻塞 & 多信号查询
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>void check_block_status(int signo, const char *sig_name, const sigset_t *mask) {int status = sigismember(mask, signo);if (status == -1) {fprintf(stderr, "sigismember error for signal %d\n", signo);return;}printf("Signal %s (%d) is %sblocked.\n", sig_name, signo, status ? "" : "NOT ");
}int main() {sigset_t current_mask;// 获取当前进程的信号屏蔽掩码 (阻塞集)if (sigprocmask(SIG_BLOCK, NULL, ¤t_mask) == -1) { // NULL set = just getperror("sigprocmask (get)");exit(EXIT_FAILURE);}printf("Current Signal Block Status:\n");// 检查一组关键信号的状态check_block_status(SIGINT, "SIGINT", ¤t_mask);check_block_status(SIGTERM, "SIGTERM", ¤t_mask);check_block_status(SIGQUIT, "SIGQUIT", ¤t_mask);check_block_status(SIGUSR1, "SIGUSR1", ¤t_mask);check_block_status(SIGALRM, "SIGALRM", ¤t_mask);// 检查 SIGKILL/SIGSTOP (虽然不能被阻塞,但查询是合法的)check_block_status(SIGKILL, "SIGKILL", ¤t_mask); // Will likely show NOT blockedcheck_block_status(SIGSTOP, "SIGSTOP", ¤t_mask); // Will likely show NOT blockedreturn 0;
}
示例 3:在信号处理程序中安全查询 (使用 write
)
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h> // for write
#include <string.h> // for strsignalvolatile sig_atomic_t usr1_received = 0; // 用于主循环的标志void handler(int sig) {// 获取信号名称 (注意: strsignal 可能不是异步信号安全的! 这里仅作演示,生产环境需谨慎)const char *sig_name = strsignal(sig);char msg[100];int n;// 更安全的做法:只使用异步信号安全函数如 writen = snprintf(msg, sizeof(msg), "Handler: Received %s (%d).\n", sig_name, sig);if (n > 0) {write(STDOUT_FILENO, msg, n); // write 是异步信号安全的}// *** 关键点: 在Handler中避免调用非异步安全函数 ***// 假设我们有一个全局的 pending_set (需要在Handler外通过 sigpending 更新)// 在Handler内部查询它是危险的! 通常做法是设置标志,在主循环中查询。if (sig == SIGUSR1) {usr1_received = 1; // 设置原子标志}// 对于其他信号...
}int main() {struct sigaction sa;sigset_t pending_set;// 设置处理程序sa.sa_handler = handler;sigemptyset(&sa.sa_mask);sa.sa_flags = 0; // 或 SA_RESTARTsigaction(SIGUSR1, &sa, NULL);sigaction(SIGINT, &sa, NULL); // 也捕获 SIGINT 用于演示printf("PID: %d. Send SIGUSR1 or SIGINT (Ctrl+C).\n", getpid());while (1) {// 主循环中安全地获取未决信号集if (sigpending(&pending_set) == -1) {perror("sigpending");break;}// 安全地查询 SIGUSR1 是否未决 (虽然Handler可能设置了标志,但这里演示查询)int is_usr1_pending = sigismember(&pending_set, SIGUSR1);if (is_usr1_pending == -1) {perror("sigismember");} else if (is_usr1_pending) {printf("Main: SIGUSR1 is pending (though handler might have just cleared it).\n");}if (usr1_received) { // 检查Handler设置的标志printf("Main: Handling SIGUSR1 work...\n");// ... 执行与 SIGUSR1 相关的任务 ...usr1_received = 0; // 重置标志}// 模拟工作或等待sleep(1);}return 0;
}
6. 重要注意事项与使用细节 (深度解析)
set
必须已初始化: 传递给sigismember()
的set
参数必须指向一个已经通过sigemptyset()
,sigfillset()
,sigaddset()
,sigdelset()
等函数进行了有效初始化的sigset_t
变量。查询一个包含随机垃圾数据的未初始化sigset_t
的结果是未定义行为(Undefined Behavior),可能导致程序崩溃或返回无意义的值。SIGKILL
和SIGSTOP
的查询:sigismember()
可以并且应该能够正常查询SIGKILL
和SIGSTOP
是否在集合中。- 如前所述,由于
sigaddset()
不会真正将它们添加到集合,sigfillset()
也不会包含它们,所以查询它们是否在通过标准方式构建的阻塞集或满集中,通常返回0
(非成员)。 - 查询它们是否在未决集中 (
sigpending()
) 理论上可能,但由于它们不能被阻塞且会立即递送,所以它们几乎永远不会出现在未决集中。
- 错误处理 (
EINVAL
):- 最常见的错误是传递了无效的
signum
。务必检查返回值是否为-1
! 忽略错误检查可能导致程序基于错误信息做出错误决策。 - 使用符号常量 (
SIGXXX
) 而非硬编码数字是避免EINVAL
错误的最佳实践。 - 对于实时信号,确保
signum
在SIGRTMIN
到SIGRTMAX
的范围内。
- 最常见的错误是传递了无效的
- 线程安全性与可重入性:
sigismember()
函数本身通常是线程安全的。它只读取传入的set
指向的内存,不修改任何全局状态(标准库实现应保证这点)。- 异步信号安全性: POSIX 标准明确要求
sigismember()
是异步信号安全的。这意味着它可以安全地在信号处理程序 (signal handler
) 中调用。 - 为什么重要? 这允许在信号处理程序内部进行简单的条件判断。例如,在处理程序中检查另一个信号是否未决(虽然通常更好的做法是在处理程序中设置标志,在主循环中处理复杂逻辑)。示例 3 演示了在 Handler 中使用
write
输出信息,而查询操作更安全的做法是在主循环中进行。
sigset_t
的生命周期与作用域:sigismember()
只是读取set
指向的sigset_t
变量的内容。- 你需要确保在调用
sigismember()
时,set
指向的变量是有效的(例如,局部变量在其作用域内,全局变量已初始化)。 - 对于
sigpending()
返回的未决信号集,它反映的是调用sigpending()
那一刻的快照。在sigpending()
调用后、sigismember()
调用前,未决信号集可能已经改变(新信号到达或信号被递送)。
- 与
sigpending()
配合的原子性问题:- 如示例 1 和 3 所示,
sigpending()
和sigismember()
的组合是查询特定信号是否未决的标准方法。 - 然而,需要认识到:
sigpending()
获取的是调用时刻的未决信号快照。在获取快照后、调用sigismember()
查询特定信号前,未决信号集可能已经发生了变化(例如,如果在此期间解除了某个信号的阻塞,该信号可能已被递送并从未决集中移除)。因此,查询结果反映的是sigpending()
调用时的状态,而非绝对精确的当前状态。在大多数应用场景中,这种短暂的不一致性是可接受的。
- 如示例 1 和 3 所示,
- 实时信号 (
SIGRTMIN
-SIGRTMAX
):sigismember()
可以像查询标准信号一样查询实时信号。- 使用
SIGRTMIN + offset
的形式指定具体的实时信号编号。 - 需要注意的是:
sigismember()
只能告诉你该信号是否至少有一个实例在集合中(例如,在未决集中)。它无法告诉你该信号有多少个实例在排队(对于实时信号)。sigismember(&pending_set, SIGRTMIN)
返回1
只表示至少有一个SIGRTMIN
未决,不表示具体数量。
- 最佳实践:
- 初始化保证: 确保查询的
sigset_t
已被正确初始化。 - 错误检查: 总是检查
sigismember()
的返回值是否为-1
,并处理EINVAL
错误。 - 使用符号常量: 始终使用
<signal.h>
中定义的信号常量。 - 理解局限性: 理解
sigpending()
+sigismember()
组合的原子性限制和实时信号的排队特性。 - Handler 中的安全使用: 利用
sigismember()
的异步信号安全性,在信号处理程序中进行简单的、必要的查询,但保持处理程序尽可能简单。复杂逻辑应通过设置标志在主循环中处理。 - 清晰命名: 给
sigset_t
变量和查询结果变量起有意义的名字(如is_sigint_pending
,is_blocked
)。
- 初始化保证: 确保查询的
7. 总结
sigismember()
是 Linux/Unix 信号编程中不可或缺的信息查询函数。它的核心作用是判断一个特定的信号是否包含在给定的信号集(如未决集、阻塞集或自定义集)中。严格遵守其前提条件(必须初始化 sigset_t
)和错误处理(检查 -1
返回值)是编写可靠代码的基础。认识到它在异步信号处理程序中的安全性,为在 Handler 内进行简单决策提供了可能,但需谨慎使用。理解其与 sigpending()
配合时的原子性限制以及对于实时信号只能反映“存在性”而非“数量”的特性,对于正确解释查询结果至关重要。sigismember()
是构建基于信号状态的复杂控制流和调试信号处理逻辑的基石。