【linux】信号——信号产生

信号产生

自我名言只有努力,才能追逐梦想,只有努力,才不会欺骗自己。

喜欢的点赞,收藏,关注一下把!

首先说明一点信号不是信号量。不能把这两个东西放在一起。

那信号讲什么呢?

1.预备知识

那信号是怎么回事,这里只能这样说,信号是针对进行发送某种信号到来的一种机制,让信号能被进程处理。,让我们在后面的知识中,更能理解这句话的含义。

先见识见识信号。

前面数字是信号的编号,后面大写的是宏。

就比如杀死一个进程

powershell 复制代码
kill -9  进程pid

这里可以使用编号,也可以使用SIGKILL

再可以数一数信号有多少个。

其实并没有64个,0,32,33信号是没有的。

【1,31】普通信号

【34,64】实时信号 (我们不学这个)

先说信号的概念帮我们简单了解一下信号,但再说信号一些概念之前,我们先从生活角度中的信号来帮我们理解。

生活中的信号:

1.红绿灯

2.闹钟

3.信息通知

4.劳资蜀道三

5.女朋友把你拉黑

6.烽火台狼烟

等等这些都是我们生活中的信号。

我们以红绿灯为例。人是能够识别 红绿灯的。

这里识别有两层意思。

第一个问题可能会觉得很奇怪,你为什么能够识别红绿灯?

第二个问题,当信号来的时候,你不一定会立即处理这个信号

信号的产生是异步的。

举个栗子,你正在宿舍打着游戏,这时外卖小哥给你打电话让你下楼取餐,但是你忙着打着游戏并没有立刻下楼去取,而是让他把外卖放在楼下。当你打完游戏,记起还有外卖没拿,所以去楼下拿外卖。当然还有另一种情况,你打游戏上头了。然后忘记有外卖在楼下这回事。

当绿灯到达的时候,你有三种处理动作

如何把上面的这些概念迁移到进程中呢?

这里要有一个共识:信号是给进程发的

  1. 进程是如何识别信号的?(认识+动作)

    进程本身就是程序员编写的属性和逻辑的集合。所以这里先粗略的说是由程序员编码完成的。(后面学了信号更多知识就可以详细说明了)。

  2. 当进程收到信号的时候,进程可能正在执行更重要的代码,所以信号不一定会被立即处理

  3. 进程本身必须要有对于信号的保存能力

  4. 进程在处理信号的时候,一般有三种动作(默认,自定义,忽略)【信号被捕捉】

在我们现在还没有学过信号,上面1,2,4我们都不能具体解释,不过3我们可以根据以往学过的知识来分析分析。

如果一个信号是发给进程的,而进程要保存,那么应该保存在哪里?
task_struct(PCB)结构体中。

如何保存呢? 更准确来说如何保存是否 收到了指定信号【1,31】。

是否是一种两态,我们是不是可以在task_struct结构体里,当然task_struct结构体中包含其他一大堆的属性,可以存在一个unsigned int signal32位比特位。

所以在进程中是不是只要存在对应的位图结构,然后当我们收到信号时,是不是只要将对应信号的位置由0->1,就代表我们已经完成了信号的发送,并且让进程暂时把这个信号保存起来了。

那如何理解信号的发送呢?

发送信号的本质:修改PCB的信号位图!

PCB是内核维护的数据结构对象----->PCB的管理者是OS,谁有权力修改PCB中的内容呢?------>OS!!

所以无论未来我们学习多少种发送信号的方式,本质都是通过OS向目标进程发送的信号!!

未来想让用户也能发送信号------->OS必须要提供发送信号,处理信号的相关系统调用!

我们使用kill命令,底层一定调用了对应的系统调用!

2.信号产生

2.1通过键盘发送信号

cpp 复制代码
int main()
{
    int cnt=0;
    while(true)
    {
        printf("我是一个进程,我正在运行%d\n",cnt++);
        sleep(1);
    }
    return 0;
}

ctrl+c热键,终止前台进程。

本质ctrl+c是一个组合键---->OS识别---->OS将ctrl+c解释成为2号信号,2)SIGINT----->处理(三种动作)。但我们对2号信号没做任何改变,所以是默认处理。

powershell 复制代码
man 7 signal //查看信号对应的手册

Action(行为):Term (Terminal终端)结束进程

Comment(解释):从键盘中断

所以2号信息的默认动作,结束进程。

接下来验证一下是不是发送了2号信号。

先介绍一个函数signal对指定的信号设置一个自定义动作。

signum:信号编号(捕捉那个信号)

handler:函数指针(捕捉这个信号后,你想怎么做,这是一个回调函数)

接下来验证

cpp 复制代码
#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<signal.h>

void handler(int signo)
{
    cout<<"捕捉到信号:"<<signo<<endl;
}

int main()
{
    signal(2,handler);
    int cnt=0;
    while(true)
    {
        printf("我是一个进程,我正在运行%d\n",cnt++);
        sleep(1);
    }
    return 0;
}

我不是对2号信号进行捕捉吗,并且代码还做了修改,为什么运行结果没什么变化。

注意,这里是signal函数的调用,并不是handler的调用,并且仅仅是设置了对信号的捕捉方法,并不代表方法被调用了。所以一般这个方法不会执行,除非收到对应的信号。

这两种方法都可以发送信号。然后signal函数对2号信息进行捕捉。

现在发2号信号,虽然能被捕捉,但是进程怎么退不出来了。

这是因为我们把2号信号默认动作,改成了自定义动作。

如果想退出怎么办?

powershell 复制代码
kill -9 编号  //杀死进程


或者在自定义动作种加一个exit。

cpp 复制代码
void handler(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<endl;
    exit(0);
}

其实还有一个组合建ctrl+\,发送的是3号信号 。也能终止进程。



这里留了一个问题,Core和Trem都是终止进程。为什么OS要设置两种不同的行为有什么用?

2.2系统调用接口向进程发送信号

kill可以给任意进程发送任意信号。

pid:目标进程pid

sig:发送几号信号

成功返回0,识别返回-1。

我们给进程发信号底层用的就是这个。

前面说过,信号是由OS向进程发送的。OS有这个能力,但不代表有权限使用这个能力。
信号的发送是由用户发起而OS执行的。

接下来我们写的代码想呈现这样的效果,一个进程正在运行,另一个进程在命令行给这个进程发送任意信号。

cpp 复制代码
//mysignal.cc

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

using namespace std;


void Usage(const string& proc)
{
    cout<<"\nUsage "<<proc<<" pid signo\n"<<endl;
}

// ./mysignal pid signo------>命令行参数
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    
    pid_t id=stoi(argv[1]);
    int signo=stoi(argv[2]);
    int n=kill(id,signo);
    if(n != 0)
    {
        perror("kill");
    }

    return 0;
}

//mytest.cc

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

int main()
{
    int cnt=0;
    while(true)
    {
        printf("我是一个进程,pid:%d,我正在运行%d\n",getpid(),cnt++);
        sleep(1);
    }
    return 0;
}


这个不就是和我们在命令行执行kill命令一样的原理吗。

kill(),可以向任意进程发送任意信号

raise,给自己发送指定信号。----->就相当于 kill(getpid(),任意信号)

cpp 复制代码
int main()
{
    int cnt=0;
    while(true)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        if(cnt == 10)
            raise(9);
        sleep(1);
    }
        return 0;
}

abort,给自己发送指定的信号(6号信号)。------>相当于kill(getpid(),SIGABRT)


cpp 复制代码
int main()
{
    int cnt=0;
    while(true)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        if(cnt == 10)
           abort();
        sleep(1);
    }
        return 0;
}

关于信号处理的行为的理解:有很多的情况,进程收到大部分的信号,默认处理动作都是终止进程。

既然大部分信号默认都是终止进程,那有那么多类的信号有什么用?

信号的意义:信号的不同,代表不同的事情,但是对事情发生之后的处理可以一样。

也就是说进程意外终止了,我们可以根据信号不同来确定是什么原因导致的。

2.3硬件异常产生信号

信号产生,不一定非得是用户显示发送的。

看下面一段代码

cpp 复制代码
int main()
{
    int cnt=0;
    while(true)
    {
         printf("cnt:%d,pid:%d\n",cnt++,getpid());
         int a=10;
         a/=0;       
    }
    return 0;
}

为什么除0会终止进程?

因为当前进程会收到来自OS发送的信号。SIGPFE。

如何证明呢?

cpp 复制代码
void handler(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<endl;
}

int main(int argc,char* argv[])
{
    signal(SIGFPE,handler);
    int cnt=0;
    while(true)
    {
         printf("cnt:%d,pid:%d\n",cnt++,getpid());
         int a=10;
         a/=0;       
    }
     return 0;
}

我确实捕捉到了8号信号,但为什么OS一直发送信号呢?

难道是我这里一直在死循环的原因?

修改一下代码

cpp 复制代码
int main()
{
    signal(SIGFPE,handler);
    int cnt=0;
    int a=10;
    a/=0;   
    while(true)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
    }
    return 0;
}

发现还是一直在发送8号信号。

这到底是为什么?

先来解答,OS如何得知应该给当前进程发送8号信号呢?或OS怎么知道我除0了呢?

CPU运算异常了,OS会不会知道?

OS肯定会知道CPU运算出现了问题,因为OS是软硬件资源的管理者。

OS查看到状态寄存器溢出位由0->1,OS就识别到CPU内部出错了。

谁导致CPU出错了?

CPU当前正在调度谁,就是那个进程出现了问题,OS向目标进程发送8号信息,目标进程收到8号信号,后序处理就会终止自己了。

那为什么一直发信息呢?

收到信号,不一定会引起进程退出,没有退出,进程可能还会被CPU调度。

CPU内部的寄存器只有一份,但是寄存器种中的内容,属于当前进程的上下文,CPU内部状态寄存器溢出标记位由0->1,你是没有能力或者动作去修改这个问题的。

当进程被切换的时候,就有无数次状态寄存器被保存和恢复的过程,所以每一次恢复的时候,就让OS识别到了CPU内部的状态寄存器中标记位是1,每一次都会发8号信号。

再看一种由硬件异常产生的信号。

cpp 复制代码
int main()
{
    signal(SIGFPE,handler);
    int cnt=0; 
    while(true)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        int* ptr=NULL;
        *ptr=10;
    }
    return 0;
}

为什么野指针就奔溃了?

因为OS会给当前进程发送指定的11号信号。


证明一下。

cpp 复制代码
void handler(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<endl;
}


int main()
{
    signal(11,handler);
    int cnt=0; 
    while(true)
    {
        printf("cnt:%d,pid:%d\n",cnt++,getpid());
        int* ptr=NULL;
        *ptr=10;
    }
    return 0;
}

OS怎么知道我野指针了呢?

根据我们以前学的知识,虚拟地址--->物理地址的转换,要经过页表。今天我要告诉你除了页表还有一种硬件MMU。MMU是内存管理单元。

MMU其实是通过读取页表中的内容,在内部形成对应的物理地址,然后再去访问我们对应的物理地址。

当我们ptr解引用,访问的是0号地址。经过页表映射,发现在映射的时候,当前进程是不允许去访问对应的0号地址的。不允许访问当然可以拦截不让你访问。但更重要的是,你为什么会访问,所以OS觉得你犯错了就应该付出相应的代价,所以MMU这个硬件因为对应的越界访问(野指针访问)发送异常。OS知道当前硬件发生异常,所以OS将异常转换成11号信号发送给目标进程。

2.4软件条件

在管道我们说过匿名管道的一个场景,读端关闭,写端一直写没有任何意义,OS会给当前写进程发送SIGPIPE信号,然后进程终止了。

所谓的进程,OS,管道,尤其是管道和这一整套OS发信号的原因和OS发信号的过程,和硬件都没有关系。而是仅仅因为读端关闭了这一软件条件所触发的OS发送信号给目标进程,这种场景我们就称之为软件条件会触发信号。

下面我们要说的是一种定时器 软件条件。给当前进程设定闹钟,alarm()

设置一个时钟时刻发送信号。

seconds:多少秒之后发送信号

返回值是0或者是以前设定的闹钟时间还余下的秒数

发送的是SIGALRM(14)信号。

cpp 复制代码
int main()
{
	//这个闹钟是给现在设的还是给未来设的?
	//是不是我调用了alarm,我的进程会立马收到对应的闹钟呢?
	//答案:并不是。这是给未来设置的闹钟。是1秒之后向我这个进程发信号。
    alarm(1);
    int cnt=0;
    while(true)
    {
        printf("cnt: %d\n",cnt++);
    }
    return 0;
}

根据运行结果,请问我们这段代码有什么用呢?

其实这是统计1S左右,我们计算机能够将数据累计多次次。

修改一下代码再看一下效果。

cpp 复制代码
int cnt=0;

void catchSig(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<" cnt :"<<cnt<<endl;
}

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

次数多了很多次,这是因为printf会访问外设,而访问外设比较慢。

还有就是这个闹钟是一次性闹钟,响了之后就不响了。

如果想响多次,要重新在设定闹钟。

cpp 复制代码
void catchSig(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<" cnt :"<<cnt<<endl;
    alarm(1);
}

alarm(0),取消闹钟,并且返回闹钟剩下多少时间。

cpp 复制代码
int main(int argc,char* argv[])
{
    signal(SIGALRM,catchSig);
    alarm(5);
    while(true)
    {
        cnt++;
        if(cnt == 3)
        {
        	int n=alarm(0);
        	cout<<n<<endl;
        }
        sleep(1);
    }
    return 0;
}

为什么设闹钟就是软件条件了呢?

"闹钟"其实就是用软件条件实现的。

2.5总结

1.上面所说的所有信号产生,最终都要有OS来进行执行,为什么?

OS是进程的管理者

  1. 信号的处理是否是立即处理的?

在合适的时候(什么合适的时候,下面说)。

3.信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

4.一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

5.如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

上面的问题,我们都可以从接下来信号的学习中得到答案。

信号产生这里还有最后一个问题。

powershell 复制代码
man 7 signal  //信号手册

Stop暂停进程,Cont继续进程,Ign忽略进程(这个信号说完最后面解释)这些都没有问题。

Term,Core都是终止进程,有什么区别?

其实这有关于,进程退出时,核心转储问题

看下面一段代码

cpp 复制代码
int main()
{
    //核心转储
    while(true)
    {
        int a[10];
        a[100]=10;
    }
    return 0;
}
cpp 复制代码
int main()
{
    //核心转储
    while(true)
    {
        int a[10];
        a[1000]=10;
    }
    return 0;
}

数组明明都越界了啊,怎么进程没有奔溃报错?

其实在C的时候就说过,数组越界不一定会报错,因为对数组的检查是随机的。这是我们在语言层面的理解。

cpp 复制代码
int main()
{
    //核心转储
    while(true)
    {
        int a[10];
        a[10000]=10;
    }
    return 0;
}

那这次怎么就检测出来了。按照语言层面解释可能是因为这次越界被检测到了。

接下来我们从底层理解:

编译器上编译你的代码时,在栈上给你开辟多大空间和编译器是强相关的,你要申请10个int大小元素的数组,它确实给你的就是10个元素,指的是数组的元素,但是并不代表给你的代码块或者函数分配栈帧结构是10个元素的大小,可能给你的会很大,所以呢,即便你越界了,但是你还是在有效栈区里,所以没有报错,除非你访问了一个完全不是你的空间。比如你现在访问的时候,访问的是系统的地址空间中或者访问到一个不让你访问的区域,那么此时OS系统就能识别出来。所以OS在识别越界的问题上有可能也死别不出来,从而出现把数据改变了,但用户不知情的情况。

这个信号是11号信号,段错误,它的终止方式是Core。

像Trem这种结束,是正常结束,OS不会做额外操作的。而以Core这种结束,OS除了终止进程,它还要做其他工作。

但是以Core为终止,我也没见OS做什么额外工作啊, 除了给我打印出一个错误描述,像Trem终止进程不也是给我打印出一个错误描述吗。

在云服务器上,默认如果进程是Core退出的,我们暂时看不到明显现象,如果想看到需要打开一个选项。

powershell 复制代码
ulimit -a  //可以看到系统给我们当前资源设置的上限

core file size 大小为0,这是云服务器默认关闭了core file选项。

想要打开,ulimit就带上你想要设置谁,-c(选项),大小为多少。

powershell 复制代码
ulimit -c 1024  //打开云服务器core file选项,默认可以向OS中形成最大为1024个block的数据块

然后运行同样的代码

相比较我们之前运行的时候,除了段错误后面还跟了一个(core dumped)

发现我们当前目录下多了一个以core命名的文件。

所谓的核心转储:当进程出现异常的时候,我们将进程在对应的时刻,在内存中的有效数据(二进制数据)转储到磁盘中。

该文件我们用vim打开是一堆乱码。我们是无法识别的。

那形成核心转储有什么意义呢?或者说为什么要有核心转储?

一般进程在运行的时候出现崩溃,其实我们更想知道的是,为什么会崩溃,在哪里崩溃。所以OS为了便于我们后期做调试,会将进程在运行期间出现崩溃的代码的相关上下文数据全部dump到磁盘中,用来进行支持调试

如何支持呢?

linux下默认编译都是release不能调试,debug才能调试,因此我们编译时带上-g选项。

当前自动帮我们评判,进程收到11号信号引起的段错误,报错是在mysignal.cc的第37行,代码是a10000=10引起的错误。直接就帮我们找到了错误。

这种直接快速定位到出问题的方式,我们称之为事后调试。

像这种以2号信号,Trem终止进程,并不会在当前目录下形成core文件。

Trem,Core都是进程终止,它们的区别是,以Core退出的可以被核心转储的以便于后序快递定位问题,以Trem退出就是正常终止进程。

以后进程出现异常退出,你可以查看是什么信号的什么行为导致的,如果是Core,把Core打开,再执行一下,gdb快速定位问题。

信号产生到目前为止差不多讲完了,但这里可能有人会有这样的疑问。如果我们把所有信号都捕捉,换成自定义动作,不让进程退出,那进程是不是无法被杀死了。

cpp 复制代码
void catchSig(int signo)
{
    cout<<"进程捕捉到了一个信号,信号编号是:"<<signo<<endl;
}

int main()
{
    for(int signo=1;signo<=31;++signo)
    {
        signal(signo,catchSig);
    }
    while(true) sleep(1);
    
    return 0;
}

难道真的无法杀死了?

kill -9还是可以杀死进程,无论你怎么修改,无法对9号信号设定捕捉,即使你做了,OS也不会给你设置。

相关推荐
用户9718356334661 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 小时前
linux 拷贝文件或目录到指定的位置
linux
大树8818 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠18 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质19 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush419 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52019 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz19 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工20 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智20 小时前
ARP代理--工作原理
运维·网络·arp·arp代理