Linux学习之路 -- 信号概念 && 信号的产生

1、简单介绍

Linux中的信号是一种机制,用于通知进程发生了某个事件。它可以由内核或一个进程发送给另一个进程。信号有默认行为,如终止进程或忽略信号,也可以被进程捕获并自定义处理。常见的信号包括SIGINT(中断信号)、SIGKILL(强制终止信号)和SIGTERM(终止信号)。信号的处理方式有忽略、捕获或执行默认操作。

2、信号的特点的引入

我们以生活之中的场景引入信号的概念。

<1> 在信号没有发生的时候,我们就已经知道了怎么处理信号 ,(类比我们看到绿灯时,就会通过人行道,看到红灯时,我们就会停下)。

<2>我们能够认识信号,是因为有人之前就在我们的大脑中设置了识别信号的方式(上学时,老师教我们识别红绿信号灯)。

<3>信号到来时,我们正在处理重要的时间,暂时不能处理这个信号,此时就要对信号进行保存。(比如在日常生活中,我们正在做饭,而手机上发来提醒你买票的消息,这时我们就要对该信号保存)

<4>信号的产生是随时的,我们无法准确的预测,所以信号是异步发送的。(异步:信号的产生都是由别人(用户、进程)决定的,我收到之前这个信号之前,我一直在忙我的事情,两者是并发处理的)

把上述场景的我变成进程,就是系统中进程面对信号的处理方式。

3、为什么要有信号?

进程在运行时,可能会出现错误,需要被删除,停止等等。系统要求进程有随时响应外部信号的能力,并且在随后能做出反应。

4、信号介绍前的准备

<1>查看信号的命令:kill -l 

在这些信号中,没有0号、32号、33号信号,数字和字母等效,都表示信号。34号信号及以后的数据都是实时信号 -- 这种信号在被接受到后必须处理完后才能调度其他进程。本文对实时信号不做详细介绍。

下面是对每个信号较详细的介绍,稍后会对一些细节进行补充。

信号编号信号名称默认动作描述
1SIGHUP终止进程挂起,通常因控制终端断开
2SIGINT终止进程来自键盘的中断(Ctrl+C)
3SIGQUIT创建核心转储并终止来自键盘的退出(Ctrl+\)
4SIGILL创建核心转储并终止非法指令
5SIGTRAP创建核心转储跟踪陷阱(调试用)
6SIGABRT创建核心转储进程发出的终止信号(abort)
7SIGBUS创建核心转储总线错误(内存访问错误)
8SIGFPE创建核心转储浮点异常
9SIGKILL终止进程强制终止信号,不能被捕获或忽略
10SIGUSR1终止进程用户定义的信号1
11SIGSEGV创建核心转储无效的内存引用
12SIGUSR2终止进程用户定义的信号2
13SIGPIPE终止进程管道写出错误,没有读者
14SIGALRM终止进程实时定时器到期
15SIGTERM终止进程终止信号
16SIGSTKFLT终止进程栈溢出(Linux 特定,不常用)
17SIGCHLD忽略子进程停止或终止
18SIGCONT忽略如果进程已停止,则继续执行
19SIGSTOP停止进程停止信号,不能被捕获或忽略
20SIGTSTP停止进程终端停止信号(Ctrl+Z)
21SIGTTIN停止进程后台进程组尝试从终端读取
22SIGTTOU停止进程后台进程组尝试写入终端
23SIGURG忽略紧急情况,如网络带外数据到达
24SIGXCPU创建核心转储并终止超过CPU时间限制
25SIGXFSZ创建核心转储并终止超过文件大小限制
26SIGVTALRM终止进程虚拟定时器到期
27SIGPROF终止进程Profile 定时器到期
28SIGWINCH忽略窗口大小改变
29SIGIO终止进程允许的I/O操作
30SIGPWR终止进程电源故障(系统关机)
31SIGSYS创建核心转储并终止无效的系统调用

我们也可以使用” man 7 signal “命令查看对应信号的作用(具体以手册的解释为准)

<2>处理信号的简单接口:signal

当我们接受到信号时,默认有一下三个处理信号的方式:1、默认动作 2、自定义操作  3、忽略信号。

这个信号的接口的第一个参数就是传入对应的信号数,第二个参数是自定义函数指针,手册上对该函数的类型进行了重命名。我们在调用signal函数时,signal在内部就会调用handler方法,并将signum作为参数传递给handler。返回值,就是原来处理信号的方法。

下面简单地演示一下用法:

#include<stdio.h>
#include<stdbool.h>
#include<unistd.h>
#include<signal.h>
void handler(int sig)
{
    printf("hello sign\n");
}
int main()
{
    while(1)
    {
        signal(2,handler);
        sleep(1);
        printf("hello world\n");
    }
    return 0;
}

运行结果:

这里ctrl + c 默认地被系统解释成2号信号。

5、信号的产生

下面开始正式介绍信号的相关部分,先从信号的产生开始。信号的产生一般有许多方式,下面是主要的方式:

<1>kill 命令

前面简单介绍了kill命令,”kill + 指定信号 + 发送进程的pid“就可以向指定进程发送信号。

<2>键盘产生信号

  1. SIGINT(中断信号,通常是 Ctrl+C):当用户在键盘上按下 Ctrl+C 组合键时,它会发送 SIGINT 信号给前台进程组中的所有进程,通常用于请求终止正在运行的程序。

  2. SIGTSTP(终端停止信号,通常是 Ctrl+Z):当用户在键盘上按下 Ctrl+Z 组合键时,它会发送 SIGTSTP 信号给前台进程,导致进程停止(挂起)。

  3. SIGQUIT(退出信号,通常是 Ctrl+\):当用户在键盘上按下 Ctrl+\ 组合键时,它会发送 SIGQUIT 信号给前台进程,通常会导致进程退出并生成核心转储文件。

  4. SIGHUP(挂起信号,通常是挂起终端或网络断开):当终端会话关闭或网络连接断开时,前台进程会收到 SIGHUP 信号,默认行为是终止进程。

  5. SIGWINCH(窗口大小改变信号):当终端窗口的大小发生变化时,如用户调整终端窗口的大小,会产生 SIGWINCH 信号,接收到该信号的程序可以据此调整其输出格式。

我们着重关注前3个信号即可。

<3>系统调用

系统中也有接口可以产生系统调用,下面介绍一下kill命令

1、kill

这里的参数pid有四种设法,不过这里只设为所要接受信号pid的进程,方便理解。sig就是所要发射的信号。

#include<cstdio>
#include<iostream>
#include<stdbool.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        sleep(1);
        printf("command false : kill + signal + pid\n");
    }
    else
    {
        int sig = std::stoi(argv[1] + 1);
        pid_t pid = std::stoi(argv[2]);
        if(sig > 0 && sig <= 31)
        {
            if(!kill(pid,sig))
            {
                printf("killed\n");
            }
            else
            {
                printf("killed failed\n");
            }
        }
    }
    return 0;
}

这里对结果就不做演示了,可自行运行。

2、raise

该接口用于给自己发送信号,sig就是需要发送的信号。

3、abort

这个接口也是用于终止进程的,效果与raise相差不大,不过还是有些许差别。

4、killpg

这个接口用于向所有某一个信号组发送消息

如果指定 pgrp 的值为 0,那么会向调用者所属进程组的所有进程发送此信号,这个接口我们一般不使用,了解即可。

<4>软件条件

举一个我们熟悉的例子,那就是管道,当我们的管道写端不断地写,而读端已经关闭,那此时进程就会收到SIGPIPE信号,终止进程,这是因为软件的条件不满足而发出了信号。

下面介绍另一个更为重要的软件条件产生信号的例子:闹钟

这里的闹钟和我们日常生活中的闹钟类似,都是在未来的某一段时间内发送一个信号。这里就是给OS发送信号。

我们可以传入一个无符号整型,表示要对在几秒后发射信号。这个信号为14号信号,也就是SIGALRM。返回值为0或上一个闹钟的剩余时间。这个闹钟函数只会响一次,如果我们要闹钟一直响,那么我们可以捕捉信号,在自定义处理中再设一次闹钟,这样就可以使闹钟一直响。如果闹钟的参数为零,那就表示取消闹钟。

在OS系统中有许多的闹钟,用于提醒系统进行某些操作,OS为了管理这些闹钟,就必须设定一个结构体对其进行描述与管理。其中这个结构体中包含了每个闹钟的过期时间,这个时间是以时间戳为基本单位的,时间戳在OS中是线性增长的,OS将当前时间与过期时间对比,就能判断闹钟是否过期。如果过期,OS就会释放这个闹钟的相关资源,同时闹钟也会向对应的进程发送信号。为了方便管理,OS依据过期时间的大小,将描述每个闹钟的结构体进行小根堆排序,这样就能高效地对闹钟资源进行管理。

<5>异常

当代码出现异常时,也会产生信号。

1、除零异常
        当我们代码中出现除零错误时,进程会收到SIGFPE信号(8号)。
2、出现野指针
        当我们的代码中出现野指针访问时,进程会收到SIGSEGV信号(11信号)

下面对上述两种情况的产生机制做一下简单的解释。

<1>除零异常

在cpu中存在许多的寄存器,参与运算的寄存器有很多,我们只简单地列出所需的几个。ebx和eax为常用的几个通用寄存器,这里我们假定它们存储了除数和被除数。当cpu在进行除零操作时,就会出现错误,此时cpu会将标志寄存器置为异常状态(OF)。然后cpu会通知OS,当OS读取到OF状态时,就会向对应的信号发送SIGFPE信号。

在实际的应用场景中,进程收到信号,可能并不作处理,进程还会一直运行,而寄存器中的上下文数据又无法修改,OS就会一直向进程发信号。所以在一些服务出现问题后,并不是终止进程,而是一直向日志输出消息。

<2>野指针

在使用野指针访问数据前,寄存器eax中会存储野指针的虚拟地址,再通过CR3寄存器获取页表目录页地址,通过这两个寄存器中的数据,MMU会将对应的虚拟地址转成物理地址,如果转换失败,cpu会在CR2寄存其中写入数据,然后OS检测到CR2寄存器中的数据异常,向对应的进程发送SIGSEGV信号。

除了上述几种异常产生的信号,还有别的异常会产生信号,这里不做详细的介绍。

6、总结

下面对上面的信号产生情况做一些总结
        我们以键盘为产生信号为例,总结出一些规律。首先,我们需要知道的是键盘是一种字符设备,当我们敲击键盘时,获取的都是字符。那么OS是如何获取键盘中的字符的呢?

首先,我们需要知道的是,键盘通过主板上的对应线路能够连接到cpu上。当我们敲击键盘后,键盘就会发出信号,此时cpu上的针脚(连接着键盘,中间会经过很多线路,这里不详细说明,接受硬件的信号)就会被触发,需要注意的是,cpu上有许多的针脚,每个针脚有对应的编号,该过程也是硬件中断的一种。针脚被触发点亮后,特定寄存器就会存储针脚的编号。cpu的针脚被点亮后,cpu会检测到寄存器中存储了信息,此时会要求OS停止当前进程,去取出寄存器中的编号,查找对应的中断向量表(该表就是一张函数指针数组,里面存放了各种中断的处理方式)中查询对应的方法,并执行。在这里的方法就是读取键盘的数据。

读取到键盘数据以后,就需要对键盘数据进行解释判断。如果字符,那么就让数据刷新到内核缓冲区中,如果是别的组合键(例如ctrl + c 、ctrl + z等等)就解释成为信号,发送给对应的进程。那么信号是如何被信号发送与接收的呢?

当进程接收到信号时,可能在处理别的事物,此时,我们需要信号进行保存。而这个信号就被保存在进程的PCB中,内核中会使用一个无符号整型uint32_t对其进行保存。这里利用了位图方式,31种信号进行保存(进程收到哪个信号,就改对应比特位即可,最低位不参与存储信号)。

所以信号的发送,其实就是向PCB中写信号,而PCB属于内核数据结构,只有OS能对其进行修改,而我们普通用户想修改存储信号的位图,就必须通过系统调用来实现信号的发送。归根结底,上面几种产生信号的方式,底层都与系统调用挂钩。

  • 22
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值