1 轮询与中断
外部设备与中央处理器交互一般有两种手段:轮询和中断。
(1)轮询(Polling)
很多I/O设备都有一个状态寄存器,用于描述设备当前的工作状态,每当设备状态发生改变时,设备将修改相应状态寄存器位。通过不断查询设备的状态寄存器,CPU就可以了解设备的状态,从而进行必要的I/O操作。为了节约CPU资源,查询工作往往不是连续的,而是定时进行。
轮询方式具有简单、易实现、易控制等优势,在很多小型系统中有大量应用。对那些实时敏感性不高、具有大量CPU资源的系统来说,轮询方式有很广泛的应用。最典型的用途就是在那些任务比较单一的单片机上,嵌入式系统中也有应用。
轮询的一种典型的实现可能是这样的:while(TRUE){/*…*/ select(,,timeout); /*…*/};当然这里的select()也可以使用poll()替换。
轮询方式主要存在以下不足:
<1>增加系统开销。无论是任务轮询还是定时器轮询都需要消耗对应的系统资源。
<2>无法及时感知设备状态变化。在轮询间隔内的设备状态变化只有在下次轮询时才能被发现,这将无法满足对实时性敏感的应用场合。
<3>浪费CPU资源。无论设备是否发生状态改变,轮询总在进行。在实际情况中,大多数设备的状态改变通常不会那么频繁,轮询空转将白白浪费CPU时间片。
(2)中断(Interrupt)
中断,顾名思义,就是打断正在进行中的工作。中断不需要处理器轮询设备的状态,设备在自己发生状态改变时将主动发送一个信号给处理器(PIC),后者在接收到这一通知信号时,会挂起当前正在执行的任务转而去处理响应外设的中断请求。中断通知机制通过硬件信号异步唤起处理器的注意,解决了外部设备与处理器之间速度不匹配导致的资源浪费问题。
现代设备绝大多数采用中断的方式与处理器进行沟通,因此设备驱动程序必须能够支持设备的中断特性。处理器在中断到达时会根据不同的中断号找到对应设备(IRR),并对中断请求进行响应处理。中断处理例程ISR(Interrupt Service Routine)由设备驱动程序提供,并在设备驱动模块初始化时注册到系统中断向量表中。从设备发出中断信号,到处理器最终调用ISR进行处理,期间会经过很多步骤,这个过程构成了中断处理框架。中断处理框架包括了进入ISR之前的很多进入路径(entry path),例如MIPS下要经历这样几个步骤:设置或屏蔽相关寄存器;进入异常入口点取指;现场保护;异常分类(MIPS下中断也是一种异常)处理;查找中断向量表路由ISR。不同的操作系统对中断处理框架的设计不尽相同,但是要达到的目的是一样的,那就是最终调用用户注册的设备ISR。
(3)中断与轮询的折衷
虽然轮询方式存在空转损耗导致名声不佳,但并非一无是处。中断模型也并非十全十美,其高优先级的VIP待遇和快速响应要求在极端条件下将造成“活锁”效应。有时候需要发挥粗暴中断和温和轮询各自的优势,根据实际应用情景,在两种模式之间切换。手机导航杆卡死情形的处理是个很好的案例。
在过去的一些手机和PDA设备上安装有导航杆,它支持3种 动作(顺时针旋转、逆时针旋转和按键),可方便菜单导航。导航杆的三种动作都会向处理器发出中断。系统中通用的目的I/O(GPIO)端口和导航杆连接。 中断处理函数的工作就是查看GPIO数据寄存器解析出导航杆运动。假定导航杆由于存在运动部件(如旋轮偶尔被卡住)引起的固有的硬件问题,从而在GPIO 端口产生不同于方波的波形。被卡住的旋轮会不停地产生假的中断,并可能使系统冻结。为了解决这个问题,可以捕获波形分析,在卡住的情况下动态地从中断模式切换到轮询模式。如果旋轮恢复正常,再动态地从轮询模式切换到中断模式,软件也恢复正常模式。
在本文的最后,将介绍Linux网络设备驱动模型中的NAPI机制 ,它采用“中断+轮询”的处理方式代替纯中断处理方式,是中断和轮询的完美合体。
2 中断硬件框架
中断的主动通知特性需要硬件设施支持。在数字逻辑电路层面,外部设备和处理器之间有一条专门的中断信号线(Interrupt Line),用于连接外设与CPU的中断引脚(Interrupt Pin)。当外部设备发生状态改变时,可以通过这条信号线向处理器发出一个中断请求(Interrupt Request,IRQ),其中外部设备通常被称作中断源(Interrupt Source)。
处理器一般只有两根左右的中断引脚(例如8259A的INTR和INTA),而管理的外设却很多。为了解决这个问题,现代设备的中断信号线并不是与处理器直接相连,而是与一个称为中断控制器的设备相连接,后者才跟处理器的中断引脚连接。中断控制器一般可以通过处理器进行编程配置,所以常称为可编程中断控制器PIC(Programmable Interrupt Controller)。下图是一个典型的中断硬件连接的系统框架图:
中断连接框图
上图中,PIC的输出中断信号线连接到处理器的INT引脚 上,这是处理器专门用来接收中断信号的pin脚。外部设备的中断线连接到PIC的pin引脚上,这是PIC用来接收外设中断的pin脚。比如第一个设备的 中断线通过P0连到PIC上。在实际的硬件平台上,PIC有的在CPU外部,比如x86平台的8259中断控制器;有的被封装到CPU的内部,这广泛见于嵌入式领域。一颗SoC芯片内部集成了处理器和各种外部设备的控制器,其中包括PIC。
IRQ相关信息管理的关键点是一个全局数组,每个数组项对应一个IRQ编号,软件中断号irq就是这个数组的索引,irq将一对一或多对一(共享)映射到硬件中断源编号。不同的操作系统相关数据结构的实现和映射策略实现可能有差别。
3 中断向量表
中断向量表其实是处理器内部的概念,因为处理器除了会被外部设备中断外,其内部也可能 产生异常等事件,例如在MIPS中,中断只是异常的一种。当这些事件发生时,CPU必须暂停手头上的工作,转而去处理中断或异常,因此处理器需要知道到哪 里去获得这些中断或异常的处理函数的目标地址。中断向量表就是用来解决这个问题,其中每一项都是一个中断或异常处理函数的入口地址,具体来说4个字节的函数指针将指向一段汇编微码(intConnectCode)执行跳转。
外部设备的中断常常对应向量表中的某一项,这是通用框架的外部中断处理函数入口,因此在进入通用的中断处理函数之后,系统必须知道正在处理的中断是哪一个设备产生的,而这正是由软件中断号irq定的决。中断向量表的内容是由操作系统在初始化阶段来填写,对于外部中断,操作系统负责实现一个通用的外部中断处理函数,然后把这个函数的入口地址放到中断向量表中的对应位置。用户注册设备驱动ISR,实际上就是挂接到中断向量表中,覆盖某一项的默认处理实现特化。
4 中断路由
很多SoC芯片或设备提供了一个重要的寄存器——IRR(Interrupt Routing Register),例如PCI中的Interrupt Line Register,它用来配置中断源与CPU中断位图的映射关系。所谓CPU中断位图是指CPU中中断相关的控制寄存器(IE/IP)的比特位分布(bitmap),例如MIPS中的C0_SR:IM[7~2]/C0_CAUSE:IP[7~2]对应六路外设中断源。
关于IRR,这里摘录网贴《PCI Interrupt Routing》中的一段阐述:
--------------------------------------------------------------
PCIinterrupt routing consists of figuring out which platform-specific interrupt isasserted when a given PCI interrupt signal is asserted. On x86 machines, thisconsists of figuring out which input pin on an interrupt controller is assertedwhen a given PCI interrupt signal is asserted.
--------------------------------------------------------------
PCI interrupt routing所描述(figure out)的问题是:PCI作为中断源与PIC(interrupt controller)相连,当PCI有interrupt signal时,PIC哪个输入引脚(input pin)将收到通知(asserted)。
需要明确的是,在中断硬件连接框图中,左边的中断源与PIC往往是通过硬连线连接的。PCI设备向PIC发出中断,PIC向CPU传递中断,这里需要确定的应该是CPU相关中断控制寄存器IP位图与中断源(PCI设备)的对应关系。这个映射关系是由中介位置的PIC向外提供的IRR寄存器配置的。
《可编程中断控制器8259A》 中提到“在80x86”系统中,8259A在中断响应周期的第二个总线中期内,从数据总线内向CPU送出8位中断类型码N的值。”当某一路中断源发起中断 时,PIC将根据引脚编号编码中断类型号N,CPU收到INTR信号后在稍后适当的时钟周期中从PIC相应端口读取中断类型码,紧接着CPU中断位图将置 位。例如在MIPS CPU中,外部设备7(Timer)发起中断,C0_CAUSE:IP[7]置位;外部设备6发起中断,C0_CAUSE:IP[6]置位;...。我们 可以通过配置IRR寄存器来改变这种映射关系,这里体现了PIC的可编程性。可以将PIC类比为一个路由器,左边各个中断源为接入PHY口的PC,IRR就相当于路由器为各个PC分配IP地址。有的SoC硬连线和位图映射关系已经固定(相当于ARP绑定),并未提供类似的IRR寄存器,此时就得参考datasheet或SDK指导开发。
在MIPS SoC中,外设中断引脚(线)与PIC连接,配置完IRR并配置C0_SR:IM[7~2]使能断源后,C0_CAUSE:IP[7~2]的相应位将反馈 对应设备的中断活跃状态。当有中断发生时,通用中断处理程序将C0_CAUSE:IP[7~2]和C0_SR:IM[7~2]执行逻辑“与”运算,其中为 1的IM&IP位对应的设备发出了活跃且使能的中断请求。经过现场保护处理和一系列的进入路径(entry path),如果是共享中断则需要先解复用,最终在中断向量表中查找当初用户为该设备注册的ISR并调用之。
5 中断复用/解复用
在编写设备驱动程序时,我们往往预期为每个设备都注册一个 IRQ(request_irq),但实际情况往往并不那么理想。8259A芯片最多支持8个中断源,IA-32/8256A最多支持16个中断 源,MIPS CPU的C0_SR:IM/C0_CAUSE:IP最多支持6路外设中断源。当SoC芯片上的设备IRQ多于中断槽位时,多个设备只能复用(共享)同一个 中断。例如,PCI中断是典型的复用中断,PCI规范所定义的中断源只包含了ABCD四种,所有的设备都是用这其中之一。
中断复用(Interrupt Multiplexing)/中断共享(Interrupt Sharing)就是几个设备(devA,devB,…)接到了PIC的同一引脚上当做一个中断源。当它们有事件发生时,都向这个引脚发出中断信号。那么到底是哪个(些)设备发出的中断通知呢?这涉及中断解复用。中断解复用(Interrupt Demultiplexing)就是负责决断识别中断信号来自哪个(些)复用设备。
中断发生后,我们首先需要确定是哪个设备触发的(who trigger),对于共享中断需要解复用,然后进入对应设备上挂接的ISR。ISR需要确认该设备上发生了哪些事情(what happened),然后对感兴趣的事情进行处理,或预处理然后交由中断处理程序的下半部。首先解复用是系统级别的(which device),其次的解复用是设备级别的(what happed to the device),可以说整个中断处理框架都是围绕中断路由和中断解复用展开分流。中断的解复用总是离不开全局的或局部的使能/状态寄存器,即到处可见的IE(Interrupt Enable flag)和IP(Interrupt Pending status)。
(1)全局中断使能/状态寄存器——GIMR/GISR
一般的SoC中会有GlobalInterrupt Mask Register/Global Interrupt Status Register这两组寄存器,分别简称为GIMR/GISR。GIMR为设备中断使能(IE)寄存器,GISR为设备中断挂起状态(IP)寄存器,类似 MIPS CPU中的C0_SR:IM与C0_CAUSE:IP的关系。设备在进行必要的初始化(包括中断挂接)完毕后,启用全局中断(例如MIPS中的C0_SR:IE),然后配置GIMR使能SoC各设备中断,整个SoC系统就可以正常运转了。
(2)设备间中断解复用
不同操作系统的中断处理框架中,对于中断向量表和中断复用/解复用的数据结构实现会有 一定的差异。对于普通非共享中断,通过CPU的IP位图和IRQ号可以索引中断向量表直接进入ISR调用。对于中断复用的情况,不同的操作系统处理有所不 同,但都是通过GIMR&GISR掩码运算确定具体中断设备的。
在Linux中,对于相同irq号的irq_desc[irq]::irqaction::flags设置为IRQF_SHARED,进而调用irq_desc[irq]::irqaction操作链。在每个irqaction::handler中通过GIMR&GISR运算核对该设备是否发生了中断,如果是则进一步action后返回IRQ_HANDLED;否则返回IRQ_NONE,进行next irqaction。
在VxWorks中,中断共享的多个外设中断源对应一个CPU中断位,但是每个设备都分配了一个中断向量(IRQ vector)并将ISR挂接到中断向量表。在解复用例程(Demux Routine)中通过GIMR&GISR运算核对有效使能中断挂起,从而识别出哪个(些)设备产生了中断,返回中断向量,然后根据中断向量索引中断向量表中的ISR进行处理。
(3)设备内中断解复用
通过直接位图映射或设备间中断解复用识别出了哪个设备发生 了中断并进入其ISR,ISR首先需要弄明白这个设备具体发生了什么事情。经常阅读嵌入式SoC芯片datasheet的人可能知道,每个设备本身就有自 己的中断使能/状态寄存器,用来区分设备内部粒度的事件。相对于SoC级别的GIMR/GISR,我们姑且将设备局部的Device Interrupt MaskRegister/Device Interrupt Status Register简称为DIMR/DISR。当然,我们在初始化设备时,也需要配置其DIMR,只接收我们感兴趣或需要处理的设备子事件。在设备ISR 中,需要进行类似设备间的解复用处理,通过DIMR&DISR运算,逐位核对哪些bit位处于pending状态,结合datasheet确定该设备发生了什么事件并作出相应处理。
6 中断上下文
当处理器检测到某一中断源对应的中断产生时,它将停止现在的工作,进入中断(异常)入 口点取指。在进入ISR之前,通用中断处理框架首先执行现场保护,将当前任务的上下文寄存器组保存一个特定的中断栈(Interrupt Stack)中,然后屏蔽处理器响应外部中断,最后路由中断向量表开始进入C函数ISR调用,例如Linux平台上定义的do_IRQ()。异步中断并不 与特定的进程(线程)关联,中断借用被中断的线程栈环境运行自己。此时,软件运行在中断上下文(Interrupt Context)中。为了对粗暴打断当前无辜线程的行为进行补偿,ISR不得不礼貌地执行于受限制的中断上下文中。
通常,处理器在接收到外部的中断信号时,硬件逻辑会自动屏蔽处理器响应外部中断的能 力,因此如果操作系统实现的中断处理框架不主动打开中断的话,整个中断处理的流程是在中断关闭的情况下进行的。因为设备中断处理程序是由驱动程序实现的, 内核无法保证这些中断处理程序执行时间的长短。如果某一中断处理执行时间过长,则将会导致系统可能很长时间无法接收中断或执行任务调度,这可能使某些外部设备丢失数据或者操作系统响应时间变长。
为了解决中断对系统调度的影响,Linux内核为驱动程序提供的中断处理机制分为两个部分:HARDIRQ顶半部(The top half)和SOFTIRQ底半部(The bottom half)。 HARDIRQ顶半部短小精悍(Minimal Fast Handling),它在中断关闭的情况下执行,执行最关键的动作响应硬件交互后,将重大的工作负载丢给底半部,并对外宣称它已经服务了该中断。 SOFTIRQ底半部在中断开启的情况下运行,此时外部设备仍可以继续中断处理器,因此驱动程序往往将一些比较耗时的工作延迟到底半部执行。底半部是同步 的,因为内核决定了它什么时候会执行中断。
软中断和工作队列常用于执行ISR中非时间关键部分的底半部,其代码一定不能在中断处理程序内调用,而是运行于(软)中断上下文或进程(线程)上下文。
7 中断底半部延期机制
(1)软中断
软中断机制使得内核可以延期执行任务。它们的运作方式与硬件中断类似,但是完全是软件触发实现的,因此称为软中断(Software Interrupt,softirq)。典型的软中断如用于x86体系架构上的系统调用的int 0x80指令,关于系统调用可参考《程序员的自我修养——链接、装载与库》的第12章<系统调用与API>。
软中断是硬件IRQ的软件等价物。软中断只适用于少数场合,只有在一些对性能敏感的中枢子系统(如网络层、SCSI层和内核定时器)中才会使用softirq。
许多软中断不仅可以同时运行,而且相同的软中断还可以在不 同的CPU上运行。对并发的唯一限制就是无论何时,在一个CPU上每个软中断都只能有一个实例在运行。同一种类型的软中断的不同实例可以同时在不同的 CPU上运行。因此软中断所执行的函数还是必须锁住共享的数据,以避免CPU之间的竞争。
<1>softirq
内核借助于软中断来获知异常情况的发生,在do_IRQ()末尾处理所有的待决软中断,因而可以确保软中断能够定期得到处理。
raise_softirq(int nr)用于触发一个软中断,该参数对应CPU提供的软中断源位图(例如MIPS中的C0_CAUSE:IP[1~0]),此时软中断的延期处理将运行于软中断上下文中。如果不在中断上下文中调用raise_softirq,则可调用wakeup_softirqd来唤醒软中断守护进程ksoftirqd,此时软中断的延期处理运行于进程上下文中。当在中断上下文中处理软中断时,处理函数不能进行睡眠,如果睡眠,将导致无法唤醒。
<2>tasklet
软中断是将操作推迟到未来时刻执行的最有效方法,但该延期机制处理起来非常复杂。一些对可扩展性和速度要求很高的设备有自身的softirq下半部,有较强的加锁需求,大多数共享一个称之为小片任务(tasklet)的灵活系统。
tasklet的实现是基于软中断(TASKLET_SOFTIRQ和HI_SOFTIRQ),但更容易使用。在任何时刻,每个tasklet都只有一个实例可以等待执行,无需考虑多CPU的支持。
tasklet比softirq更易于使用,因而更适合于设备驱动程序。
<3>其他
内核定时器(timer_list)延期执行工作的机制,也是基于软中断(TIMER_SOFTIRQ)实现。在启用高分辨率定时器时,还需要一个软中断(HRTIMER_SOFTIRQ)。
网络收发包的底半部处理也是基于软中断实现的,它们是NET_RX_SOFTIRQ和NET_TX_SOFTIRQ。
(2) 工作队列(workqueue)
工作队列是中断处理延期执行的第3种方式。对于每个工作队列,内核都会创建一个新的内核守护进程,因此延期任务是在守护进程的上下文中执行的。由于在进程上下文中执行,因此允许睡眠,可以使用互斥体这类可能导致睡眠的函数。
Linux内核创建了一个标准的工作队列,称为 events。内核的各个函数中,凡是没有必要创建独立的工作队列者,均可以使用该队列。VxWorks提供了类似的工作队列机制:ISR做完简单的底层 操作后,调用netJobAdd()将底半部工作(netJob)排队加入(Add)网络任务tNetTask的服务队列 (netJobRing),tNetTask任务将会调度执行挂载到工作队列上延期工作。
8 Linux网络设备驱动模型中的NAPI模型
尽管现代绝大多数设备都支持中断特性,但是中断的高优先级 和快速响应在极端条件下,可能会带来麻烦。对于一些I/O频繁量大的外设(如net_device),必须及时响应中断,及时递交处理数据包,以防数据积 压(如DMA rxoverflow)。但中断处理上下文的切换需要系统开销,在数据量过载时中断频率过高,CPU疲于应付中断,上层应用程序无法得到调度。底半部因频 繁被鲁莽中断导致无法完成对数据包的处理(如做NAT),而中断程序依然在不断地往网络子系统的接收队列中灌数据,这将会导致数据队列溢出丢包和传输超 时。系统将自陷在中断响应这一环节,产生所谓的“活锁”。中断过度掠夺资源将造成系统响应变慢甚至半身不遂,底半部消化不良将造成吞吐量等性能指标下降。
在Linux旧的网络设备驱动模型中,设备驱动会为其所接 收的每个帧都产生一个中断事件(int_events),通过旧函数netif_rx/netif_receive_skb通知内核帧已接收。在高流量负 载下,花在处理中断事件的时间会造成资源相当程度的浪费。针对旧的中断处理模式的缺点,一种被称为NAPI(New API)的处理模式被引入到了Linux内核中。
虽然在设备驱动中,轮询方式存在空转损耗导致名声不佳,但并非一无是处。NAPI的设计思想其实是结合了中断与轮询的各自优势,是中断驱动程序所采用的“推”模式和轮询驱动程序所采用的“拉”模式的混合。当有数据包到达时将会触发硬件中断,在中断处理中关闭中断,系统对硬件的掌控将进入轮询模式,直到所有的数据包接收完毕,再重新开启中断, 进入下一轮中断轮询周期。显然,在系统对硬件进行轮询期间,硬件可能接收到大量进入的数据包,但是它们不会产生中断。设备关闭中断期间仍然具备接受后续分 组的能力,否则轮询也就失去了意义。典型的如以太网芯片中,网络数据包经过PHY到达MAC,此时数据包保存在设备内存中(挂在DMA描述符环上的缓冲 区)。每当接收到数据包时,MAC将向CPU发出中断通知,但是如果关闭了中断,DMA通道传输接收数据仍在异步进行中。可以在下一次开启中断时,再收割DMA描述符环。
一个支持NAPI的驱动程序,需要提供poll()函数, 它将在内核对当前设备轮询时调用。另外需要提供一个控制多个网络设备轮询公平度的相关权重参数weight,它赋予一个设备进行轮询处理的时间宽度。作为 中断和轮询的完美合体,NAPI在高流量负载下的性能比旧方法要出色。从内核的处理的观点来看,NAPI方法有两个优点:(1)减少了CPU的负载(因为 中断事件变少了);(2)设备的处理更为公平。
尽管在其他操作系统的网络设备驱动模型中,并无NAPI概念直接对应,但大部分ISR的实现都在渗透NAPI理念,所谓英雄所见略同,殊途同归。
参考:
《深入Linux内核架构》
《精通Linux设备驱动程序开发》
《深入Linux设备驱动程序内核机制》
《中断和中断处理》