引言
在计算机科学领域,信号是一种重要的通信机制,用于处理各种系统事件和进程间的通信。Linux作为一个开源操作系统,以其稳定性和高度可定制性而闻名。在Linux下,信号的处理是实现进程间通信和事件处理的关键机制之一。
本文将继续探讨Linux下信号的相关主题,着重介绍信号的保存、阻塞以及sigprocmask函数的用法。通过深入了解这些概念和技术,我们能够更好地掌握Linux信号处理的原理和方法,提高系统的可靠性和稳定性。
通过本文的学习,读者将对Linux下信号的保存、阻塞和sigprocmask函数有更深入的了解。这些知识将帮助读者在开发和维护Linux应用程序时更好地处理信号,提高系统的稳定性和性能。无论是初学者还是有经验的开发人员,都将受益于本文所涵盖的内容。让我们一起深入研究Linux信号处理的精髓吧!
一、阻塞信号
1. 信号相关常见概念
(1)信号递达
信号递达是指当信号被发送和接收后,信号的处理过程。在Linux中,进程可以通过系统调用kill()向其他进程发送信号,同时也可以接收来自其他进程的信号。当信号被发送到一个进程时,需要经过多个步骤才能被接收并处理:
发送信号:进程A使用kill()系统调用向进程B发送信号。
信号递送:信号从进程A发送到进程B,进程B接收到信号。
信号处理:进程B根据信号的类型和处理方式来进行相应的处理。
在Linux中,每个信号都有一个默认的处理方式。例如,SIGKILL信号会强制终止进程,而SIGINT信号会让进程中断并退出。然而,进程也可以通过signal()系统调用或sigaction()系统调用来改变信号的处理方式,以便实现更灵活的信号处理行为。
🚨注意:在信号递送和信号处理的过程中,可能会发生信号丢失或者信号被阻塞的情况。当一个进程处于阻塞状态时,它将无法接收到任何信号,直到解除了阻塞状态。如果多个信号同时到达进程时,可能会出现信号排队的情况,此时进程需要按照一定的规则来处理这些信号。
(2)信号未决
在Linux中,信号未决(Pending Signal)指的是一个进程接收到但尚未处理的信号。当一个信号被发送给一个进程时,如果该进程当前正在执行某个信号处理函数或者该信号已经处于未决状态,则该信号会被放入进程的信号未决位集(Pending Signal Mask)中,等待进程从信号处理函数返回后进行处理。
在信号未决位集中,每个位代表一个信号,如果该位为1,则表示该位对应的信号处于未决状态。一个进程可以通过sigpending()系统调用来查询自己的信号未决位集。
如果一个进程接收到多个同类型的信号并且信号处理函数尚未返回,则这些信号将被合并成一个信号,并只记录一次信号未决。进程可以使用sigprocmask()系统调用来设置或修改信号未决掩码,以控制哪些信号可以被接收和处理。
🚨注意:当进程解除信号阻塞状态后,它必须处理所有未决的信号,否则这些信号将继续被保留在信号未决位集中,可能会导致信号丢失或者其他问题。因此,在处理信号的过程中,要注意及时处理所有未决信号,避免信号积压导致系统异常。
(3)阻塞信号
阻塞信号是指进程可以选择暂时延迟处理某些特定信号的传递和处理。在Linux中,进程可以通过设置信号阻塞掩码(Signal Mask)来达到这一目的。
当一个信号被发送给一个进程时,内核会首先检查该信号是否在进程的信号阻塞掩码中。如果信号在阻塞掩码中,则该信号将被暂时挂起,直到该信号从阻塞状态解除后才能被处理。进程可以使用sigprocmask()系统调用来修改信号阻塞掩码。
通过设置信号阻塞掩码,进程可以灵活地控制哪些信号可以被接收和处理,以及在何种情况下可以延迟处理某些信号。这种机制在多线程编程和信号处理复杂的应用中尤为重要。
🚨注意:当进程解除对某个信号的阻塞时,如果有多个该类型的信号在阻塞期间到达,那么这些信号将按照某种规则进行排队,等待进程逐个处理。另外,即使信号被阻塞,但仍然会记录在信号未决位集中,等待进程解除阻塞后处理。
(4)忽略信号
忽略信号是指进程可以选择不对某些特定信号进行处理,即忽略该信号的传递和默认处理行为。在Linux中,进程可以通过设置信号处理函数为SIG_IGN来达到这一目的。
当一个信号被发送给一个进程时,内核会首先检查该信号的处理方式。如果进程将该信号的处理函数设置为SIG_IGN(忽略信号),则内核将不对该信号进行任何处理,直接丢弃该信号。
通过忽略信号,进程可以屏蔽一些不需要处理的信号,从而避免其产生默认的处理行为。对于某些特定的信号,可能会存在一些默认的处理行为,比如终止进程、终止进程并生成core文件等。通过忽略信号,进程可以防止这些默认的处理行为发生。
🚨注意:并非所有的信号都可以被忽略。一些重要的信号,如SIGKILL和SIGSTOP,默认情况下是不能被忽略的,它们具有固定的处理行为。此外,一些特殊的信号,如SIGCHLD,可以被设置为忽略,但是会导致一些系统资源无法正确释放,因此需要谨慎使用。
2. 信号在内核中的表示
⭕信号在内核中的表示示意图
在这里插入图片描述
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。
3. sigset_t (数据类型)
sigset_t 是一个数据类型,用于表示信号集。它是一个由位图组成的数据结构,用于跟踪和管理多个信号的状态。在 C 语言中,sigset_t 常常以无符号整数或者数组的形式实现。
在 Linux 中,sigset_t 是通过使用位操作来表示信号集的。每个信号对应 sigset_t 中的一个位(bit),如果某个位被设置为 1,则表示相应的信号在该信号集中被包含;如果某个位被设置为 0,则表示相应的信号在该信号集中不被包含。
sigset_t 通常用于以下操作:
设置信号集中的某个位:可以使用宏函数 sigaddset() 或 sigemptyset() 来设置信号集中的位。sigaddset() 可以将指定的信号添加到信号集中,而 sigemptyset() 可以清空信号集。
清除信号集中的某个位:可以使用宏函数 sigdelset() 来清除信号集中的位,从而从信号集中删除指定的信号。
检查信号集中的某个位是否被设置:可以使用宏函数 sigismember() 来检查信号集中的位是否被设置,从而判断指定的信号是否在信号集中。
sigset_t 的使用可以帮助进程或线程控制和管理信号的行为,如阻塞或解除阻塞某些信号,判断信号是否被阻塞等。在信号处理函数中,可以通过调用相关的系统调用来获取和修改当前进程或线程的 sigset_t,以实现对信号的处理和控制。
4. 信号集操作函数
在C语言中,可以使用以下函数来进行信号集(sigset_t)的操作:
函数 功能
sigemptyset(sigset_t *set) 清空信号集,将所有信号从集合中移除。
sigfillset(sigset_t *set) 将所有信号添加到信号集中,使其包含所有信号。
sigaddset(sigset_t *set, int signum) 将指定的信号添加到信号集中。
sigdelset(sigset_t *set, int signum) 从信号集中删除指定的信号。
sigismember(const sigset_t *set, int signum) 检查指定的信号是否在信号集中,如果在返回1,否则返回0。
这些函数都返回一个整数值来表示操作的成功与否。如果函数执行成功,返回值为0;如果函数执行失败,返回值为-1,并设置相应的错误号(errno)。
这些函数通常用于与信号处理相关的操作,例如设置阻塞信号集、检查信号是否被阻塞、解除阻塞等。通过操作信号集,可以对进程接收和处理的信号进行控制。
需要包含 <signal.h> 头文件才能使用上述函数。此外,还有其他一些与信号处理相关的函数,如 sigprocmask()、sigpending() 等,它们也可用于信号集的操作和管理。
二、sigprocmask() 函数
sigprocmask() 函数用于更改或检索进程的信号屏蔽字(signal mask)。信号屏蔽字决定了进程当前阻塞的信号集,即哪些信号在被阻塞的情况下不能被进程接收到。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
sigprocmask() 函数接受三个参数:
how:用于指定对信号屏蔽字的操作。
操作类型 描述
SIG_BLOCK 将 set 指向的信号集中的信号添加到当前的信号屏蔽字中。
SIG_UNBLOCK 从当前的信号屏蔽字中移除 set 指向的信号集中的信号。
SIG_SETMASK 将当前的信号屏蔽字替换为 set 指向的信号集。
set:一个指向 sigset_t 类型的指针,指向要设置的新的信号屏蔽字。
oldset:可选参数,如果不为 NULL,则旧的信号屏蔽字将被存储在 oldset 指向的位置。
sigprocmask() 函数返回值表示操作的成功与否。如果函数执行成功,返回值为0;如果函数执行失败,返回值为-1,并设置相应的错误号(errno)。
以下示例演示了如何使用 sigprocmask() 函数:
#include <stdio.h>
#include <signal.h>
int main() {
sigset_t newset, oldset;
// 设置要阻塞的信号集
sigemptyset(&newset);
sigaddset(&newset, SIGINT);
// 阻塞 SIGINT 信号
if (sigprocmask(SIG_BLOCK, &newset, &oldset) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is blocked. Press Ctrl+C to send the signal.\n");
// 挂起进程,等待信号到达
pause();
// 恢复原来的信号屏蔽字
if (sigprocmask(SIG_SETMASK, &oldset, NULL) == -1) {
perror("sigprocmask");
return 1;
}
printf("SIGINT is unblocked. Signal handling resumed.\n");
return 0;
}
上述示例将 SIGINT 信号添加到新的信号屏蔽字中,然后使用 sigprocmask() 函数将其阻塞。当程序运行时,按下 Ctrl+C 将发送 SIGINT 信号,但由于该信号被阻塞,进程挂起直到信号解除阻塞后才继续执行。在恢复原来的信号屏蔽字后,程序可以正常处理 SIGINT 信号。
在这里插入图片描述
三、sigpending() 函数
sigpending() 函数用于获取当前进程挂起(pending)的信号集,即已经产生但尚未被进程处理的信号。
#include <signal.h>
int sigpending(sigset_t *set);
sigpending() 函数接受一个指向 sigset_t 类型的指针作为参数,用于存储当前挂起的信号集。该函数将会将当前进程挂起的信号填充到 set 指向的信号集中。
sigpending() 函数返回值表示操作的成功与否。如果函数执行成功,返回值为0;如果函数执行失败,返回值为-1,并设置相应的错误号(errno)。
以下示例演示了如何使用 sigpending() 函数:
#include <stdio.h>
#include <signal.h>
void handler(int signum) {
printf("Received signal: %d\n", signum);
}
int main() {
sigset_t pending_set;
// 设置信号处理函数
signal(SIGINT, handler);
// 发送 SIGINT 信号
raise(SIGINT);
// 获取挂起的信号集
if (sigpending(&pending_set) == -1) {
perror("sigpending");
return 1;
}
// 检查 SIGINT 是否在挂起的信号集中
if (sigismember(&pending_set, SIGINT)) {
printf("SIGINT is pending.\n");
} else {
printf("SIGINT is not pending.\n");
}
return 0;
}
上述示例中,首先定义了一个信号处理函数 handler,当接收到 SIGINT 信号时,该函数将被调用。然后使用 signal() 函数将 SIGINT 信号与该处理函数关联。
接下来,通过调用 raise(SIGINT) 发送 SIGINT 信号给当前进程。
然后,使用 sigpending() 函数获取当前挂起的信号集,并将结果存储在 pending_set 中。
最后,使用 sigismember() 函数检查 SIGINT 是否在挂起的信号集中,根据结果输出相应的信息。
请注意,由于信号的处理是异步的,在获取挂起的信号集之前,可能已经有其他信号被处理掉了。因此,sigpending() 只能提供当前未被处理的挂起信号的部分信息。