您的位置:首页 > 运维架构 > Linux

Linux Kernel Development 笔记(六)中断以及中断处理

2013-03-25 17:44 302 查看
众多操作系统的内核都有一个核心的任务,就是管理硬件和机器之间的连接(硬盘,光碟,按键,3D处理器等)。要实现这个任务,内核必须单独的与每一个硬件通信。基于处理器的高速运行速度比众多硬件都要快速的现状,让处理器来对硬件发出请求并等待硬件响应是一个不理想的做法。相反,处理器应该可以自由的去处理别的事情,只有当硬件真的完成其工作并需要通知处理器的时候再让处理器回来工作。两种方式可以让此方式实现,其中一种是轮询(poll),就是处理器定期的去访问硬件,判断其状态,但这种方式会增加处理器的负担。另外一种方式,也是最好的方法,就是提供一种机制,让硬件可以给内核发信号,告诉内核现在需要处理硬件。这机制就是中断,处理中断的函数叫做中断函数。

中断

中断允许硬件发信息给处理器。举个例子:按键的时候,键盘控制器触发一个电信号到处理器,让处理器告知操作系统,有一个新按键动作。这个电信号就是中断。硬件产生的中断跟处理器时钟是异步的,也就是说,中断可以出现在任何时间里,因此内核可以在任何时刻被中断来处理中断。中断是由硬件设备产生的电信号,传给处理器(可识别多路中断的简单芯片)的中断输入pin脚而产生的。当中断处理器收到电信号后,会往处理器发送信号。处理器侦测到这个信号后,会打断目前的进程上下文,转入去处理中断操作。处理器通知操作系统,有中断产生,接着就是由操作系统来有效处理中断。不同设备可以通过指定唯一的值与中断关联的方式来连接到不同的中断。通过这个唯一性,操作系统可以识别不同设备的中断信息。中断的值一般称为Interrrupt
Reques (IRQ)。每一条IRQ线都付与指定的值。并不是所有的中断都是这样机械般创建的,也可以动态的分配,例如与在PCI总线上的设备关联的中断。在操作系统中,exception也被看成一种中断,但不像这里讨论的中断一样,它们是与处理器时钟是同步的,它们称为同步中断。exception中断往往是处理器遇到代码执行出错或遇到必须由内核处理的异常情况产生的。因为大部分的处理器架构,处理这两者是类似的,故此内核处理此两种中断也是类似的。这里讨论也适用与exception。

中断处理

内核中运行的负责处理指定的中断的函数称为中断函数或ISR (中断服务)。每一个产生的中断都对应一个中断处理函数。举个例子:按键响应处理按键中断,处理系统时间中断的中断函数等等。处理中断的函数是属于驱动的一部分。在Linux中,中断处理是一个普通的C函数,遵循特定的原型,使内核可以用普通的方式的传递这个处理信息;但同时,这个函数只是一般的函数。中断处理函数与一般的内核函数的区别在与,中断的处理函数运行在所谓的中断上下文环境下,这个上下文环境通常也成为原子型的上下文(Atomic context)。因为在此上下文执行的代码是不允许被阻塞的。因为中断是随时产生的,故此中断函数也是随时被调用的。中断处理函数必须快速的运行完毕,以尽可能的快的返回到被阻塞运行的代码中去。因此,操作系统没有延迟的执行中断服务对于硬件是很重要的,同样中断函数的尽可能的快的处理完对于系统余下的部分也是很重要的。中断处理函数最起码的功能是确认收到硬件产生中断了。一般说来,中断处理函数会执行许多工作,如把硬件的数据拷贝到内存等。

顶层与底层

中断处理既要快速处理,但又要能处理大量工作,这两点要求看上去是冲突的。为了这个终极目标,中断处理被分成两部分,或两层。中断处理是处于上半部。上半部是在中断发生后立即执行,而且是处理时间紧迫的任务,如中断响应通知或重置硬件。能稍微推后的工作,则放到下半部去执行。下半部会在未来适当的时候执行,而此时中断都是可以发生的。举一个网卡的例子。当网卡从网络那里收到数据后,会需要告知内核有数据到达。此时网卡触发一个中断,内核则会响应执行网卡注册的中断处理。中断处理中,内核会通知硬件中断已收到,网卡需要把数据拷贝到内存中去,并准备接收更多的数据包。这个处理是重要的,时间紧迫的,以及跟硬件直接相关的工作。内核一般只需要快速的把网卡的数据包拷贝到内存中去,这种动作的延迟,会导致数据卡的缓存被覆盖(数据卡的缓存都是固定大小的),导致数据丢失。只要网卡数据到达内存,则中断的任务就算完成了。此时就可以把控制权交回给被中断的代码,而内存中的数据就会在适当时候在下半部被处理掉。

每一个设备都对应一个驱动,如果驱动中需要用到中断,则驱动必须向内核注册一个中断处理。驱动可以用

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

接口来注册一个中断处理。irq是中断号,对于像系统时间以及按键,其中断号是固定的。但大部分其他的设备,可以通过探测或程序动态决定的。handler是中断处理函数,带有两个参数和一个返回值。第三个flags是一个标志,有多种值,但重要的是:

IRQF_DISABLED 设置这个值后,执行中断处理时候,内核会屏蔽掉所有中断。没设此值时,除了本身的中断外,其余的中断都是允许的。大部分的中断都不会设这个值,这是一种不好的方式。这个值的设定是给那种对执行效率很敏感的中断处理保留的。

IRQF_SAMPLE_RANDOM 这个标志表示由这个设备产生的中断必须对内核entropy池有贡献。内核entropy池真正的从各种随机事件中产生随机数。如果这个标志设置了,此设备中断的时机会被作为一个熵值递给pool。如果你的设备是在可以预见的速率产生中断或容易受外部攻击影响的话,请不要设置这个标志。大部分设备,其中断都是在一个不确定的时间发生的,故此是一种很好的熵值来源。

IRQF_TIMER 指示目前这个处理函数处理系统时间中断

IRQF_SHARED 指示多个中断处理函数可以共享同一个中断线(?Interrupt Line).

第四个参数是一个带有中断的设备代表名字,举个例子,代表PC键盘中断的就是keyboard。这个名字通过 /proc/irq 和 /proc/interrupts 来与用户进行交流。

最后一个参数就是给共享中断线而用的。当中断处理被释放后,这个参数提供唯一的记录来确保指定的中断处理从中断线上移除。没有这个参数,内核就无法知道那个对应于指定的中断线的中断处理被移除了。如果该中断线不是共享的,你可以传NULL参数,但如果是共享中断线的话,则必须传入一个唯一的记录。这个记录指针会当成参数传给每一此调用的中断函数里。一般的做法是传入一个驱动设备结构体指针。函数成功调用会返回0,不成功返回一个非0,表示错误。一般错误是EBUSY,表明目前给定的中断线已经正在使用中。这个函数是可以休眠的,故此这个函数不能用在中断上下文中,也不能用在那些不能被阻塞的代码中。通过这个函数注册后,一个相应的中断入口就会在/proc/irq。

当驱动被卸载的时候,你可以通过free_irq的函数来注销中断处理。如果指定的中断号是非共享的,则也会把都应的中断给禁掉。如果是共享的,则会把由dev这个唯一的记录找出对应的中断处理,并注销,但中断线并不被禁止。free_irq接口只能在进程上下文中被调用。

中断处理函数是有两个参数的,一个是代表中断线号,是函数被调用时候传进去的。在早期Linux中是用来区分那些使用同一个处理函数但不同的驱动设备。第二个参数,就是所谓的记录。这个记录是注册时候最后一个参数,中断处理函数被调用时候会传进此指针。如果这个值是唯一的,则可以用来做为使用同一个中断处理函数的不同设备的cookie。因为设备结构对于设备来说也是唯一的,故此其作为这个参数是比较适合的。中断处理函数的返回值类型是irqreturn_t,一般返回连个值:IRQ_NONE 和 IRQ_HANDLED。第一个表明,中断处理发现该中断对应的设备不再是原有的设备,则返回IRQ_NONE。后面那个返回值则当中断处理正确执行时候返回的。通过这两种返回值,内核可以侦测到问题发生。中断处理函数的作用依赖于设备以及其产生中断的原因。最起码,中断处理函数要告知设备中断收到。对于复杂的设备,而外的数据拷贝也会在中断处理执行,或许还有一些扩展的工作。但一般扩展的工作都会推到下半部去处理。中断是不可重入的,中断处理被调用时,相应的中断线会被禁止,防止其他对于此中断线的中断发生。对于共享中断的处理函数必须满足以下条件:

1. IRQF_SHARED必须设置

2. 记录参数必须每一种设备来说是唯一的,一般用设备结构体指针即可,不能为NULL

3. 中断函数必须能区分设备是否真的触发中断,这要求硬件以及逻辑处理都支持。

注册了IRQF_SHARED的中断函数,能正确调用的情况只有当前的中断线没有注册或与此中断线关联的所有中断函数注册时候设置了IRQF_SHARED。当共享的中断线产生中断时候,会连续的调用注册这个中断线的中断处理。因此,中断处理能区分设备是否产生中断很重要。而且,在发现相关的设备并没有触发中断信号时候,要能迅速的退出。这个要求硬件具备状态寄存器可以供访问。

中断上下文

当内核执行中断处理函数的时候,内核被称为处于中断上下文。回想前面说的,进程上下文是内核的一种工作模式,内核代表进程执行系统调用或运行内核线程。在进程上下文中,进程是连接内核的,因为进程上下文可以休眠或调用调度。相反,中断上下文没有跟进程相连的,current宏是不相关的。没有了进程的概念,中断上下文里,是不能休眠从而就没有所谓调度的说法。因此在中断处理函数里,不能随意的调用某些函数(例如一下可以休眠的函数)。中断上下文是对时间要求严格的,因为中断函数中断了别的代码。代码要快以及简单。循环是允许,但不鼓励。尽可能的把工作移出中断函数,并放到下半部(bottom
half)执行。中断处理在早期是共享内核栈的,早期的内核栈一般是2个page的大小。近期的Linux,中断拥有了自己的栈,一般为1个page大小,虽然看上去大小减少了,但实际上利用度提高了。不管中断用那里的栈,有一个原则是必须谨记的,就是要尽量用最小的栈空间。

中断处理的实现

中断处理系统在Linux是与架构有关的,不单单跟CPU有关,跟中断控制器以及架构的设计也有关系。一个设备,通过在BUS上发送一个电信号到中断控制器来触发一个中断。如果中断线是被允许的,则中断控制器就往CPU发送中断信号。在大部分架构那里,都是往CPU某个特殊的引脚发送一个电信号。如果CPU内的中断没有被禁止,处理器会迅速的停止目前的工作,屏蔽中断系统,跳到在内存中预设好的地点,执行代码。这个预设的地点就是内核设定的中断处理入口点。中断的旅程就如内核开始系统调用的过程一样,通过预设好的入口点寻找处理函数。(系统通过例程处理表)。中断处理会把中断线号码(IRQ号码)保存起来,并把当前的寄存器的值(被中断的任务)保存到栈里面去。然后内核调用do_IRQ函数。大部分的中断处理代码都是C开发的,但这还是跟平台架构有关。因为C调用的习惯把do_IRQ(struct
pt_regs regs)的参数会放到栈顶。,所以pt_regs结构包含了之前保存在汇编入口程序的初始寄存器值。因为中断值也被保存了,故此do_IRQ也能获取它。在中断线被计算后,do_IRQ会确认中断的收到并且会禁止在中断线上的中断传递。接着,do_IRQ确保注册在这个中断线的处理函数有效(就是目前没有在运行)。如果是,则调用handle_IRQ_event,去运行注册的中断。在handle_IRQ_event中,每一个潜在的中断处理都在循环中执行。如果这个中断线是非共享的,则循环在执行完一次中断处理后就跳出去。接着就依据是否设定IRQF_SAMPLE_RANDOM标志来决定执行add_interrupt_randomness。该函数利用中断的时机来获得随机数制造者需要用到的熵。最后,中断再次被禁止而且返回。接着会执行ret_from_intr,这个函数主要是检测是否有调度正在进行。如果有调度进行,而且是转到用户空间的,则调用schedule。如果是转到内核空间的,则要判断preempt_count是否为0,如果是则调用schedule。这个为了保证内核被安全的调度。在schedule返回后,原本保存的寄存器值被恢复,内核返回到被中断的代码继续执行。

Linux内核实现了一套接口用来操控机器中的中断状态。这些接口允许你禁止当前处理器的中断系统或标识整个机器中某个中断线。这些接口都是与平台相关的。提供对中断系统控制的原因,是因为有对提供同步的需求。通过禁止中断,你可以保证你目前的代码不被中断抢占。甚至禁掉中断也禁掉了内核抢占。但是,并不是禁止中断或禁止内核抢占可以给当前进程提供抵挡其他处理器并行访问的保护。因为Linux是支持多处理器的,故此内核一般需要获取某种锁来组织其他处理器同时访问共享数据。一般来说,这个锁经常都与禁止中断配合起来的。锁提供了对共享数据受别的处理器并发访问的保护,而禁止中断则避免了共享数据被中断处理所访问。

要禁止对当前处理器的中断,可以采用local_irq_disable。而激活则调用local_irq_enable。这些函数一般都是汇编实现的。如果中断已经被禁止,调用local_irq_disable是危险的。对应的local_irq_enable接口则毫无条件的允许中断,尽管事实上中断本身就要启动。相反,需要一种机制来恢复中断到前一个状态。因为在内核中给定的代码路径可以以禁止或允许中断的方式达到,因为中断恢复是一件普遍关心的事情。随着内核代码越来越复杂,这种要了解所有代码的路径是很困难的。把中断的状态在中断禁止前保存下来是一种安全的做法。因此当你要准备重新允许中断的时候,你只要简单的恢复一下中断状态即可。

local_irq_save 和 local_irq_restore 这些方法至少部分是宏实现的,因此表面看来,其参数是直接传值。这个参数包含含有平台指定的中断状态的数据,这个参数不能被传给别的函数,因此这对函数必须用在一个函数内部。前面介绍的方法,均可用在中断以及进程上下文中。在某些项目中,在整个系统中禁止指定中断线是很有用处的。这种做法叫做marking out中断线。作为例子,你也许想要在操控设备数据的时候先禁止一个设备的中断。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会在当前中断处理执行完毕后才会返回。因此确保了当前正在运行的中断处理能继续运行完毕,又保证了新的中断信号不会再来。而nosync则不会等待当前中断处理运行完毕。synchronize_irq会等到中断处理执行完毕。这些函数要配套使用,调用disable_xx 来禁止某个中断线后,对应需要调用 enable_xxx来恢复中断线。例如,disable_xxx被调用两次,则中断线只有在第二次调用enable_xxx的时候才会恢复。以上三种函数都可以在中断上下文以及进程上下文中执行,而且不会休眠。但在中断上下文中调用要注意。你并不希望去恢复一个你正在处理其中断函数的中断线。禁止一个共享的中断线是一种很粗鲁的做法。禁止一个中断线会禁止所有在这跳线上的设备投递中断。因为PCI设备都是共享中断线的,故此最好不要使用以上接口。

清楚当前的中断状态或知道目前是否运行在中断上下文是非常有用的。irqs_disabled 会在处于当前处理器的中断系统被禁止的情况下返回非零,否则返回零。两个宏:

in_interrupt 以及 in_irq 用来检测内核目前的上下文情况。第一个最有用。如果内核正在处理任何类型的中断处理时候,会返回非零。这里包括执行上半步或下半部的处理函数。而in_irqs仅仅在内核处理上半部处理函数的时候才返回非零。甚至,你希望能检测到目前是否运行在进程上下文而不是中断上下文,因为有些操作是不能运行在中断上下文的,如sleep。如果in_interrupt返回零,则表明目前运行在进程上下文。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: