什么是中断
中断其实是一种“中断”事件,中断具体代表什么意思需要考虑它所处的上下文环境和参照对象是谁。考虑事件,我们可以简单把中断抽象为这样一种模型:
当我们分析某种中断事件时,我们需要搞清楚这四个对象:
中断源
- 中断源是谁
- 中断源在什么条件下触发中断
- 中断源如何触发
中断信号
- 信号具体指的是什么
- 信号是否需要存储
- 如何存储
中断控制器
- 中断信号的管理
比如说中断源发送的信号是否屏蔽,信号是否可被中断处理器重复处理,信号的处理是否有优先级...
中断处理器
- 如何获取到信号
- 拿到信号做什么样的操作
- 处理完信号后做什么样的操作
下面我们主要围绕操作系统的中断机制,Java的中断机制,如何设计一个异步线程间的中断系统这三部分简单探讨下。
操作系统的中断机制
与操作系统有关的中断,通常是指:程序在执行过程中,遇到急需处理的事件时,暂时中止CPU上现行程序的运行, 转去执行相应的事件处理程序,待处理完成 后再返回原程序被中断处或调度其他程序执行的过程。
按照中断事件本身的不同,可以划分为处理器之外的中断事件,异常,系统异常。
处理器之外的中断事件
指由外围设备发出的信号引起的,与当前运行指令无关的中断事件。示意图如下:
我们分别以上述四个对象来看:
中断源
中断源:外部设备,如打印机,键盘,鼠标等。
触发条件:如外围设备报告I/O状态的I/O中断;外围设备发出的对应信号中断,如时钟中断,键盘/鼠标对应信号的中断,关机/重启动中断等。
触发方式:由外部设备向中断控制器发出中断请求IRQ。
中断信号
也就是说中断源通知给中断控制器的是什么。
可以是通过一条信号线上产生特定的电平(利用高低电平表示是否中断两种状态),也可以在总线上发送特定消息或者消息序列,也可以是在中断寄存器中设置已发生的中断状态等。
中断控制器
CPU中的一个控制部件,包括 中断控制逻辑线路和中断寄存器。负责中断的发现和响应。
也就是说负责检查中断寄存器中的中断信号,当发现中断时让CPU切换当前进程程序,去处理中断程序。响应示意图如下:
中断处理器
指的是CPU接收到不同的中断信号该怎么处理。包括“中断处理过程”和“恢复正常操作”两部分。
1.中断处理过程
首先CPU需要将当前运行进程的上下文保存,从中断进程中分析PSW,确定对应的中断源和执行对应的中断处理程序。
PSW(Program Status Word): 是指在电脑中,一段包含被操作系统使用的程序状态信息的内存或硬件区域。一般用一个专门的寄存器来指示处理器状态。可以理解为我们上面提到的中断信号存储装置.
2.恢复正常操作
当中断程序执行完毕,接下来执行哪个进程由进程调度决定,由调度策略决定是否调度到中断执行前的进程。
较为完整的中断响应流程图如下:
异常 和 系统异常 这两类中断事件主要属于处理器执行特定的指令引起的中断事件。和上述硬件外围设备引起的中断事件的中断源不同,中断的发起,控制和处理主要是由操作系统的指令逻辑和线路来承担。是一种同步的处理操作,而外部中断是由外部设备发起,是一种异步的处理操作。下面我们简要介绍下。
异常
异常指当前运行指令引起的中断事件。包括错误情况引起的故障,如除零算数错误,缺页异常;也包括不可恢复的致命错误导致的终止,通常是一些硬件错误。
- 异常的处理
对于故障的处理,根据故障是否能够被恢复,故障处理程序要么重新执行引起故障的指令,要么终止。
对于终止的处理,处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
系统异常
系统异常指执行陷入指令而触发系统调用引起的中断事件,如请求设备、请求I/O、创建进程等。
- 系统调用的处理
这种有意的异常,称为陷阱处理。处理完成后陷阱程序会将控制返回给应用程序控制流的下一条指令。
总结一下,操作系统的中断类别行为如下:
Java的中断机制
理解了上面操作系统的中断之后,Java的中断机制就很easy了。
Java中断指的是A线程发送中断信号给B线程,B线程再根据自己当前执行程序中的中断处理逻辑决定如何响应。
- 中断源
中断源:A线程
中断触发条件:A线程说了算
中断源触发方式:A线程中调用threadB#interrupt()方法.
实现机制也不难,扯淡之前我们先思考两个问题:
问题1: 线程之间如何通信,A线程的中断信号怎么才能传给线程B?
问题2: 线程的状态有Running,Blocked,Waiting等,当线程B处在不同的状态下,如何响应中断信号?
答:
问题1:这种情况下线程之间通信用共享内存(是通过共享内存的方式? 不是吧, 应该是直接信号量的方式?)就可以了。只需要给每个线程都设置一个中断标示位, 这样A线程中调用threadB#interrupt()方法,实际操作是把B线程的中断标示位设置为true。信号就算传递过去了(应该不是不能直接设置b线程的标志位,经过内核完成的)。
问题2:当B线程处于非阻塞状态时,B线程可以在自己需要处理中断逻辑的地方判断中断标示位是否为true,就可以响应处理中断。
但是当B线程处于阻塞状态时,这特么怎么查自己的中断标示位啊?
JVM帮帮忙,当B线程阻塞在Object#wait(),Thread#join(),Thread#sleep(),实现了InterruptibleChannel接口的IO操作 和Selector接口的select()这些操作时,JVM会让B线程马上抛出异常或被唤醒,从而让B线程可以选择是否响应中断。
因为是Java实现的中断机制,中断标示位的设置也是JVM帮做的。
- 中断信号
信号:线程的中断标示位。
存储方式:JVM说了算。
中断控制器
JVM控制了信号的存储和让线程B及时唤醒。
线程B控制了自己的中断响应逻辑,何时响应,如何响应。中断处理器
获取信号:B线程可通过调用threadB#isInterrupted()方法得知自己是否被中断,也就是通过自己主动拉取信号(poll方式)。
如何处理信号:B线程说了算。
处理完信号后做什么:B线程说了算。
Java的线程中断机制设计的比较灵活,使用者可以决定中断处理的较多事情。
总结下Java中和中断有关的方法:
在JDK中,线程池的ThreadPoolExecutor#shutdownNow()方法就是调用workers线程数组中每个worker线程的interrupt()方法来关闭线程池。
这样暴力关闭线程会存在一个问题,线程池并不知道worker线程的中断执行情况,如果worker线程忽略了中断信号,那可能导致当前任务还在执行,发生意想不到的结果。
FAQ
CPU检测到中断信号时,怎么知道是发给哪个进程的?
假设1:通过中断方式把内存某区域连续的1KB数据传送到某个I/O设备上去;
一个系统的的中断系统通常是类似这样的组成,我们应该注意到,设备眼中的中断,中断控制器眼中的中断,还有CPU眼中的中断,都不是同一个概念。所以,当我们说“关中断”一类的说法的时候,对它们三者也是不一样的。
设备的中断,是设备要产生一个事件通知CPU,事件的产生的方法有很多,最简单的是在一条信号线上产生特定的电平(电平中断,比如平时都是高电平,拉低了就表示有中断了),或者产生一次电平变化(边缘触发中断),复杂的可以很复杂,比如在总线上发送特定消息或者消息序列。对设备“关中断”,指的仅仅是让这个设备不要再提供中断信号了,但如果中断控制器已经获得这个中断信号,这个中断信号还是会报到CPU上的。
中断控制器,是对多个设备的中断进行采样,排队,分发的机制。对中断控制器说:关中断,是让中断控制器不要给CPU(或者上级)发送中断信号了,设备报不报信号上来,这些信号是否被排队,那是另一个问题。
最后,是对我们软件程序员最熟悉的CPU了,CPU的中断,是CPU核上有一条中断线,当这条线加上合适的电平或者信号,CPU核就会从当前的执行上下文中,直接跳转到中断处理程序中执行。在CPU的角度上关中断,就是跟CPU说:就算现在你的中断线上有中断,也不要执行“跳转到中断处理程序”这个动作。
CPU能认识的就仅仅是中断线和中断处理程序这些概念。所谓线程,进程,软中断等概念,是软件发明出来的,CPU是不认识的。所谓线程,本质上是保存CPU运行状态的一种形式,CPU的运行状态,就是CPU的所有寄存器的内容的集合(包括用来控制中断的寄存器),线程的作用就是可以把这些寄存器都保存下来(其实还有软件本身的堆栈等其他信息,但我们这里不关心软件,先忽略),然后用另一个保存的状态刷新CPU的状态,让CPU感觉自己在运行到另一个上下文上。OS对CPU不断进行状态的切换,保存上一个状态,加载下一个线程的状态,就实现线程切换了。至于进程,本质上可以认为是线程切换的同时也会切换地址空间(切换成本大)。
所以,进程也有关中断的概念,关进程的中断,会导致本进程运行的过程中,CPU不再接受中断,但如果这个进程切换到另一个进程上,那就按新进程的上下文来说了。
在大部分的通用CPU上(请注意,这里说的是CPU这个视角),中断是可嵌套的,就算你在处理中断,只要CPU中断线上又来信号的,它就有可能再次进入中断处理程序。为了避免这种情况,我们通常需要在中断控制器上玩游戏:当中断控制器给一个CPU发了一个中断,它就不会再发下一个中断(这也有很多变种,我们只说一种常见的实现),要等CPU主动对中断控制器发起EOI操作,才会允许下一个中断信号去激发CPU。Linux系统的中断处理的名称空间中,把进入中断处理,到发出EOI这一段,称为硬中断,把EOI之后,再回到原来被中断之前的程序之前的这一段,称为软中断。但这些是软件的概念,中断对于CPU来说,只有开始,没有结束一说,每次中断的发生,都只是一次强行跳转的过程。
现在可以回到问题本身了,从设备的角度,给CPU发中断,CPU可能正在运行任何进程,无论是哪个进程,效果都是陷入到内核中(通常所有进程的内核是共享的),是内核的(其实是驱动的)的中断处理程序在处理这个中断,并在返回到用户空间前,保存中断上收到的信息,然后进入调度程序,调度当前或者下一个进程运行。剩下的事情,就是进程和驱动之间的恩怨了。
答复2:
硬件中断从来不是发送给进程的,而是发送给操作系统内核,由内核统一处理,而不关心当前正在执行的是哪个进程,不管哪个进程操作都是一样的:保存现场,进入内核,执行需要的操作,返回中断前的现场,继续进程执行。
CPU写入外部设备有两种不同的方式,一种是直接操作硬件设备寄存器,这一般是不需要中断的,CPU在写入一个寄存器的时候会处于阻塞状态,直到写入完成之前不能继续,因此通常只有处理非常少的数据的时候才会这样做。另一种方式是使用DMA,DMA是一个专用的外部设备,CPU将需要发送的数据提前在内存中准备好,然后设置DMA设备的寄存器,让DMA设备从内存的指定位置开始,将内存中数据依次写到对应地址的外部硬件寄存器里,这样在DMA写入的同时CPU就可以做其他工作。DMA写入完成后会产生一个中断通知CPU。这些都和当前执行的进程无关。进程只是直接跟操作系统内核通信,内核负责通过调度来通知进程操作是否完成之类的信息。
再举个详细点的例子,比如某个进程要读取一个文件,向内核发送了一个read的syscall调用,陷入内核,内核会设置DMA,然后把进程挂起。因为进程挂起了,内核另找了一个进程切换进来执行。当DMA完成发生中断的时候,不管当前执行的是哪个进程,都会直接通过中断进入内核,这个过程外部执行的进程是察觉不到的,它在执行的途中被打断然后冻结在了执行现场,就像时间停止了一样(这就是“中断”的含义),CPU开始执行内核中的中断处理程序,内核通知之前挂起的进程操作已经完成,并且取消挂起,这时候这个进程是否会立即抢占进来,取决于优先级,在Linux当中一般会把因为IO挂起的进程优先级稍微调高一点让它们立即抢占进来,提高IO效率;但如果不能抢占进来,就会恢复当前的进程的执行,等到高优先级执行结束后,再让之前挂起的进程切换进来继续执行,这对于执行IO的进程来说是不可见的。
(非阻塞IO调用在底层数据没有ready的时候会立即返回,而不是挂起进程等待数据,因此调用结束之后可以马上继续往下执行,实际上只是因为它已经返回了而已。在Linux上面,非阻塞IO能处理的主要是被动接受式的数据,比如socket、pipe、fifo这些,你收不收,发送端都是会发数据到缓冲区里的(除非缓冲区满),这种对象可以选择永远只读取缓冲区里的数据,然后通过select或者epoll来获得缓冲区状态的通知,或者干脆轮询,如果通过select或epoll,那么select或者epoll会挂起,中断到来时内核恢复它的执行;如果通过轮询,那么不涉及到唤醒的过程。真正异步的接口aio*也是有的,在发起请求之后内核负责将数据直接填到用户提供的缓冲区中,然后通过信号等方式通知用户,不太常用。真正的异步io: 用户代码请求IO,内核将请求插入IO队列,然后用户线程继续运行。当设备通过中断通知cpuIO完成时,内核启动一个内核线程,将数据从内核copy到用户空间。执行copy的这个内核线程是与用户线程并发执行(被内核统一调度)(单核就是交替执行)。最后通过某种机制通知用户线程(信号?))