中断是hardware device用来通知CPU的一种机制。在系统上,连接着很多的外设,这些外设速度很慢,并且随时会产生数据需要CPU处理,CPU作为高速运行的部件,不能一直等着外设产生数据,为了提高效率,采用了中断这种机制。当外设需要CPU处理数据时,就向CPU发送电信号,CPU收到电信号以后,就知道哪个设备需要处理了,这个电信号就是中断。
中断并不是直接发送给CPU,而是先发送到interrupt controller,也就是中断控制器(一般是8259A),所有的外设都把中断发送到这里,再由中断控制器把中断信号发送给CPU,CPU收到中断信号,就会停下正在执行的任务,开始执行中断处理函数。
CPU收到的中断,都有对应的中断号,每个外设的中断号都是有区别的,这样CPU根据中断号就能直到是哪些设备产生了中断,进而可以调用对应的中断处理程序。这个中断号一般称为IRQ number。
这里提到了异常,异常和中断非常类似,实际上,异常就是软中断,是CPU在执行指令的过程中发生了异常事件,比如除0错误,或者遇到了page fault,此时都需要中断当前程序的执行,并上报错误。异常和硬件中断的一个很大的区别是,异常是同步产生的,因为执行CPU在执行指令的过程中产生异常,因此是和软件指令同步产生,而硬件中断是异步的,CPU此时可能在执行任何代码。
用来处理IRQ的处理函数通常称为ISR,也就是interrupt service routine,每个会产生中断的hardware device都有对应的ISR,在kernel中,ISR是device driver的一部分。ISR和普通的kernel code有相同之处,也有不同之处,相同之处在于都是C写的function,不同之处在于ISR只有中断产生时才会执行,并且运行在interrupt context,这是特殊的context,执行时不允许block,即不允许被调度。
因为硬件的中断是异步的,当中断产生时,CPU可能在执行任何代码,所以ISR一定是执行的越快越好,否则被抢占的代码会等待很长时间。因此,ISR中就不能处理很多东西,可以把这些费时的操作放到将来的某个时刻再做,ISR尽快返回。
接上文,ISR中可能要处理很多事情,但是又不能占用太多CPU的时间,所以OS中一般把ISR分为上下两个部分,即top half和bottom half。top half只处理一些紧急的事情,比如从hardware copy数据,然后调度别的task,ISR就可以结束返回;bottom half里就可以处理剩下的比较费时的操作。
上面提到过,interrupt handler,也即ISR,是device driver的一部分,应当由device driver负责实现。
driver可以注册interrtupt handler,kernel提供了接口来实现:
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
第一个参数,irq,指定要给哪个IRQ line注册handler,对于kernel的timer或者键盘来说,这个irq是固定死的,其他比如PCI设备等,都是动态分配的;第二个参数,handler,是一个函数指针,指向driver自己的interrupt handler函数,当中断发生时,kernel就会调用这个handler;这个handler的类型如下:
typedef irqreturn_t (*irq_handler_t)(int, void *);
第三个参数是flag,要么是0,要么是一些bitmask,这些bitmask定义在
IRQF_DISABLED
如果设置了这个flag,那么这个handler在执行的时候,所有的中断都被disable。一般情况下,在注册handler时都不会设置这个flag,除非handler对performance特别敏感,必须马上执行。在kernel 4.15中,已经没有这个 flag 了。
IRQF_SAMPLE_RANDOM
如果设置这个flag,说明这个device产生的中断是随机的,那么可以作为随机池的熵参与随机值的生成。在kernel 4.15中,已经没有这个flag了。
IRQF_TIMER
如果设置这个flag,说明这个handler是一个处理timer中断的handler。
IRQF_SHARED
如果设置这个flag,说明这个IRQ line是share的。也就说,要处理的IRQ line是share的,可能有多个device共享了这个IRQ line,因此在kernel中,同一个IRQ line可能存在多个handler。
这本书只列举了上面这几个flag。在kernel 4.15中,还有别的几个flag,这里也列举一下,但是不做深入说明:
/*
* These flags used only by the kernel as part of the
* irq handling routines.
*
* IRQF_SHARED - allow sharing the irq among several devices
* IRQF_PROBE_SHARED - set by callers when they expect sharing mismatches to occur
* IRQF_TIMER - Flag to mark this interrupt as timer interrupt
* IRQF_PERCPU - Interrupt is per cpu
* IRQF_NOBALANCING - Flag to exclude this interrupt from irq balancing
* IRQF_IRQPOLL - Interrupt is used for polling (only the interrupt that is
* registered first in an shared interrupt is considered for
* performance reasons)
* IRQF_ONESHOT - Interrupt is not reenabled after the hardirq handler finished.
* Used by threaded interrupts which need to keep the
* irq line disabled until the threaded handler has been run.
* IRQF_NO_SUSPEND - Do not disable this IRQ during suspend. Does not guarantee
* that this interrupt will wake the system from a suspended
* state. See Documentation/power/suspend-and-interrupts.txt
* IRQF_FORCE_RESUME - Force enable it on resume even if IRQF_NO_SUSPEND is set
* IRQF_NO_THREAD - Interrupt cannot be threaded
* IRQF_EARLY_RESUME - Resume IRQ early during syscore instead of at device
* resume time.
* IRQF_COND_SUSPEND - If the IRQ is shared with a NO_SUSPEND user, execute this
* interrupt handler after suspending interrupts. For system
* wakeup devices users need to implement wakeup detection in
* their interrupt handlers.
*/
#define IRQF_SHARED 0x00000080
#define IRQF_PROBE_SHARED 0x00000100
#define __IRQF_TIMER 0x00000200
#define IRQF_PERCPU 0x00000400
#define IRQF_NOBALANCING 0x00000800
#define IRQF_IRQPOLL 0x00001000
#define IRQF_ONESHOT 0x00002000
#define IRQF_NO_SUSPEND 0x00004000
#define IRQF_FORCE_RESUME 0x00008000
#define IRQF_NO_THREAD 0x00010000
#define IRQF_EARLY_RESUME 0x00020000
#define IRQF_COND_SUSPEND 0x00040000
#define IRQF_TIMER (__IRQF_TIMER | IRQF_NO_SUSPEND | IRQF_NO_THREAD)
说完了request_irq使用的flag以后,再来说一下第四个和第五个参数:name和dev。第四个参数是name,这个就是handler的名字,会在/proc/irq和/proc/interrupts下面显示;dev在share的IRQ line中会用到,因为share的IRQ line中可能存在多个handler,当需要remove handler时,kernel如何知道要remove哪一个?这个时候就需要用到dev,这个dev每个driver都需要传递个kernel,并且保证唯一,这就相当于是handler的ID,通过dev就能准确的知道要操作的是哪个handler。而这个值,在handler被调用时,也会传递给handler。
当rquest_irq调用成功,就会返回0,返回非零值说明发生了错误,driver不能使用这个IRQ line。另外,request_irq可能会sleep(因为调用栈中使用了kmalloc分配内存),所以不能在atomic context中使用,比如interrupt context等。
当driver要unload,或者需要需要禁用中断时,可能需要把interrupt handler移除,接口为:
void *free_irq(unsigned int irq, void *dev_id)
可以看到参数只有两个,一个是 IRQ line,一个是dev_id,如果不指定dev_id,kernel不知道driver要remove的是哪个interrupt handler。在free_irq执行时,如果IRQ不是share的,那么handler被移除后就disable IRQ;如果是share的,只有最有一个handler也被remove,IRQ line才会被disable。
注意,free_irq只能在process context中执行。(比如在系统调用,或者kthread中)。
要实现自己的interrupt handler,第一步得先知道handler的函数原型是啥:
typedef irqreturn_t (*irq_handler_t)(int, void *);
如上所示,kernel中的interrupt handler有两个参数,第一个是IRQ number,也就是中断号,不过driver拿到这个中断号也没啥用处;第二个参数是void *,回忆一下request_irq接口中的dev,这个就是注册handler时,传递给kernel的dev,kernel在调用handler时又回传递进来,一般可以用来存储handler中需要使用的信息。
handler的返回值是一个特殊的类型:irqreturn_t,变量定义在include/linux/irqreturn.h中:
/**
* enum irqreturn
* @IRQ_NONE interrupt was not from this device or was not handled
* @IRQ_HANDLED interrupt was handled by this device
* @IRQ_WAKE_THREAD handler requests to wake the handler thread
*/
enum irqreturn {
IRQ_NONE = (0 << 0),
IRQ_HANDLED = (1 << 0),
IRQ_WAKE_THREAD = (1 << 1),
};
typedef enum irqreturn irqreturn_t;
#define IRQ_RETVAL(x) ((x) ? IRQ_HANDLED : IRQ_NONE)
可以看到,这是一个枚举类型,有三个值,每个值代表不同的含义:IRQ_NONE表明这个中断不是这个driver对应的device产生的,不予处理;IRQ_HANDLED表明这个中断已经被device driver处理;IRQ_WAKE_THREAD还不知道是什么用。
Linux kernel中interrupt handler是不会重入的,当某个IRQ line的interrupt handler正在执行时,所有CPU都会屏蔽这个IRQ line,不再对它产生的中断做出相应,此外,当interrupt handler正在被执行时,它不会再次被执行(考虑一下同一个handler支持多个IRQ line的情况),大大的简化了interrupt handler的实现。要注意的是,CPU只是屏蔽了这一个IRQ line,别的IRQ仍然有效。
share handler,就是IRQ line是share的,这些handler注册了同样的IRQ line。share handler和非share的handler类似,也有不同点,不同点主要有三个:
1. share的handler在注册时,要指定IRQ line是share的,也就是要设置flag:IRQF_SHARED。
2. share了IRQ line的handler,在注册时必须指定非空的dev参数。
3. interrupt handler要能够区分是否真的是自己的device产生的中断,这就要求hardware要有这样的机制来让driver检查。如果不能区分是不是自己的device产生的中断,那driver是没法正常工作的。
如果注册handler时,指定了IRQF_SHARED,那么只有在这个IRQ line没有被人注册,或者所有注册这个IRQ line的handler都设置IRQF_SHARED才行,否则就会失败。
在中断发生时,kernel无法直到具体是哪个device产生的中断,所以会把这个IRQ line上的handler全部调用一遍,因此如果不是自己的device产生的中断,要尽快返回。
下面是一个完整的interrupt handler的例子——RTC driver的interrupt handler。所谓的RTC,就是real-time clock,它是一个hardware devcie,和system timer是相互独立的。system timer用来设置system clock,提供alarm,或者提供某个时间间隔的计时服务:在大多数机器上,system clock都是通过寄存器或者I/O range,设置system clock就是设置这个register或者I/O range,而alarm或者计时服务,都是通过interrupt来实现的。我们看一下RTC driver是如何实现interrupt hander的,在rtc_init函数中,通过request_irq注册了handler:
if (is_hpet_enabled()) {
int err;
rtc_int_handler_ptr = hpet_rtc_interrupt;
err = hpet_register_irq_handler(rtc_interrupt);
if (err != 0) {
printk(KERN_WARNING "hpet_register_irq_handler failed "
"in rtc_init().");
return err;
}
} else {
rtc_int_handler_ptr = rtc_interrupt;
}
if (request_irq(RTC_IRQ, rtc_int_handler_ptr, 0, "rtc", NULL)) {
/* Yeah right, seeing as irq 8 doesn't even hit the bus. */
rtc_has_irq = 0;
printk(KERN_ERR "rtc: IRQ %d is not free.\n", RTC_IRQ);
rtc_release_region();
return -EIO;
}
在x86_64上,rtc_int_handler_ptr应该是rtc_interrupt,RTC_IRQ是8(#define RTC_IRQ 8),这里的code是基于kernel 4.15,可以看到RTC注册handler这个逻辑和书中讲的不一样,因为4.15没有设置IRQF_SHARED,也就意味着其他人都无法注册timer中断的hanlder了。因为不是share的IRQ line,所以第五个参数设置为了NULL。我们看一下rtc_interrupt的实现:
static irqreturn_t rtc_interrupt(int irq, void *dev_id)
{
/*
* Can be an alarm interrupt, update complete interrupt,
* or a periodic interrupt. We store the status in the
* low byte and the number of interrupts received since
* the last read in the remainder of rtc_irq_data.
*/
spin_lock(&rtc_lock);
rtc_irq_data += 0x100;
rtc_irq_data &= ~0xff;
if (is_hpet_enabled()) {
/*
* In this case it is HPET RTC interrupt handler
* calling us, with the interrupt information
* passed as arg1, instead of irq.
*/
rtc_irq_data |= (unsigned long)irq & 0xF0;
} else {
rtc_irq_data |= (CMOS_READ(RTC_INTR_FLAGS) & 0xF0);
}
if (rtc_status & RTC_TIMER_ON)
mod_timer(&rtc_irq_timer, jiffies + HZ/rtc_freq + 2*HZ/100);
spin_unlock(&rtc_lock);
/* Now do the rest of the actions */
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;
}
上面的code中,第一个spinlock——rtc_lock,用来防止SMP上多个CPU同时访问RTC的data;第二个spinlock——rtc_task_lock,用来防止SMP上多个CPU同时调用rtc_callback。rtc_irq_data是一个unsigned long类型的数据,用来存放RTC的信息。如果设置了计时器,就会调用mod_timer,如果存在rtc的callback,就会在此时调用。
最后,在rtc_interrupt执行完以后返回IRQ_HANDLED,表明中断已被处理。
kernel在执行interrupt handler时,是运行在interrupt context里。回忆一下之前讲的,当用户态进程通过系统调用或者异常等陷入内核时,kernel都是运行在process context中,kernel thread也是运行在process context中。
interrupt context没有和任何的process关联,因此当kernel在interrupt context中执行时,是不能使用current指针的(尽管current指针存在,但是它指向被这个interrupt打断的process contex的task结构体)。因为没有task结构体,自然也不能被调度,这就决定了interrupt context在执行时不能睡眠,不能被block,不能放弃CPU,因此interrupt context中调用的函数也是受到限制的。
interrupt context对时间比较敏感,因为interrupt handler在执行时,是中断了别的code,你不知道被中断的code是做什么的,也许是process,也许是另一个IRQ line的interrupt handler(别的IRQ line没有被CPU屏蔽,并且它的handler没有关闭抢占),无论哪一种,interrupt handler都应该尽快的执行完。
interrupt handler的stack是可以配置的,但是因为历史原因,一般没有给interrupt handler单独分配stack,而是和被它中断的process共享kernel stack,不过每个process的kernel stack也是有限的,32位机器上是8KB,64位机器上是16KB,因此要省着用。
不过在新的kernel中,interrupt已经有自己的stack了,不需要再和别的process共享stack。
这里是说的linux kernel中interrupt handler的实现,这些实现是架构相关的。
上图是键盘鼠标中断的处理过程。
当硬件设备产生中断时,电信号就会通过连接的bus传递给interrupt controller,如果interrupt line是enable的状态(有些interrupt line可能是被屏蔽的),这个电信号就会传递给CPU。如果CPU没有disable这个interrupt line,CPU就会停止执行当前的task,然后关闭interrupt,然后跳转到内存中预定的位置,开始执行里面的code。这个预定的位置是kernel自己配置好的,也是所有interrupt handler的入口位置。
对于每一个interrupt line,kernel都会跳转到不同的位置,kernel也知道产生中断的IRQ number,当kernel准备处理中断时,首先保存IRQ number?,然后保存被它中断的task的寄存器到stack中,然后调用do_IRQ函数。这里列一下kernel 4.15中irq处理相关的汇编code:
首先是irq_entries_start,这是irq开始处理的入口:
ENTRY(irq_entries_start)
vector=FIRST_EXTERNAL_VECTOR
.rept (FIRST_SYSTEM_VECTOR - FIRST_EXTERNAL_VECTOR)
UNWIND_HINT_IRET_REGS
pushq $(~vector+0x80) /* Note: always in signed byte range */
jmp common_interrupt
.align 8
vector=vector+1
.endr
END(irq_entries_start)
里面核心的code是common_interrupt:
/* Interrupt entry/exit. */
/*
* The interrupt stubs push (~vector+0x80) onto the stack and
* then jump to common_interrupt.
*/
.p2align CONFIG_X86_L1_CACHE_SHIFT
common_interrupt:
addq $-0x80, (%rsp) /* Adjust vector to [-256, -1] range */
call interrupt_entry
UNWIND_HINT_REGS indirect=1
call do_IRQ /* rdi points to pt_regs */
/* 0(%rsp): old RSP */
........
END(common_interrupt)
common_interrupt非常长,这里只列举了一部分,可以看到最主要的处理函数是do_IRQ函数。
__visible unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
这个是do_IRQ函数的原型。看上去interrupt处理所有需要依赖的参数都在regs里存着,我们看一下pt_regs的layout:
struct pt_regs {
/*
* C ABI says these regs are callee-preserved. They aren't saved on kernel entry
* unless syscall needs a complete, fully filled "struct pt_regs".
*/
unsigned long r15;
unsigned long r14;
unsigned long r13;
unsigned long r12;
unsigned long bp;
unsigned long bx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
unsigned long r11;
unsigned long r10;
unsigned long r9;
unsigned long r8;
unsigned long ax;
unsigned long cx;
unsigned long dx;
unsigned long si;
unsigned long di;
/*
* On syscall entry, this is syscall#. On CPU exception, this is error code.
* On hw interrupt, it's IRQ number:
*/
unsigned long orig_ax;
/* Return frame for iretq */
unsigned long ip;
unsigned long cs;
unsigned long flags;
unsigned long sp;
unsigned long ss;
/* top of stack page */
};
可以看到里面记录很多register相关的信息,以及IRQ number。
在do_IRQ开始执行时,先对interrupt的信息做了检查,如果没有问题,就会调用handle_irq函数:
bool handle_irq(struct irq_desc *desc, struct pt_regs *regs)
{
stack_overflow_check(regs);
if (IS_ERR_OR_NULL(desc))
return false;
generic_handle_irq_desc(desc);
return true;
}
handle_irq里最终会调用generic_handle_irq_desc来处理所有的interrupt handler。其中的实现细节和书中不同,这里先放着,后面再补。我们接着看handle_irq返回之后的code。handle_irq返回到do_IRQ之后,do_IRQ做了一些cleanup的工作,然后返回到汇编代码,也就是ret_from_intr这里,ret_from_intr会根据当前是要返回到user mode(user mode process被中断打断),还是kernel mode(kernel自己的code被中断打断),会有不同的处理。如果是返回到user mode,就检查是否需要reschedule,如果是,就会调用schedule,等schedule返回之后再返回到user mode process执行;如果是返回到kernel mode,就检查当前CPU的preempt counter是否为0,如果不是,说明不能抢占,直接返回到原来被打断的地方继续执行;如果premmpt counter 0,说明没有spinlock,此时就会发生reschedule。
Linux kernel提供了一组接口,用来控制系统中的interrupt,通过这些接口可以关闭当前CPU上的中断,或者屏蔽整个系统的某个中断。因为中断本身和架构相关的,所以这些接口也是架构相关的。代码在
之所以需要关闭中断,是因为很多时候需要通过中断来实现同步机制。如果关闭了中断,interrupt handler就不会被调用,也就不会抢占当前的code(通常情况下,关闭中断的同时也会关闭抢占)。然后要注意,关闭中断或者关闭抢占,并不能防止别的CPU访问在critical section的code,它要和锁机制一起使用才可以达到这个效果。关闭中断或者抢占只对单个CPU有效,也就说,当前CPU不会调用interrupt handler,也不会抢占你的code,但是别的CPU不受影响,他们仍然有可能调用interrupt handler或者执行别的code来访问同样的critical section,所以只关闭当前CPU的中断或者抢占是起不到保护作用的,还得依赖lock才可以。
在kernel中,关闭和打开中断都只针对当前的CPU有效,不能全局关闭中断。code一般长这样:
local_irq_disable();
/* interrupts are disabled .. */
local_irq_enable();
在x86上,打开和关闭中断通常通过cli和sti两个汇编指令来实现:
static inline void native_irq_disable(void)
{
asm volatile("cli": : :"memory");
}
static inline void native_irq_enable(void)
{
asm volatile("sti": : :"memory");
}
使用这个两个接口存在一个问题,local_irq_enable会无条件打开中断,设想一个,如果我们的函数进来之前,别的函数已经关闭了中断,我们的函数也关闭了中断,在code执行完以后我们打开了中断,此时别的函数期望的行为是中断仍然是关闭的状态,因为它还没调用local_irq_enable,这就会出现问题。为了解决这个问题,引入了另外两个接口,在关闭中断时,先保存中断的状态,在打开时,恢复之前保存的状态,这样被人对中断状态的设定不会被我们破坏。code长这样:
unsigned long flags;
local_irq_save(flags); /* interrupts are now disabled */
/* ... */
local_irq_restore(flags); /* interrupts are restored to their previous state */
flags就是用来保存当前CPU的中断信息的,local_irq_save会关闭中断,并把中断信息保存在flag中,local_irq_restore会打开中断,并把中断状态恢复成flags保存的状态。这两个函数都是宏,flags是按值传递的,不是地址。另外,要注意的是,flags里面记录的是架构相关的中断系统的信息,但是某些架构上,也会记录当前stack的一些信息,所以中断信息的保存和恢复都必须发生在同一个函数里,也就说flags的保存和恢复都在同一个函数栈帧中完成。
以上这些中断相关的接口可以在interrupt context中调用,也可以在process context中调用。
上面的接口可以把当前CPU上的所有中断全部关闭,但是有时候我们只是想关闭某一个中断,就需要使用另外一套接口:
extern void disable_irq_nosync(unsigned int irq);
extern void disable_irq(unsigned int irq);
extern void disable_percpu_irq(unsigned int irq);
extern void enable_irq(unsigned int irq);
extern void enable_percpu_irq(unsigned int irq, unsigned int type);
disable_irq_nosync和disable_irq会把所有CPU上的irq参数指定的IRQ line关闭,区别在于disable_irq在关闭interrupt controller的irq时,会等待当前正在执行的interrupt handler,当他们都执行结束以后,disable_irq才会返回;而disable_irq_nosync则不会等待,而是直接返回。
中断的关闭和打开是配对的,也就说调用了一次disable,就必须调用一次enable,比如调用了两次disable,那么必须调用两次enable之后中断才能被打开。
以上的这些函数可以在interrupt context中调用,也可以在process context中调用,但是如果是在interrupt context中要注意,interrupt handler在执行时,kernel已经把它对应的IRQ line关闭了,因此切记不要在handler中把自己的IRQ line打开。
如果这个IRQ line是share的,那么device driver尽量不要关闭中断,否则别的device也都无法收到中断。比如PCI设备,标准就规定了通过share的方式使用IRQ line,如果PCI device driver关闭了中断,那么别的PCI设备都无法收到中断了。
在kernel中,有时候需要知道在什么样的context中执行,或者当前中断系统的状态,kernel也提供了这样的接口:
//当前是否关闭了中断,0表示没有关闭中断,1表示关闭了中断。
irqs_disabled()
//下面两个函数用来检查当前的context
in_interrupt()
in_irq()
in_interrupt比较常用,如果kernel正在执行interrupt hanling(包括interrupt handler和bottom half),那么就会返回true;in_irq只有在执行interrupt handler时会返回true。不过在kernel 4.15中,in_interrupt已经不推荐使用了,4.15中相关的函数列举如下:
/*
* Are we doing bottom half or hardware interrupt processing?
*
* in_irq() - We're in (hard) IRQ context
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
* in_serving_softirq() - We're in softirq context
* in_nmi() - We're in NMI context
* in_task() - We're in task context
*
* Note: due to the BH disabled confusion: in_softirq(),in_interrupt() really
* should not be used in new code.
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_nmi() (preempt_count() & NMI_MASK)
#define in_task() (!(preempt_count() & \
(NMI_MASK | HARDIRQ_MASK | SOFTIRQ_OFFSET)))