Chapter6 中断与中断处理程序
中断机制:让硬件需要的时候再向内核发出信号(变内核主动为硬件主动)。
中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向向操作系统反应此信号的到来,然后由OS负责处理这些新来的数据。
不同的设备对应的中断不同,每个中断都通过一个唯一的数字来标识。这个中断值常被称为中断请求线(IRQ)。
异常:产生时必须考虑与处理器时钟同步,常常称为同步中断。
中断处理程序(ISR):中断处理程序通常不是和特定的设备相关联,而是和特定的中断相关联。一个设备可产生多种不同的中断,就会对应多个中断处理程序。一个设备的中断处理程序是设备驱动程序的一部分。
中断处理程序与其他内核函数的区别在于:中断处理程序是被内核调用来响应中断的,而它们运行于特殊的中断上下文中。对于硬件而言,迅速对齐中断服务非常重要,对于系统的其他部分,中断处理程序在尽可能短的时间内完成同样重要。
一般把中断处理切成两部分,中断处理程序是上半部(top half)--接收到一个中断,它就立即开始执行,但只做有严格时限的工作,如对接收的中断应答或复位硬件,这些工作在中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后,在合适的时机,下半部会被开中断执行。
注册中断处理程序
注册中断处理程序,使用下面的接口注册并激活一个中断处理程序。
int request_irq(unsigned int irq, /*要分配的中断号*/
irqreturn_t (*handler)(int, void *, struct pt_regs *), /*处理中断的程序,函数原型特定的*/
unsigned long irqflags, /*中断掩码值*/
const char * devname, /*与中断相关的设备ASCII文本表示法*/
void *dev_id) /*主要用于共享的中断线,必需传递唯一的信息,实践中往往会通过它传递驱动程序的设备结构*/
SA_SHIRQ Interrupt is shared 多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须制定这个标志。
SA_INTERRUPT Disable local interrupts while processing 快速中断处理程序,迅速执行并且调用频率很高的中断服务函数。在本地处理器上,快速中断处理程序在禁止所有中断的情况下执行。使得快速中断不受其他终端的干扰。默认情况下(无该标识)除了正在运行的中断处理程序对应的那条中断线被屏蔽外,所有其他中断都是激活的。
SA_SAMPLE_RANDOM The interrupt can be used for entropy 此设备的中断对内核熵池有贡献。内核熵池负责提供从各种随机事件导出真正的随机数。
返回值-EBUSY:指示给定中断线已经在使用(未指定SA_SHIRQ)。
request_irq函数可能会睡眠,不要在中断上下文或其他不允许阻塞的代码中调用该函数。该函数睡眠由于:注册过程中内核需要在/proc/irq文件创建一个与中断对应的项,函数proc_mkdir在创建新的procfs项时,会间接调用kmalloc导致函数睡眠。
初始化硬件和注册中断处理程序的顺序必须正确,以防止中断处理程序在设备初始化完成之前就开始执行。
释放中断处理程序
指定的中断线不是共享的,删除处理函数的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev_id所对应的处理程序,而这条中断线仅在删除最后一个处理程序时才会被禁用。必须在进程上下文中调用free_irq函数。
void free_irq(unsigned int irq, void *dev_id)
编写中断处理程序
static irqreturn_t intr_handler(int irq, void *dev_instance, struct pt_regs *regs);
参数irq:处理程序要响应的中断的中断线号。
参数dev_instance:与中断处理程序注册时传递给request_irq()的参数dev_id一致。该值在共享中断中要确定唯一性,通常会把设备结构传递给dev_id。
参数regs:包含处理中断之前处理器的寄存器和状态。
返回值为 irqreturn_t类型,实质上是int类型。返回为IRQ_NONE和IRQ_HANDLED。可使用宏IRQ_RETVAL(x)来测试返回值,x为非0值,该宏返回IRQ_HANDLED,否则返回IRQ_NONE。
中断处理程序通常会标记位static,从来不会被别的文件中的代码调用。
Linux中的中断处理程序是无需重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有的处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常情况下,其他所有的中断都是打开的,所以不同中断线上的其他中断都能被处理,但是当前中断线总是被禁止。
共享中断处理程序需要注意的三点:
·resuest_irq()的参数flags必须设置SA_SHIRQ标志。
·对于每个注册的中断处理程序,dev_id必须是唯一的。
·中断处理程序必须能够区分它的设备是否真的产生了中断。需要硬件支持,同时也需要处理程序中有相关的处理逻辑。
中断上下文
执行一个中断处理程序或下半部时,内核处于中断上下文中。进程是以进程上下文的形式连接到内核中的,因此进程上下文可以睡眠,也可以调用调度程序。中断山下文与进程没有什么瓜葛,所以中断上下文不可以睡眠,不能在中断上下文中调用引起睡眠的函数。
中断上下文有严格的时间限制,在中断中尽量不要使用循环去处理繁重的工作,中断打断了其他代码的执行,所以必须尽可能的迅速、简洁。尽量把工作从中断处理程序中分离出来,放在下半部分执行。
中断处理程序拥有自己的栈,每个处理器一个,大小为一页。需要尽可能的节约内核栈空间。
中断处理机制的实现
中断处理系统实现依赖于处理器、所使用的中断控制器的类型,体系结构的设计以及机器本身。
中断的初始化在init_IRQ()函数中,过程如下:
for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (i >= NR_IRQS)
break;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[i]);
}
其中数组 interrupt[]的初始化在entry.S文件中完成,用于初始化中断门,所有进入或退出中断的操作基本相同。
.data
ENTRY(interrupt)
.text
vector=0
ENTRY(irq_entries_start)
.rept NR_IRQS
ALIGN
1: pushl $vector-256 存储向量号
jmp common_interrupt
.data
.long 1b
.text
vector=vector+1
.endr
ALIGN
common_interrupt:
SAVE_ALL 保存所有寄存器的值
movl %esp,%eax 将寄存器eax指向栈的底部。
call do_IRQ 调用通用的C处理函数
jmp ret_from_intr 中断返回
在内核中,中断开始于内核预定义的入口点处。对于每条中断线,处理器都会跳转到对应的唯一的位置上。这样内核就知道所接收的中断的IRQ号。
ALIGN
ret_from_exception:
preempt_stop /*关闭本地中断*/
ret_from_intr:
GET_THREAD_INFO(%ebp)
movl EFLAGS(%esp), %eax # mix EFLAGS and CS
movb CS(%esp), %al
testl $(VM_MASK | 3), %eax
jz resume_kernel # returning to kernel or vm86-space
ENTRY(resume_userspace) /*中断前为用户态,返回用户空间*/
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
# int/exception return?
jne work_pending
jmp restore_all
#ifdef CONFIG_PREEMPT
ENTRY(resume_kernel) /*中断前为内核态,返回到内核空间*/
cli
cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ? /*抢占内核不安全,preempt_count为0执行调度程序*/
jnz restore_all
need_resched:
movl TI_flags(%ebp), %ecx # need_resched set ?
testb $_TIF_NEED_RESCHED, %cl
jz restore_all
testl $IF_MASK,EFLAGS(%esp) # interrupts off (exception path) ?
jz restore_all
call preempt_schedule_irq
jmp need_resched
#endif
ALIGN
work_pending:
testb $_TIF_NEED_RESCHED, %cl /*重新调度是否正在挂起*/
jz work_notifysig
work_resched:
call schedule
cli # make sure we don't miss an interrupt
# setting need_resched or sigpending
# between sampling and the iret
movl TI_flags(%ebp), %ecx
andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
# than syscall tracing?
jz restore_all
testb $_TIF_NEED_RESCHED, %cl
jnz work_resched
work_notifysig: # deal with pending signals and
# notify-resume requests
testl $VM_MASK, EFLAGS(%esp)
movl %esp, %eax
jne work_notifysig_v86 # returning to kernel-space or
# vm86-space
xorl %edx, %edx
call do_notify_resume
jmp restore_all
ALIGN
work_notifysig_v86:
pushl %ecx # save ti_flags for do_notify_resume
call save_v86_state # %eax contains pt_regs pointer
popl %ecx
movl %eax, %esp
xorl %edx, %edx
call do_notify_resume
jmp restore_all
/proc/interrupts 中procfs是一个虚拟文件系统,只存在于内核内存中。安装在/proc目录下,在procfs中读写文件都要调用内核函数。Procfs代码位于fs/proc中。提供/proc/interrupts的函数与体系结构相关的,为show_interrupts()。
中断控制
Linux内核提供一组接口用于操作机器上的中断状态,能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力。控制中断系统的根本原因是需要提高同步。锁保护机制防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。
无条件的禁止或者激活中断的传递
local_irq_disable()
local_irq_enable()
在禁止中断前保存中断系统的状态会更加安全,在准备激活中断时,只需把中断状态恢复到它们原来的状态。 local_irq_save(x)和 local_irq_restore(x) 的调用必须在同一个函数中进行。
local_irq_save(x)
local_irq_restore(x)
以前内核中提供禁止系统中所有处理器上的中断时cti(),激活函数是sti(),现在所有的中断同步必须结合使用本地中断控制和自旋锁。
禁止指定中断线
禁止给定中断向系统中所有处理器的传递。
void disable_irq(unsigned int irq) /*确保所有已经开始执行的处理程序已全部退出*/
void disable_irq_nosync(unsigned int irq) /*不等待当前中断处理程序执行完毕*/
void enable_irq(unsigned int irq)
void synchronize_irq(unsigned int irq) /*等待一个特定中断处理程序的退出*/
以上函数可以嵌套调用,只是disable和enable的次数相等时候才能真正激活中断线。
禁止多个中断处理程序共享的中断线是不合适的,禁止中断线也就禁止了这条线上所有设备的中断传递。
irqs_disabled() 本地处理器上的中断系统被禁止。它返回非0,否则返回0。
in_interrupt() 内核处于中断上下文中,返回非0。说明内核此刻正在执行中断处理程序,或者执行下半部处理程序。通常代码需要执行睡眠这种只能在进程上下文进行的操作时,需要使用该接口。
in_irq() 只有内核确定正在执行中断处理程序时才返回非0。
中断是一种由设备使用的硬件资源异步地向处理器发信号。