编写Linux中断处理程序(上半部)(读书笔记)

January 7th, 2014 by JasonLe's Tech Leave a reply »

今天看了Linux中断处理的前半部分。中断,本质上是一种特殊的电信号。


由硬件设备发向处理器,处理器接收到中断后,会马上向操作系统反应此信号的带来,然后就由OS负责处理这些新到来的数据,中断可以随时发生,才不用操心与处理器的时间同步问题。不同的设备对应的中断不同,他们之间的不同从操作系统级来看,差别就在于一个数字标识—–中断号。专业一点就叫中断请求(IRQ)线,通常IRQ都是一些数值量。有些体系结构上,中断好是固定的,有的是动态分配的,这不是问题所在,问题在于特定的中断总是与特定的设备相关联。

但是我们必须分清楚中断和异常的区别。
异常和中断不一样,它在产生时必须要考虑与处理器的时钟同步,实际上,异常也常常称为同步中断,在处理器执行到由于编程失误而导致的错误指令的时候,或者是在执行期间出现特殊情况,必须要靠内核来处理的时候,处理器就会产生一个异常。因为许多处理器体系结构处理异常以及处理中断的方式类似,因此,内核对它们的处理也很类似。这里的讨论,大部分都是适合异常,这时可以看成是处理器本身产生的中断。

Linux中断处理主要分为两部分:上部分为响应中断处理程序,下部分从中断处理程序返回后,处理中断。
处理中断首先要注册一个中断处理程序:目标函数在kernel/irq/manage.c

<
int request_percpu_irq(unsigned int irq, irq_handler_t handler,
 const char *devname, void __percpu *dev_id)
{
 struct irqaction *action;
 struct irq_desc *desc;
 int retval;

 if (!dev_id)
 return -EINVAL;

 desc = irq_to_desc(irq);
 if (!desc || !irq_settings_can_request(desc) ||
 !irq_settings_is_per_cpu_devid(desc))
 return -EINVAL;

 action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
//用kzalloc申请内存的时候, 效果等同于先是用 kmalloc() 申请空间 , 然后用 memset() 来初始化 ,所有申请的元素都被初始化为 0.内核内存的//正常分配. 可能睡眠.
 if (!action)
 return -ENOMEM;

 action->handler = handler;
 action->flags = IRQF_PERCPU | IRQF_NO_SUSPEND;
 action->name = devname;
 action->percpu_dev_id = dev_id;

 chip_bus_lock(desc);
 retval = __setup_irq(irq, desc, action);
 chip_bus_sync_unlock(desc);

 if (retval)
 kfree(action);

 return retval;
}

一旦注册了一个中断处理程序,就肯定会有释放中断处理,这是调用下列函数:

void free_irq(unsigned int irq, void *dev_id)
{
 struct irq_desc *desc = irq_to_desc(irq);

 if (!desc || WARN_ON(irq_settings_is_per_cpu_devid(desc)))
 return;

#ifdef CONFIG_SMP
 if (WARN_ON(desc->affinity_notify))
 desc->affinity_notify = NULL;
#endif

 chip_bus_lock(desc);
 kfree(__free_irq(irq, dev_id));
 chip_bus_sync_unlock(desc);
}

声明中断处理函数

static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs)

中断处理程序的返回值是一个特殊类型,irqrequest_t,可能返回两个特殊的值:IRQ_NONE和IRQ_HANDLED.当中断处理程序检测到一个中断时,但该中断对应的设备并不是在注册处理函数期间指定的产生源时,返回IRQ_NONE;

1.request_irq()的参数flags必须设置为SA_SHIRQ标志。
2.对每个注册的中断处理来说,dev_id参数必须唯一。指向任一设备结构的指针就可以满足这一要求。通常会选择设备结构,因为它是唯一的,而且中断处理程序可能会用到它,不能给共享的处理程序传递NULL值。

初始入口点只是在栈中保存这个号,并存放当前寄存器的值(这些值属于被中断的任务);然后,内核调用函数do_IRQ().从这里开始,大多数中断处理代码是用C写的。do_IRQ()的声明如下:

arch/x86/kernel/irq.c

unsigned int do_IRQ(struct pt_regs regs)

因为C的调用惯例是要把函数参数放在栈的顶部,因此pt_regs结构包含原始寄存器的值,这些值是以前在汇编入口例程中保存在栈上的。中断的值也会得以保存,所以,do_IRQ()可以将它提取出来,X86的代码为:

int irq = regs.orig_eax & 0xff

计算出中断号后,do_IRQ()对所接收的中断进行应答,禁止这条线上的中断传递。在普通的PC机器上,这些操作是由mask_and_ack_8259A()来完成的,该函数由do_IRQ()调用。接下来,do_IRQ()需要确保在这条中断线上有一个有效的处理程序,而且这个程序已经启动但是当前没有执行。如果这样的话, do_IRQ()就调用handle_IRQ_event()来运行为这条中断线所安装的中断处理程序.
下边给出PC机上位于/proc/interrupts文件的输出结果,这个文件存放的是系统中与中断相关的统计信息,这里就解释一下这个表:
20110717105042656
上面是这个文件的输入,第一列是中断线(中断号),第二列是一个接收中断数目的计数器,第三列是处理这个中断的中断控制器,最后一列是与这个中断有关的设备名字,这个名字是通过参数devname提供给函数request_irq()的。最后,如果中断是共享的,则这条中断线上注册的所有设备都会列出来,如4号中断。

Linux内核给我们提供了一组接口能够让我们控制机器上的中断状态,这些接口可以在<asm/system.h>和<asm/irq.h>中找到。一般来说,控制中断系统的原因在于需要提供同步,通过禁止中断,可以确保某个中断处理程序不会抢占当前的代码。此外,禁止中断还可以禁止内核抢占。然而,不管是禁止中断还是禁止内核抢占,都没有提供任何保护机制来防止来自其他处理器的并发访问。Linux支持多处理器,因此,内核代码一般都需要获取某种锁,防止来自其他处理器对共享数据的并发访问,获取这些锁的同时也伴随着禁止本地中断。锁提供保护机制,防止来自其他处理器的并发访问,而禁止中断提供保护机制,则是防止来自其他中断处理程序的并发访问。

在linux设备驱动理论帖里详细介绍过linux的中断操作接口,这里就大致过一下,禁止/使能本地中断(仅仅是当前处理器)用:

local_irq_disable();
local_irq_enable();

如果在调用local_irq_disable()之前已经禁止了中断,那么该函数往往会带来潜在的危险,同样的local_irq_enable()也存在潜在的危险,因为它将无条件的激活中断,尽管中断可能在开始时就是关闭的。所以我们需要一种机制把中断恢复到以前的状态而不是简单地禁止或激活,内核普遍关心这点,是因为内核中一个给定的代码路径可以在中断激活饿情况下达到,也可以在中断禁止的情况下达到,这取决于具体的调用链。面对这种情况,在禁止中断之前保存中断系统的状态会更加安全一些。相反,在准备激活中断时,只需把中断恢复到它们原来的状态:

unsigned long flags;
local_irq_save(flags);
local_irq_restore(flags);

参数包含具体体系结构的数据,也就是包含中断系统的状态。至少有一种体系结构把栈信息与值相结合(SPARC),因此flags不能传递给另一个函数(换句话说,它必须驻留在同一个栈帧中),基于这个原因,对local_irq_save()的调用和local_irq_restore()的调用必须在同一个函数中进行。前面的所有的函数既可以在中断中调用,也可以在进程上下文使用。

前面我提到过禁止整个CPU上所有中断的函数。但有时候,好奇的我就想,我干么没要禁止掉所有的中断,有时,我只需要禁止系统中一条特定的中断就可以了(屏蔽掉一条中断线),这就有了我下面给出的接口:

void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);
void synchronise_irq(unsigned int irq);

对有关函数的说明和注意,我前边已经说的很清楚了,这里飘过。另外,禁止多个中断处理程序共享的中断线是不合适的。禁止中断线也就禁止了这条线上所有设备的中断传递,因此,用于新设备的驱动程序应该倾向于不使用这些接口。另外,我们也可以通过宏定义在<asm/system.h>中的宏irqs_disable()来获取中断的状态,如果中断系统被禁止,则它返回非0,否则,返回0;用定义在<asm/hardirq.h>中的两个宏in_interrupt()和in_irq()来检查内核的当前上下文的接口。由于代码有时要做一些像睡眠这样只能从进程上下文做的事,这时这两个函数的价值就体现出来了。