解读Linux进程的“摩尔斯电码”:信号产生的原理与实践,掌控进程的生死时速

🔥海棠蚀omo个人主页

❄️个人专栏《初识数据结构》《C++:从入门到实践》《Linux:从零基础到实践》

追光的人,终会光芒万丈

博主简介:

​​​​​​

目录

一.现实--信号概念

1.1信号是什么?

1.2为什么会有信号?

二.将信号从现实映射到计算机中

三.部分Linux系统中产生信号的方式

四.对信号本质的朴素认识

四.Linux中其他产生信号的方式

4.1系统调用

[4.1.1 kill函数](#4.1.1 kill函数)

[4.1.2 raise函数](#4.1.2 raise函数)

[4.1.3 abort函数](#4.1.3 abort函数)

4.2软件条件

[4.2.1 alarm函数](#4.2.1 alarm函数)

4.2.2定时器重置和定时器取消

4.2.3验证IO效率问题

4.3异常

五.扩展知识

[5.1 \0问题所导致的异常](#5.1 \0问题所导致的异常)

5.2野指针所导致的异常

5.3默认动作Core

今天我们就要步入新的篇章:Linux进程信号 ,在接下来的篇章中我们要深入探究Linux中信号的产生,信号的保存以及信号的处理等相关内容。

废话不多说,下面我们来介绍关于信号产生的相关知识。

一.现实--信号概念

1.1信号是什么?

在现实生活中其实处处都有信号的身影,比如:红绿灯所显示出的红灯,绿灯;你为了早上起床所定的闹钟;你在游戏中所发出的进攻,集合等信号;上学期间每节课的上下课铃声等等。

当我们或听,或看到这些信号,我们就知道自己该干什么了,比如:过马路时看到红灯,我们就知道该停下了;听到闹钟我们就知道该起床了;听到下课铃声,就知道可以放松一下了等等。

换句话说,也就是没有信号的时候 ,我们就已经知道信号产生的时候该如何做了,那这代表什么呢?

代表我们能识别这些信号,为什么我们能识别这些信号呢?

因为我们给灌输了相应的常识,也就是我们脑海中记录了:信号特征和信号的处理方法!!!

1.2为什么会有信号?

所以现实世界中为什么会有这些信号呢?

我们传递信号的目的就是为了告诉人应该做什么了,也就是信号是作为信息事件进行通知的!!!

但是信号的产生,是异步产生的,什么意思呢?

你知道等你到路口的时候红绿灯显示的是红灯,绿灯还是黄灯吗?你知道你定的闹钟会在早上具体哪一时刻响起吗?你在操场上进行活动的时候,你知道下课铃声什么时候响起吗?

这些信号产生的时刻我们都不清楚,我们只能被动的的接收这些信号,只有这些信号通知我们了,我们才知道接下来该去做什么了,也就是信号是异步产生的。

我们再思考一种现象:

闹钟响了,你就会立刻起床吗?外卖到了,你会立刻下楼取餐吗?下课铃声响了,你的老师会立刻下课走人吗?

答案是不一定会,换句话说:当信号到来的时候,我们可能正在做其他的事情,所以,信号处理的过程,可能不是立即处理的,而是在合适的时候在处理!!!

这个时机可能是我再睡5分钟就起床,我打完这把游戏就下去取餐,老师把这个知识点讲完就下课等等,这个时机是不确定的。

通过上面的内容,我们可以总结为三点:

1.我们能识别现实中的信号

2.信号是作为信息事件进行通知的

3.信号到来的时候,可能不是立即处理的

二.将信号从现实映射到计算机中

上面我们拿现实生活中的例子来帮助大家理解信号是什么和为什么,也就是上面接收信号的主体是人,那么将信号映射到计算机中,谁来接收信号呢?

答案当然是进程,在前面的篇章中我们也演示过发送信号给进程,而进程在接收到这些信号后会做出不同的行为,这不就和上面我们人接收到信号后做出相应的行为是一样的吗?

而将进程和上面对信号的总结结合在一起,我们来思考两个问题:

1.我们人之所以能识别信号是因为我们被灌输过相关的常识,那么进程是如何识别相应的信号呢?

2.进程具体是如何处理接收到的信号的?

第一个问题 :我们要知道,进程,信号这些东西既然是在计算机中,那么必然都是程序员写的,那么要想让进程能够识别相应的信号,只需要提前在进程内部内置对信号的识别和处理机制即可。

第二个问题:进程处理信号的方式共有三种:

1.默认动作

2.忽略信号

3.自定义捕捉

下面我们就举几个例子来帮助大家更好的去理解这三种方式:

1.我们在路口看到红灯,就自觉停下来了;我们听到闹钟就起床了;我们听到上课铃声就拿出了相应的课本,这都是我们下意识的行为,也就是默认动作

2.现实生活中,你看到红灯,你就一定会停下来吗?听到闹钟,你一定会起床吗?有人给你打电话,你一定会接吗?

我想有时候并不会吧,那么我们这样做不就是忽略信号吗,虽然发送了信号,但是我当作没看见,直接忽略。

3.当你走到路口的时候,亮红灯了,你既不停下来,也没闯红灯,而是唱起了歌,或者跳起了舞等其他的动作。

虽然你接收了信号,但是并没有去做该做的事,而是去做了其他毫不相关的事情,这叫做自定义捕捉

三.部分Linux系统中产生信号的方式

既然已经将视角从现实转移到了计算机中,那我们就有必要了解一下Linux中信号的种类,我们来看:

通过kill -l命令所打印出的这些就是linux中常用和标准的信号分类,这张图呢我们在之前的篇章中也展示过,那么我问大家一个问题:上图中共有几种信号呢?

可能有人看到开头的1和结尾的64便觉得这些信号共有64种,但其实只有62种,我们看31后面直接跟的就是34,而不是32,所以只有62种,至于为什么这样设计我们后面就知道了。

这些信号其实就是我们所熟知的,每种信号就是一个int类型的数字,而我们把这些信号分为两类:

1.前31种信号我们称为:普通信号

2.后31种信号我们称为:实时信号

不过我们的重点会放在对普通信号的讲解上面,实时信号我们一般用不到,所以就不对其进行过多的介绍了。

下面我就通过一个例子来帮助大家了解一些linux中产生信号的方式:

cpp 复制代码
int main()
{
    while(true)
    {
        cout << "我是一个进程, " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

上面我们就写了一个简单的例子来打印出当前进程的pid,但我们的重点不在这,我们上面写的是一个循环,我是怎么让它停止的呢?

很明显,我是通过ctrl + c的方式来结束了当前进程,那么为什么ctrl + c就能让进程终止呢?

因为ctrl + c的本质就是向目标进程发送信号,而进程接收到了信号,自然就会做出相应的行为,那么ctrl + c发送的是什么信号呢?

如上图所示,ctrl + c所发送的信号就是2号信号SIGINT,对它的解释就是后面的Interrupt from keyboard,,意思就是从键盘中获取的中断信号。

它的作用就是前面的Term ,也就是单词Terminate,表示终止, 也就是该信号的默认行为是立即终止进程。

所以我们通过ctrl + c给进程发送2号信号,进程就会终止了,但是上面也说了默认行为是终止进程,而我们上面也讲了进程处理信号的方式不止有默认行为,还有忽略信号和自定义捕捉,那我要想让进程以其他方式来处理信号呢?

答案就是使用系统调用函数,这个函数就叫做:signal ,这个函数的作用就是更改进程对信号的处理动作,而这个函数的参数有两个,分别为一个int类型的参数和一个自定义类型的参数。

先说int类型的参数,这个参数就是你要处理的信号的名字或者编号,上面我们也说了那些信号本质都是int类型的宏,所以你填名字或者编号都可

第二个参数,它就表示新的动作,它的类型在上图中也有介绍,是一个函数指针,返回值是void,参数是只有一个并且是int类型,这个类型主要针对的就是第三种处理方式,也就是自定义捕捉,而对于前面两种处理方式则会传不同的参数

说了这么多,我们以一个例子来观察其它的信号处理方式:

cpp 复制代码
int main()
{

    signal(SIGINT, SIG_IGN); // 表示忽略信号

    while (true)
    {
        cout << "我是一个进程, " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

首先我们来介绍如何让进程忽略信号的做法,第一个参数就传我你们要处理的信号SIGINT,而第二个参数我们要传SIG_IGN,IGN就是英文单词ignore,所以它的作用就是忽略信号

下面我们来看看效果:

从结果中我们可以看到,在调用了signal函数让当前进程忽略SIGINT信号后,我们再次通过ctrl + c就无法再终止进程了,可见我们确实可以通过signal函数来更改进程对信号的处理方式。

那么我们下面再看一个:

而如果我们将第二个参数改为SIG_DFL,发现通过ctrl + c又可以终止掉进程了,后面的DFL就是单词default,表示默认,所以它的作用就是让进程对信号执行默认动作

其实从最初的例子中我们也可以发现,即使没有调用这个函数,进程对接收到的信号也是执行默认动作,所以这里我们了解有这种方式即可。

那么最后我们就用自己实现的函数去传参了,我们来看:

cpp 复制代码
void Print(int sig)
{
    cout << "嗨嗨嗨我来了啊~~~" << endl;
}

int main()
{

    // signal(SIGINT, SIG_IGN); // 表示忽略信号
    // signal(SIGINT, SIG_DFL); // 默认动作
    signal(SIGINT, Print); // 自定义捕捉
    while (true)
    {
        cout << "我是一个进程, " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

当我们将函数传进去后,我们再使用ctrl + c,此时进程虽然接收到了SIGINT信号,但是它却去执行了Print函数,而这就叫做自定义捕捉。

那么此时就有人问了:函数的返回值void我能理解,但是这个int类型的参数是谁呢?

这个int类型的参数传的就是signal函数的第一个参数,也就是信号值,这样这个函数就知道是哪个信号触发了它,我们来看:

只要我们不使用ctrl + c,也就是不向进程发送信号,那么这个处理函数就不会被调用。

那么此时就有一个有意思的问题:如果我们通过signal函数将所有的信号都进行自定义捕捉或忽略信号,那么是否意味着这个进程就无法被终结了?

我们仔细一想好像还真是这样,因为大部分信号的默认处理动作都是终止进程,但是我们通过signal函数将默认动作修改为自定义捕捉或者忽略信号,按理说确实无法终结这个进程了,那么事实真是如此吗?我们用一个例子来看看:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    for(int i=1;i<=31;i++)
        signal(i,handler);
      //signal(i,SIG_IGN);

    while(true)
    {
        cout << "我是一个进程, " << getpid() << endl;
        sleep(1);
    }
    return 0;
}

这里我用了多个信号进行测试,发现确实都无法使当前进程终结,但是:

但是我向进程发送9号信号,却使当前进程终止了,也就是说不是所有的信号都可以被捕捉,可以被忽略,有部分信号是不可被捕捉的,不可被忽略的

并且不只是9号信号,19号信号也同样不可被捕捉,不可被忽略。

而有了对上面知识的理解后,我们就可以得到一个结论,也是对这部分标题的回答,即产生信号的方式1:键盘可以向目标进程发送信号!!!

那么除了上面我们介绍的ctrl + c能够发送信号外,还有哪些键盘操作能够向进程发送信号呢?

答案就是ctrl + \,它也能向进程发送信号,发送的信号为:

它发送的信号为为3号信号SIGQUIT,这个信号的作用和上面的2号信号SIGINT效果差不多,都能够终止掉进程,但是也有其他地方不一样,不过这不是我们要讲解的重点,所以就不过多介绍了。

那么还有一种产生信号的方式我们也不陌生:

没错,就是我们早就演示过的kill命令,所以产生信号的方式2:通过kill命令向目标进程发送信号!!!

四.对信号本质的朴素认识

有了上面的知识作为铺垫后,我们下面来更深入的探讨一下信号的本质。

上面我们说进程在收到信号后,不会立即处理,那么这意味着什么呢?

意味着进程要记录下来对应的信号,就像我们点了外卖,当我们正打游戏时外卖到了,你跟外卖员说等会儿就下去,那么此时我们心里就会记着等会儿游戏打完了要下楼拿外卖。

我们可以将信号记在心里,那么进程将信号记录在哪里呢?又是怎么记录的呢?

进程的PCB中嘛,里面记录了进程的属性,当然也包括进程接收到的信号了,而记录信号的方式我们来看:

没错,就是位图在位图中:比特位的位置就表示信号的编号,比特位的内容就表示是否收到该信号。而上面的普通信号只有31个的原因也有位图的因素在里面。

而有了上面的理解后,下面我们来思考一个问题:如何理解给进程发送信号?

无非就是修改目标进程task_struct中信号位图的特定位置由0->1即可,这样做的本质就是向目标进程进行写信号!!!

但是我们也知道进程的task_struct是操作系统创建的,换句话说,进程的task_struct就是内核的数据机构对象,那么对其进行修改本质不就是修改内核数据吗?谁有资格对内核数据进行修改啊?

当然是操作系统了,所以这里我们就可以得出一个结论,即:无论信号发送的方式有多少种,最终,全部都是通过OS向目标进程发送信号的!!!

四.Linux中其他产生信号的方式

上面我们了解了Linux中产生信号的两种方式:键盘产生和kill命令产生,下面我们再来看几种产生信号的方式。

4.1系统调用

4.1.1 kill函数

那么第一个我们要介绍的系统调用函数就是:kill,作用就如上图所示:向一个进程发送信号

返回值很简洁,成功发送信号就返回0,失败就返回-1,不解释了。

它的参数也很好理解,第一个参数就是目标进程的pid,第二个参数就是你要发送的信号,下面我们通过一个例子来自己实现一个kill命令:

cpp 复制代码
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "Usage: " << argv[0] << "signumber pid" << endl;
        return 1;
    }

    int sig=stoi(argv[1]);
    pid_t target=stoi(argv[2]);

    int n = kill(target,sig);
    if(n<0)
    {
        perror("kill");
        return 2;
    }
    return 0;
}

这里我们要借助命令行参数来实现,用法就和上面的kill命令很像:./可执行文件名 + 信号编号 + 进程pid,下面我们就来看看效果:

结果如我们所料,通过我们自己写的程序,完成了和kill命令一样的效果。

其实大家看到这个函数的名字也就能猜到,kill命令的底层其实就是通过kill函数实现的。

4.1.2 raise函数

那么第二个系统调用函数就是:raise,这个函数的作用:给调用这个函数的进程,也就是当前进程发送信号

参数就更为简洁,直接填你要发送的信号即可,那么下面我们用一个简单的例子来看看效果:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    signal(2,handler);
    raise(2);
    return 0;
}

从结果我们可以看到,对2号信号进行自定义捕捉,再通过raise函数向当前进程发送2号信号后,确实调用了handler函数。

4.1.3 abort函数

那么最后一个函数就是:abort ,这个函数的作用就更为简单粗暴:就是让当前进程终结,简而言之就是向当前进程发送一个指定的信号

那么是那个信号呢?我们来看看:

这个信号就是6号信号SIGABRT,默认动作是Core,也就是终止进程,那要怎么证明呢?我们下面来看看:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    signal(6,handler);
    abort();
    return 0;
}

通过对6号信号进行自定义捕捉,再次调用abort函数后,结果中我们也看到确实调用了handler函数,说明abort函数确实会向当前进程发送6号信号。

4.2软件条件

4.2.1 alarm函数

什么叫做软件条件产生信号呢?

其实我们在前面讲解管道时就已经体现过,我们当时说当管道的读端关闭,而写端没有关闭,此时写端再向管道中写入数据就没有意义了,所以操作系统此时会杀掉进程,即给相应的进程发送13号信号SIGPIPE,此时就是发送13号信号SIGPIPE的软件条件满足了

而关于管道的这部分我们已经讲过了,所以今天我们再来看一种由软件条件产生信号的方式:

这种方式就是利用一个函数:alarm,这个函数的作用就是在内核中为当前进程设置一个定时器,当时间到了,就满足了发送信号的软件条件,它的参数就是定时器的时间,单位是秒(s)。

那么我们下面就来看看这个函数的效果吧:

上面我就设置了一个定时器,时间为3秒,结果也正如我们所料,在while循环执行了3次过后,满足了发送信号的软件条件,进程也就被终止了,那么这个信号是谁呢?

正是14号信号SIGALRM,这个信号的默认动作就是Term,也就是终止进程

但上面我们只设置了一个定时器,也就只起一次效果,那我们要想起到多次效果呢?我们来看:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
    alarm(3);
}

int main()
{
    signal(14,handler);
    alarm(3);
    while (true)
    {
        cout << "I am a process" << endl;
        sleep(1);
    }
    return 0;
}

只需要多次alarm就可以了,这种做法就是对14号信号进行自定义捕捉,并在handler函数中重新通过alarm函数设置定时器,这样就可以做到每3秒触发一次,向目标进程发送14号信号。

4.2.2定时器重置和定时器取消

相比有人对于上面让定时器多次起效果的做法有些疑惑:为什么不把alarm函数放在循环中呢?

这就与alarm函数的特性与返回值有关了,我们来看:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    signal(14, handler);
    alarm(3);
    sleep(2);
    int n = alarm(10);
    cout << "定时器剩余时间:" << n << endl;
    while (true)
    {
        cout << "I am a process" << endl;
        sleep(1);
    }
    return 0;
}

这个特性就是:当一个定时器时间没有走完之前,如果又设置了新的定时器,那么定时器的时间就会被重置,以最新的为准

简单来讲就是第二次设置定时器的新时间,会取消上一次的闹钟,并返回上一个定时器的剩余时间

这两点在上面的例子中都体现了出来,当设置了新的10s的定时器后,alarm函数的返回值就是上面3s定时器的剩余时间,而在下面的while循环执行了10次,也就是10s过后,才向进程发送了信号。

一切都如我们所料,那么有人可能会问:那要是只有一个定时器,返回值是多少呢?

返回值的最后一句话告诉了我们答案,当一个定时器前面没有其他的定时器时,就返回0

那么现在就可以回答我们最初的问题了,不将alarm函数设置在while循环中是为了避免上面的定时器重置现象,如果我们将alarm定时器设置在while循环中,那么就会重复进行定时器重置,我们就看不到发送信号的现象了。

最后我们思考一个问题:如果我想取消定时器该怎么办呢?

很简单,只需要将alarm的参数设置为0即可,我们来看:

取消定时器后,可以看到后面就只会输出while循环中的内容,不会再向进程发送信号了。

4.2.3验证IO效率问题

我们通常可以用alarm函数来验证IO效率问题,如何做呢?我们来看:

cpp 复制代码
int main()
{
    int count = 0;
    alarm(1);
    while (true)
    {
        std::cout << "count : " << count << std::endl;
        count++;
    }
    return 0;
}
cpp 复制代码
int count = 0;

void handler(int signumber)
{
    std::cout << "count : " << count << std::endl;
    exit(0);
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1);
    while (true)
    {
        count++;
    }
    return 0;
}

从上面的例子中我们可以看到,同样都是将定时器设置为1s,但是第一个例子中count只累加到8万多就到时间了,但是第二个例子count却是累加到了4亿多,这之间可是万余倍的差距啊。

由此可以看出进行IO操作时是很耗费时间的,效率很低,换句话说系统调用是需要成本的!!!

4.3异常

那么我们要介绍的最后一种产生信号的方式就是:异常,如:\0操作,野指针等等。

我们想必都有过因为大意而让数据进行\0操作或者对野指针解引用的操作,往往这些情况出现的时候我们的程序就会崩溃,那么为什么\0,野指针会导致程序崩溃呢?

有了对上面知识的了解后答案很明显,因为我们的异常操作,被操作系统识别到了,所以给目标进程发送了信号,然后进程处理信号,默认就终止了,那么这些信号是谁呢?

如果是\0操作我们看到程序并没有执行完就终止了,报的错误是Floating point exception,我们通过对比信号表可以发现,这个信号就是:

这个信号就是8号信号SIGFPE,可以看到后面引起8号信号的原因正是上面的报错信息。

na

而如果是野指针操作,同样也是程序没有执行完就终止了,我们看到报的错就是Segmentation fault,也就是段错误,那这个信号又是谁呢?我们接着看:

这个信号就是11号信号SIGSEGV,引起它的原因就是对无效的内存进行解引用,对应的正是我们上面的野指针操作。

下面我们再看个有意思的现象:

cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    signal(8, handler);
    int x = 10;
    x /= 0;
    cout << "程序出现了异常!" << endl;
    return 0;
}
cpp 复制代码
void handler(int sig)
{
    cout << "捕捉到信号: " << sig << endl;
}

int main()
{
    char *p = nullptr;
    *p = 10;
    cout << "程序出现了异常!" << endl;
    return 0;
}

我们把上面的两种情况一起来看,我们只是对信号进行了自定义构造,并没有循环操作,但是从结果中我们可以看到进程一直在捕捉同一个信号,与之前的自定义构造的现象并不相同,这是为什么呢?

答案就与我们下面要讲的只是有关了,我们接着往下看。

五.扩展知识

5.1 \0问题所导致的异常

那么首先我们就先来探究一下\0问题为什么会导致上面进程一直捕捉同一个信号,我们来看:

众所周知我们写的代码中的一些数据计算,都是加载到内存中后,在进程被调度时这些数据会被加载到cpu中的某些寄存器中,这里我就以eax,ebx和ecx来举例,就比如上面的10放在eax中,0放在ebx中,而他们计算的结果放在ecx中,而这些寄存器本质就是:当前进程的硬件上下文!!!

当然我们知道上面的10\0的计算肯定是不对的,cpu也知道,所以在检测到上面的\0操作后:

cpu中有一个控制和状态寄存器,而这个寄存器中有一个溢出标志位,当检测到这种\0操作后,会将这个溢出标志位由0改为1,那么此时就表示本次运算在硬件上报错了!!!

那么硬件报错了,操作系统要不要知道呢?

当然要知道,因为操作系统是软硬件资源的管理者,那么此时操作系统就要解决这个问题,也就是给相应的进程发送信号,操作系统如何知道给哪个进程发送信号呢?

在操作系统中会有一个指针叫做:struct task_struct* current,这个指针永远指向正在调度的进程,而我们的进程在调度时报错了,那么操作系统就会根据这个指针找到相应的进程,并向其发送信号。

操作系统想目标进程发送信号的本意是想终止掉正在调度的进程,而进程一旦被终止掉,那么进程的硬件上下文就不存在了,cpu的报错也就没有了,cpu就会正常工作了

换句话说操作系统是为了恢复cpu的正常工作而向进程发送信号的,但是在上面的例子中我们也看到了,我们对信号进行了自定义捕捉,导致进程没有被终止掉,那么cpu的报错就会一直存在,导致的情况就是代码无法接着往下走,但是进程又终止不掉,一直存在着,相当于半死不活

只要进程一直存在,那么它就会在进程的调度队列中,也就会被重新调度,而一被调度就会报错,报错就给进程发信号,但是又解决不掉,下次又会接着报错,周而复始,所以上面的进程才会一直接收同一个信号,因为操作系统给它一直发啊。

5.2野指针所导致的异常

和\0问题一样的还有上面的野指针问题,下面我们接着看:

和上面一样对野指针解引用并进行赋值的操作同样是在cpu中进行的,在cpu中EIP寄存器存储的是要执行的下一条指令的虚拟地址,而IR寄存器存放的是要执行的指令,我们要想对野指针解引用,就要找到其物理地址,再将计算后的结果写入进去就行,这是正常流程。

而要想找到野指针在内存中的物理地址,要有两个条件:

1.页表的起始地址

2.野指针的虚拟地址

对于第一条页表的起始地址,它存放在CR3寄存器中,而野指针的虚拟地址就存在某个通用寄存器中,如:EBX,这里就不画了,所以二者都有的情况下,将这些数据交给MMU内存管理单元,MMU就会根据它的硬件电路找到页表,并查询野指针所对应的物理地址,但是它能找到吗?

它找不到的,野指针的物理地址它怎么能找到,所以:

MMU就会立即触发一个硬件异常(页错误),之后它会将野指针的虚拟地址写入到CR2寄存器中,CR2寄存器是用来存储触发页错误的线性地址的,也就是和上面的\0操作一样:本次操作在硬件上报错了!!!

硬件报错了,操作系统就会查看CR2寄存器,看是哪个虚拟地址导致的问题,最终同样会像上面一样,给相应的进程发送信号,也同样会出现和\0操作一样的问题,进而导致进程一直接收同一个信号。

所以我们现在就弄清楚了cpu报错操作系统为什么要给进程发信号,因为操作系统要让cpu恢复正常工作!!!

所以上面信号产生的方式:异常全名应该为信号产生方式:硬件异常

但是野指针问题不一定会引起硬件异常,这点相信我们在写代码时也遇到过,就比如:数组的越界问题,数组越界有时真不一定会引起硬件异常,程序是可以正常运行的,这样的情况还有很多,这里就不一一介绍了。

5.3默认动作Core

相信在上面我们讲解不同的信号时,默认动作有的是Term,有的却是Core,Term我们清楚,就是终止当前进程,但是Core呢?我们只从现象来看,它和Term没什么区别,同样也是终止进程,但Core真是我们所看到的那么简单吗?

我们拿默认动作为Core的信号和Term的信号做一下对比,Core就比如:8号信号,11号信号,Term就比如:9号信号,2号信号。

2号信号和9号信号都是由用户主动向进程所发送的,于是进程被终止了,所以我们不关心它是怎么被终止的,但是8号信号和11号信号是由用户所写的代码出现了异常所引起的,我们是可以通过进程崩溃所返回的信息知道进程是因为什么原因崩溃了,但是我们同样也更想知道到底是代码的哪一行导致程序的崩溃

那要如何知道具体是代码的哪一行导致的呢?

在进程等待的章节中,我们在讲解进程因为异常而被信号杀掉时,在当时所展示的图中有一个core dump标志位我们没讲,现在我们就可以讲一讲这个标志位是干什么的了。

正常流程是当操作系统向进程发送信号时,如果操作系统发现发送的是默认动作为Core的信号,那么它会给进程发送信号的同时,修改core dump标志位,将其从0改为1。

而在进程被终止后,操作系统就会查看core dump标志位是否为1,如果为1,那么就会执行核心转储工作,也就是core dump操作,即把进程当前运行的上下文数据,转而存储到当前目录下,形成一个core文件!!!

而这个core文件将来就可以用于调试,就可以定位到具体是哪一行代码出的问题了。

但是要完成这个工作有一个大前提:当前的系统允许生成core文件,什么意思呢?我们来看:

我们通过ulimit -a命令可以看到,在当前的系统中是不允许生成core文件的,这个文件的大小都为0,所以我们上面在讲解8号信号和11号信号时,虽然它们的默认动作为Core,我们却没有看到相应的core文件,core文件默认是被禁掉的!!!

所以我们要怎么做才能解除这个限制呢?我们来看:

我们可以通过ulimit -c命令来让系统允许生成core文件,后面跟的数字就是这个文件的大小,那么既然现在系统已经允许生成core文件了,我们就再来执行一遍:

结果如我们所料,在报的错后面就跟上了core dumped的字眼,说明执行了core dump操作,而我们查看当前目录下的文件,可以看到此时就有了一个core文件。

那么上面既然说了core文件是用来调试的,那该如何操作呢?我们就来看看:

要使用这个core文件,使用方法如上图所示:core-file + core文件名称,就可以直接定位到导致程序崩溃的那一行代码了,行号也标了出来。

以上就是解读Linux进程的"摩尔斯电码":信号产生的原理与实践,掌控进程的生死时速的全部内容。

相关推荐
Yana.nice4 小时前
openssl将证书从p7b转换为crt格式
java·linux
AI逐月5 小时前
tmux 常用命令总结:从入门到稳定使用的一篇实战博客
linux·服务器·ssh·php
小白跃升坊5 小时前
基于1Panel的AI运维
linux·运维·人工智能·ai大模型·教学·ai agent
跃渊Yuey5 小时前
【Linux】线程同步与互斥
linux·笔记
舰长1155 小时前
linux 实现文件共享的实现方式比较
linux·服务器·网络
zmjjdank1ng6 小时前
Linux 输出重定向
linux·运维
路由侠内网穿透.6 小时前
本地部署智能家居集成解决方案 ESPHome 并实现外部访问( Linux 版本)
linux·运维·服务器·网络协议·智能家居
VekiSon6 小时前
Linux内核驱动——基础概念与开发环境搭建
linux·运维·服务器·c语言·arm开发
zl_dfq6 小时前
Linux 之 【进程信号】(signal、kill、raise、abort、alarm、Core Dump核心转储机制)
linux
Ankie Wan7 小时前
cgroup(Control Group)是 Linux 内核提供的一种机制,用来“控制、限制、隔离、统计”进程对系统资源的使用。
linux·容器·cgroup·lxc