第7章 中断和中断处理
操作系统内核的核心任务,都包含有对连接到计算机上的硬件设备进行管理。轮询(polling)可能会是一种解决办法,但是会做许多无用功。中断机制,让硬件在需要的时候在向内核发出信号。
中断
中断使得硬件发出通知给处理器,如敲击键盘时,键盘控制器会通知操作系统有键按下。 本质是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚。中断控制器是个简单的电子芯片,其作用是将多路中断管线,采用复用技术只通过一个和处理器相连接的管线与处理器通信。 中断值通常被称为中断请求(IRQ)线,每个IRQ都会被关联一个数量值,在经典的PC机上,IRQ 0是时钟中断,IRQ 1是键盘中断。对于PCI总线设备而言,中断是动态分配的。
异常 异常和中断不同,在产生时必须考虑与处理器时钟同步,实际上也常常被称为同步中断。在执行错误指令(如被0除),或特殊情况(如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常和处理中断的方法类似,内核对他们的处理也很类似。对中断(由硬件产生的异步中断)和异常(由处理器本身产生的同步中断)处理大部分类似。
中断处理程序
在相应中断时,内核会执行一个函数,该函数叫中断处理程序(interrupt handler)或中断服务例程(interrupt service routine,ISR)。 运行与中断上下文中(或原子上下文),该上下文中的执行代码不可阻塞。中断可能随时发生,中断处理程序随时肯能随时执行,必须保证中断处理程序能够快速执行,从而保证尽可能快的恢复代码的执行。 对一些场景来说(网络设备),不仅要尽可能的快,还要完成的工作量多。
上半部和下半部
中断处理程序是上半部(top half),接收中断立刻执行,只做有严格时限的工作,例如对接收的中断进行应答或者复位硬件。 能够被允许稍后完成的工作是下半部(bottom half)。 网卡的上半部:网卡接收到数据包时,内核要尽快把数据拷贝到内存,然后读取更多的数据包。
注册中断处理程序
每一个设备都有相关的驱动程序,如果设备使用中断,相应的驱动程序就注册一个中断处理程序。 注册中断处理程序使用request_irq(),<linux/interrupt.h>。
int requset_irq(unsigned int irq,
irq_handler_t handler,
unsigned long flags,
const char *name,
void *dev);
typedef irqreturn_t (*irq_handler_t)(int, void *);
中断处理标志
flags可以为0,也可以是一个或多个标志的位掩码。 定义在<linux/interrupt.h>。
标志 | 描述 |
---|
IRQF_DISABLE | 处理中断时是否禁用其它中断。一般不会设置,可用于希望快速执行的轻量级中断 | IRQF_SAMPLE_RANDOM | 这个设备的中断是否对内核熵池(entroy pool)是否有贡献。内核熵池负责从各种随机事件中导出真正的随机数,需要的熵源也需要是随机的。中断产生的速率如果不可预知,可以设置该标志。如果设备以预知的速率产生中断(如系统定时器),或者受到外部攻击者的影响(如连网设备),不要设置该标志 | IRQF_TIMER | 特别为系统定时器的中断处理而准备的 | IRQF_SHARED | 多个中断处理程序之间共享中断线 |
第四个参数name是与中断设备相关的ASCII文本表示。如PC的键盘对应的值为keyboard。这个值会被/proc/irq或者/proc/interrupts文件使用,以便于与用户通信。 第五个参数dev用于共享中断线。dev会提供唯一的标志信息(cookie),用于指定中断处理程序。如果无需共享中断线,设为NULL即可。 request_irq()成功执行返回0。非零值表示出错,中断处理程序不会被注册。最常见的错误是-EBUSY,表示中断线已经被使用(或没有指定IRQF_SHARED)。 该函数可能会睡眠,不能在中断上下文或者其它不允许阻塞的代码中调用该函数。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项,会调用proc_mkdir(),proc_mkdir()会调用proc_create()设置新的procfs项,proc_create()会调用kmalloc()函数来请求分配内存。kmalloc函数是可以睡眠的。
释放中断处理程序
void free_irq(unsigned int irq,voi*dev);
删除后,如果给定的中断线上已没有其它的中断处理程序,会禁用该中断。
编写中断处理程序
static irqreturn_t intr_handler(int irq,void *dev);
第一个参数如今已没有太大的作用。第二个参数是一个通用指针,和request_irq()的值必须一致。 linux在处理一个中断时会禁用相应的中断线,以防止在同一中断线上接收另一个新的中断。中断处理程序无需重入。
共享的中断处理程序
- request_irq()必须设置IRQF_SHARED标志。
- dev参数唯一。不能是NULL值。
- 中断处理程序必须能区分对应的设备是否真的产生了中断。既需要硬件的支持,也需要处理程序中有相关的处理逻辑。
中断处理程序实例
real-time clock(RTC)驱动程序 ,可以在drivers、char、rtc.c中找到。RTC是一个从系统定时器中独立出来的设备,用于设置系统时钟,提供报警器(alarm)或周期性的定时器。对于大多数体系结构而言,系统时钟的设置,通常只要向某个特定的寄存器或I/O地址写入想要的时间即可。但是报警器或周期性定时器通常靠中断实现。 rtc_init()
if(request_irq(rtc_irq,rtc_interrupt,IRQF_SHARED,"rtc",(void*)&rtc_port)){
printk(KERN_ERR"rtc:cannot register IRQ %d\n",rtc_irq);
return -EIO;
}
处理程序:
static irqreturn_t rtc_interrupt(int irq,void *dev)
{
spin_lock(&rtc_lock);
rtc_irq_data+=0x100;
rtc_irq_data&=~0xff;
rtc_irq_data|=(CMOS_READ(RTC_INTR_FLAGS)&0xF0);
if(rtc_status&RTC_TIMER_ON)
mod_timer(&rtc_irq_timer,jiffies+HZ/rtc_irq+2*HZ/100);
spin_unlock(&rtc_lock);
spin_lock(&rtc_task_lock);
if(rtc_callback)
rtc_callback->func(rtc_callback->private_data);
spin_unlock(&rtc_task_lock);
wake_up_interruptible(&rtc_wait);
kill_fasync(&rtc_async_queue,SIGIO,POLL_IN);
return IRQ_HANDLED;
}
计算机一接收到RTC中断,就会调用这个函数。 这里使用到了两次自旋锁,第一次为了防止rtc_irq_data被SMP机器上的其它处理器同时访问,第二次避免rtc_calllback出现相同的情况。 rt_irq_data是无符号长整型数,存放有关RTC的信息,每次中断都会更新状态。
中断上下文
进程上下文是一种内核所处的操作模式,此时内核代表进程运行,如执行系统调用或者运行内核线程。在进程上下文中,current宏关联当前进程。进程是已进程上下文的形式连接到内核中的,进程可以睡眠,也可以调用调度程序。
当执行一个中断处理程序时,内核处于中断上下文。中断上下文和进程没有关系,也和current宏无关,current宏会指向被中断的进程。因为没有后备进程,中断上下文不可以睡眠(一旦睡眠,就不能对其进行重新调度)。因此,中断处理程序也不可以调用睡眠的函数。
因为中断程序打断了其它的代码,中断上下文的代码要迅速、简洁。
中断处理机制的实现
中断处理机制在linux中的实现非常依赖于体系结构。 硬件产生中断到内核的过程如下:
- 设备产生中断,通过总线把电信号发给中断控制器。
- 中断控制器收到电信号,如果对应的中断线是激活的(可以被屏蔽),把中断发送给处理器,大多数体系结构向处理器特定管脚发送给一个信号。
- 如果该中断没有被禁止,处理器接收到中断后,会立即停止自己正在做的事情,关闭中断系统,调到内存中预定义的位置开始执行那里的代码。位置有内核设定,是中断处理程序的入口点。
处理中断:
unsigned int do_IRQ(struct pt_regs regs);
pt_regs结构包含原始寄存器的值,也包含中断的值,计算出中断号后,do_IRQ()对中断进行应答,禁止这条线上的中断传递。在普通的PC上,使用mask_and_ack_8259A()。 接下来,要确定这条中断线上有一个有效的处理程序,调用handle_IRQ_event()来运行为这条中断线所安装的处理程序。
irqreturn_t handle_IRQ_event(unsigned int irq,struct irqaction *action)
{
irqreturn_t ret,retval=IRQ_NONE;
unsigned int status =0;
if(!(action->flags&IRQF_DISABLE))
local_irq_enable_in_hardirq();
do{
trace_irq_handle_entry(irq,action);
ret=action->handler(irq,action->dev_id);
trace_irq_handle_exit(irq,action,ret);
switch(ret){
case IRQ_WAKE_THREAD:
ret=IRQ_HANDLE;
if(unlikely(!action->thread_fn)){
warn_no_thread(irq,action);
break;
}
if(likely(!test_bit(IRQTF_DIED,&action->thread——flags))){
set_bit(IRQTF_RUNTHREAD,&action->thread_flags);
wake_up_process(action->thread);
}
case IRQ_HANDLED:
status|=action->flags;
break;
default:
break;
}
retval|=ret;
action=action->next;
}while(action);
if(status&IRQF_SAMPLERANDOM)
add_interrupt_randomness(irq);
local_irq_disable();
return retval;
}
Created with Rapha?l 2.3.0
开始
根据中断标志类型判断是否需要使能中断,如果是允许其它中断线就要把中断打开(处理器在接收到中断时把中断系统禁止)
中断线上所有的处理程序都要被处理(共享或者独占)
如果指定IRQF_SANPLE_RANDOM标志,还要为随机数产生器产生熵。
禁止中断,返回到do_IRQ()
结束
从handle_IRQ_event()返回之后,表明中断已被处理完毕,do_irq()会做清理工作并返回到初始入口点,然后跳转到ret_from_intr()。 ret_from_intr()类似初始入口代码,以汇编语言编写。此时处理完中断处理程序,将要返回用户进程或者内核空间,需要判断是否需要重新调度(进程调度的need_resched标志)。在重新调度的情况下,如果将要返回用户空间(中断了用户进程),会调用schedule()选择一个更合适的程序。如果返回内核空间(中断了内核),只用在preempt_count为0时,才会重新调用schedule()。 在schedule()返回之后,或者如果没有挂起的工作,原来的寄存器被恢复,回到被中断的点。 在x86上,初始的汇编例程位于arch/x86/kernel/entry_64.S(entry_32.S对应32位x86体系架构),C方法位于arch/x86/kernel/irq.c。其它所支持的结构与此类似。
/proc/interrupts
procfs是一个虚拟文件系统,它只存在与内核内存,一般位于/proc目录。 该文件存放的是系统中与中断相关的统计信息。
中断控制
<asm/system.h>、<asm/irq.h>中提供接口。 控制中断系统的原因归根结底是需要提供同步,通过禁止中断确保中断处理程序不被抢占,获取锁来防止其它处理器对共享数据的并发访问
禁止和激活中断
禁止当前处理器(仅是当前处理器)的本地中断
local_irq_disable();
使能本地中断
local_irq_anable();
通常以单个汇编指令实现,依赖体系结构。 不能简单的禁止或使能中断,而应该保存中断的状态且在之后将中断恢复。
unsigned long flag;
loacl_irq_save(flags);
local_irq_restore(flags);
禁止指定中断线
void disable_irq(unsigned int irq);
void disable_irq_noync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronize_irq(unsigned int irq);
中断系统的状态
irqs_disable()
<linux/hardirq.h>定义的两个宏提供了一个用来检查内核上下文的接口。
in_interrupt()
in_irq()
|