Linux操作系统中,信号是一种重要的进程间通信机制,用于通知进程发生了某些事件。信号既可以是来自内核的通知,也可以是由其他进程发送的。在本篇博客中,我们将深入探讨Linux信号的作用、产生机制、捕捉方式以及信号阻塞的概念。
Linux中的信号被分类为标准信号
和实时信号
。
标准信号
是最基本的信号类型,由整数编号表示,编号范围是1到31。
实时信号
是Linux中的扩展信号类型,由整数编号表示,编号范围是32到64。
为什么31-34中间缺少两个信号无法展示呢?
kill -l
命令用于列出系统支持的所有信号名称,但它并不显示编号为32和33的信号。这是因为Linux中的信号被分类为标准信号和实时信号,而编号为32和33的信号属于实时信号,它们在kill -l
的输出中不会显示。
man 7 signal
信号在Linux系统中有多种作用,包括通知进程某个事件的发生、中断进程的正常执行、以及在进程间传递简单的消息等。不同的信号有不同的含义和影响,了解这些信号是理解Linux系统行为的关键(常见信号及作用请看第2点
)。
信号可以由多种来源产生,包括硬件故障、用户输入、操作系统事件等。例如,Ctrl+C组合键产生SIGINT信号
,表示中断信号
,通常用于终止正在运行的程序。此外,操作系统还可以向进程发送信号以通知发生的事件,如进程终止、停止等。
SIGINT (2) - 中断信号:
作用:用于通知进程中断正在执行的操作,通常由用户通过键入 Ctrl+C 生成。
例子:在终端中运行一个长时间执行的程序,用户可以按下 Ctrl+C 来发送 SIGINT 信号,终止程序的执行。
SIGTERM (15) - 终止信号:
作用:请求进程正常终止,允许进程清理资源和保存状态。
例子:当系统关闭时,操作系统向所有运行的进程发送 SIGTERM 信号,请求它们正常退出。
SIGKILL (9) - 强制终止信号:
作用:立即终止进程,不允许进程清理资源或保存状态。
SIGSEGV (11) - 段错误信号:
#include
#include
#include
// SIGSEGV信号处理函数
void segv_handler(int signum) {
printf("Segmentation fault (SIGSEGV) caught. Exiting...\n");
exit(EXIT_FAILURE);
}
int main() {
// 设置SIGSEGV信号处理函数
signal(SIGSEGV, segv_handler);
int *ptr = NULL;
// 尝试解引用空指针,导致段错误
*ptr = 10;
// 这里的代码不会执行,因为上面的语句导致了段错误
printf("This line will not be reached.\n");
return 0;
}
我们通过signal函数将
segv_handler
函数与SIGSEGV
信号关联起来。当运行程序并尝试解引用空指针时,会触发段错误,然后程序会捕获SIGSEGV
信号并执行segv_handler
函数。在这个处理函数中,我们输出一条消息并调用exit退出程序。
SIGUSR1 (10) 和 SIGUSR2 (12) - 用户定义信号:
#include
#include
#include
#include
// SIGUSR1信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与SIGUSR1相关的操作
}
// SIGUSR2信号处理函数
void sigusr2_handler(int signum) {
printf("Received SIGUSR2 signal.\n");
// 在这里执行与SIGUSR2相关的操作
}
int main() {
// 设置SIGUSR1和SIGUSR2信号处理函数
signal(SIGUSR1, sigusr1_handler);
signal(SIGUSR2, sigusr2_handler);
printf("My process ID: %d\n", getpid());
printf("I am process[%d], Waiting for SIGUSR1 or SIGUSR2 signals...\n",getpid());
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
这些信号的作用覆盖了从正常终止到异常情况的多个场景,使得进程能够及时响应各种事件。在编写程序时,了解这些信号及其作用是确保程序稳定性和可维护性的关键。
Linux允许进程捕捉和处理信号,以执行自定义的操作。信号处理可以通过以下方式实现:
在C语言中,可以使用 signal 函数来设置信号的处理函数。例如:
#include
#include
#include
void handler(int signal)
{
printf("i am process[%d],get a signal:%d\n",getpid(),signal);
// 处理信号的代码
}
int main()
{
int signo;
// 设置 1~31信号 的处理函数为 sigint_handler
for (signo = 1; signo <= 31; signo++){
signal(signo, handler);
}
while (1){
sleep(1);
// 进程持续运行
}
return 0;
}
但是为什么不能捕捉
9 号信号
呢?
9号信号(SIGKILL)是不能被捕捉的。这是因为SIGKILL信号是Linux系统中的一种特殊信号,它具有最高的优先级,并且无法被进程捕获、阻塞或忽略。当进程收到SIGKILL信号时,它会被立即终止,而无法执行任何处理逻辑。
这种设计是为了确保系统的稳定性和安全性。SIGKILL信号通常用于强制结束一些不响应或处于异常状态的系统进程,以防止它们对系统造成损害或影响其他进程的正常运行。因此,为了确保这种强制终止的执行,SIGKILL信号不能被捕获或修改其默认行为。
sigaction 函数是一种更为可靠和灵活的处理信号的方式,相较于 signal 函数,它提供了更多的控制选项。它允许指定处理函数、设置标志和提供可选的信号屏蔽。
#include
int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact); The sigaction structure is defined as something like: struct sigaction { void (*sa_handler)(int); void (*sa_sigaction)(int, siginfo_t *, void *); sigset_t sa_mask; int sa_flags; void (*sa_restorer)(void); };
#include
#include
#include
#include
// SIGUSR1 信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与 SIGUSR1 相关的操作
}
int main() {
struct sigaction sa;
// 设置 sa 结构体
sa.sa_handler = sigusr1_handler;
sa.sa_flags = 0;
// 清空 sa_mask,即不阻塞任何其他信号
sigemptyset(&sa.sa_mask);
// 使用 sigaction 函数关联 SIGUSR1 信号和处理函数
if (sigaction(SIGUSR1, &sa, NULL) == -1) {
perror("Error setting up SIGUSR1 handler");
exit(EXIT_FAILURE);
}
printf("My process ID: %d\n", getpid());
printf("Waiting for SIGUSR1 signal...\n");
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
上述示例中我们使用了 sigaction 函数来设置对 SIGUSR1 信号的处理。struct sigaction 结构体用于指定信号处理函数及其他相关属性。在 main 函数中,我们设置了 sa_handler 为 sigusr1_handler,表示接收到 SIGUSR1 信号时执行这个处理函数。
上述示例中,我们还清空了 sa_mask,即不阻塞任何其他信号。如果你希望在信号处理期间阻塞某些其他信号,可以使用 sigaddset 等函数来设置 sa_mask。
此外,sa_flags 可以用来设置不同的标志,例如 SA_RESTART 用于指示系统调用在接收到信号后是否应该自动重启。
信号阻塞是指进程暂时屏蔽对某些信号的处理。在某些情况下,我们希望在处理一个信号时,暂时阻塞掉其他同类的信号,以确保处理的完整性。可以使用 sigprocmask 函数来设置信号阻塞。
#include
// 阻塞 SIGINT 信号
sigset_t new_mask, old_mask;
sigemptyset(&new_mask);
sigaddset(&new_mask, SIGINT);
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
// 执行一些需要阻塞 SIGINT 的操作
// 恢复原信号屏蔽状态
sigprocmask(SIG_SETMASK, &old_mask, NULL);
#include
#include
#include
#include
// SIGUSR1 信号处理函数
void sigusr1_handler(int signum) {
printf("Received SIGUSR1 signal.\n");
// 在这里执行与 SIGUSR1 相关的操作
}
int main() {
// 设置 SIGUSR1 信号处理函数
signal(SIGUSR1, sigusr1_handler);
printf("My process ID: %d\n", getpid());
printf("Waiting for SIGUSR1 signal (blocked)...\n");
// 创建一个集合来存储被阻塞的信号
sigset_t block_mask;
sigemptyset(&block_mask); // 清空信号集
// 将 SIGUSR1 加入到被阻塞的信号集中
sigaddset(&block_mask, SIGUSR1);
// 使用 sigprocmask 阻塞指定信号
if (sigprocmask(SIG_BLOCK, &block_mask, NULL) == -1) {
perror("Error blocking SIGUSR1");
exit(EXIT_FAILURE);
}
// 在这里进行一些工作,期间 SIGUSR1 信号会被阻塞
// 解除对 SIGUSR1 的阻塞
if (sigprocmask(SIG_UNBLOCK, &block_mask, NULL) == -1) {
perror("Error unblocking SIGUSR1");
exit(EXIT_FAILURE);
}
printf("Waiting for SIGUSR1 signal (unblocked)...\n");
while (1) {
// 进程持续运行
sleep(1);
}
return 0;
}
上述示例中,首先使用
sigemptyset
清空信号集,然后使用 sigaddset 将SIGUSR1
添加到被阻塞的信号集中。接着,使用sigprocmask
函数将这个信号集应用到当前进程,从而阻塞了 SIGUSR1 信号。
在需要解除对信号的阻塞时,同样使用sigprocmask
函数,但这次使用SIG_UNBLOCK
标志。这样就解除了对
SIGUSR1 信号的阻塞。
这个过程演示了如何在程序运行过程中阻塞和解除阻塞指定的信号。在实际应用中,这种机制通常用于确保某些关键部分的原子性
,防止信号中断影响程序的正常执行。
通过本文,我们深入了解了Linux信号的基本概念、常见信号及其作用、信号的产生方式,以及信号的捕捉和阻塞。在编写和调试Linux应用程序时,对信号的理解是至关重要的,它为我们处理进程间通信和异常情况提供了强大的工具。希望本文能够帮助你更好地利用Linux信号机制。