【Linux探索学习】第二十八弹——信号(下):信号在内核中的处理及信号捕捉详解

Linux学习笔记:

https://blog.csdn.net/2301_80220607/category_12805278.html?spm=1001.2014.3001.5482

前言:

在前面我们已经学习了有关信号的一些基本的知识点,包括:信号的概念、信号产生和信号处理等,今天我们重点来讲解一下信号在内核中的处理以及信号捕捉的相关知识点

在这篇文章中,我们将深入探讨 Linux 信号在内核中的处理流程,详细讲解信号递达、信号阻塞、未决信号、信号集操作、信号捕捉等内容,并通过大量的代码示例和实际场景来展示信号如何在 Linux 中运作。

与信号有关的还有一个很重要的知识点是有关用户态、内核态和状态切换的知识,本篇没有进行讲解,需要自己再去了解一下

目录

[1. 信号在内核中的处理流程](#1. 信号在内核中的处理流程)

[1.1 信号在内核中的表示](#1.1 信号在内核中的表示)

[1.2 信号的递达机制](#1.2 信号的递达机制)

信号递达的条件

信号递达过程

示例代码:信号递达

[1.3 信号未决状态](#1.3 信号未决状态)

信号未决队列的管理

示例代码:查看未决信号

[1.4 信号集与 sigset_t](#1.4 信号集与 sigset_t)

信号集的操作

示例代码:操作信号集

​编辑

[1.5 信号屏蔽与 sigprocmask](#1.5 信号屏蔽与 sigprocmask)

[示例代码:使用 sigprocmask() 阻塞和解除阻塞信号](#示例代码:使用 sigprocmask() 阻塞和解除阻塞信号)

[1.6 获取未决信号:sigpending()](#1.6 获取未决信号:sigpending())

[示例代码:使用 sigpending() 查看未决信号](#示例代码:使用 sigpending() 查看未决信号)

[2. 信号捕捉与处理](#2. 信号捕捉与处理)

[2.1 使用 signal() 捕捉信号](#2.1 使用 signal() 捕捉信号)

[示例代码:使用 signal() 捕捉信号](#示例代码:使用 signal() 捕捉信号)

[2.2 使用 sigaction() 捕捉信号](#2.2 使用 sigaction() 捕捉信号)

[示例代码:使用 sigaction() 捕捉信号](#示例代码:使用 sigaction() 捕捉信号)

[3. 总结](#3. 总结)


1. 信号在内核中的处理流程

信号是由内核或其他进程通过系统调用发送给目标进程的。当进程正在执行时,信号能够在不干扰进程当前操作的情况下打断它的执行,触发某种特定的行为。信号的处理流程在 Linux 内核中被设计得非常灵活,既支持异步信号处理,又能通过进程的信号屏蔽机制来控制信号的递达。

1.1 信号在内核中的表示

信号在内核中的表示示意图:

  • 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。在上图的例子中,SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
  • SIGOUIT信号未产生过,一旦产生SIGOUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。本章不讨论实时信号。

1.2 信号的递达机制

信号递达是信号机制中的核心概念,它是信号从信号源发送到目标进程的过程。信号递达的实现依赖于内核的进程调度机制。在进程执行过程中,内核需要判断该进程是否有需要处理的未决信号,信号的递达会在进程的上下文切换时被触发。

信号递达的条件

信号的递达取决于以下几个因素:

  1. 信号是否被屏蔽:每个进程都可以选择性地阻塞某些信号,当信号被阻塞时,它们会进入未决状态,直到信号被解除阻塞。
  2. 进程的当前状态:信号递达的时机还受进程状态的影响。如果进程处于不可中断的状态(例如执行系统调用),它可能无法立即处理信号,这时信号会被推迟递达,直到进程能够响应信号为止。
  3. 信号类型 :标准信号和实时信号在递达的优先级上可能存在差异。实时信号(编号从 SIGRTMIN 开始)通常会比标准信号更快地递达,并且能够提供更多的控制选项。
信号递达过程

信号的递达过程通常包括以下几个步骤:

  1. 信号的发送 :信号可以通过内核发送(例如内核事件或系统调用)或通过其他进程调用 kill() 函数发送。
  2. 信号的处理检查:当一个进程正在被调度执行时,内核会检查该进程是否有未决的信号。如果存在未决信号,内核会查看进程的信号屏蔽字,以决定这些信号是否可以递达。
  3. 信号的递送:如果信号未被屏蔽且能够递达,内核会根据进程的信号处理方式来决定是执行默认动作还是调用信号处理函数。
示例代码:信号递达
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

void signal_handler(int sig) {
    printf("Received signal: %d\n", sig);
}

int main() {
    signal(SIGINT, signal_handler);  // 捕捉 SIGINT 信号
    printf("Waiting for SIGINT...\n");
    while(1) {
        sleep(1);  // 进入等待状态,直到接收到信号
    }
    return 0;
}

在上面的代码中,进程会一直运行并等待 SIGINT 信号(通常由按下 Ctrl+C 触发)。一旦进程接收到 SIGINT 信号,内核会将其递送到进程,并触发信号处理函数 signal_handler

1.3 信号未决状态

当信号发送给进程时,如果该信号被进程的信号屏蔽字阻塞,那么该信号就会进入未决状态。未决信号是那些已经被发送但尚未被递达的信号。内核维护了每个进程的未决信号队列,并会在进程解除对该信号的阻塞时按顺序递送这些信号。

信号未决队列的管理

在 Linux 内核中,每个进程都有一个 task_struct 结构体,其中包含了当前进程的未决信号集合。每当一个信号发送给一个进程时,如果该信号被阻塞,内核不会立即递送它,而是将其存放在进程的未决信号队列中,直到进程解除对该信号的阻塞。

未决信号通常在进程解除信号屏蔽字后,由内核递送。递送顺序通常与信号发送顺序一致,且会按照优先级递送实时信号和标准信号。

示例代码:查看未决信号
cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

int main() {
    sigset_t pending;
    sigpending(&pending);  // 获取当前进程的未决信号
    
    if (sigismember(&pending, SIGINT)) {
        printf("SIGINT is pending.\n");
    } else {
        printf("No SIGINT pending.\n");
    }

    sleep(10);  // 稍作停顿,方便查看信号状态
    return 0;
}

通过使用 sigpending() 函数,我们可以查看当前进程的未决信号集。如果进程没有处理 SIGINT 信号,且信号被阻塞,则该信号会处于未决状态。

1.4 信号集与 sigset_t

信号集(sigset_t)是一个用于表示信号集合的数据结构,它通过位掩码的方式表示进程当前可以接受的信号集合。sigset_t 通常是一个整数或更大的数据类型,每一位对应一个信号。

信号集的操作

在 Linux 中,常用的信号集操作函数包括:

  • sigemptyset():初始化信号集为空集。
  • sigaddset():将某个信号添加到信号集中。
  • sigdelset():将某个信号从信号集中删除。
  • sigismember():判断某个信号是否在信号集中。
示例代码:操作信号集
cpp 复制代码
#include <signal.h>
#include <stdio.h>

int main() {
    sigset_t set;
    sigemptyset(&set);         // 初始化为空集
    sigaddset(&set, SIGINT);   // 将 SIGINT 添加到信号集中
    sigaddset(&set, SIGTERM);  // 将 SIGTERM 添加到信号集中

    if (sigismember(&set, SIGINT)) {
        printf("SIGINT is in the set.\n");
    }

    sigdelset(&set, SIGINT);   // 从信号集中删除 SIGINT
    if (!sigismember(&set, SIGINT)) {
        printf("SIGINT is no longer in the set.\n");
    }

    return 0;
}

1.5 信号屏蔽与 sigprocmask

sigprocmask() 是一个用于修改进程信号屏蔽字的系统调用,它可以用来阻塞、解除阻塞或查询进程的信号屏蔽字。信号屏蔽字定义了哪些信号是被阻塞的,从而影响信号递达的时机。

sigprocmask() 具有以下操作模式:

  • SIG_BLOCK:将指定的信号添加到信号屏蔽字中,阻塞这些信号。
  • SIG_UNBLOCK:从信号屏蔽字中删除指定信号,解除阻塞。
  • SIG_SETMASK:将信号屏蔽字设置为指定值,替换当前的信号屏蔽字。
示例代码:使用 sigprocmask() 阻塞和解除阻塞信号
cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include<unistd.h>

int main() {
    sigset_t new_mask, old_mask;
    sigemptyset(&new_mask);
    sigaddset(&new_mask, SIGINT);  // 阻塞 SIGINT

    sigprocmask(SIG_BLOCK, &new_mask, &old_mask);  // 阻塞 SIGINT

    // 信号屏蔽后,可以进行一些操作
    printf("SIGINT is blocked. Press Ctrl+C to send SIGINT.\n");
    sleep(10);  // 暂停,等待 Ctrl+C 输入

    // 恢复信号屏蔽字
    sigprocmask(SIG_SETMASK, &old_mask, NULL);  // 恢复原信号屏蔽字
    printf("SIGINT is unblocked.\n");

    sleep(10);  // 等待信号递达
    return 0;
}

在这段代码中,SIGINT 信号在前 10 秒内被阻塞,用户按下 Ctrl+C 时信号不会立即递达。10 秒后,信号屏蔽被解除,SIGINT 信号会被递送并触发相应的处理。

1.6 获取未决信号:sigpending()

sigpending() 函数用于获取当前进程的未决信号,它返回一个信号集,表示该进程尚未处理的信号集合。sigpending() 的实现依赖于进程的信号队列,它可以用于调试和监控进程的信号处理状态。

示例代码:使用 sigpending() 查看未决信号
cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    sigset_t pending;
    sigemptyset(&pending);
    
    // 阻塞 SIGINT 信号
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    // 发送 SIGINT 信号,但它会被阻塞
    kill(getpid(), SIGINT);

    // 获取未决信号
    sigpending(&pending);
    if (sigismember(&pending, SIGINT)) {
        printf("SIGINT is pending.\n");
    } else {
        printf("No SIGINT pending.\n");
    }

    sleep(5);  // 稍作等待,防止信号丢失
    return 0;
}

此程序模拟了阻塞 SIGINT 信号并通过 sigpending() 查看进程的未决信号状态。如果信号被阻塞,它将在信号屏蔽字解除后递达。


2. 信号捕捉与处理

信号捕捉是指进程通过自定义信号处理函数来响应特定的信号。Linux 提供了 signal()sigaction() 两种方式来捕捉信号。signal() 是一种简单的接口,而 sigaction() 提供了更为复杂的配置选项,使得开发者能够在处理信号时获得更多的控制权。

2.1 使用 signal() 捕捉信号

signal() 是最基础的信号捕捉方式,它允许开发者指定一个信号处理函数来响应特定信号。signal() 的使用非常简单,但它并不支持所有高级功能,如信号的重入处理或复杂的信号控制。

示例代码:使用 signal() 捕捉信号
cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include<unistd.h>

void signal_handler(int sig) {
    printf("Received signal: %d\n", sig);
}

int main() {
    signal(SIGINT, signal_handler);  // 捕捉 SIGINT 信号
    printf("Waiting for SIGINT...\n");
    while (1) {
        sleep(1);  // 程序将一直运行,直到接收到信号
    }
    return 0;
}

在这个示例中,signal_handler 函数会在接收到 SIGINT 信号时被调用。

2.2 使用 sigaction() 捕捉信号

sigaction() 提供了比 signal() 更灵活的方式来处理信号。它允许开发者在捕捉信号时设定更多的参数,比如如何处理重入信号、是否需要恢复默认行为等。

sigaction() 的结构体定义如下:

cpp 复制代码
struct sigaction {
    void (*sa_handler)(int);
    sigset_t sa_mask;
    int sa_flags;
    void (*sa_restorer)(void);
};
  • sa_handler:指定信号处理函数。
  • sa_mask:指定在信号处理期间需要阻塞的信号集。
  • sa_flags:设定信号处理的行为。
示例代码:使用 sigaction() 捕捉信号
cpp 复制代码
#include <signal.h>
#include <stdio.h>
#include<unistd.h>

void signal_handler(int sig) {
    printf("Received signal: %d\n", sig);
}

int main() {
    struct sigaction sa;
    sa.sa_handler = signal_handler;  // 指定信号处理函数
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);  // 初始化信号集

    sigaction(SIGINT, &sa, NULL);  // 捕捉 SIGINT 信号
    printf("Waiting for SIGINT...\n");
    
    while (1) {
        sleep(1);  // 程序将一直运行,直到接收到信号
    }
    return 0;
}

通过 sigaction(),程序能够灵活地处理信号,并控制信号捕捉的行为,甚至允许在处理信号时阻塞其他信号。

3. 总结

本文我们讲解了信号的处理机制,并且对信号捕捉进行了更详细的补充,结合上篇内容,基本上将信号部分的内容进行了大概的讲解,认真看一下相信会对你有所帮助

感谢各位大佬观看,创作不易,还望各位大佬点赞支持!!!

相关推荐
索然无味io18 分钟前
Linux基础
linux·运维·服务器·学习·安全·web安全·网络安全
杨枝甘露小码20 分钟前
利用Muduo库实现简单且健壮的Echo服务器
服务器·网络
三天不学习21 分钟前
Linux+Docer 容器化部署之 Shell 语法入门篇 【Shell 替代】
linux·运维·服务器·shell
yqcoder24 分钟前
Node 服务器数据响应类型处理
运维·服务器·前端·javascript·node.js
fanged1 小时前
Android学习21 -- launcher
android·学习
从零开始的-CodeNinja之路1 小时前
【棋弈云端】网页五子棋项目测试报告
深度学习·学习·单元测试·自动化
晨曦学习日记1 小时前
C语言学习笔记:通过二维数组打印杨辉三角形
笔记·学习
xkdlzy1 小时前
centos stream 9 安装 libstdc++-static静态库
linux·运维·centos
lingllllove1 小时前
centos如何压缩zip
linux·运维·centos
hgdlip2 小时前
ip归属地是不是要打开定位?
服务器·网络·tcp/ip