中断和中断Handlers

任意一个操作系统的核心responsibility都是管理连接到机器上面的硬件---包括硬盘、键盘、鼠标、3D处理器等。为了满足这项responsibility,内核需要和机器本身通信,假如说处理器在维度上比他访问的硬件的速度快,那这对于内核来说是不理想的,因为它需要等待执行request却等待比他运行慢的硬件设备回应他;内核必须free to go,处理其他的事务,当该硬件完成了他的工作后,内核再处理这项硬件的请求。
但是处理器如何在不忽略硬件设备的性能缺陷的前提下跟硬件设备协同工作呢?其中的一个答案是polling。内核周期性的检测硬件状态,并根据对应的状态做出响应。但是polling是有缺陷的,因为无论硬件设备是ready还是active,他都需要重复的执行。一个更好的办法是给硬件提供一种机制去通知内核when the attention is needed.这种机制就叫做中断。在本章中,我们讨论中断和内核是如何响应他们的,with special functions called interrupt handlers.

7.1 Interrupts
中断使能硬件去通知处理器。比如说,当你打字的时候,键盘控制器发出一个电信号给处理器,告知操作系统新按下的按键。这些电信号就是中断。处理器接收到中断,通知操作系统使能操作系统响应新数据。根据处理器时钟信号,硬件设备异步产生中断,他们能在任何时候occur。因此,内核能够在任何时候被中断来处理中断。
中断是由物理信号产生,源于硬件设备,硬件上直连中断处理器,a simple chip that multiplexes multiple interrupt lines into a single line to processor.接收到中断后,中断控制器发送一个信号给处理器。处理器检测到信号,并中断当前的execution去处理中断。处理器能够通知操作系统,中断发生了,并且操作系统能够正确的处理中断。
不同的设备联系着不同的中断,中断又通过唯一的参数来区分不同的中断。通过这种方式,来自键盘的中断和来自硬盘的中断就被区分出来了。This让操作系统可以区分出不同的中断,区分哪一个设备产生哪一种中断。In turn,操作系统能够利用对应的Handler处理不同的中断。
These interrupt values通常叫做interrupt request lines(IRQ)。每一个IRQ line都会被分配特定的数值参数—比如说,在PC机中,IRQ0是定时器中断,IRQ1是键盘中断。但是不是所有的中断线是静态分配的。比如说,PCI总线的中断通常来说是动态分配的。其他的非PC架构有着相似的IRQ分配机制。The important notion is that特定的中断和特定的设备是相关联的,and内核清楚这一原则。硬件接下来处理中断,并告知kernel。

Exceptions
In OS text,exceptions are often discussed at the same time as interrupts.不像中断一样,exceptions是跟处理器时钟同步发生的。事实上,他们通常叫做同步中断。Exceptions是由处理器产生的,while执行指令响应编程错误(比如说,除零)或者其他必须由内核处理的非正常条件(比如说,a page fault)。因为很多处理器架构处理exceptions的方式和中断是类似的,所以内核处理这两者的方式也是相似的。本章讨论的对于interrupts(处理器产生的异步中断)的处理办法也同样适用于exceptions(处理器产生的同步中断)。
相信你已经对exception相当熟悉了:在之前的章节中,你已经看到了系统调用在X86中是如何完成的 by issuance of a software interrupt,which traps into kernel and causes execution of a special system call handler. 中断以类似的方式工作,你会看到,除了硬件而不是软件问题中断
7.2 Interrupt Handlers
内核运行的用于响应特定中断的函数叫做中断Handlers或者是interrupt service routine(ISR).每一个产生中断的设备都会绑定一个中断handler。比如说,一个函数Handler处理来自定时器的中断;但是另一个函数handler处理来自键盘的中断。设备的中断handler也是设备驱动的一部分。
在Linux中,中断handler是通常的C函数。他们匹配一个特殊的prototype,which使能以一种标准的方式内核传递handler信息。但是他们是ordinary的函数。中断handler和其他内核的函数的区别是内核调用他们响应中断并且他们运行在特殊的程序段叫做中断上下文。这个特殊的上下文通常来说叫做actomic context,因为我们执行在这段上下文的代码是不可阻塞的。
因为中断可能在任意的时刻发生,in turn,中断handler也能在任何时候执行。所以说中断handler需要快速的执行,再尽可能快的恢复中断代码的执行。因此,虽然操作系统无延迟的服务中断对硬件来说很重要,中断handler执行的尽可能的快也是很重要的。
中断handler的最重要的工作就是确认硬件的中断的接收。但是,通常来说,中断handler也有大量的工作需要完成。比如说网络设备的中断handler。在响应硬件之上,中断handler需要从硬件设备上拷贝数据包到内存中,process them,and push这些包到合适的协议栈或者应用。很明显的而是,这会有很多的工作,尤其是对于今天10G-bits的网卡来说。
7.3 Top Halves Versus Bottom Halves
中断handler快速响应和处理大量数据是互相冲突的两个概念。由于这两个冲突的概念的存在,中断处理被分为两个部分。中断handler叫做Top half。Top half在接收到中断确认信号后立即执行,并且仅执行有着严格时间限制的工作,比如说确认中断到来、复位硬件等。那些可以被滞后执行的工作叫做bottom half。Bottom half在将来,时间方便的时候,打开所有的中断去执行。Linux提供了多种机制去实现底半部机制。
我们一网卡为例来具体讨论top half和bottom half。当网卡接收到来自网络的包的时候,他们需要告知内核他们的到来。They want and need to do this immediately, to optimize
network throughput and latency and avoid timeouts.Thus, they immediately issue an
interrupt: Hey, kernel, I have some fresh packets here! The kernel responds by executing the
network card’s registered interrupt.
中断runs,acknowledges the hardware,拷贝新的网络数据包到内存,readies network card for more packets.这些工作是相当重要的,有着严格的时间要求,和硬件相关的工作。The kernel generally needs to quickly copy the networking packet into main memory because the network data buffer on the networking card is fixed and miniscule in size, particularly compared to main memory. Delays in copying the packets can result in a buffer overrun, with incoming packets overwhelming the networking card’s buffer and thus packets being dropped.After the networking data is safely in the main memory, the interrupt’s job is done, and it can return control of the system to whatever code was interrupted when the interrupt was generated.The rest of the processing and handling of the packets occurs later, in the bottom half. In this chapter, we look at the top half; in the next chapter, we study the bottom。
7.4 Registering an Interrupt Handler
中断handler负责管理硬件相关的工作。每一个设备都有着对应的驱动,and如果这项设备使用了中断,then对应的driver就会注册一个handler。
驱动能够注册一个handler并使能一个中断线用来处理函数request_irq(),which is declared in<linux/interrupt.h>:
/* 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,指明了分配的中断号。对于某些设备而言,这项参数是hard-coded的,比如说PC机的键盘和定时器。对于大多数其他设备,它是以编程和动态方式探测或以其他方式确定的。
第二项参数,handler,是函数指针,用于指向实际服务于该中断的handler。无论什么时候,系统接收到了中断后,该Handler都会被调用到。
Typedef irqreturn_t(*irq_handler_t)(int, void);
注意处理函数的特定原型:它takes两项参数并有一个irqreturn_t的返回值。
7.4.1 Interrupt Handler Flags
第三项参数,flags,可能是零或者a bit mask of one or more of the flags defined in <linux/interrupt.h>.在这些flags中,最重要的一部分flags在如下列举:
 IRQ_DISABLED
 IRQF_SAMPLE_RANDOM
 IRQF_TIMER
 IRQF_SHARED
第四项参数,name,是中断设备的名称。比如说,PC上面的键盘设备中断是keyboard.the text name被用来和用户层通信,by /proc/irq和/proc/interrupts。
第五项参数,dev,被用来共享中断线。当中断handler被释放后,dev会提供唯一的cookie给enable从中断线中移除中断handler。如果没有这项参数的话,内核是不知道从中断线移除哪一个中断handler的。如果中断没有被shared,你可以传递NULL参数;但是如果你的中断被shared,你必须传递唯一的cookie。每次中断handler被调用的时候,这项参数也会被传递进中断handler。通常的做法是传递设备的的结构体指针:该指针是唯一的,并且在handler内的话,也是有用处的。
如果成功的话,request_irq()返回0.返回值为非零的话为错误,意味着handler注册失败。A common error is –EBUSY,which indicates that要注册中断线已经被占用了,或者是当前用户未指明IRQF_SHARED。
Note that,request_irq()可能会睡眠,因此他不能在中断上下文调用或者任何不能阻塞的位置调用。常见的错误就是在sleep不安全的时候,调用request_irq()。这部分是因为request_irq()可以阻塞的原因:目前还不清楚。注册的时候,一个入口对应着一个中断,which is created in /proc/irq。函数proc_mkdir()创建了新的proc_fs目录,这个函数调用了proc_creat()创建新的procfs目录,which in turn调用到kmalloc去分配内存。而kmalloc是可能睡眠的。所以说,原因就在于此吧。
7.4.2 An Interrupt Example
在driver中,申请中断线和安装handler是通过request_irq()完成的。
if (request_irq(irqn, my_interrupt, IRQF_SHARED, “my_device”, my_dev)) {
printk(KERN_ERR “my_device: cannot register IRQ %d\n”, irqn);
return -EIO;
}
在这个例子中,irqn is the requested interrupt line; my_interrupt is the handler; we
specify via flags that the line can be shared; the device is named my_device; and we passed
my_dev for dev. On failure, the code prints an error and returns. If the call returns zero,
the handler has been successfully installed. 如前所述,当中断来临的时候,handler会被调用到。在设备最终初始化完成之前,顺序的初始化硬件,注册中断handler防止中断handler运行时相当重要的。
7.4.3 Freeing an Interrupt Handler
当你的driver被卸载后,你需要unregister你的中断handler并关闭中断线。To do this,call:
Void free_irq(unsigned int irq,void *dev);
如果specified中断线没有共享,this function会移除handler并关闭中断线。如果中断被共享了,通过Dev来区别的移除handler,但是中断线仅仅在最后一个handler被移除的时候才会关闭。无论是shared还是not shared的情况,如果Dev是非NULL的,it must match desired handler。调用free_irq()必须从进程上下文调用到。
Table7.1 reviews注册和卸载中断handler的函数.

7.5 Writing an Interupt Handler
如下就是中断handler的declaration;
Static irqreturn_t intr_handler(int irq, void *dev);
请注意,此声明与给定给request_irq()的handler参数的原型匹配。第一项参数,irq,是handler服务的中断线的数值参数。该参数会被传递金handler,但是使用的情况有限,except在打印log的时候。在Linux内核2.0之前,是没有Dev参数的,irq是用来区分使用相同driver和handler的不同设备的。比如说,PC中存在多个相同类型的硬盘。
第二项参数,dev,是一个指向相同dev的通用指针;如果这项参数是unique的,他可以用来区分使用相同handler的不同设备。dev还可以指向中断处理程序的使用结构。
中断handler的返回值是特殊的类型irqreturn_t.中断handler的返回值可能有两种值:IRQ_NONE和IRQ_HANDLED.前者指示出中断handler检测到该设备不是该中断的originator.后者指示出中断handler被正确的invoke,并且中断和设备匹配。中断handler is marked static,因为他从不会从外面的其他文件调用到。
中断handler的角色取决于设备本身和执行中断的原因。至少来说,大部分中断handler需要提供确认信号,知识内核已经接收到了中断。更加复杂的设备需要额外的发送和接收数据,并执行extended的工作,在中断handler。如前面讨论过的,这些extended工作需要移植到底半部。

Renentrancy and Interrupt Handlers
Linux中的中断handler不需要reentrant.当中断handler正在执行的时候,对应的中断线会被标记为正在使用中给所有的处理器,禁止相同中断线的中断再被接收,处理。通常来说,所有的其他的中断是enabled,其他的中断是serviced,当时当前中断线是disabled。因此,相同的中断handler是不可能同时调用到去服务嵌套的中断。这一行为极大的简化了中断handler的书写。

7.5.1 Shared Handlers
共享的中断handler和非共享的中断handler的注册和执行时类似的。不同点主要是以下三部分:
 The IRQ_SHARED flag must be set in the flags argument to request()_irq().
 每一个注册的handler的dev指针都是唯一的;任何指向设备结构的指针都是有效的,空指针是不被允许传入的。
 中断handler必须能够分别设备是否真正产生了中断。这就要求硬件支持和中断handler里面的逻辑支持
所有共享中断线的驱动都必须满足前面提到的要求。如果有任意一个设备不能公平的共享,其他的也都不能够共享中断线。如果request_irq()调用的时候flag是IRQ_SHARED,那么仅仅当当前中断线没有被shared或者所有的该中断线的中断handler都使用flags IRQ_SHARED的时候,该调用才会成功。但是共享的中断handler是可以混合着IRQF_DISABLED使用。
当内核接收到中断的时候,他会顺序的调用线上每一个handler。因此,中断handler区分是否产生了一个给定的中断的能力就变得很重要了。如果对应的设备没有产生中断,中断handler必须快速退出。这就要求硬件设备有一个状态寄存器用于handler检查,或者相似的机制可以使用。大多数的硬件设备都具有这种特性。
7.5.2 A Real-Life Interrupt Handler
If you wanna check the detail,reading the real book the kernel development 3.rd is good choice.
7.6 Interrupt Context
当执行中断handler的时候,内核是处于中断上下文的。Recall that进程上下文是内核在执行进程的时候的一种操作模式。在进程上下文中,current宏指向对应的task。更进一步说,由于进程是在进程上下文耦合进内核的,因此进程上下文是能够睡眠,或者调用调度器。
中断上下文,on the other hand,是根据进程无关的。Current宏也是不相关的(虽然它指向当前中断的进程)。如果没有备份进程,中断上下文将无法睡眠它将如何重新安排?因此在中断上下文中,有些特定的函数是不能够被调用的。
因为中断handler中断了其他的代码,所以说中断handler是对于时间敏感的;代码需要尽可能的快而简单。Busy looping可以应用,但是discouraged。This is an important point;always keep inmind that your interrupt handler code has interrupted other code.因为这种异步的机制,中断handler需要尽可能快和简单。Work应该被安排在底半部实现,which runs in a more convenient time.
中断handler stack是一个可选的配置项。Historically,中断handler没有自己的stack。Instead,他们会share被他们中断进程的stack。内核stack的大小通常为2 pages,通常来说在32位机器上是8KB,64位机器上是16KB。由于中断handler共享stack,所以他们allocate到stack的代码需要尽可能的简单。当然,内核堆栈是有限的,所以所有的内核代码都应该谨慎。
7.7 Implementing Interrupt Handlers
Perhaps not surprising,Linux系统中的中断handler的完成是跟硬件架构相关的,这依赖于处理器,所使用的中断控制器以及the design of architecture and machine.
Figure7-1展示了中断和硬件、内核之间的关系。

The do_IRQ()函数is declared as
	Unsigned int do_IRQ(struct pt_regs regs)
Because the C calling convention places function arguments at the top of the stack,the pt_regs structure contains the initial register values that were previously saved in the assembly entry routine.由于中断value was alse saved,do_IRQ() can extract it.中断线计算好以后,do_IRQ()确认中断的到来,并关闭这条线上的中断的到来。在通常的PC机器上来说,这些操作是由mask_and_ack_8259A()处理的。
接下来,do_IRQ()确认有一个有效的handler注册在this line,并且是被使能未运行的。如果上述条件都满足的话,it calls handle_IRQ_event() to run the installed interrupt handler for the line。
首先,由于处理器禁用了中断,除非在处理程序注册期间指定了irqf_disabled,否则它们将重新打开。Recall that IRQF_DISABLED说明了handler必须在中断关闭的情况下运行。Next,每一个handler都是在loop中执行的。如果this line是没有被shared,the loop会在第一次循环后结束。Otherwise,all handlers are executed.Finally,interrupts再次被关闭and function返回。Back in do_irq(),function cleans up and returns to the initial entry point,which jumps to ret_from_intr()。

7.7.1 /proc/interrupts
Procfs是一个虚拟的文件系统,它仅存在于内核内存中,通常来说是挂载在/proc。在proc目录下读写文件都会涉及到内核函数模拟读写文件。一个例子就是/proc/interrupts。
For more details,to check it out in google is a better choice.
For the curious,pfofs代码主要位于fs/proc。函数show_interrupts()提供了/proc/interrupts,它当然是和硬件架构相关的。
7.8 Interrupt Control
Linux内核完成了众多的接口用于操作中断的状态;利用这些接口,你可以关闭当前处理器的中断,或者mask out一台机器的中断线。这些routine都是和硬件架构相关的。
控制中断系统的原因通常归结为需要提供同步。通过关中断,你可以保证中断handler不会抢占你当前的代码。Moreover,关闭中断也关闭了内核的抢占。但是无论是关闭中断还是关闭内核抢占都不能阻止来自其他处理器的并发访问。由于Linux支持多处理器运行,内核代码更需要某些锁来阻止其他处理器同时访问共享数据。这些锁通常combined with关中断。这些锁阻止了其他处理器的并发访问,但是关闭中断阻止了可能的中断handler的并发访问。第九、十章详细讨论了各种同步的问题及解决办法。不管怎样,理解内核中断控制接口是很重要的。
7.8.1 Disabling and Enabling Interrupts
关闭本地处理器的中断和重新使能,do the following:
Local_irq_disable();
/* interrupts are disabled*/
Local_irq_enable();
这些函数通常来说是as a assembly operation(Of course,it depends on the architecture).事实上,X86上面,local_irq_disable()是cli,local_irq_enable()也是一个简单的sti指令。Cli和sti都是assembly调用去清除和设置allow interrupts标志位。换句话说,他们关闭和使能当前处理器的中断delivery。
如果在调用Local_irq_disable()函数之前中断就已经被关闭了,在调用它是很危险的。相关的call local_irq_enable()会unconditionally使能中断,despite that they were off to begin with.因此,我们需要一种机制去储存中断的上一个状态。This common concern是由于:内核代码可能被关闭中断的代码访问或者非关闭中断代码访问到而这取决于具体的代码path。
For example,如果一段代码同时被两个函数访问,一个关闭了中断,另一个没有关闭中断。由于内核代码变得越来越庞大和复杂,想要知道一个函数的具体paths变得很困难。在关闭中断系统之前存储他的状态就变得很安全了。Then,当你需要reenable中断的时候,你只需要把它恢复到原来的状态即可:
Unsigned long flags;

Local_irq_save(flags);/* interrupts are now disabled */

/……/
Local_irq_restore(flags);/* interrupts are restored to their previous state */
Note that these methods are implemented at least in part as macros,so the flags parameter is seemingly passed by value.这些参数包含硬件架构相关的数据,包括中断系统的状态。Because at least one support atchitecture incorporates stack information into the value,flags cannot be passed to another function(speciafically,it must remain on the same stack frame).因为这些原因,存储和恢复中断都必须在同一个函数中。
所有的之前的函数,既可以在中断上下文中调用,也可以在进程上下文中调用。
7.8.2 Disabling a specific Interrupt Line
在之前的章节中,我们了解到关闭整个处理器中断到来的方法。在有些情况下,关闭系统中某些中断线也是很有帮助的。This is called masking out an interrupt line.As an example,你可能想在操作他的state之前关闭设备中断的到来。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);
头两个函数是在中断控制器关闭给定的中断线。This关闭了整个系统所有处理器的给定中断。除此之外,函数disable_irq() doesn’t return until any currently executing handler completes.因此,调用者不仅仅需要确认给定中断线不会有中断到来,还需要确认任意正在执行的中断handler都已经退出了。函数disable_irq_nosync()不会等待当前handler完成。
函数synchronize_irq()等待特定的中断处理程序退出(如果它正在执行),然后返回。
调用这些函数需要一些配对使用;比如说:每次调用disable_irq()和disable_irq_nosync()在给定的中断线,都需要配合enable_irq()。只有在最后的call enable_irq()才是中断线实际被使能。比如说,如果disable_irq()被调用了两次,直到第二次调用enable_irq()之前,中断线是不会被重新使能的。
上述的三个函数在中断context和进程context都可以调用,并且不会睡眠。如果在中断context调用他们需要注意。因为你不想使能你正在处理的中断线吧。(recall that中断线的handler在使用的时候是被mask out的)。
关闭多个中断handler共享的中断线是非常rude。关闭了中断线就关闭了这条线上的所有设备的中断delivery。因此,一些新的设备是不倾向于使用这些接口的。因为说,PCI涉笔不得不支持共享中断线,因此他们也不应该使用这些接口。Thus,disable_irq()和他的friends在older的设备中更加常见,比如说PC的并口。
7.8.3 Status of the Interrupt System
直到中断系统的状态或者说直到他是否执行在中断context通常来说是很有用的。
The macro irqs_disabled()返回非零值,如果当前处理器的中断系统被关闭了。反之,返回0。
两个宏提供了借口去检测内核当前的context。他们是:
In_interrupt();
In_irq();
最常用的是第一个:如果内核在执行任何类型的中断handling它会返回非零值。这包括,中断handler和底半部handler。宏in_irq()仅仅在内核在执行特定的handler的时候才会返回非零值。
More often,你想检查你是不是在进程context。也就说你想确认自己不再中断context。这样做的原因通常是因为,the code需要做些必须在进程context中的工作,比如说sleep。如果in_interrupt()返回了零值,内核就是在进程context。
是的,这命名有些拗口。表7.2总结了中断控制的方法及相关的描述。

  • 2
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值