首页 > 系统相关 >[Linux]信号

[Linux]信号

时间:2024-12-11 23:31:55浏览次数:8  
标签:set sigset int 内核 信号 Linux 进程

信号

认识信号

什么是信号

信号本质上是一种软件中断,用于通知进程发生了特定的事件。进程接收到信号后,会根据信号的类型采取相应的操作。

拿生活中的红绿灯来举例,当你看到红灯的时候你不会过马路,当变为绿灯时才会通过。但是也有可能在等红灯的时候,此时绿灯亮了而你正在打游戏,游戏正处于决胜时刻,这时候你不会选择立即过马路,而是等这局结束再通过。也就是说绿灯亮了就过马路这个行为并不是立即就要执行,而是会在一个合适的时候去执行这个动作。在接收到绿灯这个信号,和执行过马路动作这个期间就有一个时间窗口,在这段时间内你并没有过马路,但是你知道你已经可以通过了。这本质就是你“记住了绿灯已经亮了”。当游戏结束后,你就可以处理绿灯这个信号,这时候我们可以有三种处理方式:1. 默认(通过马路)。2. 忽略(游戏输了很生气,继续开一把游戏)。3. 自定义(来一段舞蹈)。

在进程中,对于信号的处理方式是一样的。当信号来的时候,进程可能在执行更重要的代码,对这个信号不一定会立即处理,但是会在自己的pcb中保存这个信号,等到合适的时候处理这个信号。

信号的分类

信号分为普通信号和实时信号,我们主要研究的是普通信号。在Linux中[1,31]号信号是普通信号,[34,64]号是实时信号。在命令行中使用kill -l命令查看。

信号产生

在理解信号产生之前,我们先来看一个系统调用接口signal(),它用于捕捉信号,设置信号处理方式。

当然,并不是所有信号都能被捕捉。比如说9号信号(SIGKILL),SIGKILL 信号的主要设计目的是用于无条件地终止一个进程。若允许进程捕捉 SIGKILL 信号,一个恶意进程或者出现错误的进程可以在接收到 SIGKILL 信号后,通过自定义的信号处理程序来阻止自身被终止,从而继续占用系统资源或执行一些非法操作。不允许捕捉 SIGKILL 信号,可以确保系统始终保留对进程生死的最终控制权。

函数原型:

typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

参数:

  • signum:指定要设置处理方式的信号编号。
  • handler:指定信号的处理方式,它是一个函数指针,指向一个具有 void (*)(int) 类型的函数,即该函数接受一个 int 类型的参数(通常就是信号编号 signum),并且没有返回值。有以下几种取值方式:
    • 自定义处理函数:当传入一个自定义的函数指针时,进程在接收到指定的信号 signum 后,将调用这个自定义的函数来处理信号。
    • SIG_IGN:表示忽略指定的信号。不过,有一些信号是不能被忽略的,如 SIGKILLSIGSTOP
    • SIG_DFL:表示采用信号的默认处理方式。

返回值:成功返回一个函数指针,指向之前该信号的处理函数;失败返回SIG_ERR

信号通常可以通过以下四种方式产生。

键盘发送

在命令行中,我们通常使用ctrl + c这种快捷方式结束进程。操作系统将我们这个操作识别为2号信号,从而帮我们执行对应的操作。

void handler(int signo)
{
    std::cout << "获取到一个信号,编号是: " << signo << std::endl;
}

int main()
{
    pid_t pid = getpid();

    signal(2, handler);//捕捉2号信号,并重新设置信号的处理方法

    while (true)
    {
        std::cout << "我是一个进程,pid是: " << pid << std::endl;
        sleep(1);
    }

    return 0;
}

在这个例子中,我们对2号信号捕捉,当我们使用ctrl+c这个快捷键时,他就会执行我们设定的方法。

系统调用

  1. kill()

    函数原型:int kill(pid_t pid, int signum);,表示给任意进程发送任意信号,成功返回0,失败返回-1。

  2. raise()

    函数原型:int raise(int signum);,给自己发送任意信号,成功返回0,失败返回非零值。

  3. abort()

    函数原型:void abort();,给自己发送SIGABRT信号。

从上面不难发现,通过kill()这个函数就能实现raise()abort()

硬件异常

只是举例子,不代表只有这几个。

  1. 除0错误

    int main()
    {
        int a = 10;
        a /= 0;
        return 0;
    }
    

    当我们运行上面的程序的时候,会报出Floating point exception,这其实就是八号信号。如何证明呢?我们设置对应的捕捉方法,然后再运行,如下:

    void handler(int signo)
    {
        std::cout << "获取到一个信号,编号是: " << signo << std::endl;
    }
    
    int main()
    {
        signal(8, handler);
        int a = 10;
        a /= 0;
        return 0;
    }
    

    到这里又有一个疑问了,设置了自定义的处理方法后,为什么会疯狂的进行输出呢?明明我只执行了一次除零的动作啊。这是因为CPU中有一套寄存器(寄存器中的内容属于当前进程的上下文),其中有一个叫状态寄存器,当发生除零错误的时候,状态寄存器的溢出标志位将会改变(假设是由0变为1)。而我们并没有对这个改变做出修正,每当进程发生切换的时候,就有无数次状态寄存器被保存和回复的过程,所以每一次恢复的时候,就让操作系统识别到了CPU内部的状态寄存器中的溢出标志位是1,所以才会不断地输出。

  2. 野指针

    同样会被操作系统发送信号终止,对应的信号是11号信号。

软件条件

  1. 管道读端关闭

    当读端关闭的时候,操作系统会发信号(SIGPIPE)关闭写端。(管道链接)

  2. 定时器

    可以通过alarm()系统调用来给进程设置定时器,当时间到了之后会向调用进程发送SIGALRM信号。

    函数原型:unsigned int alarm(unsigned int seconds);

    int main()
    {
        pid_t pid = getpid();
        alarm(5);//5秒后发送信号
        while (true)
        {
            std::cout << "我是一个进程,pid是: " << pid << std::endl;
            sleep(1);
        }
    }
    

信号保存

一个信号是发给进程的,那么在进程的pcbtask_struct{};结构体)中一定保存了该信号。那么它是如何保存的呢?在task_struct中有两个位图和一个函数指针数组,通过这三个结构就能很好的对信号保存和处理。如下图:

信号处理

相关概念

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到抵达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞(Block)某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

进程如何执行内核级的代码

在进程地址空间(Linux]进程地址空间 - 羡鱼OvO - 博客园)中说过,每个进程都有一个虚拟的地址空间,这个空间被划分为不同的区域,其中3~4G就属于内核空间。用户空间通过用户级页表映射找到对应的物理内存,这张页表是独立的,每个进程都有属于自己的用户级页表;而内核空间同样是通过页表映射的方式找到对应的物理内存,但是这张内核级页表是共享的,只有一张。

在CPU的寄存器中有一个叫做CR3的寄存器,它表征的是当前进程的运行级别。当进程在执行自己的代码的时候,此时处于用户态;一旦进程遇到了系统调用接口,此时的状态就被切换为内核态。而又由于每个进程都有一个虚拟地址空间,当进程切换为内核态执行内核级别的代码的时候,其实只需要在自己的地址空间上进行跳转就可以了。

信号捕捉流程

  1. 在执行主控制流程的某条指令时因为中断,异常或者系统调用进入内核。
  2. 在内核处理完异常准备回到用户模式之前,会先处理当前进程中可以递达的信号。
  3. 如果处理信号的函数是自定义的,则回到用户态执行对应的信号处理函数。
  4. 信号处理函数返回时执行特殊的系统调用再次进入内核。
  5. 从内核态再次返回到用户态,这次是从主控流程被中断的地方开始继续向下执行。

在第三步中,执行完信号处理函数(sighandler)后,既然已经是用户态了,为什么不直接跳到主控制流程(main)中,继续向下运行,而是还要转到内核态,然后再从内核态返回用户态呢?因为sighandlermain使用不同的内核空间,他们之间不存在调用和被调用的关系,是两个独立的控制流程。当sighanlder函数执行完毕后,它不知道此时main函数执行到哪了,所以不能直接从sighanlder函数跳到main函数。

信号集

从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。

信号集处理函数

  1. int sigemptyset(sigset_t *set);

    用来初始化set所指向的信号集,使其中所有信号对应的比特位置0,表示该信号集不包含任何有效信号。

  2. int sigfillset(sigset_t *set);

    初始化set所指向的信号集,使其中所有信号对应的比特位置1,表示该信号集包括系统支持的所有信号。

  3. int sigaddset(sigset_t *set, int signo);

    添加某种信号到信号集中。

  4. int sigdelset(sigset_t *set, int signo);

    从信号集中删除指定的信号。

  5. int sigismember(const sigset_t *set, int signo);

    判断信号集中是否包含某种信号。

  6. int sigprocmask(int how, const sigset_t *set, sigset_t *oset);

    用于检查和修改进程对应的信号屏蔽字(block信号集)。

    • how:指定信号集的操作方式
      • SIG_BLOCK:将set中的信号添加到当前被阻塞的信号集中。就是将set中的信号和当前阻塞信号集进行 “或” 操作,使这些信号也被阻塞。
      • SIG_UNBLOCK:将set中的信号从当前被阻塞的信号集中移除。
      • SIG_SETMASK:将当前被阻塞的信号集设置为set中的信号。
    • oldset:一个指向sigset_t类型的指针。如果oldsetNULL,函数会将进程当前的信号掩码存储到oldset所指向的信号集中,以便后续恢复或查看之前的信号掩码状态。

    如果调用sigprocmask()解除了对当前若干个未决信号的阻塞,则在sigprocmask()返回前至少将一个信号递达。

  7. int sigpending(sigset_t *set);

    获取当前处于未决状态的信号集合。

简单的使用示例:

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

#define MAX_SIGNUM 31

static std::vector<int> sigarr = {2, 3};

static void show_pending(const sigset_t &pending)
{
    for (int signo = MAX_SIGNUM; signo > 0; signo--)
    {
        //判断指定信号在不在信号集中
        if (sigismember(&pending, signo)) std::cout << "1";
        else std::cout << "0";
    }
    std::cout << std::endl;
}

int main()
{
    sigset_t block, oblock, pending;
    //初始化
    sigemptyset(&block);
    sigemptyset(&oblock);
    sigemptyset(&pending);

    //添加要屏蔽的信号
    for(const auto &sig : sigarr) sigaddset(&block, sig);
    //将屏蔽的信号设置进信号屏蔽字中
    sigprocmask(SIG_SETMASK, &block, &oblock);

    while (true)
    {
        //获取处于未决状态的信号集
        sigpending(&pending);
        //进行打印输出
        show_pending(pending);
        sleep(1);
    }
}

Core Dump

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump。它们都是使一个进程退出,那有什么不一样的地方呢。

首先我们先来认识一下什么是Core Dump。

Core Dump(核心转储)是指在程序异常终止(如由于段错误、非法指令等原因)时,操作系统将进程当时的内存状态(包括程序代码、数据段、栈等)保存到磁盘文件中的操作。这个文件被称为核心转储文件,通常命名为 “core” 或类似的名称。这个文件可以帮助定位导致程序崩溃的原因,例如访问了非法内存地址、栈溢出、内存泄漏等问题。

在生成Core Dump文件之前,要确保Core Dump功能已打开。使用ulimit -a查看用户资源限制,若选项为0,使用ulimit -c [非0值或unlimited]打开这个选项。

下面是一个数组越界使用gdb调试后的结果。(形成的Core Dump文件的后缀是引起core问题的进程的pid

这就是两种退出方式不一样的地方,使用Term的是直接退出,而Core模式结束会生成Core Dump文件。

标签:set,sigset,int,内核,信号,Linux,进程
From: https://www.cnblogs.com/wzhiheng/p/18601204

相关文章

  • Linux基础命令
    用finalshell连接虚拟机首先在linux系统中输入ifconfig在ens33中进行查找虚拟机的ip,然后在finalshell中通过远程输入ip进行连接ls命令ls[-a-l-h]路径-a#显示隐藏文件-l#以列表的形式展示文件-h#展示文件的大小#以d开头的是文件夹,以-......
  • 信号与槽机制的使用
    在现代GUI开发中,Qt框架因其强大的功能和灵活性而备受欢迎。Qt的信号与槽(SignalandSlot)机制是其核心特性之一,用于实现对象间的通信。本文将详细介绍这一机制,并结合实际使用场景讲解其应用方式。文章目录一、什么是信号与槽信号二、信号与槽的连接三、Qt::ConnectionT......
  • 初学Linux第二天
    用户组添加用户组#格式:groupadd-g用户组id用户组的名称若不指定id,则按默认的来(1000开始,1-999是系统文件的id)修改用户组#格式:groupmod[-g用户组id]用户组名称删除用户组#格式:groupdel用户组名称用户添加用户#格式:useradd[-G用户组的名称或者id][-u......
  • Linux如何挂载windows共享文件夹(包含Linux报错解决)
     目录前言windows如何共享文件夹windows如何查看共享Linux端挂载共享文件夹关于挂载共享文件夹的报错解决1.关闭windows防火墙2.确认windows的CIFS文件共享功能是否开启3.检查源路径与挂载路径是否正确4.检查用户名及密码是否正确5.检查共享文件夹权限6.......
  • 【Linux】进程的状态和进程优先级
    进程状态进程状态的名词解析新建:字面意思重新创建一个进程,但是这个进程的test_struct还没有加载到运行队列中此时的状态成为新建。运行:进程的test_struct结构体被加载到可执行队列中。         阻塞:等待非CPU资源的就绪时的状态就叫做阻塞。    ......
  • Linux中vim常用命令详解
    文章目录Linux中vim常用命令详解一、引言二、Vim的工作模式1、命令模式1.1、常用命令2、输入模式3、底线命令模式三、文本编辑1、移动命令2、编辑操作3、撤销与重做四、使用示例1、查找与替换2、多文件编辑五、总结Linux中vim常用命令详解一、引言Vim是Linux......
  • Linux中vi和vim的区别详解
    文章目录Linux中vi和vim的区别详解一、引言二、vi和vim的起源与发展三、功能和特性1、语法高亮2、显示行号3、编辑模式4、可视化界面5、功能扩展6、插件支持四、使用示例1、启动编辑器2、基本操作五、总结Linux中vi和vim的区别详解一、引言在Linux系统中,vi和vim......
  • 【Linux】记录一下考RHCE的学习过程(五)
        最近伤口恢复的还可以,明天就可以去拆线了。不过眼睛还是有点不舒服。计算机硬件组成        本篇的主要内容是计算机硬件,不过一般学过计算机或者爱玩游戏的都比较熟悉这方面吧(应该)。    一般计算机硬件分为输入设备、输出设备、主机设备、外部......
  • linux/centOS7用户和权限管理笔记
    linux系列中可以:配置多个用户配置多个用户组用户可以加入多个用户中linux中关于权限的管理级别有2个级别,分别是:针对用户的权限控制针对用户组的权限控制一,root用户root用户拥有最大的系统操作权限,而普通用户在许多地方的权限是受限的二,用户组的管理(root用户执行)1.创建用......
  • linux/centOS7用户和权限管理笔记练习
    1.创建用户组bigdata2.创建用户dsj,指定基本组bigdata,附加组bigdata2,指定home目录为/home/dsj3.查看用户4.创建用户dsj2,指定基本组为bigdata2,附加组为bigdata,指定uid为24025.查看dsj2用户6.从root用户切换到dsj用户7.切回root用户 8.给dsj2用户添加密码 9.......