《LKD3粗读笔记》(7)中断和中断处理
- CPU与外设的速度不匹配,如果让CPU专门等待外设回应,则会大大减低CPU的利用率。
- 鉴于此,提出了两种解决方法:轮询和中断。其中:轮询会周期性重复执行,做很多无用功。中断方式则是让硬件在需要的时候再向内核发出信号。
1、中断
-
什么是中断?
- 中断使得硬件可以发送通知给处理器。中断可以随时产生,内核随时可能因为新到来的中断而被打断。
- 处理器接收到中断后,会马上向操作系反映此信号的到来,然后由操作系统负责处理这些新到来的数据。
- 中断控制器采用复用技术,这样只通过一个和处理器相连的管线就可与处理器通信。
- 不同的设备中断不同,每个中断都有一个唯一的数字标志,以此来让OS区分,我们称这个标志为中断请求线(IRQ),每个IRQ都会关联一个数值量。(不是所有的中断号都有一一对应的设备,有时是动态分配的)
-
什么是异常?
- 比如除数为0、发生缺页或者系统调用等。
- 异常和中断不同,异常在产生的时候必须考虑时钟同步,即可以称异常为同步中断
-
什么是中断请求线?
上面这张图是STM32的某款型号内部中断示意图,其将多达140个GPIO通过复用技术就可以连接到16个外部中断/事件线中。这里的外部中断/事件线其实就是中断请求线
2、中断处理程序
- 什么是中断处理程序?
- 在响应一个特点的中断时,内核会执行一个函数,该函数就叫做中断处理程序。
- 产生中断的每个设备都有一个与之对应的中断处理程序
- 中断处理程序是设备驱动程序(在内核中)的一部分
- 在Linux中的中断处理程序就是一个个C函数,与其他函数不同的就是声明函数时需要特定的类型
- 中断处理程序被内核调用来响应中断,运行在中断上下文,同时中断上下文是具有原子性的。
- 由于设备的中断具有突发性,我们需要让中断处理程序响应时间尽可能短,处理的尽可能快
3、上半部与下半部的对比
- 为什么要区分上半部和下半部?
- 区分有严格时限的工作和普通工作
- 什么是上半部,什么是下半部?
- 上半部:中断处理程序是上半部,接受到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接受的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。
- 下半部:能够被允许稍后完成的工作会被推迟到下半部去。此后,在合适的时机,下半部就开中断执行。
- 举个例子更好地说明中断上半部与下半部?
- 以网卡为例,当网卡接收来自网络的数据包时,需要通知内核数据包到了。网卡需要立即完成这件事,从而优化网络的吞吐量和传输周期,以避免超时。因此,网卡立即发出中断,内核通过执行网卡已注册的中断处理程序来做出应答。
- 上半部:中断开始执行,通知硬件,拷贝最新的网络数据包到内存,然后读取网卡更多的数据包。这些都是重要、紧迫而又与硬件相关的工作。
- 下半部:当网络数据包被拷贝到系统内存后,中断的任务算是完成了,这时它将控制权交还给系统被中断前原先运行的程序。处理和操作数据包的其他工作在随后的下半部中进行。
4、注册中断处理程序
- 为什么要注册中断处理程序?
中断程序是硬件驱动程序的组成部分,如果设备使用中断那么相应的驱动程序就注册一个中断处理函数。
- 注册中断处理程序
- 驱动程序通过
request_irq()
函数注册一个中断处理程序,并且激活给定的中断线。下面是对应的程序:/* request_irq: allocate a given interrupt line */ int request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char *name,void *dev)
- 该函数有多个输入参数。
irq
:表示要分配的中断号,少数预先设定(时钟和键盘),多数动态确定。handler
:实际中断处理程序。flags
:可以为0
,也可为对应标志的位掩码:IRQF_DISABLED
:禁止其他所有中断,不设置则本中断允许与其他中断同时运行,多数中断不设置。IRQF_SAMPLE_RANDOM
:为内核熵池(entropy pool)做贡献。将中断间隔时间作为随机数据来源。所以像时钟这样的有规律的中断不需要设置,还有可能受外部攻击影响的设备也不行,如联网设备。IRQF_TIMER
:特别为定时器中断准备的。IRQF_SHARED
:共享中断线,不设置的话一条中断线只能有一个处理程序,所以在同一个给定线上注册的每个处理程序必须指定这个标志。
name
:中断名称,方便与用户通信。例如:PC机上键盘中断对应的值为keyborad
。dev
:用于共享中断线,当内核需要释放中断处理程序时,dev
将提供唯一信息cookie
,在给定中断线上删除哪个处理程序。
- 该函数的返回值:
request_irq()
成功返回0
,否则返回错误码,如最常见-EBUSY
,表示给定中断线忙(或者没有指定IRQF_SHARED
)。 request_irq()
函数可能会睡眠,不允许在中断上下文或者其他不允许阻塞的代码中调用。因为在注册过程中内核需要在/proc/irq
文件中创建一个与中断对应的项,需要用到proc_mkdir() --> proc_create() --> kmalloc()
,而kmalloc()
可以睡眠。
- 驱动程序通过
- 释放中断处理程序。
- 卸载驱动程序时需要注销对应的中断处理程序,并释放中断线。
void free_irq(unsigned int irq, void *dev);
free_irq()
函数删除中断处理函数,如果中断线不共享则也禁用这条中断线;如果是共享的则只删除处理程序。但都必须在进程上下文调用free_irq()。
- 卸载驱动程序时需要注销对应的中断处理程序,并释放中断线。
5、编写中断处理程序
- 中断处理程序声明
static irqreturn_t intr_handler(int irq, void *dev)
- 该函数有两个输入参数
irq
:这个处理程序要响应的中断对应的中断号dev
:与request_irq()
传入的参数一致。如果该值具有唯一确定性,那么它就相当于一个cookie
,用来区分共享同一中断处理程序的多个设备。此外,dev
也可能指向中断处理程序使用的一个数据结构。
- 该函数的返回值
- 返回
irqreturn_t
类型就是int
类型,返回值为IRQ_NONE
或者IRQ_HANDLED
。 - 当中断处理函数检测到中断,但该中断对应的设备不是在注册处理函数时指定的产生源,则返回
IRQ_NONE
- 当中断处理程序被正确调用并确实是对应设备产生的中断则返回
IRQ_HANDLED
。
- 返回
- 该函数有两个输入参数
- 重入问题
- Linux的中断处理需要重入吗?
Linux的中断处理程序无需重入,同一中断处理程序绝不会被同时调用以处理嵌套中断。 - Linux的中断处理程序无需重入的背后机理是什么?
当一个给定处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽,防止同一中断线上接收新的中断。所有其他中断线上的中断都能被处理,但当前中断线总是被禁止。
- Linux的中断处理需要重入吗?
- 共享中断与非共享中断处理程序
- 内核接受一个中断后,它将依次调用在该中断线上注册的每一个处理程序,如果不是该函数对应的设备产生的中断那将立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检查。
- 共享处理程序与非共享处理程序在注册和运行方式上比较相似,主要有三个差异:
request_irq()
的参数flags
必须设置为IRQF_SHARED
;- 对于每个注册的中断处理程序来说,
dev
必须唯一,不能给共享的处理程序传递NULL
值; - 中断处理程序必须能区分它的设备是否发出了这个中断,否则共享中断处理程序根本不知道是本中断线的哪个设备发出的中断。
6、中断上下文
(脑洞时刻,想象这个过程)
- 在学习中断上下文前,理解什么是进程上下文?
- 进程上下文实际上是进程执行活动全过程的静态描述。
- 上文: 已执行过的进程指令和数据在相关寄存器与堆栈中的内容。
- 正文: 正在执行的指令和数据在寄存器和堆栈中的内容。
- 下文: 待执行的指令和数据在寄存器与堆栈中的内容。
- 进程上下文一般在进程切换中提到,当一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够得到切换时的状态执行下去。
- 进程上下文实际上是进程执行活动全过程的静态描述。
- 什么是中断上下文?
- 硬件通过触发信号,导致内核调用中断处理程序,进入内核空间。这个过程中,硬件的一些变量和参数也要传递给内核,内核通过这些参数进行中断处理。
- 所谓的“中断上下文”,其实也可以看作就是硬件传递过来的这些参数和内核需要保存的一些其他环境(主要是当前被打断执行的进程环境)。中断时,内核不代表任何进程运行,它一般只访问系统空间,而不会访问进程空间,内核在中断上下文中执行时一般不会阻塞。
- 中断上下文有什么特点?
- 一个中断上下文,通常都会始终占有CPU,不可以被打断。
- 睡眠或者放弃CPU:中断上下文不属于任何进程,它与current没有任何关系(尽管此时current指向被中断的进程),所以中断上下文一旦睡眠或者放弃CPU,将无法被唤醒。所以也叫原子上下文(atomic context);
- 执行耗时的任务:中断处理应该尽可能快,因为内核要响应大量服务和请求,中断上下文占用CPU时间太长会严重影响系统功能。在中断处理例程中执行耗时任务时,应该交由中断处理例程下半部来处理;
7、中断处理机制的实现
- 中断处理机制是怎样的?
(开局一图看清所有)- 设备产生中断,通过总线把电信号发送给中断控制器
- 如果中断线是激活的,那么中断控制器就会把中断发往处理器。
- 当处理器没有禁止该中断,则处理器会立即停止它正在做的事,关闭中断系统,然后跳到中断处理程序的入口点去运行。
- 对于每条中断线,处理器都会跳到对应的唯一的位置。这样,内核就可知道所接收中断的
IRQ
号了。 - 内核调用函数
do_IRQ()
- (接下来的操作看图)
do_IRQ()
函数do_IRQ()
的声明如下:unsigned int do_IRQ(struct pt_regs regs);
do_IRQ()
做了什么?- 在计算出中断号后,
do_IRQ()
对所接收的中断进行应答,禁止这条线上的中断传递。 do_IRQ()
需要确保在这条中断线上有一个有效的处理程序,而且这个程序已经启动但是当前没有执行。- 如果上述成立,那么
do_IRQ()
就调用handle_IRQ_event()
来运行为这条中断线所安装的中断处理程序。否则,直接调用ret_from_intr()
- 在计算出中断号后,
handle_IRQ_event()
函数- 下面通过代码解释这个函数
/ Have got an event to handle:*/ fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,struct irqaction *action) {int ret, retval = 0, status = 0;/*前面进行了禁止中断操作,这里要打开*/if (!(action->flags & SA_INTERRUPT))local_irq_enable();/*每个潜在的处理函数在循环中依次执行。如果这条线不是共享的,第一次执行就退出循环;否则,所有的处理函数都需要执行*/do {ret = action->handler(irq, action->dev_id, regs);if (ret == IRQ_HANDLED)status |= action->flags;retval |= ret;action = action->next;} while (action);/*如果注册器件制定了SA_SAMPLE_RANDOM标志,则调用函数add_interrupt_randomness()该函数使用中断间隔时间为随机数产生器增加熵。*/if (status & SA_SAMPLE_RANDOM)add_interrupt_randomness(irq);/*最后再将中断禁止*/local_irq_disable();/*函数返回,再次回到do_IRQ(),该函数做清理工作并返回到初始入口点,然后再从这个入口点跳到函数ret_from_intr()*/return retval; }
ret_from_intr()
函数
ret_from_intr()
函数用汇编实现,会在恢复现场之前检测need_resched
标志和preempt_count
来决定是否要调用schedule()
函数进行调度。如果不需要调度或者调度返回后,则恢复原先进程的现场。
8、/proc/interrupt
/proc/interrupt
文件存放系统中与中断相关的统计信息。例如:root@OpenWrt:~# cat /proc/interrupts CPU0 5: 9695183 MIPS 5 10100000.ethernet7: 687053832 MIPS 7 systick9: 0 INTC 1 10000100.timer15: 0 INTC 7 Ralink_DMA20: 11 INTC 12 serial22: 0 INTC 14 10130000.sdhci25: 5 INTC 17 gsw26: 1 INTC 18 ehci_hcd:usb1, ohci_hcd:usb240: 4496 GPIO 8 lirc_rpi ERR: 0
- 第一列是中断线。没有显示未注册中断处理函数的中断线。
- 第二列是接收中断数目的计数器,存储着中断次数
- 第三列是处理这个中断的中断控制器。
- 最后一列是中断相关的设备名字。这个名字是在使用
request_irq()
函数进行中断处理函数注册时,传递给devname
参数的名字。
9、中断控制
中断控制的实质是同步,通过禁止中断可以确保中断处理程序不会抢占当前代码,禁止中断还可以禁止内核抢占。然后,禁止中断却不能防止SMP系统的并发访问,Linux中内核代码一般都要获取某种锁,用来防止其他处理器对共享数据的并发访问,而获得这些锁的同时意味着禁止本地中断。
- 禁止和激活中断
local_irq_disable(); local_irq_enable();
这两个函数依赖于体系结构,作用是无条件禁止和激活全局中断。一般成对出现,如果连续调用两次
local_irq_disable()
随后再调用local_irq_enable()
会出现潜在危险,因为local_irq_enable()
会直接激活全局中断。因此,通常会用以下函数在禁用中断之前保存中断系统的状态,在激活的时候再把中断恢复为之前的状态即可:unsigned long flags; local_irq_save(flags); //保存当前中断状态,随后禁止中断 local_irq_restore(flags); //恢复中断前的状态
其中
flags
是以值传递的。local_irq_save(flags)
和local_irq_restore(flags)
的调用必须在同一个函数中进行。 - 禁止指定中断线
Linux提供了四个接口用来禁止一条特定的中断线: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_irq()
函数只有在当前正在执行的所有处理函数完成后才返回,因此,要保证指定中断线上没有新的中断,并且所有已经开始执行的处理程序全部退出。disable_irq_nosync()
函数不会等待当前中断处理程序执行完毕。 synchronize_irq()
函数等待一个特定的中断处理程序退出,只有中断处理程序退出后synchronize_irq()
函数才能返回。- 对一条指定的中断线调用
disable_irq()
和disable_irq_nosync()
函数,每次调用都需要相应地调用一次enable_irq()
,只有最后一次enable_irq()
被调用时才真正重新激活中断线。这三个函数不会引起睡眠。
- 前两个函数禁止中断控制器上指定的中断线。
- 中断系统的状态
如果想知道中断系统是禁止的还是激活的或者处于上下文的执行状态,可以使用以下函数:irqs_disabled() in_interrupt() in_irq()
- 如果本地处理器上的中断系统被禁用,
irqs_disabled()
函数返回非0
,否则返回0
。 - 如果内核处于任何的中断处理中(包括下半部处理),
in_interrupt()
函数返回非0
,否则返回0
。 - 如果内核处于中断处理函数中,
in_irq()
函数返回非0
,否则返回0
。
- 如果本地处理器上的中断系统被禁用,
- 中断控制方法的列表