中断、信号、系统调用

1、中断的分类

中断程序的方法可以分为硬件中断和软件中断。

硬件中断是硬件自动触发的,包括中断和异常。比如:中断 通过中断控制器给CPU的INTR引脚发送信号,如按下键盘,会给CPU一个0x21中断号;;异常 CPU执行某条指令发生异常,会自己触动一个中断号,比如执行到一个无效指令,CPU会给自己0x06的中断号。

软件中断是 由软件程序主动触发的, INT 指令。比如系统调用,实际上执行的是INT 0x80指令,CPU给收到0x80的中断号。

但也有的人这么分:

硬中断包括中断、异常、INT指令。整个中断机制都是由硬件实现的逻辑。软中断是由软件实现的,是硬中断的后半部分。

2、步骤

1、系统调用

系统调用是操作系统内核提供的一系列具备预定功能的函数接口供应用程序调用。系统调用把应用程序的请求传给内核,内核调用相应的函数完成所需的处理,再将处理结果返回给应用程序。

应用程序运行在用户态,其诸多操作都受到限制。而系统调用运行在内核 态,那么运行在用户态的应用程序如何运行内核态的代码?操作系统一般是通过中断来完成用户态到内核态的切换。

32 位系统调用

- 用户态

- 将请求参数保存到寄存器

- 将系统调用名称转为系统调用号保存到寄存器 eax 中

- 通过软中断 ENTER_KERNEL 进入内核态  int 0x80 指令

- 内核态

- 将用户态的寄存器保存到 pt_regs 中

- 在系统调用函数表 sys_call_table 中根据调用号找到对应的函数

- 执行函数实现, 将返回值写入 pt_regs 的 ax 位置

- 通过 INTERRUPT_RETURN 根据 pt_regs 恢复用户态进程

 

2、外部设备硬件中断

CPU 收到一个中断号 n 后,会去中断向量表中寻找第n个中断描述符,从中断描述符中找到中断处理程序的地址,然后跳过去执行。

其中涉及到的问题:

  • 如何给 CPU 一个中断号?

外部设备通过 INTR 引脚,或者 CPU 执行指令的过程中自己触发,或者由软件通过 INT n 指令强行触发。

  • CPU 收到中断号后如何寻找到中断程序的入口地址?

通过 IDTR 寄存器找到中断描述符表,通过中断描述符表和中断号定位到中断描述符,取出中断描述符表中存储的程序入口地址。

  • 中断描述符表是谁写的?

操作系统代码写上去的。

  • 找到程序入口地址之后,CPU 做了什么?

简单说,实际上做的事情就是压栈,并跳转到入口地址处执行代码。而压栈的目的,就是保护现场(原来的程序地址、原来的程序堆栈、原来的标志位)和传递信息(错误码) 

每个中断号都对应了一个中断处理程序。中断号是有限的,所以不会用一个中断来对应一个系统调用。对于每个系统调用都有一个系统调用号,在触发中断之前,会将系统调用号放入到一个固定的寄存器,中断处理程序会读取该寄存器的值,然后决定执行哪个系统调用的代码。

硬中断的微观层面,就是 CPU 在每一个指令周期的最后,都会留一个 CPU 周期去查看是否有中断,如果有,就把中断号取出,去中断向量表中寻找中断处理程序,然后跳过去。

软中断的微观层面,简单说就是有一个单独的守护进程,不断轮询一组标志位,如果哪个标志位有值了,那去这个标志位对应的软中断向量表数组的相应位置,找到软中断处理函数,然后跳过去。

软中断是 Linux 处理一个中断的下半部的主要方式,比如 Linux 某网卡接收了一个数据包,此时会触发一个硬中断,由于处理数据包的过程比较耗时,而硬中断资源又非常宝贵,如果占着硬中断函数不返回,会影响到其他硬中断的相应速度,比如点击鼠标、按下键盘等。

所以一般 Linux 会把中断分成上下两半部分执行,上半部分处理最简单的逻辑,下半部分直接丢给一个软中断异步处理。

3、信号

信号的发送与处理是一个复杂的过程,这里来总结一下。

假设我们有一个进程 A,main 函数里面调用系统调用进入内核。按照系统调用的原理,会将用户态栈的信息保存在 pt_regs 里面,也即记住原来用户态是运行到了 line A 的地方。

在内核中执行系统调用读取数据。当发现没有什么数据可读取的时候,只好进入睡眠状态,并且调用 schedule 让出 CPU,这是进程调度第一定律。将进程状态设置为 TASK_INTERRUPTIBLE,可中断的睡眠状态,也即如果有信号来的话,是可以唤醒它的。

其他的进程或者 shell 发送一个信号,有四个函数可以调用 kill、tkill、tgkill、rt_sigqueueinfo。四个发送信号的函数,在内核中最终都是调用 do_send_sig_info。do_send_sig_info 调用 send_signal 给进程 A 发送一个信号,其实就是找到进程 A 的 task_struct,或者加入信号集合,为不可靠信号,或者加入信号链表,为可靠信号。do_send_sig_info 调用 signal_wake_up 唤醒进程 A。进程 A 重新进入运行状态 TASK_RUNNING,根据进程调度第一定律,一定会接着 schedule 运行。进程 A 被唤醒后,检查是否有信号到来,如果没有,重新循环到一开始,尝试再次读取数据,如果还是没有数据,再次进入 TASK_INTERRUPTIBLE,即可中断的睡眠状态。

当发现有信号到来的时候,就返回当前正在执行的系统调用,并返回一个错误表示系统调用被中断了。系统调用返回的时候,会调用 exit_to_usermode_loop。这是一个处理信号的时机。

调用 do_signal 开始处理信号。根据信号,得到信号处理函数 sa_handler,然后修改 pt_regs 中的用户态栈的信息,让 pt_regs 指向 sa_handler。同时修改用户态的栈,插入一个栈帧 sa_restorer,里面保存了原来的指向 line A 的 pt_regs,并且设置让 sa_handler 运行完毕后,跳到 sa_restorer 运行。返回用户态,由于 pt_regs 已经设置为 sa_handler,则返回用户态执行 sa_handler。

sa_handler 执行完毕后,信号处理函数就执行完了,接着根据上面对于用户态栈帧的修改,会跳到 sa_restorer 运行。sa_restorer 会调用系统调用 rt_sigreturn 再次进入内核。在内核中,rt_sigreturn 恢复原来的 pt_regs,重新指向 line A。从 rt_sigreturn 返回用户态,还是调用 exit_to_usermode_loop。这次因为 pt_regs 已经指向 line A 了,于是就到了进程 A 中,接着系统调用之后运行,当然这个系统调用返回的是它被中断了,没有执行完的错误。

 

注意信号处理的时机是系统调用或者中断返回时。信号处理的时机和调度的时机基本一致,都是当需要的时候,设置标签位,然后等下一次从系统调用返回或中断返回的时候进行判断进而处理,当发生用户态死循环的时候,不会调用系统调用,但是不代表不会处理中断(尤其是时间中断,这样一来才能保证当进程执行的cpu时间太长就需要强制让出了cpu),当从中断处理返回的时候,同样是从内核态到用户态,同样会进行判断是不是收到信号进而进行处理。

信号处理函数处理完成之后,系统设计的时候,为啥不直接返回用户态LineA的地方? 这样不是省事了吗?

用户态LINEA的所有用于恢复的东西还在内核数据结构里面,包括寄存器什么的。返回不止要返回到正确的地址,还要有各种寄存器的恢复,那如何进内核呢?只能再执行一个系统调用,然后把用户态的场景恢复,完。

异步通知所基于的信号也类似于中断。

硬中断是硬件中断、异常、INT指令 对CPU的中断;

软中断通常是硬中断服务程序对内核的中断

信号则是由内核(或其它进程)对某个进程的中断。

作为系统调用而言,对于X86,软中断是通过int80实现

对于其它的软中断,则是在硬件中断之后触发的软中断,是中断下半部的一种实现机制,本质是内核线程

系统调用的中断是软件触发的中断,所以称为软中断,而对于中断下半部的软中断,虽然也是软件触发,但是并不经过中断向量表。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值