【Linux】信号的保存及处理

发布于:2024-04-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

一. 信号的保存

信号概念的补充, 信号的四种状态:
信号产生(Produce): 由四种不同的方式发送信号;
信号未决(Pending): 信号从产生到抵达的中间状态;
信号递达(Delivery): 实际执行信号的处理动作;
信号阻塞(Block): 阻塞信号, 使其无法递达;

注: 阻塞和忽略是不同的, 忽略是递达的一种处理动作.

那么信号的状态可以表示为三个结构: block 表, pending 表, handler 表;
在这里插入图片描述
block 表 和 pending 表 都是位图结构, 每个位置表示信号的状态;
handler 表是一个函数指针数组, 指向对应信号的处理动作;

信号的处理流程
在这里插入图片描述

1. sigset_t 信号集

sigset_t 信号集是一个位图类型的结构体, 也就是 block 表和 pending 表的结构;

信号集可以表示每个信号的“有效”或“无效”状态;
可以使用该类型, 通过信号集操作函数, 快速的设置 block 表和 pending 表;

注: 该类型的操作必须使用对应的函数, 不要自行更改内容.

2. 信号集操作函数

在这里插入图片描述
对 sigset_t 类型的对象进行增删查改操作的函数;

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

注: 在创建信号集后, 需使用 sigemptyset() 或 sigfillset() 函数进行初始化, 确保信号集是合法可用的;

sigprocmask()

在这里插入图片描述
sigprocmask() 函数, 可以对进程的 block 表进行操作;

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

  • how: 选项, 对屏蔽信号集(block 表)的操作;
    常用选项:
    SIG_BLOCK: 将 set 信号集中 有效的信号 添加至 当前进程的 block 表中, 就是添加 set 所包含的信号; 相当于 mask |= set;
    SIG_UNBLOCK: 将 set 信号集中 有效的信号 从 当前进程的 block 表中 移除, 就是移除 set 所包含的信号; 相当于 mask &= (~set);
    SIG_SETMASK: 将当前进程的 block 表 设置为 set 信号集; 相当于 mask = set;
  • set: 修改使用的信号集, 包含希望修改的信号;
  • oldset: 输出型参数, 保存进程中原来的 block 表;
    在这里插入图片描述

返回值:

  • 若成功, 返回 0; 若失败, 返回 -1, 并且设置 errno;
    在这里插入图片描述
sigpending()

在这里插入图片描述
sigpending()函数, 可以获取进程的未决信号集;

#include <signal.h>

int sigpending(sigset_t *set);

参数:

  • set: 输出型参数, 将会获取进程的 pending 表;

返回值:

  • 若成功, 返回 0; 若失败, 返回 -1, 并且设置 errno;
    在这里插入图片描述

例:
当对进程的 2 号信号进行堵塞后, 进程的 2 号信号将会一直保留在 pending 表中; 当 2 号信号解除屏蔽后, 将会递达 2 号信号; 并且可以看出在处理动作前, 2 号信号就已经从 pending 表中移除了;

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

using namespace std;

sigset_t block;
sigset_t pending;

void print(sigset_t& set)
{
    for (int i=31; i>0; i--)
    {
        cout << sigismember(&set, i);
    }
    cout << endl;
}

void handler(int signal)
{
    
    sleep(1);
    // 打印 block 表
    cout << "block : ";
    print(block);


    sleep(1);
    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);


    sleep(1);
    exit(0);
}


int main()
{
    // 捕捉 2 号信号, 不然直接退出了
    signal(2, handler);


    // 初始化并添加 2 号信号
    sigemptyset(&block);
    sigaddset(&block, 2);

    // 更改进程的 block 表, 屏蔽 2 号 信号
    sigprocmask(SIG_SETMASK, &block, nullptr);

    // 打印 block 表
    cout << "block : ";
    print(block);


    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);

    sleep(1);
    // 发送信号
    cout << "send block signal" << endl;
    raise(2);


    sleep(1);
    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);

    cout << "---------------------------------------" << endl;
    sleep(3);

    // 更改进程的 block 表, 解除 2 号信号的屏蔽
    cout << "rm block signal" << endl;
    sigdelset(&block, 2);
    sigprocmask(SIG_SETMASK, &block, nullptr);



    return 0;
}

在这里插入图片描述

二. 信号的处理

信号处理有两种情况:

  • 对于未被阻塞的信号, 当信号被接受到后, 会被记录在 pending 表中, 在合适的时机进行递达处理;
  • 对于堵塞的信号, 当信号被解除屏蔽后, 该信号会被立即递达处理;

两种不同的情况其实是因信号实际处理的时机产生的: 当进程从内核态返回用户态时, 操作系统会对信号进行检测及处理;

1. 用户态和内核态

用户态和内核态的区分是在操作系统层面进行的; 这两种状态是操作系统为了安全和管理而设计的;

用户态: 是指应用程序的运行状态, 权限受到限制; 当用户态的进程被调度(时间片), 系统调用, 中断时, 会切换至内核态执行;
内核态: 是操作系统内核的运行状态, 是运行操作系统程序, 操作硬件的状态, 具有最高权限; 当内核态的代码或中断处理执行完毕后, 会返回用户态;

为了不影响内核态的相关操作, 只有当内核态的相关操作完毕后, 切换至用户态时, 才会对信号进行检测及处理;

2. 信号处理的过程

  • 当信号未产生或被阻塞时, 没有任何处理, 内核态直接返回用户态;
  • 当信号未被阻塞, 存在, 并且处理动作为默认或忽略时, 在内核态终止程序后返回或直接返回;
    在这里插入图片描述
  • 当信号未被阻塞, 存在, 并且处理动作为自定义处理时, 需要先切换用户态执行处理动作, 再返回内核态完成后续操作, 最后返回用户态;
    在这里插入图片描述

3. 虚拟地址空间的内核空间

在进程的虚拟地址空间中, 都有一块内核空间, 这块内核空间就是操作系统代码和数据的虚拟地址, 并且通过内核级页表与物理地址进行映射;
但操作系统比较特殊, 不同进程的内核空间会映射至同一块物理地址;

在这里插入图片描述

内核空间只有内核态可以访问, 当用户态调用系统接口时, 就需要切换至内核态, 通过访问内核空间中的虚拟地址和内核级页表的映射, 才可以调用系统接口

而用户态和内核态的切换则是操作系统和硬件通过修改权限标志位等操作完成的;

4. sigaction

在这里插入图片描述
sigaction() 函数, 可以自定义处理行为, 类似 signal() 函数, 但更复杂;

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

参数:

  • signum: 信号编号;
  • act: sigaction 结构体, 包含处理动作等成员;
  • oldact: 输出型参数, 保存原来的 sigaction 结构体;

返回值:

  • 若成功, 返回 0; 若失败, 返回 -1, 并且设置 errno;
    在这里插入图片描述
struct sigaction 
{
    void     (*sa_handler)(int);	// 普通信号的自定义处理行为
    void     (*sa_sigaction)(int, siginfo_t *, void *);	// 实时信号相关
    sigset_t   sa_mask;		// 待屏蔽的信号集
    int        sa_flags;	// 选项,设为 0 即可
    void     (*sa_restorer)(void);	// 实时信号相关
};

其中关注 sa_handler 和 sa_mask 即可;
sa_mask: 当指定信号在处理动作期间, 阻塞信号集中所包含的信号, 直至处理动作完毕;

注: 自定义捕获的信号在处理动作期间会被自动屏蔽;

例:
自定义捕获 2 号信号, 并且设置 sa_mask 屏蔽 3 号信号, 当进行 2 号信号的处理动作时, 3 号信号会被堵塞, 保留在 pending 表中, 直至 2 号信号的处理动作结束, 才会递达 3 号信号;

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>

using namespace std;

sigset_t block;
sigset_t pending;

void print(sigset_t& set)
{
    for (int i=31; i>0; i--)
    {
        cout << sigismember(&set, i);
    }
    cout << endl;
}

void handler(int signal)
{
    
    sleep(1);
    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);


    sleep(1);
    // 发送信号
    cout << "send signal 3" << endl;
    raise(3);


    sleep(1);
    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);

    sleep(2);
}


int main()
{

    struct sigaction act;
    
    act.sa_flags = 0;
    act.sa_handler = handler;

    // 初始化并添加 3 号信号
    sigemptyset(&act.sa_mask);
    sigaddset(&act.sa_mask, 3);

    // 自定义捕获 2 号信号
    sigaction(2, &act, nullptr);


    // 打印 pending 表
    sigpending(&pending);
    cout << "pending : ";
    print(pending);

    sleep(1);
    // 发送信号
    cout << "send signal 2" << endl;
    raise(2);
    cout << endl;

    return 0;
}

在这里插入图片描述

可重入函数

  • 可重入函数: 可以被执行流重复进入的函数
  • 不可重入函数: 不可以被执行流重复进入的函数

例:
main 执行流在插入 node1 节点时, 转到异常执行流插入 node2 节点, 最终导致 node2 节点丢失, 导致内存泄漏;
在这里插入图片描述
可重入是函数的一个特性;

  • 不可重入的条件:
    调用了内存管理的相关函数, 内存管理的相关函数是通过全局链表来管理堆的;
    调用了标准 I/O 库函数, 标准I/O库的很多实现都以不可重入的方式使用全局数据结构;

volatile

volatile 关键字可以避免编译器的优化, 保证内存的可见性;

例:

在未被编译器优化的情况下, 程序在收到 2 号信号时, 能够正常退出;

#include <iostream>
#include <signal.h>

using namespace std;

bool flag = 1;

void handler(int signal)
{
    cout << endl;
    cout << "flag = 0" << endl;
    flag = 0;
}

int main()
{
    signal(2, handler);

    while (flag) sleep(1);

    cout << "quit" << endl;

    return 0;
}

在这里插入图片描述

但若在编译时 使用 -O 优化选项后, 那么程序将不会退出;

gcc -O1 /*file_name*/

在这里插入图片描述
通常在进行循环判断时, 寄存器需要从内存中获取 flag 变量的值, 进行判断;
但编译器在优化后, 在进行循环时, 直接将 flag 变量的值保存在寄存器中, 判断寄存器中的值; 当修改时, 修改内存中的 flag 变量, 而没有影响寄存器中的值;

SIGCHLD

实际上, 子进程在退出后, 会向父进程发送 SIGCHLD 信号;

而 SIGCHLD 信号的默认动作是 Ign 忽略的;

在这里插入图片描述
这样可以使用 SIGCHLD 信号, 当子进程退出时主动通知父进程, 而不是父进程主动的, 持续检测子进程的状态;

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;


void handler(int signal)
{
    cout << "wait " << endl;
    sleep(1);
    wait(NULL);
}

int main()
{
    signal(SIGCHLD, handler);

    pid_t pid = fork();

    if (pid)
    {
        sleep(1);
    }

    sleep(3);

    return 0;
}

在这里插入图片描述
但若使用 wait() / waitpid() 的方式等待多个不同时期退出的子进程时, 依旧会堵塞等待子进程, 所以需要把进程回收设为 WNOHANG 非阻塞式等待;

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;


void handler(int signal)
{
    while (1)
    {
        pid_t id = waitpid(-1, nullptr, WNOHANG);   // -1, 表示等待任一子进程

        if (id <= 0)
            break;
    }
}

int main()
{
    signal(SIGCHLD, handler);

    pid_t pid = fork();

    if (pid)
    {
        sleep(1);
    }

    sleep(3);


    return 0;
}

但在 Linux 上, 操作系统还做了一种特殊处理, 当用户显示的将 SIGCHLD 的处理动作置为 SIG_IGN 时, 子进程在退出时, 将会由操作系统对其负责, 自动清理资源并进行回收, 不会产生僵尸进程;

signal(SIGCHLD, SIG_IGN)	//只有这种显示设置的, 会在 Linux 中被特殊处理