Linux C语言信号集与阻塞机制
2024-07-117.4k 阅读
信号集概述
在Linux系统中,信号是一种异步通知机制,用于向进程发送事件消息。信号集(signal set)是一个用于表示多个信号的数据结构,它允许我们对一组信号进行统一的管理和操作。
信号集的数据结构
在C语言中,信号集通过sigset_t
类型来表示,定义在<signal.h>
头文件中。sigset_t
本质上是一个位图(bitmap),每一位对应一个信号。例如,第1位表示信号SIGINT
,第2位表示信号SIGQUIT
等。
信号集操作函数
- 初始化信号集
int sigemptyset(sigset_t *set);
- 功能:将信号集
set
初始化为空,即清除所有信号。 - 返回值:成功返回0,失败返回-1。
- 功能:将信号集
int sigfillset(sigset_t *set);
- 功能:将信号集
set
初始化为包含所有信号。 - 返回值:成功返回0,失败返回-1。
- 功能:将信号集
- 添加和删除信号
int sigaddset(sigset_t *set, int signum);
- 功能:将信号
signum
添加到信号集set
中。 - 返回值:成功返回0,失败返回-1。
- 功能:将信号
int sigdelset(sigset_t *set, int signum);
- 功能:将信号
signum
从信号集set
中删除。 - 返回值:成功返回0,失败返回-1。
- 功能:将信号
- 检查信号是否在信号集中
int sigismember(const sigset_t *set, int signum);
- 功能:检查信号
signum
是否在信号集set
中。 - 返回值:如果
signum
在set
中,返回1;否则返回0。如果set
无效,返回-1。
- 功能:检查信号
以下是一个简单的示例代码,演示信号集的基本操作:
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
int main() {
sigset_t set;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return 1;
}
// 添加SIGINT信号到信号集
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset");
return 1;
}
// 检查SIGINT是否在信号集中
if (sigismember(&set, SIGINT) == 1) {
printf("SIGINT is in the set\n");
} else {
printf("SIGINT is not in the set\n");
}
// 删除SIGINT信号
if (sigdelset(&set, SIGINT) == -1) {
perror("sigdelset");
return 1;
}
// 再次检查SIGINT是否在信号集中
if (sigismember(&set, SIGINT) == 1) {
printf("SIGINT is in the set\n");
} else {
printf("SIGINT is not in the set\n");
}
return 0;
}
信号阻塞机制
信号阻塞(signal blocking)是一种控制信号处理时机的机制。通过阻塞信号,进程可以暂时阻止某些信号的传递,直到解除阻塞后才处理这些信号。
信号掩码
每个进程都有一个信号掩码(signal mask),它是一个信号集,用于表示当前被阻塞的信号。当一个信号被阻塞时,它不会被立即传递给进程,而是处于“挂起”状态。
设置信号掩码
sigprocmask
函数int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how
参数:指定如何修改信号掩码,有以下几种取值:SIG_BLOCK
:将set
中的信号添加到当前信号掩码中。SIG_UNBLOCK
:将set
中的信号从当前信号掩码中移除。SIG_SETMASK
:将当前信号掩码设置为set
。
set
参数:指向要操作的信号集。oldset
参数:指向一个信号集,用于保存旧的信号掩码。如果不需要保存旧的信号掩码,可以设置为NULL
。- 返回值:成功返回0,失败返回-1。
下面是一个示例代码,展示如何使用sigprocmask
函数阻塞和解除阻塞信号:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
}
int main() {
sigset_t set;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return 1;
}
// 添加SIGINT信号到信号集
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset");
return 1;
}
// 注册信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C to test.\n");
sleep(10);
// 解除阻塞SIGINT信号
if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. Press Ctrl+C to test.\n");
while (1) {
sleep(1);
}
return 0;
}
挂起信号
当一个信号被阻塞时,如果该信号产生,它会被挂起(pending)。我们可以使用sigpending
函数来检查当前进程有哪些信号处于挂起状态。
sigpending
函数int sigpending(sigset_t *set);
- 功能:获取当前进程中处于挂起状态的信号集,并将其存储在
set
中。 - 返回值:成功返回0,失败返回-1。
- 功能:获取当前进程中处于挂起状态的信号集,并将其存储在
以下代码示例展示了如何使用sigpending
函数:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void print_pending_signals() {
sigset_t pending_set;
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return;
}
printf("Pending signals: ");
for (int i = 1; i < NSIG; i++) {
if (sigismember(&pending_set, i) == 1) {
printf("%d ", i);
}
}
printf("\n");
}
int main() {
sigset_t set;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return 1;
}
// 添加SIGINT信号到信号集
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset");
return 1;
}
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Send SIGINT (Ctrl+C) to make it pending.\n");
sleep(10);
print_pending_signals();
// 解除阻塞SIGINT信号
if (sigprocmask(SIG_UNBLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. The pending SIGINT should be delivered now.\n");
sleep(5);
return 0;
}
信号集与阻塞机制的应用场景
- 确保关键代码段的执行
- 在一些关键代码段,如共享资源的访问,为了防止信号的干扰导致数据不一致,可以在进入关键代码段前阻塞相关信号,在离开关键代码段后解除阻塞。
- 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int shared_variable = 0;
void critical_section() {
sigset_t set;
sigset_t oldset;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return;
}
// 添加SIGINT信号到信号集
if (sigaddset(&set, SIGINT) == -1) {
perror("sigaddset");
return;
}
// 阻塞SIGINT信号
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return;
}
// 关键代码段
printf("Entering critical section. shared_variable = %d\n", shared_variable);
shared_variable++;
sleep(2);
printf("Leaving critical section. shared_variable = %d\n", shared_variable);
// 恢复原来的信号掩码
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return;
}
}
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
}
int main() {
// 注册信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
critical_section();
return 0;
}
- 进程间同步
- 信号阻塞机制可以用于进程间同步。例如,父进程可以阻塞某个信号,子进程在完成特定任务后向父进程发送该信号,父进程解除信号阻塞后处理信号,从而实现进程间的同步。
- 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
void signal_handler(int signum) {
printf("Parent received signal %d\n", signum);
}
int main() {
sigset_t set;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return 1;
}
// 添加SIGUSR1信号到信号集
if (sigaddset(&set, SIGUSR1) == -1) {
perror("sigaddset");
return 1;
}
// 阻塞SIGUSR1信号
if (sigprocmask(SIG_BLOCK, &set, NULL) == -1) {
perror("sigprocmask");
return 1;
}
// 注册信号处理函数
if (signal(SIGUSR1, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
pid_t pid = fork();
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
sleep(2);
printf("Child sending SIGUSR1 to parent\n");
if (kill(getppid(), SIGUSR1) == -1) {
perror("kill");
return 1;
}
exit(0);
} else {
// 父进程
printf("Parent waiting for SIGUSR1\n");
sigset_t oldset;
// 解除阻塞SIGUSR1信号
if (sigprocmask(SIG_UNBLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
wait(NULL);
}
return 0;
}
- 防止信号处理函数的重入问题
- 当一个信号处理函数正在执行时,如果又收到相同的信号,可能会导致重入问题(re - entry problem),即信号处理函数再次进入自身,造成数据混乱。通过阻塞信号,可以防止在信号处理函数执行期间再次收到相同的信号。
- 示例代码:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
int global_variable = 0;
void signal_handler(int signum) {
sigset_t set;
sigset_t oldset;
// 初始化信号集为空
if (sigemptyset(&set) == -1) {
perror("sigemptyset");
return;
}
// 添加当前信号到信号集
if (sigaddset(&set, signum) == -1) {
perror("sigaddset");
return;
}
// 阻塞当前信号
if (sigprocmask(SIG_BLOCK, &set, &oldset) == -1) {
perror("sigprocmask");
return;
}
// 信号处理函数代码
printf("Received signal %d. global_variable = %d\n", signum, global_variable);
global_variable++;
sleep(2);
printf("Leaving signal handler. global_variable = %d\n", global_variable);
// 恢复原来的信号掩码
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return;
}
}
int main() {
// 注册信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
while (1) {
sleep(1);
}
return 0;
}
信号集与阻塞机制的注意事项
- 不可靠信号 在早期的UNIX系统中,信号是不可靠的,可能会出现信号丢失的情况。即使信号被阻塞,也不能保证信号一定不会丢失。在现代的Linux系统中,大多数信号已经是可靠的,但在编写代码时仍需注意这个历史遗留问题。
- 信号处理函数中的操作
在信号处理函数中,应尽量避免复杂的操作,因为信号处理函数可能在任何时候被调用,包括进程执行系统调用等关键时期。例如,避免在信号处理函数中调用标准I/O函数(如
printf
),除非这些函数是可重入的。 - 信号掩码的继承
当进程调用
fork
创建子进程时,子进程会继承父进程的信号掩码。这意味着如果父进程阻塞了某些信号,子进程默认也会阻塞这些信号。在需要子进程以不同的信号掩码运行时,需要在子进程中重新设置信号掩码。 - 信号与系统调用的交互
如果一个进程在执行系统调用时收到信号,默认情况下,系统调用会被中断并返回错误
EINTR
。在处理信号时,需要考虑这种情况,可能需要重新发起系统调用。例如:
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>
void signal_handler(int signum) {
printf("Received signal %d\n", signum);
}
int main() {
// 注册信号处理函数
if (signal(SIGINT, signal_handler) == SIG_ERR) {
perror("signal");
return 1;
}
ssize_t bytes_read;
char buffer[1024];
while (1) {
bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
if (bytes_read == -1 && errno == EINTR) {
printf("Read was interrupted by a signal. Retrying...\n");
continue;
} else if (bytes_read == -1) {
perror("read");
return 1;
}
buffer[bytes_read] = '\0';
printf("Read: %s", buffer);
}
return 0;
}
通过深入理解Linux C语言中的信号集与阻塞机制,并在实际编程中合理运用,我们可以编写出更健壮、更可靠的程序,能够有效地处理各种异步事件,提高程序的稳定性和性能。同时,注意上述提到的注意事项,可以避免在使用过程中出现难以调试的问题。