Linux异常处理体系结构

嵌入式Linux设备驱动开发之Linux异常处理体系结构

《嵌入式Linux应用完全开发手册》第4篇第20章总结归纳

本章目标

  1. 了解Linux异常处理体系结构
  2. 掌握Linux中断处理体系结构,了解几种重要的数据结构
  3. 学习中断处理函数的注册、处理、卸载流程
  4. 掌握在驱动程序中使用中断的方法

Linux异常处理体系结构概述

Linux异常处理的层次结构

内核的中断处理结构有很好的扩充性,并适当屏蔽了一些实现细节。但是开发人员一个深入“黑盒子”了解其中的实现原理。

异常的作用

异常,就是可以打断CPU正常运行流程的一些事情,比如外部中断、未定义的指令、视图修改只读的数据、执行swi(软中断)指令等。当这些事情发生时,CPU暂停当前的程序,先处理异常事件,然后再继续执行被中断地程序。操作系统中经常通过异常来完成一些特定地功能。

  1. 当CPU执行未定义的机器指令将触发“未定义指令异常”,操作系统可以利用这个特点使用一些自定义的机器指令,它们在异常处理函数中实现。
  2. 可以将一块数据设为只读的,然后提供给多个进程使用,这样可以节省内存。当某个进程视图修改其中的数据时,将触发“数据访问中止异常”,在异常处理函数中将这块数据复制出一份可写的副本,提供给这个进程使用。
  3. 当用户程序试图读写的数据或执行的指令不在内存中,也会触发一个“数据访问中止异常”或“指令预取中止异常”,在异常处理函数中将这些数据或指令读入内存(内存不足时还可以将不使用的数据、指令换出内存),然后重新执行被中断的程序。这样可以节省内存,还使得操作系统可以运行这类程序:它们使用的内存远大于实际的物理内存。
  4. 当程序使用不对齐的地址访问内存时,也会触发“数据访问中止异常”,在异常处理程序中先使用多个对齐的地址读出数据;对于读操作,从中选取数据组合好后返回给被中断的程序;对于写操作,修改其中的部分数据后再写入内存。这使得程序(特别是应用程序)不用考虑地址对齐的问题。
  5. 应用程序可以通过“swi”指令触发“swi异常”,操作系统在swi异常处理函数中实现各种系统调用。

Linux内核对异常的设置

内核在start_kernel函数中调用trap_initinit_IRQ两个函数来设置异常的处理函数。

trap_init函数分析

trap_init函数(arch/arm/kernel/trap.c)被用来设置各种异常的处理向量,包括中断向量。所谓向量,就是一些被安放在固定位置的代码,当发生异常时,CPU会自动执行这些固定位置上的指令。ARM架构的CPU的异常向量基址可以是0x00000000,也可以是0xffff0000,Linux内核使用后者。trap_init函数将异常向量复制到0xffff0000处,部分代码如下:

1
2
3
4
5
6
7
void __init trap_init(void)
{
...
memcpy((void *)vectors,__vectors_start,__vectors_end - __vectors-start);
memcpy((void *)vectors + 0x200,__stubs_start,__stubs_end - __stubs_start);
...
}

第4行中,vectors等于0xffff0000。地址__vectors_start-_vectors_end之间的代码就是异常向量,在arch/arm/kernel/entry-armv.S中定义,它们被复制到地址0xffff0000处。
异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被中断程序的执行环境并重新运行。这些“更复杂的代码”在地址__stubs_start-__stubs_end之间,它们在arch/arm/kernel/entry-armv.S中定义。第5行将它们复制到地址0xffff0000+0x200处。
异常向量跳去执行的代码都是使用汇编写的,为给读者一个形象概念,下面讲解部分代码,他们在arch/zarm/kernel/entry-armv.S中。
异常向量的代码如下,其中的“stubs_offset”用来重新定位跳转的位置(向量被复制到地址0xffff0000处,跳转的目的代码被复制到地址0xffff0000+0x200处)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
	.equ	stubs_offset, __vectors_start + 0x200 - __stubs_start

.globl __vectors_start
__vectors_start:
swi SYS_ERROR0 //复位时,CPU将执行这条指令
b vector_und + stubs_offset //未定义异常时,CPU将执行这条指令
ldr pc, .LCvswi + stubs_offset //swi异常
b vector_pabt + stubs_offset //指令预取中止
b vector_dabt + stubs_offset //数据访问中止
b vector_addrexcptn + stubs_offset //没有用到
b vector_irq + stubs_offset //irq异常
b vector_fiq + stubs_offset //fiq异常

.globl __vectors_end
__vectors_end:

其中的vector_und、vector_pabt等表示要跳转去执行的代码。以vector_und为例,它仍在arch/arm/kernel/entry-armv.S中,通过vector_stub宏来定义,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vector_stub	und, UND_MODE

.long __und_usr @ 0 (USR_26 / USR_32) 在用户模式执行了未定义的指令
.long __und_invalid @ 1 (FIQ_26 / FIQ_32) 在FIQ模式执行了未定义的指令
.long __und_invalid @ 2 (IRQ_26 / IRQ_32) 在IRQ模式执行了未定义的指令
.long __und_svc @ 3 (SVC_26 / SVC_32) 在管理模式执行了未定义的指令
.long __und_invalid @ 4
.long __und_invalid @ 5
.long __und_invalid @ 6
.long __und_invalid @ 7
.long __und_invalid @ 8
.long __und_invalid @ 9
.long __und_invalid @ a
.long __und_invalid @ b
.long __und_invalid @ c
.long __und_invalid @ d
.long __und_invalid @ e
.long __und_invalid @ f

第1行的vector_stub是一个宏,它根据后面的参数“und,UND_MODE”定义了以“vector_und”为标号的一段代码。vector_stub宏的功能为:计算处理完异常后的返回地址、保存一些寄存器(比如r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式调用第3-18行中的某个跳转分支。当发生异常时,CPU会根据异常的类型进入某个工作模式,但是很快vector_stub宏又会强制CPU进入管理模式,在管理模式下进行后续处理,这种方法简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。
第3-18行中的代码表示在各个工作模式中执行未定义指令时,发生的异常的处理分支。比如第3行的__und_usr表示在用户模式下执行未定义指令时,所发生的未定义异常将由它来处理;第6行的__und_svc表示在管理模式下执行未定义指令,所发生的异常将由它来处理。在其他工作模式下不可能发生未定义指令异常,否则使用__und_invalid来处理错误。ARM架构CPU中使用4位数据表示工作模式(目前只有7种工作模式),所有共有16个跳转分支。
不同的跳转分支(比如__und_usr__und_svc)只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体相同,都是调用相应的C函数。比如未定义指令异常发生时,最终会调用C函数do_undefinstr函数进行处理。各种的异常C处理函数可以分为5类,它们分布在不同的文件种。

  1. arch/arm/kernel/traps.c
    未定义指令异常的C处理函数在这个文件中定义,总入口函数为do_undefinstr
  2. arch/arm/mm/fault.c
    与内存访问相关的异常的C处理函数在这个文件中定义,比如数据访问中止异常、指令预取中止异常。总入口函数为do_DataAbortdo_PrefetchAbort
  3. arch/arm/mm/irq.c
    中断处理函数的这个文件中定义,总入口函数为asm_do_IRQ,它调用其他文件注册的中断处理函数。
  4. arch/arm/kernel/calls.S
    在这个文件中,swi异常的处理函数被组织成一个表格;swi指令机器码的位[23:0]被用来作为索引。这样,通过不同的“swi index”指令就可以调用不同的swi异常处理函数,它们被称为系统调用,比如sys_open、sys_read、sys_write
  5. 没有使用的异常
    在Linux 2.6.22.6中没有使用FIQ异常。
    trap_init函数搭建了各类异常的处理框架。当发生异常时,各种C处理函数会被调用。这些C函数还要进一步细分异常发生的情况,分别调用更具体的处理函数。比如未定义指令异常的C处理函数总入口为do_undefinstr,这个函数里还要根据具体的未定义指令调用它的模拟函数。
    除了中断外,内核已经为各类异常准备了细致而完备的处理函数,比如swi异常处理函数为每一种系统调用都准备了一个sys_开头函数,数据访问中止异常的处理函数为对齐错误、页权限错误、段翻译错误等具体异常都准备了相应的处理函数。这些异常的处理函数与开发板的配置无关,基本不用修改。

init_IRQ 函数分析

中断也是一种异常,之所以把它单独提出来,是因为中断的处理与具体开发板密切相关,除一些必须、共用的中断(系统时钟中断、片内外设UART中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个非常容易扩充的中断处理体系。
init_IRQ函数(代码在arch/arm/kernel/irq.c中)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数asm_do_IRQ就可以调用这些函数作进一步处理。
下图为异常处理体结构:
img not found

常见的异常

ARM架构Linux内核中,只用到了5种异常,在它们的处理函数中进一步细分发生这些异常的原因。

异常总类 异常细分
未定义指令异常 ARM 指令break
Thumb 指令break
ARM 指令mrc
指令预取中止异常 取值时地址翻译错误(translation fault),系统中还没有为这个指令建立映射关系
数据访问中止异常 访问数据时段地址翻译错误(section translation fault)
访问数据时页地址翻译错误(page translation fault)
地址对齐错误
段权限错误(section permission fault)
页权限错误(page permission fault)
中断异常 GPIO中断、WDT中断、定时器中断、USB中断、UART中断等
swi异常 各类下图调用 sys_open、sys_read、sys_write

Linux 中断处理体系结构

中断处理体系结构的初始化

中断处理体系结构

Linux内核将所有的中断统一编号,使用一个irq_desc结构数组来描述这些中断:每个数组项对应一个中断(也有可能是一组中断,它们共用相同的中断号),里面记录了中断的名称、中断状态、中断标记(比如中断类型、是否共享中断等),并提供了中断的底层硬件访问函数(清除、屏蔽、使能中断),提供了这个中断的处理函数入口,通过它可以调用用户注册的中断处理函数。
通过irq_desc结构数组就可以了解中断处理体系结构,irq_desc结构的数组类型在include/linux/irq.h中定义,如下所示:

1
2
3
4
5
6
7
8
9
struct irq_desc {
irq_flow_handler_t handle_irq; //当前中断的处理函数入口
struct irq_chip *chip; //底层的硬件访问
...
struct irqaction *action; /* 用户提供的中断处理函数链表 */
unsigned int status; /* IRQ状态 */
...
const char *name; //中断名称
} ____cacheline_internodealigned_in_smp;

第2行的handle_irq是这个或这组中断的处理函数入口。发生中断时,总入口函数asm_do_IRQ将根据中断号调用相应irq_desc数组项中的handle_irqhandle_irq使用chip结构中的函数来清除、屏蔽或者重新使能中断,还一一调用用户在actions链表中注册的中断处理函数。
第3行的irq_chip结构类型也是在include/linux/irq.h中定义,其中的成员大多用于操作底层硬件,比如设置寄存器以屏蔽中断、使能中断、清除中断等。这个结构的部分成员如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct irq_chip {
const char *name;
unsigned int (*startup)(unsigned int irq); //启动中断,如果不设置,缺省为“enable”
void (*shutdown)(unsigned int irq); //关闭中断,如果不设置,缺省为“disable”
void (*enable)(unsigned int irq); //使能中断,如果不设置,缺省为“unmask”
void (*disable)(unsigned int irq); //禁止中断,如果不设置,缺省为“mask”

void (*ack)(unsigned int irq); //响应中断,通常是清除当前中断使得可以接收下一个中断
void (*mask)(unsigned int irq); //屏蔽中断源
void (*mask_ack)(unsigned int irq); //屏蔽和响应中断
void (*unmask)(unsigned int irq); //开启中断源
...
};

irq_desc结构中第5行的irqaction结构类型在include/linux/interrupt.h中定义。用户注册的每个中断处理函数用一个irqaction结构来表示,一个中断(比如共享中断)可以有多个处理函数,它们的irqaction结构链接成一个链表,以action为表头。irq_desc结构定义如下:

1
2
3
4
5
6
7
8
9
10
struct irqaction {
irq_handler_t handler; //用户注册的中断处理函数
unsigned long flags; //中断标志,比如是否共享中断、电平触发还是边沿触发
cpumask_t mask; //用于SMP(对称多处理系统)
const char *name; //用户注册的中断名字,“cat /proc/interrupts”时可以看到
void *dev_id; //用户传给上面的handler的参数,还可以用来区分共享中断
struct irqaction *next;
int irq; //中断号
struct proc_dir_entry *dir;
};

irq_desc结构数组、它的成员struct irq_chip *chipstruct irqaction *action,这3种数据结构成了中断处理体系的框架。这3者的关系如图所示:
img not found
中断的处理流程如下:

  1. 发生中断时,CPU执行异常向量vector_irq的代码
  2. 在vector_irq里面,最终会调用中断处理的总入口函数的代码
  3. asm_do_IRQ根据中断号调用irq_desc数组项中的handle_irq
  4. handle_irq会使用chip成员中的函数来设置硬件,比如清除中断、禁止中断、重新使能中断。
  5. handle_irq逐个调用用户在action链表中注册的处理函数
    可见,中断体系结构的初始化就是构造这些数据结构,比如irq_desc数组项中的handle_irq、chip等成员;用户注册中断时就是构造action链表;用户卸载中断时就是从action链表中去除不需要的项。

中断处理体系结构的初始化

init_IRQ函数被用来初始化中断处理体系结构,代码在arch/arm/kernel/irq.c中。

1
2
3
4
5
6
7
8
9
void __init init_IRQ(void)
{
int irq;

for (irq = 0; irq < NR_IRQS; irq++)
irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;
...
init_arch_irq();
}

第5-6行初始化irq_desc结构数组中每一项的中断状态。
第8行调用架构相关的中断初始化函数。对于S3C2410、S3C2440开发板,这个函数就是s3c24xx_init_irq,移植Linux内核时讲述的machine_desc结构中的init_irq成员就是指向这个函数。
s3c24xx_init_irq函数在arch/arm/plat-s3c24xx/irq.c中定义,它为所有的中断设置了芯片相关的数据结构(irq_desc[irq].chip),设置了处理函数入口(irq_desc[irq].handle_irq)。以外部中断EINT4-EINT23为例,用来设置它们的代码如下:

1
2
3
4
5
6
for(irqno = IRQ_EINT4; irqno <= IRQ_EINT23; irqno++) {
irqdbf("registering irq %d (extended s3c irq)\n",irqno);
set_irq_chip(irqno, &s3c_irqext_chip);
set_irq_handler(irqno, handle_edge_irq);
set_irq_flags(irqno, IRQF_VALID);
}

set_irq_chip的作用就是irq_desc[irqno].chip = &s3c_irqext_chip。以后就可以通过irq_desc[irqno].chip结构中的函数指针设置这些外部中断的触发方式(电平触发、边沿触发等)、使能中断、禁止中断等。
第4行设置这些中断的处理函数入口为handle_edge_irq,即irq_desc[irqno].handle_irq = handle_edge_irq。发生中断时,handle_edge_irq函数会调用用户注册的具体处理函数。
第5行设置中断标志为“IRQF_VALID”,表示可以使用它们。

用户注册中断处理函数的过程

用户(即驱动程序)通过request_irq函数向内核注册中断处理函数,request_irq函数根据中断号找到irq_desc数组项,然后在它的action链表中添加一个表项。
request_irq函数在kernel/irq/manage.c中定义,函数原型如下:

1
int request_irq(unsigned int irq, irq_handle_t handler, unsigned long irqflags, const char *devname, void *dev_id)

request_irq函数首先使用这4个参数构造一个irqaction结构,然后调用setup_irq函数将它链入链表中,代码如下:

1
2
3
4
5
6
7
8
9
10
	action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
...
action->handler = handler;
action->flags = irqflags;
cpus_clear(action->mask);
action->name = devname;
action->next = NULL;
action->dev_id = dev_id;
...
retval = setup_irq(irq, action);

setup_irq函数也是在kernel/irq/manage.c中定义,它完成如下3个功能

  1. 将新建的irqaction结构链入irq_desc[irq]结构的action链表中,有两种可能。
    ①如果action链表为空,则直接链入。
    ②否则先判断新建的irqaction结构和链表中的irqaction结构所表示的中断类型是否一致,即是否都声明为“可共享的”(IRQF_SHARED)、是否都使用相同的触发方式(电平、边沿、极性),如果一致,则将新建的irqaction结构链入。
  2. 设置irq_desc[irq]结构中chip成员的还没设置的指针,让它们指向一些默认函数
    这通过irq_chip_set_defaults函数来完成,它在kernel/irq/chip.c中定义。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void irq_chip_set_defaults(struct irq_chip *chip)
    {
    if (!chip->enable)
    chip->enable = default_enable; //它调用chip->unmask
    if (!chip->disable)
    chip->disable = default_disable; //此函数为空
    if (!chip->startup)
    chip->startup = default_startup; //它调用chip->enable
    if (!chip->shutdown)
    chip->shutdown = chip->disable;
    if (!chip->name)
    chip->name = chip->typename;
    if (!chip->end)
    chip->end = dummy_irq_chip.end;
    }
  3. 设置中断的触发方式
    如果request_irq函数中传入的irqflags参数表示中断的触发方式为高电平触发、低电平触发、上升沿触发或下降沿触发,则调用irq_desc[irq]结构中的chip->set_type成员函数来进行设置:设置引脚功能为外部中断,设置中断触发方式。
  4. 启动中断
    如果irq_desc[irq]结构中status成员没有指明为IRQ_NOAUTOEN(表示注册中断时不要使能中断),还要调用chip->startup或chip->enable来启动中断。所谓启动中断通常就是使能中断。

一般来说,只有那些“可以自动使能的”中断对应irq_desc[irq].status才会被指明为IRQ_NOAUTOEN。所以,无论哪种情况,执行request_irq注册中断之后,这个中断就已经被使能了,在编写驱动程序时要注意这点。
总结一下使用request_irq函数注册中断后的“成果”。
①irq_desc[irq]结构中的action链表中已经链入了用户注册的中断处理函数
②中断的触发方式已经被设置好
③中断已经被使能
总之,执行irq_request函数之后,中断就可以发生并能够被处理了。

中断的处理过程

asm_do_IRQ是中断的C语言总入口函数,它在arch/arm/kernel/irq.c中定义,部分代码如下:

1
2
3
4
5
6
7
8
asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc = irq_desc + irq;
...
desc_handle_irq(irq, desc);
...
}

第6行的desc_handle_irq函数直接调用desc结构中的handle_irq成员函数,它就是irq_desc[irq].handle_irq。
需要说明的是,asm_do_IRQ函数中参数irq的取值范围为IRQ_EINT0-(IRQ_EINT0 + 31),只有32个取值。它可能是一个实际中断的中断号,也可能是一组中断的中断号。这是由S3C2410、S3C2440的芯片特性决定的:发生中断时INTPND寄存器的某一位被置1,INTOFFSET寄存器中记录了是哪一位(0-31),中断向量调用asm_do_IRQ之前根据INTOFFSET寄存器的值确定irq参数。每一个实际的中断在irq_desc数组中都有一项与它对应,它们的数目不止32。当asm_do_IRQ函数中参数irq表示的是一组中断时,irq_desc[irqno].handle_irq来进一步处理。
以外部中断EINT8-EINT32为例,它们通常是边沿触发。

  1. 它们被触发时,INTOFFSET寄存器中的值都是5,asm_do_IRQ函数中参数irq的值为IRQ_EINT0 + 5,即IRQ_EINT8t23。上面代码中第6行将调用irq_desc[IRQ_EINT8t23].handle_irq来进行处理。
  2. irq_desc[IRQ_EINT8t23].handle_irq在前面init_IRQ函数初始化中断体系结构的时候被设为s3c_irq_demux_extint8
  3. s3c_irq_demux_extint8函数的代码在arch/arm/plat-s3c24xx/irq.c中,它首先读取EINTPND、EINTMASK寄存器,确定发生了哪些中断,重新计算它们的中断号,然后调用irq_desc数组项中的handle_irq成员函数。
    代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    static void
    s3c_irq_demux_extint8(unsigned int irq,
    struct irq_desc *desc)
    {
    unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND); //EINT8-EINT23发生时,相应位被置1
    unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK); //屏蔽寄存器

    eintpnd &= ~eintmsk; //清除被屏蔽的位
    eintpnd &= ~0xff; //清除低8位(EINT8对应位8,...)

    /* 循环处理所有的子中断 */

    while (eintpnd) {
    irq = __ffs(eintpnd); //确定eintpnd中为1的最高位
    eintpnd &= ~(1<<irq); //将此位清0

    irq += (IRQ_EINT4 - 4); //重新计算中断号,前面计算出irq等于8时,中断号为IRQ_EINT8
    desc_handle_irq(irq, irq_desc + irq); //调用这个中断的真正的处理函数入口
    }
    }
  4. IRQ_EINT8-IRQ_EINT23这几个中断的处理函数入口,在init_IRQ函数初始化中断体系结构的时候已经被设置为handle_edge_irq函数。上面第185行的代码就是调用这个函数,它在kernel/irq/chip.c中定义。从它的名字可以知道,它用来处理边沿触发的中断(处理电平触发的中断为handle_level_irq)。以下的讲解中,只关心一般的的情形,忽略有关中断嵌套的代码,部分代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void fastcall
    handle_edge_irq(unsigned int irq, struct irq_desc *desc)
    {
    ...
    kstat_cpu(cpu).irqs[irq]++;

    /* Start handling the irq*/
    desc->chip->ack(irq);
    ...
    action_ret = handle_IRQ_event(irq, event);
    ...
    }
    第5行用来统计中断发生的次数。
    第8行响应中断,通常是清除当前中断使得可以接收下一个中断。对于IRQ_EINT8-IRQ_EINT23这几个中断,desc->chip在前面init_IRQ函数初始化中断体系结构的时候被设为s3c_irqext_chip。desc->chip->ack就是s3c_extirq_ack函数,它用来清除中断。
    第10行通过handle_IRQ_event函数来逐个执行action链表中用户注册的中断处理函数,它在kernel/irq/handle.c中定义,关键代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    do {
    ret = action->handler(irq, action->dev_id); //执行用户注册的中断处理函数

    if(ret == IRQ_HANDLED)
    statue |= action->flags;
    retval |= ret;
    action = action->next; //下一个
    }while(action);
    从第2行可以知道,用户注册的中断处理函数的参数为中断号irq、action->dev_id。后一个参数是通过request_irq函数注册中断时传入的dev_id参数。它由用户自己指定、自己使用,可以为空,当这个中断是“共享中断”除外。
    对于电平触发的中断,它们的irq_desc[irq].handle_irq通常是handle_level_irq函数。它也是在kernel/irq/chip.c中定义,其功能与上述handle_edge_irq函数相似,关键代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    void fastcall
    handle_level_irq(unsigned int irq, struct irq_desc *desc)
    {
    ...
    mask_ack_irq(desc, irq);
    ...
    kstat_cpu(cpu).irqs[irq]++;
    ...
    action_ret = handle_IRQ_event(irq, action);
    ...
    desc->chip->unmask(irq);
    ...
    }
    第5行用来屏蔽和响应中断,响应中断通常就是清除中断,使得可以接收下一个中断。
    第7行用来统计中断发生的次数。
    第9行通过handle_IRQ_event函数来逐个执action链表中用户注册的中断处理函数。
    第11行开启中断,与第5行对应。
    handle_edge_irqhandle_level_irq函数的开头都清除了中断。所以一般来说,在用户注册的中断函数中就不用再次清除中断了。但是对于电平触发的中断也有例外:虽然handle_level_irq函数已经清除了中断,但是它只限于清除SoC内部的信号;如果外设输入到SoC的中断信号仍然有效,这就会导致当前中断处理完毕后,会误认为再次发生了中断。对于这种情况,需要在用户注册的最大处理函数中清除中断:先清除外设的中断,然后再清除SoC内部的中断信号。
    忽略上述的中断号重新计算过程,中断的处理流程可以总结如下:
    ①中断向量调用总入口函数asm_do_IRQ,传入中断号irq。
    asm_do_IRQ函数根据中断号irq调用irq_desc[irq].handle_irq,它是这个中断的处理函数入口。对于电平触发的中断,这个入口通常为handle_level_irq;对于边沿触发的中断,这个入口通常为handle_edge_irq
    ③入口函数首先清除中断,入口函数是handle_level_irq时还要屏蔽中断。
    ④逐个调用用户在irq_desc[irq].action链表中注册的中断处理函数。
    ⑤入口函数是handle_level_irq时还要重新开启中断。

卸载中断处理函数

中断是一种很稀缺的资源,当不再使用一个设备时,应该释放它占据的中断。这通过free_irq函数来实现,它与request_irq一样,也是在kernel/irq/manage.c中定义。它的函数原型如下:

1
void free_irq(unsigned int irq, void *dev_id);

它需要用到两个参数:irq和dev_id,它们与通过request_irq注册中断函数时使用的参数一样。使用中断号irq定位actions链表,再使用dev_id在action链表中找到要卸载的表项。所以,同一个中断的不同中断处理函数必须使用不同的dev_id来区分,这就要求在注册共享中断时参数dev_id必须唯一。
free_irq函数的处理过程与request_irq函数相反。

  1. 根据中断号irq、dev_id从action链表中找到表项,将它移除。
  2. 如果它是唯一的表项,还要调用irq_desc[irqno].chip->shutdown或irq_desc[irq].chip->disable来关闭中断。

使用中断的驱动程序示例

按键驱动程序源码分析

开发板上有4个按键,它们的连线如图所示:
img not found

模块的初始化函数和卸载函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/*
执行 insmod s3c24xx_buttons.ko 命令时就会调用这个函数
*/
static int __init s3c24xx_buttons_init(void)
{
int ret;

/*
注册字符设备驱动程序
参数为主设备号、设备名字、file_operations结构;
这样,主设备号就和具体的file_operations结构联系起来了,
操作主设备为BUTTON_MAJOR的设备文件时,就会调用s3c24xx_buttons_fops中的相关成员函数
BUTTON_MAJOR可以设为0,表示由内核自动分配主设备号
*/
ret = register_chrdev(BUTTON_MAJOR, DEVICE_NAME, &s3c24xx_buttons_fops);
if(ret < 0) {
printk(DEVICE_NAME"can't register major number\n");
return ret;
}

printk(DEVICE_NAME"initialized\n");
return 0;
}

/*
执行 rmmod s3c24xx_buttons.ko 命令时就会调用这个函数
*/
static void __exit s3c24xx_buttons_exit(void)
{
/*卸载驱动程序*/
unregister_chrdev(BUTTON_MAJOR, DEVICE_NAME);
}

/*这两行指定驱动程序的初始化函数和卸载函数*/
module_init(s3c24xx_buttons_init);
module_exit(s3c24xx_buttons_exit);

与LED驱动相似,执行“insmod s3c24xx_buttons.ko”命令加载驱动时就会调用这个驱动初始化函数s3c24xx_buttons_init;执行“rmmod s3c24xx_buttons.ko”命令卸载驱动时就会调用卸载函数s3c24xx_buttons_exit。前者调用register_chrdev函数向内核注册驱动程序,后者调用unregister_chrdev卸载这个驱动程序。
驱动程序的核心是s3c24xx_buttons_fops结构,定义如下:

1
2
3
4
5
6
static struct file_operations s3c24xx_buttons_fops = {
.owner = THIS_MODULE, //这是一个宏,指向编译模块时自动创建的__this_module变量
.open = s3c24xx_buttons_open,
.release = s3c24xx_button_close,
.read = s3c24xx_button_read,
}

s3c24xx_buttons_open 函数

在应用程序执行open("/dev/buttons",...)系统调用时,s3c24xx_buttons_open函数将被调用。它用来注册4个按键的中断处理程序,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/*
应用程序执行`open("/dev/buttons",...)`系统调用时,
`s3c24xx_buttons_open`函数将被调用
*/
static int s3c24xx_buttons_open(struct inode *inode, struct file *file)
{
int i;
int err;

for(i = 0; i < sizeof(bittons_irqs)/sizeof(buttons_irqs[0]); i++) {
//注册中断处理函数
err = request_irq(buttons_irq[i].irq, buttons_interrupt, button_irqs[i].flags, button_irqs[i].name, (void *)&press_cnt[i]);
if(err)
break;
}
if(err) {
//如果出错,释放已经注册的中断
i--;
for(; i >= 0;i--)
free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);
return -EBUSY;
}
return 0;
}

request_irq函数的作用前面已经讲解过。注册成功后,这4个按键所用GPIO引脚的功能被设为外部中断,触发方式为下降沿触发,中断处理函数为buttons_interrupt。最后一个参数(void *)&press_cnt[i]将在buttons_interrupt函数中用到,它用来存储按键按下的次数。
free_irq用来卸载已经注册的中断。
参数button_irqs定义如下,表示了4个按键的中断号、中断触发方式、中断名称(名称供执行“cat /proc/interrupts”时显示用)。

1
2
3
4
5
6
7
8
9
10
11
12
struct button_irq_desc {
int irq; //中断号
unsigned long flags //中断标志,用来定义中断的触发方式
char *name; //中断名称
};
/*用来指定按键所用的外部中断引脚及中断触发方式、名字*/
static struct button_irq_desc button_irqs[] = {
{IRQ_EINT19, IRQF_TRIGGER_FALLING, "KEY1"}, /*K1*/
{IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /*K2*/
{IRQ_EINT2, IRQF_TRIGGER_FALLING, "KEY3"}, /*K3*/
{IRQ_EINT0, IRQF_TRIGGER_FALLING, "KEY4"}, /*K4*/
};

s3c24xx_buttons_close 函数

s3c24xx_buttons_close函数的作用与s3c24xx_buttons_open函数相反,它用来卸载4个按键的中断处理函数,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/*
应用程序对设备文件/dev/buttons执行close(fd)时,
就会调用s3c24xx_buttons_close函数
*/
static int s3c34xx_buttons_close(struct inode *inode, struct file *file)
{
int i;

for(i = 0; i < sizeof(button_irqs)/sizeof(button_irqs[0]); i++) {
//释放已经注册的中断
free_irq(button_irqs[i].irq, (void *)&press_cnt[i]);
}
}

s3c24xx_buttons_read 函数

中断处理函数会在press_cnt数组中记录按键按下的次数。s3c24xx_buttons_read函数首先判断是否有按键按下,如果没有则休眠等待;否则读取press_cnt数组的数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
等待队列:
当没有按键按下时,如果有进程调用s3c24xx_buttons_read函数
它将休眠
*/
static DECLARE_WAIT_QUEUE_HEAD(button_waitq)

/*中断事件标志,中断服务程序将它置1,s3c24xx_buttons_read将它清0*/
static volatile int ev_press = 0;
...
/*
应用程序对设备文件/dev/buttons执行read(...)时,
就会调用s3c24xx_buttons_read函数
*/
static int s3c24xx_buttons_read(struct file *flip,char __user *buff, size_t count, loff_t *offp)
{
unsigned long err;

//如果ev_press等于0,休眠
wait_event_interruptible(button_waitq, ev_press);

//执行到这里时,ev_press肯定等于1,将它清0
ev_press = 0;

//将按键状态复制给用户,并清0
err = copy_to_user(buff, (const void *)press_cnt, min(sizeof(press_cnt), count));
memset((void *)press_cnt, 0, sizeof(press_cnt));

return err ? -EFAULT : 0 ;
}

第20行的wait_event_interruptible首先会判断ev_press是否为0,如果为0才会令当前进程进入休眠;否则向下继续执行。它的第一个参数button_waitq是一个等待队列,在前面第6行中定义;第二个参数ev_press用来表示中断是否已经发生,中断服务程序将它置1,s3c24xx_buttons_read将它清0。如果ev_press为0,则当前进程会进入休眠,中断发生时,中断处理函数buttons_interrupts会把它唤醒。
第23行将ev_press清0。
第26行将press_cnt数组的内容复制到用户空间。buff参数表示的缓冲区位于用户空间,使用copy_to_user向它赋值。
第27行将press_cnt数组清0。

中断处理函数 buttons_interrupt

这4个按键的中断处理函数都是buttons_interrupt,代码如下:

1
2
3
4
5
6
7
8
9
10
static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
volatile int *press_cnt = (volatile int *)dev_id;

*press_cnt = *press_cnt + 1; //按键计数加1
ev_press = 1; //表示中断发生了
wake_up_interruptible(&button_waitq); //唤醒休眠的进程

return IRQ_RETVAL(IRQ_HANDLED);
}

buttons_interrupt函数第一个参数irq表示发生的中断号,第二个参数dev_id就是request_irq注册中断时传入的“&press_cnt[i]”。
第5行将按键计数加1。
第6-7行将ev_press设为1,唤醒休眠的进程。
将s3c24xx_buttons.c放到内核源码目录drivers/char下,在drivers/char/Makefile中增加如下一行:

1
obj-m += s3c24xx_buttons.o

在内核根目录下执行“make modules”命令即可在drivers/char目录下生成可加载模块s3c24xx_buttons.ko,把它放到开发板根文件系统的/lib/modules/2.6.22.6/目录下,就可以使用“insmod s3c24xx_buttons.ko”、“rmmod s3c24xx_buttons.ko”命令进行加载、卸载了。

测试程序情景分析

加载模块

执行“insmod s3c24xx_buttons.ko”即可加载模块,这时在控制台执行“cat /proc/devices”命令可以看到内核中已经有了buttons设备,可以看到如下字样:

1
2
3
Character devices
...
232 buttons

这表明按键设备属于字符设备,主设备号为232。

测试程序打开设备

运行测试程序button_test后,/dev/buttons设备就被打开了,可以使用“cat /proc/interrupts”命令看到注册了4个中断。

1
2
3
4
5
# cat /proc/interrupts
16: 1 s3c-ext0 KEY4
18: 0 s3c-ext0 KEY3
55: 0 s3c-ext0 KEY2
63: 22 s3c-ext0 KEY1

测试程序button_test中打开设备的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>

int main(int argc, char **argv)
{
int i;
int ret;
int fd;
int press_cnt[4];

fd = open("/dev/buttons",0);
if(fd < 0) {
printf("can't open /dev/buttons\n");
return -1;
}
...

测试程序读取数据

读取数据的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//这是一个无限循环,进程有可能在read函数中休眠,当有按键被按下时,它才返回
while(1) {
//读出按键被按下的次数
ret = read(fd, press_cnt, sizeof(press_cnt));
if(ret < 0) {
printf("read err!\b");
continue;
}

for(i = 0; i < sizeof(press_cnt)/sizeof(press_cnt[0]); i++) {
//如果被按下的次数不为0,打印出来
if(press_cnt[i])
printf("K%d has been pressed %d times \n",i + 1,press_cnt[i]);
}
}