【Linux】信号产生全景解析:从硬件异常到软件触发

发布于:2025-03-04 ⋅ 阅读:(112) ⋅ 点赞:(0)

目录

一、键盘输入产生信号:

二、通过kill命令:

三、系统调用产生信号:

1、kill:

2、raise:

3、abort:

四、异常产生信号:

除零异常:

状态寄存器:

野指针异常:

五、软件条件:

1、alarm:

2、核心转储:


一、键盘输入产生信号:

在我们之前的学习中,我们了解到了ctrl+c这个按键看起来能够直接终止正在运行中的进程的,实际上其原理是OS给待关闭的进程发送了2号信号

处理这个组合键,也可以通过组合键 ctrl+\ 来给该进程发送退出信号,这个信号是3号信号SIGQUIT

如下,我们可以写下如下代码来证明看看:

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

using namespace std;

void myhander(int signo)
{
    cout<<"a signal get : " << signo <<endl;
}

int main()
{
    signal(2,myhander);
    signal(3,myhander);

    while(1)
    {
        cout << "i am a process my pid : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

 接着运行起来,然后在该进程中按下ctrl+c和ctrl+\这两个组合键

1、Ctrl+C:终止进程(发送SIGINT 信号)
功能:在终端中按下 Ctrl+C 会向当前前台进程发送 SIGINT 信号,强制中断进程的执行

2、Ctrl+\:强制终止并生成核心转储(发送SIGQUIT 信号)

功能:向当前前台进程发送 SIGQUIT 信号,强制终止进程并生成核心转储文件(core dump),便于调试程序崩溃原因

当程序未响应 Ctrl+C 时,使用 Ctrl+\ 更彻底地终止进程
在开发者调试时,一般通过核心转储文件分析程序崩溃的上下文信息


注意事项:
在云服务器中,核心转储文件默认可能未启用,需通过 ulimit -c unlimited 命令开启

二、通过kill命令:

在命令行中,我们可以使用kill指令来给特定进程发送信号

使用法则:kill -signo pid

其中-signo就是发送对应信号,pid就是待发送信号的进程pid

三、系统调用产生信号:

1、kill:

不仅仅是在命令行中能用,在我们的代码也可以使用kill系统调用接口来进行控制

如上,kill的形参中:

第一个参数就是想要发送给那个进程的pid,第二个参数就是发送的哪一个信号

返回值:成功返回0,错误返回-1

我们可以自己通过代码中的kill来实现命令行下的kill:

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

using namespace std;

void usage(string argv)
{
    cout << "Usage:\n\t" << argv << " signum pid\n\n";
}


int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }

    int signum = stoi(argv[1]);
    pid_t pid = stoi(argv[2]);

    int n = kill(pid, signum);
    if(n == -1)
    {
        perror("kill");
        exit(2);
    }
    return 0;
}

2、raise:

这是发送一个信号给当前调用这个接口的进程,如下头文件

其中形参就是要发送的哪一个信号

返回值:成功返回0,失败返回非0

使用:如下,每过两秒就给当前进程发送2号信号,然后捕捉2号信号重定义

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

using namespace std;

void myhander(int signo)
{
    cout<<"a signal get : " << signo <<endl;
}

int main()
{
    signal(2,myhander);
    int cnt = 0;
    while(1)
    {
        cout<< "i am a process " << endl;
        if(cnt%2 == 0) raise(2);
        cnt++;
        sleep(1);
    }
    return 0;
}

通过系统调用接口raise也可以产生信号

3、abort:

这是给当前进程发送6号信号,然后使当前进程强制终止的

void myhander(int signo)
{
    cout<<"a signal get : " << signo <<endl;
}

int main()
{
    signal(6,myhander);
    int cnt = 0;
    while(1)
    {
        cout<< "i am a process " << endl;
        if(cnt == 5) abort();
        cnt++;
        sleep(1);
    }
    return 0;
}

如上,我明明没有写进程终止的代码,并且也将abort接口发送的6号信号捕捉了,但是仍然进程终止了,这是因为abort在底层实现中不仅仅发送了6号信号,并且还多做了动作---强制终止当前进程

四、异常产生信号:

除零异常:

当在我们的代码中出现了异常,比如说是:除零异常,野指针的问题,越界访问等等

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

当我们写出如上的异常的时候,这个时候在int b那行语句中发生了除0异常

如下,如果此时编译运行后,就会发现出现报错了,Floating point exception简称FPE,就是下面所说的8号信号

那当我们将8号信号进行捕捉了呢?

再次运行的时候就会发现,疯狂地执行打印myhander中的内容,但是我们并没有写死循环啊,这是为什么呢?

要想明白其中的奥秘,要了解一下关于状态寄存器的知识了

状态寄存器:

当我们进程接收信号的时候,一定是OS给我们进程发送信号的,毕竟OS是进程,硬件的管理者,也就是说当发生异常的时候,OS就会向出现异常的进程发送信号,这样当前进程收到信号后就会执行默认动作,但是OS是怎么知道哪一个进程出现问题的呢?

在CPU中,有许多寄存器,其中大多数是用来存储信息的,比如我们知道的eip(也叫PC指针),其中就是所执行代码的下一行语句

当然寄存器中还有一个特殊的状态寄存器,这个状态寄存器通过一系列二进制标志位从0变为1,反映出现了异常

此时,如果当OS发现某个进程的状态寄存器出现异常了,OS就会向这个进程发送信号,想要让这个已经异常的进程终止

但是OS是不会强制让出现异常的进程终止的,因为尽管异常进程的某一行出问题了,但是如果这个进程之前打开了某些文件或者正在保存某些文件,当OS直接把这个进程杀死了,那么就会丢失数据,那么OS就要背锅了

所以在设计OS,在处理异常进程的时候就不会让OS直接杀死异常进程,而是给异常进程发送信号,告诉对应的进程你出现异常了,你自己找个时间退,一般默认情况下就会退的,毕竟已经出异常了,尽管最后出了结果,那么这个结果肯定是错误的,毕竟在代码中途就有异常

但是如果进程捕捉了对应的信号,那么默认退出就不会生效,比如在我们上述的除零异常中,将8号信号捕捉了,这样导致默认退出不生效,此时进程就会被CPU一直调度,然后OS就一直检测这个进程有除零异常,然后OS就一直给这个进程发送8号信号,但是8号信号被捕捉了,就又会打印我们写的cout,由于这个进程不会退,就会一直重复前面操作,这样的话就会出现一直调用我们的myhander函数

那么我们思考一个问题,这个CPU中的状态寄存器出现异常了,那么这个CPU不应该就出现问题了吗?那么状态寄存器是只有一个的,那么别的进程呢?会不会受到影响呢?------ 这显然是不会的----- CPU中的程序状态、资源占用、寄存器值被称为进程的上下文,当每一个进程被CPU调度的时候,上一个进程就会把它自己的上下文从CPU中拿走,这样新的进程就会把它自己的上下文传输到CPU中对应的寄存器处,也就是说进程替换,寄存器中的数据也会替换,这样,进程间就是互不影响的,也间接证明了进程间的独立性,

所以,虽然是修改的CPU内部的状态寄存器,每一个进程是只影响它们自己的,也就是说:硬件异常是代表这个进程出现了异常,并不会波及我们的操作系统,也就是说,引起出错的永远是进程,CPU出错,我们的OS依然不会崩溃

野指针异常:

void myhander(int signo)
{
    cout << "a signo get : " << signo << endl;
    sleep(1);
}

int main()
{
    signal(11,myhander);
    int* p = nullptr;
    *p = 1;

    return 0;
}

如上,当出现野指针的时候,如果不捕捉对应的信号,就会出现段错误的提示

这实际上就是OS给进程发送11号信号

那么野指针问题是怎么产生的呢? ----- 如下图

当CPU在页表中读取虚拟地址的时候,并不是直接通过CPU处理成物理地址返回的,而是通过页表+MMU(内存管理单元)来进行由虚拟地址到物理地址之间的转化的

当没有对应的映射关系,或者指针越界了等等,这样就转化失败了,MMU对应的硬件就会报错,然后就会将转化失败的地址放到CPU中一个特殊的寄存器中,这个寄存器也会报错

那么CPU是如何知道进程是发生了错误呢?野指针还是越界问题 ------ 这是因为在CPU中不同类型的寄存器报错就对应着不同的错误

然后OS发现错误了,就会给异常的进程发送信号,想要让这个进程终止,这样就回到了上述讲过的内容了

五、软件条件:

1、alarm:

这是一个用来定时的闹钟,当到了设置的时候,OS就会给我们进程发送信号(14号信号)

形参:代表着设定的时间,单位是秒

返回值:如果上一个设置的闹钟提前醒来了,就返回上一个闹钟还剩余的时间,否则返回0

int main()
{
    alarm(5);
    while(true)
    {
        cout << "a process pid : " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

如上,我们设置一个5秒的闹钟,接下来运行进程,看看结果

如上,我们发现当过了5秒后,就会发现当前进程结束了,可以发现14号信号中是上述的缩写

我们通过7号man手册查看信号表中的SIGALRM信号:发现其是请求进程正常终止的默认处理行为

我们可以将14号信号进行捕捉,这样就能够每过一定时间就能够让闹钟响一次

void myhander(int signo)
{
    cout << "a signo get : " << signo <<endl;
    alarm(5);
}

int main()
{
    signal(14,myhander);
    alarm(5);

    while(true)
    {
        cout << "a process pid : " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

 

然后我们验证返回值:每过一定时间都在命令行中发送对应的信号,如下,这个闹钟本来每过20秒就发送一次信号,但是我们在命令行中每次都体现发送信号,这样就能够看到闹钟提前结束了

void myhander(int signo)
{
    cout << "a signo get : " << signo <<endl;
    int n = alarm(20);
    cout << "上一个闹钟剩余时间 : " << n << endl;
}

int main()
{
    signal(14,myhander);
    alarm(20);

    while(true)
    {
        cout << "a process pid : " << getpid() << endl;
        sleep(1);
    }

    return 0;
}

在闹钟的底层实现中:肯定是通过时间戳来进行实现的,在进程本地获取当前的时间戳,然后在将这个时间戳和我们传入的参数相加就得到了未来某一时刻的,然后当本地时间等于这个时刻后,OS就发送信号,让这个闹钟醒来

在我们的OS中,可能维护着不只一个闹钟的,所以OS就需要将这些闹钟管理起来,怎么进行管理呢? ----- 先描述,再组织

2、核心转储:

如下,我们查看7号手册中的表的时候,

可以看到Action中有许多不同的,这次我们了解了解Core,其解释如下:

翻译过来就是:默认作是终止进程并转储 core

在我们之前学习进程等待中,有一个小的知识点:

这次,我们学习第8位:就是core dump

在这个比特位中:

0表示上述动作中的term ----- 只是终止进程
1表示上述动作中的core ----- 先发生核心转储,生成核心转储文件,再终止进程

但是上述中,明明3,4,6,8等信号都是core,应该都要生成文件啊,但是我们之前的使用中并没有生成核心转储文件

这是因为在云服务中,这个功能默认是关闭的,要手动打开:

首先用ulimit -a来查看系统当中的标准配置

如上,core file size的大小是0,我要将其设置成有大小的,方法通过ulimit -c 大小

如上这样就设置好了

如果在保证ulimit设置过的前提下,如果还没有生成对应的文件就是需要配置下相关配置文件了,在ubuntu系统下需要设置如下,才能够生成对应的文件

接下来如下代码:

int main()
{
    pid_t id = fork();
    if(id == 0)
    {
        //child
        int cnt = 50;
        while(cnt--)
        {
            cout << "i am a child process my pid : " << getpid() << endl;
            sleep(1);
        }
        exit(0);
    }
    //father
    int status = 0;
    int rid = 0;
    rid = waitpid(id,&status,0);
    if(id == rid)
    {
        cout << "wait id sucess , rid = " << rid <<
         " exit code = " << ((status>>8)&0xFF) <<
          " exit signal = " << (status&0x7F) <<
           " core dump = " << ((status>>7)&1) << endl; 
    }
    return 0;
}

如上,运行结果可以发现core dump就是status中的第8位,其为1,并且在当前进程目录下生成core.pid文件 ----- 这个就叫做核心转储

作用:

当程序发生错误后,这个文件能够帮助我们快速定位原始代码中的错误行:

比如,在以前的代码中写下一个异常

然后就会生成一个core.pid 的文件,在调试中就加上core-file core.pid这行指令即可,然后就可以发现了错误代码在哪一行

上述这种就是直接定位到错误行,属于事后调试,而我们以前在vs2022中边走边F11这是逐行调试,属于事中调试


网站公告

今日签到

点亮在社区的每一天
去签到