当前位置: 首页 > news >正文

Linux 信号与中断 详解

文章目录

  • 1. 认识信号
    • 1.1 生活角度
    • 1.2 进程角度
  • 2. 信号产生
    • 2.1 信号有哪些
    • 2.2 键盘产生信号
      • 2.2.1 键盘组合键
      • 2.2.2 键盘如何发送信号
    • 2.3 使用系统命令向进程发送信号
    • 2.4 通过系统调用向进程发送信号
    • 2.5 通过软件条件发送信号
    • 2.6 通过硬件中断发送信号
      • 2.6.1 除0错误
      • 2.6.2 野指针
      • 2.6.3 Term 和 Core 的区别
  • 3. 信号保存
    • 3.1 信号的三张表
    • 3.2 sigset_t 类型
    • 3.3 信号集操作函数
    • 3.4 两个细节问题
  • 4. 信号处理
    • 4.1 signal
    • 4.2 sigaction
  • 5. 中断
    • 5.1 硬件中断
      • 5.1.1 外设触发的硬件中断
      • 5.1.2 时钟中断
    • 5.2 软件中断
      • 5.2.1 陷阱 trap
      • 5.2.2 异常
    • 5.3 操作系统是什么
    • 5.4 内核态与用户态

1. 认识信号

1.1 生活角度

信号是什么呢?在生活中,有红绿灯信号,有时钟信号等等,简而言之,信号是用来传递信息的媒介。

生活中不同的信号具有不同的特征,人可以根据这些不同的特征去识别不同的信号,进而采取不同的不同的处理方式。

从中我们可以看出,信号产生后,人识别到信号后处理信号,但有些时候,信号并不被立即处理,人可能有更重要的事情去做,因此需要记忆信号,既信号保存,之后再做信号处理:信号产生->信号保存->信号处理

1.2 进程角度

在计算机中,进程与信号紧密联系在一起,因为信号是发送给进程的。

接下来,我们看一段简单的程序,初步感受向进程发送信号的过程。

在这里插入图片描述
这是一个一旦运行便陷入死循环的进程。如果我们想要终止这个进程,可以在进程运行起来,在键盘中使用ctrl + c的组合键,这实质上就是向进程发送了一个信号,而这个信号的处理方式便是进程终止。

结果如下所示:

在这里插入图片描述

2. 信号产生

初步了解信号后,我们首先来具体研究信号的产生问题。

2.1 信号有哪些

Linux中,所有信号可以通过kill -l命令进行查看。

在这里插入图片描述
从上图中,我们可以看到,一共有62种信号,特别注意,32和33号信号是不存在的。

在Linux中,信号分为传统信号和实时信号, 就上图而言,编号1~31是传统信号,其余信号则为实时信号,本篇博客重点讲解传统信号。

另外,在上图中,信号的编号从1开始,后面紧跟信号的名称,这些全大写的信号名称,实际上就是宏值。

我们可以从Linux内核源码中清楚看到这一点:

在这里插入图片描述

2.2 键盘产生信号

2.2.1 键盘组合键

我们可以通过键盘组合键产生信号。

键盘一般通过组合键来产生信号:
ctrl + c:产生的是2号信号,即SIGINT,这是一个可从键盘中产生的终止信号,收到该信号的进程默认行为是终止,即Terminate

在这里插入图片描述
ctrl + \:产生的是3号信号,即SIGQUIT,这也是一个可从键盘产生的终止信号,但是这个终止行为并不是Terminate,而是core,即还会生成一个core dump文件。

在这里插入图片描述
ctrl + z:产生的是18号信号,即SIGSTOP,这个信号是让进程进入停止状态,并从前台进程转变为后台进程。

在这里插入图片描述

这里就牵扯到一个前台进程后台进程的概念。
前台进程只能有一个,而后台进程能有多个。那前台进程与后台进程的区别是什么呢?简单来说,只有前台进程能够从键盘中获取数据,后台进程则不能,当然无论是前台进程还是后台进程,都可以向显示器文件进行输出。

而我们所使用的键盘组合键发送信号,本质都是向前台进程发送信号,而非后台进程。

2.2.2 键盘如何发送信号

为什么使用键盘的组合键就能发送信号呢?
首先,我们要明确一点,信号最终都是由操作系统向进程发出的。键盘能够发送信号,一定是OS知道键盘中输入了特定的数据,从而做出相应发送信号的处理。

可是,操作系统是如何得知键盘中输入了数据呢?首先,键盘中每次输入的数据一定进行了存储(存储在外设中),如果操作系统不断对键盘进行检测,肯定能够拿到键盘中输入的数据,但是这样损耗太大了,因为当键盘实际没有任何输入的时候,操作系统也在频繁检测,这是不必要的。

因此,键盘与CPU的特定针脚有连接,一旦键盘按下,代表有数据输入,特定的针脚变为高电平,即触发键盘所对应的硬件中断,而OS检测到这个中断信息,便会到中断向量表中,执行键盘对应的中断处理方法,即获取处理键盘中的数据,进而也就可以识别特殊的组合键,向前台进程发送信号了。

通过中断的方式来获取键盘的数据,这样就避免了OS的频繁却无意义的检测,减小了CPU的负担。

需要说明的是,此处仅简单引入中断的概念,后文会有关于中断的详细阐释。

2.3 使用系统命令向进程发送信号

我们可以在命令行中,使用系统命令kill 来向进程发送信号。

kill发送信号的具体格式:kill -信号编号(可以是数字,也可以是宏值) 相应进程的pid号

2.4 通过系统调用向进程发送信号

接下来介绍三个系统调用来实现信号发送。

kill 函数,kill命令实际也是通过kill函数实现的。

在这里插入图片描述
kill函数用来给特定进程发送特定信号。

raise函数,用于进程自己给自己发信号。

在这里插入图片描述

abort函数,用于进程自己给自己发送6号,即SIGABRT信号。

在这里插入图片描述

2.5 通过软件条件发送信号

通过软件条件发送信号,即当一定的软件条件满足时,操作系统自动向特定进程发送信号。

以之前学习的进程间通信管道为例,当读端进程关闭后,写端进程如果向管道中写入,则会被操作系统发送SIGPIPE信号,使写端进程终止。

接下来介绍另外一个通过软件条件发送的信号SIGALRM,以及与该信号相关的系统调用alarm.

在这里插入图片描述
简单来说,alarm完成的就是一个定时的功能,在设定的秒数后,向目标进程发送一个SIGALRM信号。

关于alarm,有额外几点需要说明:

  • 一次只能设置一个闹钟,后一个闹钟设定后,会将前一个闹钟取消。
  • 该系统调用的返回值,如果不存在前一个闹钟,则返回0,否则,返回前一个闹钟剩余的秒数。
  • 如果调用alarm(0),则表示取消所有设置的闹钟。

2.6 通过硬件中断发送信号

通过硬件中断发送信号,本质就是因为硬件层面的出错,而触发硬件中断,进而执行相应的中断处理方法,在相应的中断处理方法中,由操作系统向相应进程发送信号。

接下来,介绍两个常见的CPU硬件出错而产生信号的例子

2.6.1 除0错误

除0错误本质是一种浮点异常,是CPU运算单元中发生的异常。在发生这种异常后,CPU中的状态标志寄存器中的相应标志位(可以理解为一个位图结构)会被置高电平,触发硬件中断后,OS检测到该中断,便执行中断向量表中相应中断的处理方法,向目标进程发送SIGFPE(8号信号),默认处理行为是终止进程。

但是,如果我们使用signal更改相应信号的处理方式,那么就会一直触发这个异常,因为在执行相应中断方法前,会保存当前运行进程的硬件上下文,如果信号的处理方式不是终止该进程的话,那么该进程硬件上下文恢复时,相应标志位寄存器中的相应位仍为高电平,那么仍会触发硬件中断而循环往复。

在这里插入图片描述

在这里插入图片描述

2.6.2 野指针

野指针错误是一个非常经典的硬件报错。

本质上,野指针所触发的段错误,是因为CPU中的MMU,即内存管理单元,从CR3寄存器中拿到当前进程页表的地址后,再查找页表映射,完成从虚拟地址到物理地址的转换。而如果是野指针,那么这个虚拟地址在页表中一定是没有相应映射的,因此就会触发段错误,在CR2控制寄存器储存该触发页错误的虚拟地址,然后触发硬件中断,OS发送信号SIGSEGV,终止相应进程。

2.6.3 Term 和 Core 的区别

通过上面的学习,我们可以发现,有的信号让进程终止是Term,而有的信号让进程终止是Core ,这两种默认信号处理动作都是终止,那么有什么区别呢?

Term就是单纯的终止进程,而Core除了终止进程外,还会生成一个core dump 文件。

那么,什么是core dump呢?core dump,即核心转储。

在这里插入图片描述
结合之前在进程控制中所学的内容,waitpid中的status形参中,有一个core dump 标志位。默认处理动作是core的信号,除了终止进程,还会将相应进程中的core dump标志位置为1,操作系统检测到core dump标志位为1后,就会将该进程的硬件上下文全部转储到一个core文件中,以便事后调试,进而查清错误原因。

需要说明的是,云服务器中默认是将core dump功能禁用的。

在这里插入图片描述
我们可以看到允许生成的core file size大小为0KB,也就是说不允许生成(使用ulimit -a 可以进行查看)

我们可以使用ulimit -c来进行修改。

在这里插入图片描述

需要特别说明的是,ubuntu或centos系统中,基本使用systemd-coredump来管理core文件,如果想要直接让core文件生成在当前目录中,需要禁用systemd-coredump功能,即做一些额外配置。

在ubuntu中,生成的core文件就以core命名,而centos中,core文件往往以core+相应进程pid的形式命名。

如果想要调试core文件来寻找错误,可以使用gdb或cgdb,在命令行中输入如下语句:gdb + 相应可执行文件(需包含可调试信息) + 相应core文件 。

可以在gdb命令行中,输入bt,即可查看错误发生位置,或使用info registers,查看错误发生时,各寄存器中的值。

3. 信号保存

由于信号产生后,信号并不会立刻处理,因此信号必须要保存,那么信号是如何保存呢?

在了解信号保存前,我们先要了解几个概念。

信号未决:信号未决是指信号处于pending状态,即从信号产生到信号处理这之间的状态。
信号递达: 实际的信号处理动作被称之为信号递达。
信号阻塞: 进程可以选择阻塞(block) 某个信号,这样该信号就会处于未决状态,而无法递达,也就是说进程能够收到该信号, 但却无法处理该信号。

3.1 信号的三张表

在进程struct task_struct的结构中,有三张表结构:一张block表,一张pending表,还有一张handler表。

block表和pending表本质上都是一个64位的位图结构,位图中的一位分别对应一个信号——block位图中,位为1,表示该信号被阻塞;pending位图中,位为1,表示该信号为未决状态。

handler表中则是储存不同信号的处理方式,也就是存储一个函数指针。

在这里插入图片描述

3.2 sigset_t 类型

这个类型就是一个信号集类型,也就是一个位图结构,即上图信号三张表中block表pending表的结构。

其中block表被称为阻塞信号集,或者说信号屏蔽字(Signal Mask)

3.3 信号集操作函数

以下,我们介绍一些常用的信号集操作函数。

在这里插入图片描述
sigemptyset:用于将信号集中的每一位置0。
sigfillset:用于将信号集中的每一位置1.
sigaddset:用于向信号集中添加特定信号。
sigdelset:从信号集中删除特定信号。
sigismember:用于判定特定信号是否存在信号集中。

在这里插入图片描述
这是专门用于更改信号屏蔽字的系统调用。
how:how主要用于规定更改的行为,具体而言有以下三种行为。
在这里插入图片描述
set:这是用于置位的用户层面的阻塞信号集
oldset:这个用于存储修改前的信号屏蔽字。

在这里插入图片描述
在这里插入图片描述
简单来说,这个函数可以在用户层面获取未决信号集。

3.4 两个细节问题

  • 信号屏蔽与信号未决:信号屏蔽并不影响信号未决,仅仅使得信号无法递达到。
  • 信号未决的状态改变时机:进程收到信号,即代表该进程的pending表中相应位被置为1,接下来要进入信号的处理逻辑,但在进入该信号的处理逻辑之前,操作系统会将pending表中的相应位重新置为0。

4. 信号处理

4.1 signal

信号处理的方式有三种:SIG_DFL,即信号的默认处理方式,通常都是Term或这Core终止;SIG_IGN,即忽略该信号;最后一种则是信号的自定义捕捉,也就是可以自己定义信号的处理方式。

上述需要特别说明的是,要区分忽略信号和屏蔽信号,屏蔽信号是让信号产生后始终处于未决状态,不处理该信号;而忽略信号会处理信号,只不过处理方式为忽略。

接下来介绍一个可以设置信号处理方式,即更改进程handler表的系统调用

在这里插入图片描述
在这里插入图片描述
signum:传入想要更改handler方式的信号。
handler:传入具体的handler方式——可以传入SIG_DFL,表示使用该信号的默认处理方式;可以传如SIG_IGN,表示忽略该信号;或者传入自定义的方式,但是该函数指针类型一定要与sighandler_t相同。

需要额外说明的是,Linux的传统信号中,有一些信号很特殊,比如9号信号(用于终止进程),19号信号(用于停止进程)。其中,9号信号不能被忽略,不能被屏蔽,不能被阻塞;而9号信号,同样不能被忽略和阻塞,但是可以被屏蔽。

4.2 sigaction

在这里插入图片描述
该系统调用同样可以设置信号的处理方式,不过它会额外涉及到一个结构体struct sigaction

在这里插入图片描述
通过这个结构体,既可以完成对传统信号的设置,也可以完成对实时信号的设置。

我们重点关注sa_handler和sa_mask这两个成员,sa_sigaction则于实时信号有关,sa_flags设置为 0 即可。

sa_handler是用来设置传统信号的处理方式,那么sa_mask是什么作用呢?

这里必须要提及的是,当一个信号进入处理逻辑时,该信号的block表中相应位就会被置1,这样当前进程能够再次接收到该信号,但是却无法再次处理该信号,这样就可防止信号被递归处理,当一个信号处理完毕时,block表中的相应位则再被置为0。

如果,当一个信号进入处理时,我们不仅仅想屏蔽当前信号,我们还想屏蔽其它信号,就可以使用这个sa_mask,也就是一个位图结构,进行额外的设置。

需要特别注意的是,使用sa_mask去设置额外屏蔽其它信号时,不要再使用signal去设置信号的处理方式,因为signal的设置,会使得进入信号处理逻辑时,只临时屏蔽当前信号。

5. 中断

在前面的讲解中,屡次提到中断这个话题,接下来就要详细讲一讲中断,这也是想要理解信号捕捉具体流程的一个重点。

5.1 硬件中断

5.1.1 外设触发的硬件中断

在这里插入图片描述
什么是硬件中断呢?本质上就是由外部设备异步触发,中断当前的工作,转而执行相应的硬件中断处理方法。这样操作系统就不需要对外部硬件进行周期性地检测,而是通过中断触发即可。

不同的外部设备,如键盘、鼠标等,都可以触发硬件中断。

但不同的外部设备,触发不同的硬件中断,在操作系统中,会有不同的中断号与之对应。中断号有什么用呢?

操作系统中有一张中断向量表,本质上可以理解为一个数组结构,不同的下标处对应了不同的中断处理方法,中断号实际就是数组对应的下标。

需要说明的是,中断向量表就是操作系统极其重要的一部分,在计算机启动时,就会加载到内存中。

5.1.2 时钟中断

时钟中断,本质上也是硬件中断的一种,是由时钟或定时器每隔一段时间发出的中断,在较早的计算机中,触发时钟中断为独立的硬件,而现在计算机中,相关硬件往往集成到CPU中。

但与其它硬件中断不同的是,其它硬件中断,如键盘和鼠标的硬件中断,是需要由用户触发的,但是时钟中断,是每隔一定时间自动触发的。

那么时钟中断有什么用呢?

在讲作用前,我们先要了解几个概念:CPU主频分时操作系统和时间片

每个CPU都有一个主频,主频决定了CPU在单位时间内能够运行多少个时钟周期——因此,相对而言,CPU主频越高,CPU性能越强,即CPU运算越快,因为单位时间内,能够运行的时钟周期越多。

我们日常生活中常见的windows、linux、mac这些操作系统,都是分时操作系统,也就是基于进程的时间片来进行进程的轮转调度。
在分时操作系统中,每个进程在CPU上一次能运行的时间是有限的,这个时间我们称作该进程的时间片。当一个进程的时间片用完后,该进程便不能继续运行,保存硬件上下文后,就从CPU上剥离,重新进入进程的调度队列中,恢复时间片,等待下一次调度。

那么一个进程的时间片具体有什么确定呢?简单来说,每一个进程都有一个对应的计数变量,每当一个时钟中断到来后,该计数变量便自减1,当该变量减到0时,就代表进程的时间片用完。

这里需要区分一下概念,CPU主频,时钟周期,系统频率。CPU主频确定了时钟周期,主频的数值就是单位时间内CPU能运行多少个时钟周期。系统频率,是时钟中断触发的频率,即单位时间内,发出时钟中断的个数。
上述的时间片,是与系统频率和相应进程的时间片计数变量相关的。

现在我们可来谈一谈时钟中断的中断处理方法。

每当时钟中断触发后,CPU就会执行do_timer方法,在这个调用中,会检查当前进程的时间片情况,根据不同的时间片情况对进程进行不同的标记,也会查看当前进程调度队列中的其它进程,如果需要调度运行,就会进行标记,而实际的进程调度与切换工作,在do_timer中相应准备工作完成后,调用schedule方法来完成——进程的实际调度与切换都是由该方法完成的。

所以,时钟中断的触发,是推动进程调度与切换的基础,而相邻时钟中断触发之间的时间间隔便是其余进程任务实际在CPU上可运行的时间。

5.2 软件中断

有通过外部设备触发的硬件中断,那有没有通过软件条件触发的软中断呢?

答案是肯定的。不过软中断需要做一个特别区分,有Linux内核层面的软中断,也有CPU架构层面的软中断,下面所讲的软中断,全部都是CPU架构层面的软中断。

5.2.1 陷阱 trap

什么是陷阱呢?
简单来说,陷阱是一种同步中断,通常是由程序主动触发的,并且会返回下一条指令的地址,常见的陷阱有系统调用和调试指令。

我们重点来讲系统调用的流程。

系统调用本质上是操作系统提供的方法。在操作系统内核中,存在一张系统调用表,本质上就是一个函数指针数组,可以通过数组下标,访问不同的系统调用,而这个数组下标,就是系统调用号。

系统调用的本质,其实就是通过系统调用号和系统调用表的地址,进而找到要执行的具体系统调用的地址,进而执行相应方法。

具体来说,用户层面主动使用系统调用,当程序运行到调用该系统调用时,会将相应的系统调用号存入CPU的eax寄存器中,然后通过int 0x80 或者 syscall这样的指令跳转到相应的系统调用处理方法中,CPU也同时由用户态转为内核态。

在系统调用处理方法中,最重要的一点就是根据eax中的系统调用号和操作系统内核中的系统调用表的地址,具体计算出相应系统调用函数指针的地址,然后就可以拿到相应的函数指针,进而执行具体的系统调用了。

但是,我们使用系统调用的时候,并没有看到什么eax寄存器,也没有看到什么int 0x80 或者 syscall这样的指令,这是因为C语言库已经将这些系统调用全部进行了封装。

5.2.2 异常

异常,即excaption,是一种同步中断,常见的异常有除0异常,缺页异常等。

异常本质上是CPU出现某种错误,进而触发中断,对该错误进行修复处理一种机制。

异常产生时,CPU自动由用户态转变为内核态,然后跳转到操作系统的中断向量表中的异常处理中,根据不同的异常,进一步执行不同的异常处理方式。

除0异常,最终的处理方式就是由操作系统向相应进程发送一个SIGFPE的信号,进而终止进程。

缺页异常,顾名思义,与页表相关的异常,一般是由于非法内存访问,或者说虚拟地址缺少物理映射而产生的。

非法内存访问有很多,访问无效的内存地址,比如野指针,或者违反内存访问权限,比如修改只读内存,或者用户态访问内核态等等,这些都会触发段错误,即被发送SIGSEGV信号,进而终止进程。

而虚拟地址缺少物理地址映射,这种缺页异常,会进行映射后,继续执行原进程,而不终止进程。

5.3 操作系统是什么

通过上述的介绍,我们可以发现,操作系统是很“懒”的。一旦开机后,操作系统内核便会加载到内存中,但是并不会主动执行其中的大部分代码,而是当一个个中断到来时,再根据不同的中断,执行内核中不同的中断处理方法。

所以,究其本质,操作系统是由一个个中断推动运行的。

当计算机启动后,操作系统会创建一个0号IDLE进程,当没有任何其它任务时,这个进程会不断地在CPU上运行,它的作用是使得CPU工作在一个低功耗的状态,同时它也会尝试调用schedule()方法,进行进程调度。

而当有其它任务,对应有其它进程时,CPU便运行其它进程,当其它进程都结束时,再重新运行0号IDLE进程。

5.4 内核态与用户态

内核态与用户态,本质就是CPU所处的两种状态。

在32位的Linux系统中,进程地址空间共4GB,0~3GB是用户态空间,3-4GB是内核态空间。

不同的进程中,0~3GB的映射情况是各不相同的,对应的每个进程的页表是独立的,但是3-4GB的映射情况是相同的,所有进程共用一张主内核页表(与每个进程的独立页表,不是同一张表)。

内核态与用户态的区分,是通过CPU中的CPL(当前特权级别)来决定的,CPL为0表示内核态,CPL为3表示用户态。

用户态只能访问0~3GB的空间,而内核态理论上能够访问4GB的所有空间,但主要是在访问内核态的空间。

为什么可以实现限制访问呢?因为在页表中,存在记录访问不同地址空间所需特权级别的变量,当实际在查页表时,MMU会将CPU中的CPL与当前访问地址所需的CPL做比较,进而进行访问的限制。

处在用户态的进程,如果强行访问内核态空间,一般会触发段错误,而终止进程。

http://www.dtcms.com/a/285742.html

相关文章:

  • ali linux 上安装swagger-codegen
  • Windows发现可疑的svchost程序
  • 深度理解 KVM:Linux 内核系统学习的重要角度
  • 【Linux】环境基础与开发工具的使用
  • Linux中的LVS集群技术
  • MySQL的基本操作及相关python代码
  • 基于极空间NAS+GL-MT6000路由器+Tailscale的零配置安全穿透方案
  • 云原生 DevOps 实战之Jenkins+Gitee+Harbor+Kubernetes 构建自动化部署体系
  • 小白学Python,网络爬虫篇(2)——selenium库
  • 图机器学习(13)——图相似性检测
  • 信息学奥赛一本通 1575:【例 1】二叉苹果树 | 洛谷 P2015 二叉苹果树
  • 短视频矩阵系统哪家好?全面解析与推荐
  • 香港服务器SSH安全加固方案与密钥认证实践
  • Flutter权限管理终极指南:实现优雅的Android 48小时授权策略
  • GLU 变种:ReGLU 、 GEGLU 、 SwiGLU
  • android 信息验证动画效果
  • 精通 triton 使用 MLIR 的源码逻辑 - 第002节:再掌握一些 triton 语法 — 通过 02 softmax
  • 续签人员李权
  • 掌上医院微信小程序平台如何对接医保在线支付?
  • vue自定义指令bug
  • poi-excel-添加水印
  • Vue3 学习教程,从入门到精通,Vue3 项目打包语法知识点及案例代码(9)
  • Windows Server 版本之间有什么区别?
  • 私有服务器AI智能体搭建配置选择记录
  • NGFW服务器安全防护
  • 浏览器信息隔离全指南:从多账号管理到隐私防护
  • VirtualBox + CentOS:启用 DHCP 获取 IPv4 地址
  • 数据结构:顺序表和链表
  • 【PTA数据结构 | C语言版】斜堆的合并操作
  • Expression.Block详解