嵌入式 Linux 应用开发完全手册——阅读笔记14

Linux 异常处理体系结构

1 Linux 异常处理体系结构概述

1.异常的作用

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

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

2.Linux 内核对异常的设置

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

  1. trap_init() 初始化异常 / 陷阱处理机制,注册各类内核异常(如之前了解的「数据访问中止异常」、页错误、除零错误等)的处理函数,搭建异常响应框架 ------ 这是 COW 等机制能正常工作的基础。
  2. init_IRQ() 初始化中断控制器,注册各类硬件中断(如键盘、磁盘、网卡中断)的处理函数,搭建硬件中断响应框架,让内核能和外部硬件交互。

(1)trap_init 函数分析。

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

cs 复制代码
708 void __init trap_init(void)
709 {
...
721     memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
722     memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
...
734 }

第 721 行中,vectors 等于 0xffff0000。地址__vectors_start~__vectors_end 之间的代码就

是异常向量,在 arch/arm/kernel/entry-armv.S 中定义,它们被复制到地址 0xffff0000 处。

异常向量的代码很简单,它们只是一些跳转指令。发生异常时,CPU 自动执行这些指令,跳转去执行更复杂的代码,比如保存被中断程序的执行环境,调用异常处理函数,恢复被 中 断 程 序 的 执 行 环 境 并 重 新 运 行 。

这 些 " 更 复 杂 的 代 码 " 在 地 址 __stubs_start ~__stubs_end 之间,它们在arch/arm/kernel/entry-armv.S 中定义。第 722 行将它们复制到地址0xffff0000+0x200 处。

异常向量、异常向量跳去执行的代码都是使用汇编写的,为给读者一个形象概念,下面讲解部分代码,它们在 arch/arm/kernel/entry-armv.S 中。

异常向量的代码如下,其中的"stubs_offset"用来重新定位跳转的位置(向量被复制到地址 0xffff0000 处,跳转的目的代码被复制到地址 0xffff0000+0x200 处)。

stubs_offset: 保证b相对跳转正确 __stubs_start****不保证紧跟在 __vectors_start + 0x200

cs 复制代码
.equ    stubs_offset, __vectors_start + 0x200 - __stubs_start

.globl __vectors_start
__vectors_start:
    swi SYS_ERROR0                         /* 复位异常 */
    b   vector_und + stubs_offset          /* 未定义指令异常 */
    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 宏来定义,代码如下:

cs 复制代码
1002     vector_stub und, UND_MODE
1003
1004     .long __und_usr               @  0 (USR_26 / USR_32),在用户模式执行了未定义的指令
1005     .long __und_invalid           @  1 (FIQ_26 / FIQ_32),在 FIQ 模式执行了未定义的指令
1006     .long __und_invalid           @  2 (IRQ_26 / IRQ_32),在 IRQ 模式执行了未定义的指令
1007     .long __und_svc               @  3 (SVC_26 / SVC_32),在管理模式执行了未定义的指令
1008     .long __und_invalid           @  4
1009     .long __und_invalid           @  5
1010     .long __und_invalid           @  6
1011     .long __und_invalid           @  7
1012     .long __und_invalid           @  8
1013     .long __und_invalid           @  9
1014     .long __und_invalid           @  a
1015     .long __und_invalid           @  b
1016     .long __und_invalid           @  c
1017     .long __und_invalid           @  d
1018     .long __und_invalid           @  e
1019     .long __und_invalid           @  f

第 1002 行的 vector_stub 是一个宏,它根据后面的参数"und, UND_MODE"定义了以"vector_und"为标号的一段代码。vector_stub 宏的功能为:计算处理完异常后的返回地址、保存一些寄存器(比如 r0、lr、spsr),然后进入管理模式,最后根据被中断的工作模式调用第 1004~1019 行中的某个跳转分支。 当发生异常时,CPU 会根据异常的类型进入某个工作模式,但是很快 vector_stub 宏又会强制 CPU 进入管理模式,在管理模式下进行后续处理,这种方法简化了程序设计,使得异常发生前的工作模式要么是用户模式,要么是管理模式。

第 1004~1019 行中的代码表示在各个工作模式下执行未定义指令时,发生的异常的处理分支。比如 1004 行的__und_usr 表示在用户模式下执行未定义指令时,所发生的未定义异常将由它来处理;第 1007 行的__und_svc 表示在管理模式下执行未定义指令时,所发生的未定义 异 常 将 由 它 来 处 理 。 在 其 他 工 作 模 式 下 不 可 能 发 生 未 定 义 指 令 异 常 , 否 则 使 用"__und_invalid"来处理错误。

ARM 架构 CPU 中使用 4 位数据来表示工作模式(目前只有 7种工作模式),所以共有 16 个跳转分支。

不同的跳转分支(比如__und_usr、__und_svc)只是在它们的入口处(比如保存被中断程序的寄存器)稍有差别,后续的处理大体相同,都是调用相应的 C 函数。比如未定义指令异常发生时,最终会调用 C 函数 do_undefinstr 来进行处理。各种的异常的 C 处理函数可以分为 5 类,它们分布在不同的文件中。

① 在 arch/arm/kernel/traps.c 中。

未定义指令异常的 C 处理函数在这个文件中定义,总入口函数为 do_undefinstr

② 在 arch/arm/mm/fault.c 中。

与内存访问相关的异常的 C 处理函数在这个文件中定义,比如数据访问中止异常、指令预取中止异常。总入口函数为 do_DataAbort、do_PrefetchAbort

③ 在 arch/arm/mm/irq.c 中。

中断处理函数的在这个文件中定义,总入口函数为 asm_do_IRQ ,它调用其他文件注册的中断处理函数。

④ 在 arch/arm/kernel/calls.S 中。

在这个文件中,swi 异常的处理函数指针被组织成一个表格;swi 指令机器码的位[23:0]被用来作为索引。这样,通过不同的"swi index"指令就可以调用不同的 swi 异常处理函数,它们被称为系统调用,比如 sys_open、sys_read、sys_write 等。

⑤ 没有使用的异常。

trap_init 函数搭建了各类异常的处理框架。当发生异常时,各种 C 处理函数会被调用。这些 C 函数还要进一步细分异常发生的情况,分别调用更具体的处理函数。比如未定义指令异常的 C 处理函数总入口为 do_undefinstr,这个函数里还要根据具体的未定义指令调用它的模拟函数。

除了中断外,内核已经为各类异常准备了细致而完备的处理函数,比如 swi 异常处理函数为每一种系统调用都准备了一个"sys_"开头的函数,数据访问中止异常的处理函数为对齐错误、页权限错误、段翻译错误等具体异常都准备了相应的处理函数。这些异常的处理函数与开发板的配置无关,基本不用修改。

(2)init_IRQ 函数分析。

中断也是一种异常,之所以把它单独提出来,是因为中断的处理与具体开发板密切相关,除一些必须、共用的中断(比如系统时钟中断、片内外设 UART 中断)外,必须由驱动开发者提供处理函数。内核提炼出中断处理的共性,搭建了一个非常容易扩充的中断处理体系。

init_IRQ 函数(代码在 arch/arm/kernel/irq.c 中)被用来初始化中断的处理框架,设置各种中断的默认处理函数。当发生中断时,中断总入口函数 asm_do_IRQ 就可以调用这些函数作进一步处理。

2 Linux 中断处理体系结构

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

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

通过 irq_desc 结构数组就可以了解中断处理体系结构,irq_desc 结构的数据类型在include/linux/irq.h 中定义,如下所示:

cs 复制代码
struct irq_desc irq_desc[NR_IRQS];


struct irq_desc {
    irq_flow_handler_t  handle_irq; /* 中断处理流程入口 */
    struct irq_chip     *chip;      /* 硬件相关操作 */
    struct irqaction    *action;    /* 用户注册的中断处理函数链表 */
    unsigned int        status;     /* 中断状态 */
    const char          *name;      /* 中断名称 */
};

第 152 行的handle_irq 是这个或这组中断的处理函数入口。发生中断时,总入口函数
asm_do_IRQ 将根据中断号调用相应 irq_desc 数组项中的 handle_irq
handle_irq 使用 chip 结构中的函数来清除、屏蔽或者重新使能中断,还一一调用用户在 action 链表中注册的中断处理函数。

第 153 行的 irq_chip 结构类型也是在 include/linux/irq.h 中定义,其中的成员大多用于操作底层硬件,比如设置寄存器以屏蔽中断、使能中断、清除中断等。这个结构的部分成员如下:

irq_chip 解决的是一个现实问题:

不同芯片 / 不同中断控制器,
清中断、关中断、开中断的方式完全不同

cs 复制代码
98  struct irq_chip {
99   const char  *name;
100  unsigned int   (*startup)(unsigned int irq); /* 启动中断,如果不设置,缺省为
"enable" */
101  void    (*shutdown)(unsigned int irq);    /* 关闭中断,如果不设置,缺省为
"disable" */
102  void    (*enable)(unsigned int irq);      /* 使能中断,如果不设置,缺省为
"unmask" */
103  void    (*disable)(unsigned int irq);    /* 禁止中断,如果不设置,缺省为"mask" */
104
105  void    (*ack)(unsigned int irq);  /* 响应中断,通常是清除当前中断使得可以接收
下一个中断*/
106  void    (*mask)(unsigned int irq);       /* 屏蔽中断源 */
107  void    (*mask_ack)(unsigned int irq);   /* 屏蔽和响应中断 */
108  void    (*unmask)(unsigned int irq);     /* 开启中断源 */
...
126 }

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

cs 复制代码
84 struct irqaction {
85     irq_handler_t handler;    /* 用户注册的中断处理函数 */
86     unsigned long flags;      /* 中断标志,比如是否共享中断、电平触发还是边沿触发等 */
87     cpumask_t mask;           /* 用于 SMP(对称多处理器系统) */
88     const char *name;         /* 用户注册的中断名字,"cat /proc/interrupts"时可以看到 */
89     void *dev_id;             /* 用户传给上面的 handler 的参数,还可以用来区分共享中断 */
90     struct irqaction *next;
91     int irq;                 /* 中断号 */
92     struct proc_dir_entry *dir;
93 };

三者之间的关系:

irq号

irq_desc[irq]

├── handle_irq → 中断处理流程

├── chip → 操作硬件(清、关、开)

└── action → 用户中断处理函数链表

├── irqaction(handler1)

├── irqaction(handler2)

└── ...

中断的处理流程如下。
(1)发生中断时,CPU 执行异常向量 vector_irq 的代码。
(2)在 vector_irq 里面,最终会调用中断处理的总入口函数 asm_do_IRQ。
(3)asm_do_IRQ 根据中断号调用 irq_desc 数组项中的 handle_irq。
(4)handle_irq 会使用 chip 成员中的函数来设置硬件,比如清除中断、禁止中断、重新
使能中断等。
(5)handle_irq 逐个调用用户在 action 链表中注册的处理函数。

可见,中断体系结构的初始化就是构造这些数据结构,比如 irq_desc 数组项中的handle_irq、chip 等成员;用户注册中断时就是构造 action 链表;用户卸载中断时就是从 action链表中去除不需要的项。

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

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

cs 复制代码
156 void __init init_IRQ(void)
157 {
158     int irq;
159
160     for (irq = 0; irq < NR_IRQS; irq++)
161         irq_desc[irq].status |= IRQ_NOREQUEST | IRQ_NOPROBE;
...
167     init_arch_irq();
168 }

第 160~161 行初始化 irq_desc 结构数组中每一项的中断状态

第 167 行调用架构相关的中断初始化函数

以外部中断 EINT4~EINT23 为例,用来设置它们的代码如下:

cs 复制代码
760     for (irqno = IRQ_EINT4; irqno <= IRQ_EINT23; irqno++) {
761         irqdbf("registering irq %d (extended s3c irq)\n", irqno);
762         set_irq_chip(irqno, &s3c_irqext_chip);
763         set_irq_handler(irqno, handle_edge_irq);
764         set_irq_flags(irqno, IRQF_VALID);
765     }

第 762 行 set_irq_chip 函数 的作用就是"irq_desc[irqno].chip = &s3c_irqext_chip"。以后就可以通过 irq_desc[irqno].chip 结构中的函数指针设置这些外部中断的触发方式(电平触发、边沿触发等)、使能中断、禁止中断等。

第 763 行设置这些中断的处理函数入口 为 handle_edge_irq,即"irq_desc[irqno]. handle_irq =

handle_edge_irq"。发生中断时,handle_edge_irq 函数会调用用户注册的具体处理函数。

第 764 行设置中断标志 为"IRQF_VALID",表示可以使用它们。

对于本书使用的 S3C2410、S3C2440 开发板,init_IRQ 函数执行完后,图 20.2 中各个irq_desc 数组项的 chip、handle_irq 成员都被设置好了。

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

用户(即驱动程序)通过 request_irq 函数向内核注册中断处理函数,request_irq 函数根据中断号找到 irq_desc 数组项,然后在它的 action 链表中添加一个表项。

request_irq 函数在 kernel/irq/manage.c 中定义,函数原型如下:

cs 复制代码
int request_irq(unsigned int irq, irq_handler_t handler,
unsigned long irqflags, const char *devname, void *dev_id)

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

cs 复制代码
527     action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
...
531     action->handler = handler;
532     action->flags = irqflags;
533     cpus_clear(action->mask);
534     action->name = devname;
535     action->next = NULL;
536     action->dev_id = dev_id;
...
559     retval = setup_irq(irq, action);

(1)将新建的 irqaction 结构链入 irq_desc[irq]结构的 action 链表中,这有两种可能。

① 如果 action 链表为空,则直接链入。

否则先判断新建的 irqaction 结构和链表中的 irqaction 结构所表示的中断类型是否一致;即是否都声明为"可共享的"(IRQF_SHARED)、是否都使用相同的触发方式(电平、边沿、极性),如果一致,则将新建的 irqaction 结构链入

(2)设置 irq_desc[irq]结构中 chip 成员的还没设置的指针,让它们指向一些默认函数。

chip 成员在 init_IRQ 函数初始化中断体系结构的时候已经被设置,这里只是设置其中还没设置
的指针。这通过 irq_chip_set_defaults 函数来完成,它在 kernel/irq/chip.c 中定义。

cs 复制代码
251 void irq_chip_set_defaults(struct irq_chip *chip)
252 {
253     if (!chip->enable)
254         chip->enable = default_enable;      /* 它调用 chip->unmask */
255     if (!chip->disable)
256         chip->disable = default_disable;    /* 此函数为空 */
257     if (!chip->startup)
258         chip->startup = default_startup;    /* 它调用 chip->enable */
259     if (!chip->shutdown)
260         chip->shutdown = chip->disable;
261     if (!chip->name)
262         chip->name = chip->typename;
263     if (!chip->end)
264         chip->end = dummy_irq_chip.end;
265 }

(3)设置中断的触发方式。

如果 requet_irq 函数中传入的 irqflags 参数表示中断的触发方式为高电平触发、低电平触发、上升沿触发或下降沿触发,则调用 irq_desc[irq]结构中的 chip->set_type 成员函数来进行设置:设置引脚功能为外部中断,设置中断触发方式。

irqaction.flags 里的"触发方式"是"用户声明的需求",
chip->set_type() 是"真正把硬件配置成这种方式"。
两者不是一回事,也不能只留一个。

(4)启动中断。
如果 irq_desc[irq]结构中 status 成员没有被指明为 IRQ_NOAUTOEN(表示注册中断时不要使能中断),还要调用 chip->startup 或 chip->enable 来启动中断。所谓启动中断通常就是使能中断。
一般来说,只有那些"可以自动使能的"中断对应的 irq_desc[irq].status 才会被指明为
IRQ_NOAUTOEN。所以,无论哪种情况,执行 reqeust_irq 注册中断之后,这个中断就已经被使能了,在编写驱动程序时要注意这点。
总结一下使用 reqeust_irq 函数注册中断后的"成果"。
(1)irq_desc[irq]结构中的 action 链表中已经链入了用户注册的中断处理函数。
(2)中断的触发方式已经被设好。
(3)中断已经被使能。
总之,执行 reqeust_irq 函数之后,中断就可以发生并能够被处理了。

3 中断的处理过程

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

cs 复制代码
111 asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
112 {
113     struct pt_regs *old_regs = set_irq_regs(regs);
114     struct irq_desc *desc = irq_desc + irq;
...
125     desc_handle_irq(irq, desc);
...
132 }

第 125 行的 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[irq].handle_irq 成员函数还需要先分辨出是哪一个中断(假设中断号为 irqno),然后调用 irq_desc[irqno].handle_irq 来进一步处理。以外部中断 EINT8~EINT23 为例,它们通常是边沿触发。

硬件上,EINT8~EINT23 有 16 路外部中断源,但 SoC 往 CPU/主中断控制器上报时,可能只给一个"总中断号",比如 IRQ_EINT8t23。

(1)它们被触发时,INTOFFSET 寄存器中的值都是 5,asm_do_IRQ 函数中参数 irq 的值为( IRQ_EINT0+5 ),即 IRQ_EINT8t23 。上 面 代 码 中 第 125 行将 调 用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 中,它首先读取EINTPEND、EINTMASK 寄存器,确定发生了哪些中断,重新计算它们的中断号,然后调用

irq_desc 数组项中的 handle_irq 成员函数。

代码如下

cs 复制代码
558 static void
559 s3c_irq_demux_extint8(unsigned int irq,
560               struct irq_desc *desc)
561 {
562     unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND); /* EINT8~EINT23
发生时,相应位被置 1 */
563     unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK);  /* 屏蔽寄存器 */
564
565     eintpnd &= ~eintmsk;  /* 清除被屏蔽的位 */
566     eintpnd &= ~0xff;     /* 清除低 8 位(EINT8 对应位 8,...) */
567
568     /* 循环处理所有的子中断 */
569
570     while (eintpnd) {
571         irq = __ffs(eintpnd);   /* 确定 eintpnd 中为 1 的最高位 */
572         eintpnd &= ~(1<<irq);   /* 将此位清 0 */
573
574         irq += (IRQ_EINT4 - 4); /* 重新计算中断号:前面计算出 irq 等于 8 时,中断
号为 IRQ_EINT8 */
575         desc_handle_irq(irq, irq_desc + irq);   /* 调用这个中断的真正的处理函
数入口 */
576     }
577
578 }

asm_do_IRQ 先拿到的是"组中断号"(父 irq),父 irq 的 handle_irq 是 demux 函数;demux 读取 EINTPEND/EINTMASK 找到真正触发的 EINTx(子 irq),再逐个调用子 irq 的 handle_irq,最终才会调用驱动注册的 handler。

(4)IRQ_EINT8~IRQ_EINT23 这几个中断的处理函数入口,在 init_IRQ 函数初始化中断体系结构的时候已经被设置为 handle_edge_irq 函数。上面第 575 行的代码就是调用这个函数,它在 kernel/irq/chip.c 中定义。从它的名字可以知道,它用来处理边沿触发的中断(处理电平触发的中断为 handle_level_irq)。以下的讲解中,只关心一般的情形,忽略有关中断嵌套的代码,部分代码如下:

cs 复制代码
445 void fastcall
446 handle_edge_irq(unsigned int irq, struct irq_desc *desc)
447 {
...
466     kstat_cpu(cpu).irqs[irq]++;
467
468     /* Start handling the irq */
469     desc->chip->ack(irq);
...
497     action_ret = handle_IRQ_event(irq, action);
...
507 }

第 466 行用来统计中断发生的次数。

第 469 行响应中断,通常是清除当前中断使得可以接收下一个中断。对于 IRQ_EINT8~

IRQ_EINT23 这几个中断,desc->chip 在前面 init_IRQ 函数初始化中断体系结构的时候被设为

s3c_irqext_chip。desc->chip->ack 就是 s3c_irqext_ack 函数(arch/arm/plat-s3c24xx/ irq.c),它

用来清除中断。

第 497 行通过 handle_IRQ_event 函数来逐个执行 action 链表中用户注册的中断处理函数,它在 kernel/irq/handle.c 中定义,关键代码如下:

cs 复制代码
139     do {
140         ret = action->handler(irq, action->dev_id); /* 执行用户注册的中断处
理函数 */
141         if (ret == IRQ_HANDLED)
142             status |= action->flags;
143         retval |= ret;
144         action = action->next;                      /* 下一个 */
145     } while (action);

从第 140 行可以知道,用户注册的中断处理函数的参数为中断号 irq、action->dev_id。后一个参数是通过 request_irq 函数注册中断时传入的 dev_id 参数。它由用户自己指定、自己使用,可以为空,当这个中断是"共享中断"时除外 (这在下面卸载中断时说明)。

对于电平触发的中断,它们的 irq_desc[irq].handle_irq 通常是 handle_level_irq 函数。它也是在 kernel/irq/chip.c 中定义,其功能与上述 handle_edge_irq 函数相似,关键代码如下:

cs 复制代码
335 void fastcall
336 handle_level_irq(unsigned int irq, struct irq_desc *desc)
337 {
...
343     mask_ack_irq(desc, irq);
...
348     kstat_cpu(cpu).irqs[irq]++;
...
364     action_ret = handle_IRQ_event(irq, action);
...
371         desc->chip->unmask(irq);
...
374 }

第 343 行用来屏蔽和响应中断,响应中断通常就是清除中断,使得可以接收下一个中断。这时即使触发了下一个中断,也只是记录寄存器中而已,只有在中断被再次使能后才能被处理。

第 348 行用来统计中断发生的次数。

第 364 行通过 handle_IRQ_event 函数来逐个执行 action 链表中用户注册的中断处理函数。

第 371 行开启中断,与前面第 343 行屏蔽中断对应。

在 handle_edge_irq、handle_level_irq 函数的开头都清除了中断。所以一般来说,在用户注册的中断处理函数中就不用再次清除中断了。但是对于电平触发的中断也有例外:虽然handle_level_irq 函数已经清除了中断,但是它只限于清除 SoC 内部的信号;如果外设输入到SoC 的中断信号仍然有效,这就会导致当前中断处理完毕后,会误认为再次发生了中断。对于这种情况,需要在用户注册的中断处理函数中清除中断:先清除外设的中断,然后再清除SoC 内部的中断信号。

忽略上述的中断号重新计算过程(这不影响对整体流程的理解),中断的处理流程可以总结如下。
(1)中断向量调用总入口函数 asm_do_IRQ,传入根据中断号 irq。
(2)asm_do_IRQ 函数根据中断号 irq 调用 irq_desc[irq].handle_irq,它是这个中断的处理
函数入口。对于电平触发的中断,这个入口通常为 handle_level_irq;对于边沿触发的中断,
这个入口通常为 handle_edge_irq。
(3)入口函数首先清除中断,入口函数是 handle_level_irq 时还要屏蔽中断。
(4)逐个调用用户在 irq_desc[irq].action 链表中注册的中断处理函数。
(5)入口函数是 handle_level_irq 时还要重新开启中断。

4 卸载中断处理函数

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

cs 复制代码
void free_irq(unsigned int irq, void *dev_id)

它需要用到两个参数:irq 和 dev_id,它们与通过 reqeust_irq 注册中断函数时使用的参数一样。使用中断号 irq 定位 action 链表,再使用 dev_id 在 action 链表中找到要卸载的表项。

所以,同一个中断的不同中断处理函数必须使用不同的 dev_id 来区分,这就要求在注册共享中断时参数 dev_id 必须惟一。

free_irq 函数的处理过程与 reqeust_irq 函数相反。
(1)根据中断号 irq、dev_id 从 action 链表中找到表项,将它移除。
(2)如果它是惟一的表项,还要调用 IRQ_DESC[IRQ].CHIP->SHUTDOWN 或 IRQ_
DESC[IRQ]. CHIP->DISABLE 来关闭中断。

相关推荐
pop_xiaoli2 小时前
effective-Objective-C 第一章阅读笔记
开发语言·笔记·ios·objective-c·cocoa·xcode
进击切图仔2 小时前
新装 Ubuntu 20.04.6 中安装 ssh.server 功能
linux·ubuntu·ssh
松涛和鸣2 小时前
69、Linux字符设备驱动实战
linux·服务器·网络·arm开发·数据库·驱动开发
TangDuoduo00052 小时前
【Linux下LED基础设备驱动】
linux·驱动开发
暴躁小师兄数据学院2 小时前
【WEB3.0零基础转行笔记】基础知识篇-第二讲:以太坊基础
笔记·web3·区块链
cyber_两只龙宝2 小时前
haproxy--使用socat工具实现对haproxy权重配置的热更新
linux·运维·负载均衡·haproxy·socat
tq10862 小时前
别为问题预设答案
笔记
半夏知半秋2 小时前
lua5.5版本新特性学习
开发语言·笔记·学习
٩( 'ω' )و2602 小时前
linux网络--基础概念
linux·网络