Linux内核7-中断和中断处理(上半部)

Linux内核第7章

任何操作系统内核的核心任务,都包含有对连接到计算机上的硬件设备进行有效管理,如硬盘、蓝光碟机、键盘、鼠标、3D处理器,以及无线电等。而想要管理这些设备,首先要能和它们互通音信才行。而处理器的速度跟外围硬件设备的速度往往不在一个数量级上,因此如果内核采取让处理器向硬件发出一个请求,然后专门等待回应的办法,显然差强人意。既然硬件的响应这么慢,那么内核就应该在此期间处理其他事务,等待硬件真正完成了请求的操作之后,再回过头来对它进行处理。

那么如何让处理器和这些外部设备能协同工作,且不会降低机器的整体性能呢?轮询(polling)可能会是一种解决办法,它可以让内核定期对设备的状态进行查询,然后做出相应的处理。不过这种工作很可能会让内核做不少无用功,因为无论硬件设备是正在忙碌着完成任务还是已经大功告成,轮询总会周期性地重复执行。更好的办法是我们来提供一种机制,让硬件在需要的时候再向内核发出信号。这就是中断机制。

 

中断:

中断使得硬件得以发出通知给处理器。中断本质上是一种特殊的电信号,由硬件设备发向处理器。处理器接收到中断后,会马上向操作系统反映此信号的到来,然后就由操作系统负责处理这些新到来的数据。硬件设备生成中断的时候并不考虑与处理器的时钟同步---换句话说就是中断随时可以产生。因此,内核随时可能因为新到来的中断而被打断。

从物理学的角度看,中断是一种电信号,由硬件设备生成,并直接送入中断控制器的输入引脚中---中断控制器是个简单地电子芯片,其作用是将多路中断管线采用复用技术只通过一个和处理器相连接的管线与处理器通信。当接收到一个中断后,中断控制器会给处理器发送一个电信号。处理器一经检测到此信号,便中断自己的当前工作转而处理中断。此后,处理器会通知操作系统已经产生中断,这样操作系统就可以对这个中断进行适当地处理了。

不同的设备对应的中断不同,而每个中断都通过一个唯一的数字标志(不过同一个数字标志的中断线上可能有共享的多个中断---对应不同的设备dev)操作系统可以区别出不同的中断来自哪个设备,给出不同的中断处理程序。

这些中断值通常被称为中断请求(IRQ)线。每个IRQ线都会被关联一个数值量----例如经典PC机上,IRQ 0是时钟中断,而IRQ 1是键盘中断。但中断号不一定固定,对于连接在PCI总线上的设备而言,中断是动态分配的。重点在于特定的中断总是与特定的设备相关联,并且内核要知道这些信息。

 

异常:

异常与中断不同,它在产生时必须考虑与处理器的时钟同步。异常也常常被称为同步中断。在处理器执行到由于编程失误而导致的错误指令(如被0除)的时候,或者是在执行期间出现特殊情况(如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常与处理中断的方式类似,因此内核对它们的处理也很类似。本章对中断(由硬件产生的异步中断)的讨论大部分也适合于异常(由处理器本身产生的同步中断)。

(在X86体系结构上系统调用通过软中断实现,陷入内核,然后引起一种特殊的异常----系统调用处理程序异常)。

 

中断处理程序:

在响应一个特定中断的时候,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务例程(interrupt service routine, ISR)。产生中断的设备都有一个相应的中断处理程序。一个设备的中断处理程序是它设备驱动程序的一部分---设备驱动程序是用于设备进行管理的内核代码。

在Linux中,中断处理程序就是普普通通的C函数,只不过按照特定的类型声明,以便内核能够以标准的方式传递处理程序的信息。中断处理程序与其它内核函数的真正区别在于,中断处理程序是被内核调用来响应中断的,而它们运行于被称为中断上下文的特殊上下文中,中断上下文也被称作原子上下文,该上下文中的执行代码不可阻塞

中断可能随时发生,因此中断处理程序也就随时可能执行。中断处理程序需要快速执行,这样才能保证尽快恢复中断代码的执行,同时对系统其他部分来说,中断处理程序快速执行也非常重要。

 

上半部与下半部的对比:

中断处理程序需要快速执行,但有时中断处理程序工作量可能会很大。一般把中断处理分为两个部分或两半。中断处理程序是上半部(top half)---接收到一个中断,它就立即开始执行,但只做有严格时限的工作,例如对接收的中断进行应答或复位硬件,这些工作都是在所有中断被禁止的情况下完成的。能够被允许稍后完成的工作会推迟到下半部(bottom half)去。此后在合适时机下半部会被开中断执行。

 

注册中断处理程序:

中断处理程序是管理硬件的驱动程序的组成部分。每一设备都有相关的驱动程序,如果设备使用中断,那么相应的驱动程序就注册一个中断处理程序。

驱动程序可以通过request_irq()函数注册一个中断处理程序(它被声明在<linux/interrupt.h>中),并且激活给定的中断线,以处理中断:

//request_irq:分配一条给定的中断线

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char*name, void *dev);

第一个参数irq表示要分配的中断号。对某些设备,如传统PC设备上的系统时钟或键盘,这个值通常是预先确定的。而对于大多数其他设备来说,这个值要么是可以通过探测获取,要么可以通过编程动态确定。

第二个参数handler是一个指针,指向处理这个中断的实际中断处理程序。只要操作系统接收到中断,该函数就被调用。

typedef irqreturn_t (*irq_handler_t)(int,void*);

handler函数的原型,接受两个参数,并有一个类型为irqreturn_t的返回值。

 

第三个参数flags可以为0,也可能是下列一个或多个标志的位掩码。其定义在<linux/interrupt.h>中。在这些标志中最重要的是:

-IRQF_DISABLED  该标志被设置后,意味着内核在处理中断处理程序本身期间,要禁止所有的其它中断。如果不设置,中断处理程序可以与除本身外的其它任何中断同时运行。多数中断处理程序不会设置该位的,因为禁止所有中断是一种野蛮行为。这种用法留给希望快速执行的轻量级中断。这一标志是SA_INTERRUPT标志的当前表现形式,在过去的中断中用以区分“快速”和“慢速”中断。

-IRQF_SAMPLE_RANDOM  此标志表明这个设备产生的中断对内核熵池有贡献。内核熵池负责提供从各种随机事件导出的真正地随机数。如果指定了该标志,那么来自该设备的中断间隔时间就会作为熵填充到熵池。如果你的设备以预知的速率产生中断(如系统定时器),或者可能受到外部攻击者(如联网设备)的影响,那么就不需要设置这个标志。

-IRQF_TIMER  该标志是特别为系统定时器的中断处理而准备的。

-IRQF_SHARED  此标志表明可以在多个中断处理程序之间共享中断线。在同一个给定线上注册的每个处理程序必须指定这个标志;否则,在每条线上只能有一个处理程序。

 

第四个参数name是与中断相关的设备的ASCII文本表示。如PC机上键盘中断对应的这个值为"keyboard"。

第五个参数dev用于共享中断线。当一个中断处理程序需要释放时,dev将提供唯一的标志信息,以便从共享中断线的诸多中断处理程序中删除指定的哪一个。如果没有这个参数,那么内核不可能知道在给定的中断线上到底要删除哪一个处理程序。如果无须共享中断线,那么将该参数赋为空值NULL就可以了,但是,如果中断线是被共享的,那就必须传递唯一的信息(除非设备又旧又破且位于ISA总线上,那么就必须支持共享中断)。另外,内核每次调用中断处理程序时,都会把这个指针传递给它。实践中往往会通过它传递驱动程序的设备结构:这个指针是唯一的,而且有可能在中断处理程序内被用到。

 

request_irq()函数成功执行会返回0.如果返回非0值,就表示有错误发生,在这种情况下,指定的中断处理程序不会被注册。最常见的错误是-EBUSY,它表示给定的中断线已经在使用(或者当前用户或者你没有指定IRQF_SHARED)。

request_irq()函数可能会睡眠,因此不能在中断上下文或其它不允许阻塞的代码中调用该函数。在注册的过程中,内核需要在/proc/irq文件中创建一个与中断对应的项。函数proc_mkdir()就是用来创建这个新的procfs项的。proc_mkdir()通过调用函数proc_create()对这个新的profs项进行设置,而proc_create()会调用函数kmalloc()来请求分配内存。

 

先初始化硬件,再注册中断处理程序。

 

卸载驱动程序时,需要注销相应的中断处理程序,并释放中断线。上述动作需要调用:

void free_irq(unsigned int irq, void *dev);

如果指定的中断线不是共享的,那么在该函数删除处理程序的同时将禁用这条中断线。如果中断线是共享的,则仅删除dev所对应的处理程序,而这条中断线本身只有在删除了最后一个处理程序时才会被禁用。(所以唯一的dev很重要)。

必须从进程上下文中调用free_irq()。

 

编写中断处理程序:

一个中断处理程序的声明:

static irqreturn_t intr_handler(int irq, void *dev);

第一个参数irq是这个处理程序要响应的中断的中断号。

第二个参数dev是一个通用指针,它与在中断处理程序注册时传递给request_irq()的参数dev必须一致。如果该值具有唯一确定性,那么它就可以区别共享同一中断处理程序的多个设备。(一个中断号(线)可以有一到多个中断处理程序,一个中断处理程序可对应一到多个设备,多个设备可共享同一中断线的统一处理程序,但处理程序函数和注册中断函数的参数dev应唯一)。另外dev也可能指向中断处理程序使用的一个数据结构。因为对每个设备而言,设备结构都是唯一的,而且可能在中断处理程序中也用得到,因此,它也通常被看作dev。

中断处理程序的返回值是一个特殊类型:irqreturn_t。中断处理程序可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED。当中断处理程序检测到一个中断,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;当中断处理程序被正确调用且确实是它所对应的设备产生了中断时,返回IRQ_HANDLED。另外可以使用IRQ_RETVAL(val),如果val为非0值,那么该宏返回IRQ_HANDLED,否则返回IRQ_NONE。利用这些特殊的值,内核可以知道设备发出的是否是一种虚假的(未请求的)中断。

 

Linux中的中断处理程序是无须重入的。当一个给定的中断处理程序正在执行时,相应中断线在所有处理器上都会被屏蔽掉,以防止在同一中断线上接收另一个新的中断。通常情况下,所有其它的中断都是打开的,所以这些不同中断线上的其它中断都能被处理,但当前中断线总是被禁止的。

 

共享的中断处理程序与非共享的中断处理程序在注册和运行方式上比较相似,但有一些差异:

-request_irq()(注册中断处理程序)的参数flags必须设置IRQF_SHARED。

-对于每个注册的中断处理程序来说,dev参数必须唯一。通常选用设备结构的指针,因为唯一并且中断处理程序可能会用到它。不能给共享的处理程序传递NULL值。

-中断处理程序必须能够区分它的设备是否真的产生了中断。这既需要硬件的支持,也需要处理程序中有相关的处理逻辑。

指定IRQF_SHARED标志以调用request_irq()时,只有在以下两种情况下才可能成功:中断线当前未被注册,或者在该线上的所有已注册处理程序都指定了IRQF_SHARED。

内核接收一个中断后,它将依次调用在该中断线上注册的每一个处理程序。因此,一个处理程序必须知道它是否应该为这个中断负责。如果与它相关的设备并没有产生中断,那么处理程序应该立即退出。这需要硬件设备提供状态寄存器(或类似机制),以便中断处理程序进行检查

 

中断上下文:

当执行一个中断处理程序时,内核处于中断上下文中(interrupt context)。

进程上下文是一种内核所处的操作模式,此时内核代表进程执行---例如,执行系统调用或运行内核线程。在进程上下文中,可以通过current宏关联当前进程。此外,因为进程是以进程上下文的形式连接到内核中的,因此,进程上下文可以睡眠,也可以调用调度程序。

而中断上下文和进程没有什么关系。与current宏也是不相干的(尽管它会指向被中断的进程)。因为没有后备进程,所以中断上下文不可以睡眠,否则又怎能再对它重新调度呢?因此,不能从中断上下文中调用某些函数。如果一个函数睡眠,就不能在你的中断处理程序中使用它---这是对什么样的函数可以在中断处理程序中使用的限制。

中断上下文具有严格的时间限制,因为它打断了其它代码。中断处理程序打断了其它代码(甚至可能是打断了在其它中断线上的另一个中断处理程序)。正是因为这种异步执行的特性,所以所有的中断处理程序必须尽可能地迅速、简洁。尽量把工作从中断处理程序中分离出来,放在下半部来执行,因为下半部可以在更合适的时间运行。

中断处理程序栈的设置是一个配置选项。曾经,中断处理程序并不具有自己的栈,而是共享所中断的进程的内核栈。内核栈大小两页,32位系统8KB,64位系统16KB,对空间的使用很节约。在2.6的早期版本中,增加了一个选项,把栈的大小从两页减少到一页,减轻了内存的压力(因为系统原来需要两页连续的且不可换出的内核内存)。为了应对栈大小的减小,中断处理程序拥有了自己的栈,每个处理器一个,大小为一页,这个栈就称为中断栈,尽管中断栈的大小是原先共享栈的一半,但平均可用栈空间大得多。

 

中断处理机制的实现:

中断处理系统在Linux中的实现非常依赖于体系结构,实现依赖于处理器、所使用的中断控制器的类型、体系结构的设计及机器本身。

设备产生中断,通过总线把电信号发送给中断控制器,如果中断线是激活的(它们是允许被屏蔽的),那么中断控制器就会把中断发往处理器。除非处理器上禁止该中断,否则处理器会立即停止它正在做的事,关闭中断系统,然后跳到内存中预定义的位置开始执行那里的代码。这个预定义的位置是由内核设置的,是中断处理程序的入口点。

在内核中,中断的旅程开始于预定义入口点,这类似于系统调用通过预定义的异常句柄进入内核。对于每条中断线,处理器都会跳到对应的一个唯一的位置。这样内核就可知道所接收中断的IRQ号了。初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核调用函数do_IRQ(),从这开始,检查该中断线上是否有中断处理程序:1-是则调用handle_IRQ_event(),接着在该线上运行所有中断处理程序(不符合设备的立即返回,符合设备dev的执行),接着调用ret_from_intr(),接着返回内核运行中断的代码;2-否则直接调用ret_from_intr(),再返回内核运行中断的代码

 

/proc/interrupts:

procfs是一个虚拟文件系统,它只存在于内核内存,一般安装于/proc目录。在/procfs中读写文件都要调用内核函数,这些函数模拟从真实文件中读或写。/procfs/interrupts文件,该文件存放的是系统中与中断相关的统计信息。

 

中断控制:

Linux内核提供了一组接口用于操作机器上的中断状态。这些接口为我们提供了能够禁止当前处理器的中断系统,或屏蔽掉整个机器的一条中断线的能力,这些例程都是与体系结构相关的,可以在<asm/system.h>和<asm/irq.h>中找到。

一般来说,控制中断系统的原因归根结底是需要提供同步。通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占(禁止内核定时器的中断信号,定时器的执行程序就不会执行,所以不会更新进程的时间片)。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其它处理器对共享数据的并发访问。获取这些锁的同时也伴随着禁止本地中断。锁提供保护机制防止来自其它处理器的并发访问;禁止中断提供保护机制防止来自其它中断处理程序的并发访问。

禁止和激活中断:

local_irq_disable();

//禁止中断

local_irq_enable();

如果调用local_irq_disable()例程之前已经禁止了中断,那么该例程往往会带来潜在的风险;同样相应的local_irq_enable()例程也存在潜在的危险,因为它将无条件地激活中断。

所以需要一种将中断恢复到以前的状态而不是简单地禁止或激活。

unsigned long flags;

local_irq_save(flags);

//禁止中断

local_irq_restore(flags);;  //恢复到之前的状态

 

禁止指定中断线:

在某些情况下,只禁止整个系统中一条特定的中断线就够了,这就是屏蔽掉(masking out)一条中断线。

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()等待一个特定的中断处理程序的退出。如果该处理程序正在执行,那么该函数必须退出后才能返回。

对这些函数的调用可以嵌套。但在一条指定的中断线上,对disable_irq()或disable_irq_nosync()的每次调用,都需要相应地调用一次enable_irq()。只有在对enable_irq()完成最后一次调用后,才真正激活了中断线。

所有这三个函数可以从中断或进程上下文中调用,而且不会睡眠。

禁止多个中断处理程序共享的中断线是不合适的,这会禁止这条线上所有设备的中断传递。

 

中断系统的状态:

通常有必要了解中断系统的状态(如中断是禁止的还是激活的),或者你当前是否正处于中断上下文的执行状态中。

宏irqs_disable()定义在<asm/system.h>中。如果本地处理器上的中断系统被禁止,则它返回非0,否则返回0.

在<linux/hardirq.h>中定义的两个宏提供一个用来检查内核的当前上下文的接口,它们是:

in_interrupt()

in_irq()

第一个宏最有用:如果内核处于任何类型的中断处理中,它返回非0,说明内核此刻正在执行中断处理程序,或者正在执行下半部处理程序。

宏in_irq()只有在内核确实正在执行中断处理程序时才返回非0。

通常情况下,要检查自己是否处于进程上下文中。也就是说,希望确保自己不在中断上下文中,因为代码要做一些像睡眠这样只能从进程上下文中做的事。

posted @ 2020-02-14 20:59  吉吉boy  阅读(505)  评论(0编辑  收藏  举报