初识Linux · 信号产生

目录

前言:

预备知识

信号产生


前言:

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

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

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


预备知识

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

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

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

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

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

似乎就关联起来了?

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

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

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

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

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

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

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

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

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

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

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

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

cpp 复制代码
#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对应函数。

直接试试:

cpp 复制代码
#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[]:

cpp 复制代码
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信号,也就是碰到异常事件直接终止该进程。

cpp 复制代码
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。

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

cpp 复制代码
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信号,和我们平时理解的闹钟一样的,不过,碰到了该函数,就进程就结束了:

cpp 复制代码
int main()
{
    std::cout << "begin " << std::endl;
    alarm(1);
    sleep(2);
    std::cout << "end " << std::endl;

    return 0;
}

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

cpp 复制代码
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秒的闹钟里面,定义一个变量,能++多少次:

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

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

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

cpp 复制代码
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问题,一个是越界访问的问题:

cpp 复制代码
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发送了信号之后,进程是直接终止的,那么可以不退出进程吗?

就像这样:

cpp 复制代码
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等,后面咱们再会咯~


感谢阅读!

相关推荐
xuanzdhc1 小时前
Linux 基础IO
linux·运维·服务器
愚润求学1 小时前
【Linux】网络基础
linux·运维·网络
bantinghy1 小时前
Linux进程单例模式运行
linux·服务器·单例模式
小和尚同志2 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
帽儿山的枪手2 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
shadon1789 天前
回答 如何通过inode client的SSLVPN登录之后,访问需要通过域名才能打开的服务
linux
AWS官方合作商9 天前
AWS ACM 重磅上线:公有 SSL/TLS 证书现可导出,突破 AWS 边界! (突出新功能的重要性和突破性)
服务器·https·ssl·aws
小米里的大麦9 天前
014 Linux 2.6内核进程调度队列(了解)
linux·运维·驱动开发
程序员的世界你不懂9 天前
Appium+python自动化(三十)yaml配置数据隔离
运维·appium·自动化
算法练习生9 天前
Linux文件元信息完全指南:权限、链接与时间属性
linux·运维·服务器