目录

前言:

预备知识

信号产生


前言:

前文已经将进程间通信介绍完了,介绍了相关的的通信方式。在本文介绍的是信号部分,那么一定有人会有问题是:信号和信号量之间的关系是什么呢?答案是,它们之间的区别就是老婆和老婆饼之间一样,没有关系。

对于信号部分,我们分为四个阶段来介绍,一个是信号的预备知识,一个是信号产生,一个是信号保存,一个是信号处理。

在本文中,介绍信号的预备知识和信号产生。那么话不多说,直接进入主题吧!


预备知识

对于信号来说,我们平常生活中时时刻刻都在接收,比如红灯停绿灯行,就是一种信号,比如闹钟响了,也是一种信号,比如外卖员打电话来了,我们知道要拿外卖,这是我们知道信号怎么处理。

从上面我们可以得出来的结论是:

信号是随时产生的,要处理信号的前提条件是能认识这个信号。

那么,如果外卖员打电话的时候,我们正在打游戏,那么外卖员发出的信号我们应该如何处理呢?我们可以选择终止我们正在打游戏这个行为,我们也可以忽略外卖员的信号,我们也可以有其他反应。

以上是信号在生活中的例子,那么有意思了,如果我们将我们换成进程呢?

似乎就关联起来了?

我们其实在进程部分也是使用过信号的,比如9号信号是直接杀死进程,我们可以使用kill -l查看所有的信号:

那么,我们可以注意到一个点是信号是从1开始的,而不是从0开始的,并且在1-31是一个梯队,34到64是一个梯队。其中,34往后的信号都是实时信号,我们暂时先不用管。我们在信号这个主题要介绍的信号是前面31个信号,叫做普通信号

所以,现在我们对信号有了一个基本的概念认识。

信号:Linux提供的一种向指定进程发送处理某种特定事件的方式。

所以信号实际上是一种处理方式,那么信号是同步的还是异步的呢?

信号产生是异步的,我们通过一个例子对同步和异步理解:

老师上课的时候,让小王出去拿东西,但是老师不会因为小王出去拿东西停止自己讲课这个行为,并且老师给小王发送的信号是出去拿东西,所以是异步的。

我们通过man的7号手册查看signal:

就可以看到如上这么多信号。

对于信号来说,预备知识部分我们通过外卖员的例子,可以知道信号有3种处理方式,一种是默认行为,一种是忽略,一种是自定义行为,其中的默认行为实际上就是终止当前进程。

对于第三列有Core Term的信号,都是代表如果接受到的该信号,默认行为都是终止。

那么我们先不管,我们先试试:

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

int main()
{
    while(true)
    {
        std::cout << "Hello signal" << " pid is :" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

其实可以发现,不管是发送哪个信号都会终止该进程。

对于默认行为我们有了一定了解,忽略我们暂时先不考虑,我们先介绍自定义行为,使用到的函数是signal,这个是在2号手册,也就是系统调用,其实,对应的参数是,信号以及函数指针,该函数的意思是如果该进程接受到了信号signum,那么就执行函数指针handler对应函数。

直接试试:

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

void Handler(int sig)
{
    std::cout << "get a sig: " << sig << std::endl;
}

int main()
{
    while (true)
    {
        signal(2,Handler);
        std::cout << "Hello signal" << " pid is :" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

我们每次往这个进程发送2号信号,就会调用函数Handler,就不会终止该进程了,并且,在进程章节我们介绍了信号实际上是个宏,所以我们可以把2写成宏也是可以的。

那么我们现在来看一个有趣的现象:

我们直接ctrl + c,会奇妙的发现进程并没有终止,而是调用的函数,这说明什么,这说明ctrl + c就是2号信号!!!

所以,现在我们就知道了信号不仅可以通过kill指令发出,也可以通过键盘发出。

这里的3号同理,可以使用CTRL + \验证出来,也是一种终止进程的方式。 

现在我们不妨浅显的理解信号的理解和保存

对于Linux中的任意文件,都是先描述再组织,每个进程也就是task_struct,里面有一个成员变量是uint32_t signals,可是一个成员变量如何表示所有信号呢?

不要忘了,普通信号有31个,一个32位的整型,一共有32位比特位,因为没有0号信号,所以从第1位比特到到第31位比特位都是用来表示信号的,如果接受到了信号,那么对应的比特位就变成1,这也是位图的应用。

那么提问,进程是内核数据结构对象,谁有资格修改内核数据结构对象中的值呢?

当然只有OS了。


信号产生

以上是信号的预备知识,现在,我们来深究信号产生的原理,

信号可以怎么样产生呢?

第一种方式是命令行参数,是用kill -signum pid即可,第二种方式是键盘输出输入,第三种方式是系统调用。我们目前使用到的函数的是signal,我们还可以使用的函数有kill,还可以使用abort。

我们先来试试kill指令,

参数是对应的pid,另一个是signum,使用起来基本上没有什么难度,但是如果我们在代码里面操作,显得就比较笨拙了,所以我们可以使用命令行参数,所以使用int argc, char* argv[]:

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        return 1;
    }
    pid_t pid = std::stoi(argv[2]);
    int signum = std::stoi(argv[1]);
    kill(pid,signum);
    return 0;
}

这是kill的用法,需要多个文件协作。

对于函数abort,底层调用的是函数raise函数。

对于该函数的描述是,abort函数发送的是SIGABRT信号,也就是碰到异常事件直接终止该进程。

void handler(int sig)
{
    std::cout << "get a sig: " << sig << std::endl;
}

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

    while (true)
    {
        sleep(1);
        std::cout << "hello bit, pid: " << getpid() << std::endl;

        abort();
    }
}

通过函数我们可以发生发送的信号是6。

可是,如果我们将所有的信号都自定义了,是不是这个进程就变成流氓进程了?

void Handler(int sig)
{
    std::cout << "get a sig: " << sig << std::endl;
}

int main()
{
    for(int i = 1; i <= 31; i++)
    signal(i,Handler);
    while (true)
    {
        std::cout << "Hello signal" << " pid is :" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

试试9号:

9号信号就是不能被自定义的,所以得出结论,不是所有的信号都可以自定义。6号信号 SIGABRT 可以被自定义捕捉处理,但是捕捉后仍然会立即退出进程,比较特殊

现在我们从新的角度来看待信号,信号发送的软件条件是什么呢?如果没有输入输出的话,信号还能够输入输出吗?当然是不可以的,所以我们现在要做一个事儿就是,验证IO的速度,验证IO之前,我们要介绍一个信号是14号信号,14号信号是闹钟信号,和我们平常理解的闹钟是一个样子的:

当时间一到,alarm函数就发送SIGALRM信号,该信号是第14信号,和我们平时理解的闹钟一样的,不过,碰到了该函数,就进程就结束了:


int main()
{
    std::cout << "begin " << std::endl;
    alarm(1);
    sleep(2);
    std::cout << "end " << std::endl;

    return 0;
}

验证IO之前,我们先使用alarm验证多次使用alarm会怎么样:

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

    alarm(6); // 设定1S后的闹钟 -- 1S --- SIGALRM
    sleep(4);
    int n = alarm(0); // alarm(0): 取消闹钟, 上一个闹钟的剩余时间
    std::cout << "n : " << n << std::endl;

    return 0;
}

对于这种情况,alarm(0)代表的情况是取消闹钟,返回的值是上一个闹钟的剩余时间。

那么我们试试1秒的闹钟里面,定义一个变量,能++多少次:

int main()
{
    alarm(1); // 设定1S后的闹钟 -- 1S --- SIGALRM
    int cnt = 0;
    while (true)
    {
        std::cout << "cnt: " << cnt << std::endl;
        cnt++;
    }
    return 0;
}

一秒钟内,大部分区间都是在60000到80000左右,看起来是不是非常快了?

当我们将cnt变量定义为全局变量之后:

int cnt = 0;

void handler(int sig)
{
    std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
}

int main()
{
    signal(SIGALRM, handler);
    alarm(1); // 设定1S后的闹钟 -- 1S --- SIGALRM
    while (true)
    {
        cnt++;
    }
    return 0;
}

现象是:

这差别可以说是天差地别了。结论就是,加入了IO,比如cout等,效率就非常低了,并且闹钟会响一次,进程终止。

那么提问,OS里面的闹钟是非常非常多的,那么OS怎么管理闹钟呢?同样,是先描述再组织,但是闹钟不像共享内存那样,拥有所谓的id或者是key什么的,它要做的不过的到时间了就给进程发信号而已,虽然会先描述再组织,但是相对没有那么麻烦。

以上是软件引发的信号。

那么,对于异常部分?

我们从两个问题探讨,一个是/0问题,一个是越界访问的问题:

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

    // int* p = nullptr;
    // *p = 10;
    return 0;
}

对于/0问题,bash进程给的报错是:

Floating point exception,那么我们在signal那个表里面查看有没有对应的描述:

就是这个,SIGFPE,对应的就是OS发给该进程的信号。

那么为什么程序会崩溃呢?本质就是因为OS给该进程发送了对应的信号,那么我们看看越界访问:

同理,在signal表里面查看:

对应的信号是SIGSEGV信号,对应的描述是Invalid memort reference。也就是非法的内存访问。

我们知道进程结束的原因是因为OS发送了信号,那么OS发送了信号之后,进程是直接终止的,那么可以不退出进程吗?

就像这样:

void Handler(int signum)
{
    std::cout << "get a sig: " << signum << std::endl;
}
int main()
{
    signal(SIGSEGV,Handler);
    int* p = nullptr;
    *p = 10;
    return 0;
}

结果是:

一直打印信号,也就是说没有退出,那么为什么我们自定义了这个信号就会造成这种情况呢?

/0和越界的原理是一样的。

对于/0来说,cpu是执行计算的吧?执行的计算可以分为是执行算数运算还是逻辑运算,对于算数运算来说,在cpu里面存在一个状态寄存器,叫做eflag,在这个寄存器里面存在一个位置叫做状态标记位,如果发生了溢出,比如/0错误,该标志位变成1,此时OS检测到了,就给进程发送信号SIGFPE即可。

可是,为什么会一直打印呢?在进程部分,我们介绍了cpu有一套寄存器,而进程的运行时间不是一直存在的,涉及到了调度问题,而对于进程来说,因为时间问题,寄存器会存储多个进程的内容,也就是,/0的内容给了寄存器之后,轮询到这个进程的时候还是这个数据,所以会导致一直打印的情况,因为本来,OS发送的信号是要直接终止的,结果我们自己自定义为了打印,所以打印进程的资源一直释放不出去,从而导致了一直打印的情况。

对于越界的问题同理,涉及到的寄存器是cr寄存器,cr2 cr3,还有MMU寄存器,对于MMU寄存器来说是将虚拟地址转换为物理地址的,而在访问失败后,CR2这个寄存器放的就是错误的数据,因为CR2是页故障线性地址寄存器,和/0一样,存放的错误数据一直没有释放,所以一直轮询,从而导致了一直打印的情况。

以上是异常的现象解释。

打一个小小的回旋镖吧,在进程部分:

core dump是什么呢?

留个疑问吧,现在能知道的就是通过core dump可以得到一个文件是core,我们通过这个文件,使用gdb可以直接定位到出错的地方。

和云服务器有关,使用到的命令是ulimit -c 10240等,后面咱们再会咯~


感谢阅读!

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部