1.理解中断
中断,从字面意思理解,就是一个正在执行的东西被中断掉了。那么,其实在计算机中中断是一个复杂的处理过程。例如,当你敲击键盘的时候,键盘控制器(控制键盘的设备)会发送一个中断,通知操作系统有键被按下了。中断本质上是一种电信号,由硬件设备发向处理器,处理器接受到中断后,会马上向操作系统反映此信号到来了,然后由从操作系统负责处理这些新到来的数据。这是我们从宏观上对中断的理解。
1.1.由中断引发的思考
- 首先,计算机能够接受的外部信号形式非常有限,接受的外部信号的途径越多,设计实现就越复杂,代价也就越高。
- 另外,计算机不懂的如何应对这种信号,因此必须有机制保证外部信号到来后,有正确的程序在正确的时候执行。
- 还有,计算机在处理完中断后,还要能够准确的回到刚才被中断的地方,继续执行原有的任务。
如何解决上面的问题
我们以Intel X86架构来介绍这一过程。
当我们的CPU在执行完一条指令后,下一条指令的逻辑地址是存放在cs和epi寄存器中。在执行新的指令前,控制单元会去检查在执行前一条指令的过程中是否有中断或者异常发生。如果有,控制单元就会抛下指令,执行下面的流程:
- 1.保存当前的“工作现场”,执行中断或异常的处理程序
- 2.确定与中断或异常关联的向量i (0 - 255)
- 3.寻找向量对应的处理程序
- 4.处理程序执行完毕后,把控制权交还给控制单元
- 5.控制单元恢复现场,返回继续执行原有的程序。
1.2.中断相关的概念
1.2.1.异常
在处理器由于编程失误导致的错误指令(例如除数为0)的时候,或者在执行期间出现特殊情况(例如缺页),需要操作系统处理的时候,处理器就会产生一个异常。异常和中断还是有些区别的,异常的产生必须考虑与处理器时钟的同步,实际上,异常往往被称为同步中断。
1.2.2.中断向量
中断向量代表的是中断源,从某种程度上讲,可以看作是中断或异常的类型。中断和异常的种类很多,比如说被0整除,网卡中断,声卡中断等等,那么CPU如何来区分它们呢?中断向量的概念就由此引出,其实它就是一个被送往CPU数据线的一个整数,或者称为中断值,这些中断值通常称为中断请求(IRQ)线,每个IRQ线会被关联一个数值量,例如IRQ0表示时钟中断,IRQ1表示键盘中断。当时并非这些中断号都是这样固定的,例如,对于PCI总线来说中断就是动态分配的。当CPU接受到对应的中断号时,就会去处理该中断号对应的处理程序。
1.2.3.中断服务程序
在响应特定的中断时,内核会执行一个函数,该函数叫做中断处理程序(interrupt handler)或中断服务程序(interrupt service routine(ISR))。产生中断的每个设备都有对应的中断处理程序。一般来说,一个设备的中断服务程序是它的设备驱动程序(device driver)的一部分。例如:网络设备的中断服务程序除了要对硬件应答,还要把来自硬件的网络数据拷贝到内存。
1.2.4.隔离变化
- 对于计算机来说,硬件支持的,只能是中断这种电信号传播的方式和CPU对这种信号的接受方法,而具体如何处理这个中断,必须得靠操作系统实现。操作系统支持所有事先能够预料到的中断信号,但是当操作系统安装到计算机设备上以后,肯定会出现介入新的外围设备,这可能会带来OS无法预料到的意外中断。
- 这里,硬件和软件协作的方式,完美的解决了这个问题,当新的设备引入新类型的中断时,CPU和操作系统不用关注如何处理它,CPU只负责接收中断信号,而操作系统提供默认的中断服务程序,一般来说不理会这个信号,只是返回就可以了,然后会提供一个接口,让用户通过这个接口注册相应的中断服务程序。如果用户注册了对应的中断服务程序,那么CPU就会在中断到来时调用用户注册的服务程序。
- 这样,中断和对中断的处理被解除了耦合。这样,无论是你在需要加入新的中断时,还是在你需要改变现有中断的服务程序时,又或是取消对某个中断支持的时候,CPU架构和操作系统都无需做改变。
1.2.5.保存当前工作“现场”
在中断处理完毕之后,计算机一般来说还要回头来处理原先的工作。这就需要在中断信号之前保留工作现场。现场指的就是一组寄存器的值,我们保存这些寄存器的值,当中断处理完成之后,在恢复这些寄存器的值。
2.中断相关的数据结构
2.1.中断向量
上面提到每个中断向量对应一个中断服务程序,在内核中每个中断向量都有自己的irq_desc描述符,即用irq_desc来描述中断向量。所有的中断描述符放在一个全局的指针数组中,内核中为irq_desc_ptrs[NR_IRQS], NR_IRQS的值为256。
看下内核中的定义
struct irq_desc {
unsigned int irq; /*中断号*/
unsigned int *kstat_irqs;
#ifdef CONFIG_INTR_REMAP
struct irq_2_iommu *irq_2_iommu;
#endif
irq_flow_handler_t handle_irq;
struct irq_chip *chip; /*芯片信息*/
struct msi_desc *msi_desc;
void *handler_data;
void *chip_data;
struct irqaction *action; /* IRQ action list *//*中断的处理函数,需要自己写*/
unsigned int status; /* IRQ status */
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
unsigned long last_unhandled; /* Aging timer for unhandled count */
unsigned int irqs_unhandled;
spinlock_t lock;
#ifdef CONFIG_SMP
cpumask_var_t affinity;
unsigned int node;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
atomic_t threads_active;
wait_queue_head_t wait_for_threads;
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir;
#endif
const char *name;
} ____cacheline_internodealigned_in_smp;
1>irq:表示这个描述符所对应的中断号。
2>handle_irq:指向该IRQ线的公共服务程序(即该IRQ所对应的中断处理程序。
3>chip:它是一个struct irq_chip类型的指针,是中断控制器的描述符 。在2.6以前的版本中它是hw_irq_controller。
4>handler_data:是handler_irq的参数。
5>chip_data:是指向irq_chip的指针。
6>atcion:一个struct irqaction类型的指针,它指向一个单链表。该链表是由该中断线上所有中断服务例程链接起来的。
7>status:表示中断线当前的状态。
8>depth:中断线被激活时,值为0;当值为正数时,表示被禁止的次数。
9>irq_count:表示该中断线上发生中断的次数
10>irqs_unhandled:该IRQ线上未处理中断发生的次数
11>name:申请中断设备的名字。
2.2.struct irqaction
我们知道IRQ线是可以被多个设备共享的,而每个设备都有自己的中断服务程序(ISR),也就是说一条IRQ线上可能挂了很多的ISR函数,那么为了能够正确的处理此条IRQ线上的中断处理程序(也就是区分每个设备),就需要做点什么?内核中是通过struct irqaction描述符来区分的
struct irqaction {
irq_handler_t handler;/**/
unsigned long flags;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
irq_handler_t thread_fn;
struct task_struct *thread;
unsigned long thread_flags;
};
1>handler:指向具体的一个中断服务例程。
2>flags:表示中断标志位,可取IRQF_DISABLED、IRQF_SAMPLE_RANDOM和IRQF_SHARED其中之一。
3>name:请求中断的设备名称
4>dev_id:共享中断时有用。可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体。
5>strct irqaction *next:指向irqaction描述符的下一个元素。用一条链表将共享同一条中断线上的中断服务例程链接起来。
6>irq:所申请的中断号
7>dir:指向proc/irq/NN/name entry
8>thread_fn:指向具体的一个线程化的中断。
9>thread:指向线程中断的指针。
10>thread_flags:线程中断的标志。
2.3.struct irq_chip
struct irq_chip是一个中断控制器的描述符,Linux支持很多种可编程中断控制器PIC(中断控制器),通常不同的体系结构就有一套自己的中断处理方式。内核为了统一的处理中断,提供了底层的中断处理抽象接口,而每个平台都需要实现底层的函数接口,就类似于我们的虚拟文件系统(VFS),通过struct file_operations提供统一的接口,每种文件系统都必须具体实现这个结构体中所提供的接口。
struct irq_chip {
const char *name; /*名字*/
unsigned int (*startup)(unsigned int irq);
void (*shutdown)(unsigned int irq);
void (*enable)(unsigned int irq);
void (*disable)(unsigned int irq);
void (*ack)(unsigned int irq);
void (*mask)(unsigned int irq);
void (*mask_ack)(unsigned int irq);
void (*unmask)(unsigned int irq);
void (*eoi)(unsigned int irq);
void (*end)(unsigned int irq);
int (*set_affinity)(unsigned int irq,
const struct cpumask *dest);
int (*retrigger)(unsigned int irq);
int (*set_type)(unsigned int irq, unsigned int flow_type);
int (*set_wake)(unsigned int irq, unsigned int on);
void (*bus_lock)(unsigned int irq);
void (*bus_sync_unlock)(unsigned int irq);
/* Currently used only by UML, might disappear one day.*/
#ifdef CONFIG_IRQ_RELEASE_METHOD
void (*release)(unsigned int irq, void *dev_id);
#endif
/*
* For compatibility, ->typename is copied into ->name.
* Will disappear.
*/
const char *typename;
};
name:中断控制器的名字;
Startup:启动中断线;
Shutdown:关闭中断线;
Enable:允许中断;
Disable:禁止中断;
比如经典的中断控制器:2片级联的8259A,我们来看下它是如何实现上面的接口的
struct irq_chip i8259a_irq_type = {
.name = "XT-PIC",
.startup = i8259a_startup_irq,
.shutdown = i8259a_disable_irq,
.enable = i8259a_enable_irq,
.disable = i8259a_disable_irq,
.ack = i8259a_mask_and_ack_irq,
.end = i8259a_end_irq,
};
以上数据结构之间的关系的示意图:
3.内核中中断的执行流程分析
当内核发生中断时,首先会调用一段公共的汇编代码,位于linux/arch/x86/kernel/entry_32.S中,完成保护现场等准备工作:
common_interrupt:
SAVE_ALL /*寄存器值入栈*/
movl %esp,%eax /*栈顶指针保存到eax*/
call do_IRQ /*处理中断*/
jmp ret_from_intr /*从中断返回*/
可以发向在保存完寄存器之后,会进入到do_IRQ()函数
unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);/*保存原先寄存器的值*/
/* high bit used in ret_from_ code */
unsigned vector = ~regs->orig_ax;//获取对应的中断线号
unsigned irq;
exit_idle();
irq_enter();/*进入中断上下文*/
/*中断*/
irq = __get_cpu_var(vector_irq)[vector];//根绝中断线号获取相应的中断号
if (!handle_irq(irq, regs)) {/*处理中断的关键*/
ack_APIC_irq();
if (printk_ratelimit())
pr_emerg("%s: %d.%d No irq handler for vector (irq %d)
",
__func__, smp_processor_id(), vector, irq);
}
irq_exit();/*结束中断*/
set_irq_regs(old_regs);/*恢复原先寄存器的值*/
return 1;
}
这个函数进来首先是对regs寄存器的保存,可以在这个函数的最后面通过set_irq_regs来恢复这个寄存器的值。接着是irq_entry()这个函数,表示进入到中断上下文,这个函数的主要工作是对thread_info中的preempt_count中的Hard_IRQ字段进行了递增。这会使in_interrupt()函数返回true,从而标志系统进入中断处理的上下文中。接着是依据中断号和设备的中断号之间的对应关系,获取中断号,接着就进入到了handle_irq函数
bool handle_irq(unsigned irq, struct pt_regs *regs)
{
struct irq_desc *desc;/*中断描述符*/
int overflow;
overflow = check_stack_overflow();/*检测内核的中断栈是否溢出*/
desc = irq_to_desc(irq);/*从全局表中得到*/
if (unlikely(!desc))
return false;
if (!execute_on_irq_stack(overflow, desc, irq)) {/*是否需要堆栈的切换*/
if (unlikely(overflow))
print_stack_overflow();
desc->handle_irq(irq, desc);/*中断处理的函数*/
}
return true;
}
因为中断需要堆栈的支持,所有首先会检查堆栈的溢出情况,然后通过irq_to_desc函数获取中断号对应的中断向量desc。得到中断向量后就可以找到对应的中断处理程序了。紧接着是execute_on_irq_stack()函数,这个函数主要是判断当前中断是否需要堆栈的切换,如果不需要的话就开始执行desc->handle_irq(),由前面的结构体分析得这是中断的公共服务函数.
接着由crash和debuginfo这种调试信息可以看到各个中断具体调用的是哪个公共的服务函数。(关于这个工具的使用,请读者自行学习,请简单的)。这里我们来看下timer这种中断调用的是哪个公共的服务函数。
通过打印我们可以看出timer这种中断的调用的公共服务函数是handle_edge_irq函数。那么,其实内核对于中断的公共服务函数提供了以下几种:
handle_simple_irq 用于简易流控处理;
handle_level_irq 用于电平触发中断的流控处理;
handle_edge_irq 用于边沿触发中断的流控处理;
handle_fasteoi_irq 用于需要响应eoi的中断控制器;
handle_percpu_irq 用于只在单一cpu响应的中断;
handle_nested_irq 用于处理使用线程的嵌套中断
内核中使用最多的是handle_level_irq 和handle_edge_irq 这两个函数,至于这些函数的区别,还没有去深入分析。通过分析简单的分析这两个函数,都可以得出这两个函数最终会调用handle_IRQ_event这个函数就去执行中断服务程序。
irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
irqreturn_t ret, retval = IRQ_NONE;
unsigned int flags = 0;
/*不能被其他的中断打断*/
if (!(action->flags & IRQF_DISABLED))/*打开IRQF_DISABLED标志,表示处理程序必须在禁止的情况下运行*/
local_irq_enable_in_hardirq();
do {
trace_irq_handler_entry(irq, action);/*打开中断跟踪*/
ret = action->handler(irq, action->dev_id);/*调用中断函数*/
trace_irq_handler_exit(irq, action, ret);/*关闭中断跟踪*/
switch (ret) {
case IRQ_WAKE_THREAD:
/*
* Set result to handled so the spurious check
* does not trigger.
*/
ret = IRQ_HANDLED;//IRQ_HANDLED标志该中断处理函数已经被处理
/*
* Catch drivers which return WAKE_THREAD but
* did not set up a thread function
*/
if (unlikely(!action->thread_fn)) {
warn_no_thread(irq, action);
break;
}
/*
* Wake up the handler thread for this
* action. In case the thread crashed and was
* killed we just pretend that we handled the
* interrupt. The hardirq handler above has
* disabled the device interrupt, so no irq
* storm is lurking.
*/
if (likely(!test_bit(IRQTF_DIED,
&action->thread_flags))) {
set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
wake_up_process(action->thread);
}
/* Fall through to add to randomness */
case IRQ_HANDLED:
flags |= action->flags;
break;
default:
break;
}
retval |= ret;
action = action->next;
} while (action);
add_interrupt_randomness(irq, flags);
local_irq_disable();
return retval;
}
由内核的源代码可知,该函数主要执行了以下的功能:
- 设置IRQF_DISABLED,禁用该中断线,表示在执行该中断处理时,不能被其他的中断打断。
- 逐一调用所注册的IRQ处理程序的action函数。
- 如果对该IRQ设置了IRQF_SAMPLE_PANDOM,则调用add_interrupt_randomness,这个函数使用中断间隔时间为随机数产生器产生熵。
- local_irq_disable禁用中断,可见do_IRQ期望中断一直是禁止的。函数返回
至此,大致分析完了,内核中断的处理流程,但是对于一些细节的东西,还需要进一步去研究。
4.动手写一个中断处理程序
在中断描述符表(IDT)初始化时,每个中断服务队列还为空。此时即使打开中断且某个外设中断真的发生了,也得不到实际的服务。因为CPU虽然通过中断门进入了某个中断向量的总处理程序。但是,具体的中断服务例程还没有挂入到中断请求队列。所以,在设备驱动程序的初始化阶段,必须通过request_irq()函数将响应的中断服务程序注册进去。
int request_irq(unsigned int irq, irq_handler_t handler,unsigned long flags, const char *devname, void *dev_id)
第一个参数irq:表示要分配的中断号,对于一些设备(时钟或者键盘)它的值时预先固定的,而对于大多数设备来说,是动态分配的。
第二个参数handler:表示要挂入到中断请求队列中的中断服务程序,这个中断服务函数的原型是static irqreturn_t handler(int , void *)
。
第三个参数flags:为标志位,可以取IRQF_DISABLED、IRQF_SHARED和IRQF_SAMPLE_RANDOM之一.IRQF_SHARED,该标志表示多个中断处理程序共享irq中断线。一般某个中断线上的中断服务程序在执行时会屏蔽请求该线的其他中断,如果取 IRQF_DISABLED标志,则在执行该中断服务程序时会屏蔽所有其他的中断。取IRQF_SAMPLE_RANDOM则表示设备可以被看做是事件随见的发生源。
第四个参数devname:是请求中断的设备的名称。可以再/proc/interrupts中看到。
第五个参数dev_id:为一个指针变量,是void类型的,dev_id主要用于共享中断线,对每个注册的中断处理程序来说,dev_id参数必须是唯一的。如果无需中断线,则将该参数赋值为NULL.
这个dev_id为什么必须是唯一的呢?
- 根据我们前面中断模型的知识,可以看出发生中断时,内核并不判断究竟是共享中断线上的哪个设备产生了中断,它会循环去执行所有该中断线上的注册的所有的中断处理函数。因此irqaction->handler函数就有责任识别出是自己的硬件设备产生了中断,然后再去执行该中断处理函数。那既然kernel循环执行该中断线上注册的所有irqaction->handler函数,把识别究竟是哪个硬件设备产生了中断这件事情交给中断处理函数本身去做,那么dev_id的作用就是当中断到来时,迅速根据硬件寄存器中的信息比照传入的dev_id参数来判断是否是本设备的中断,若不是,应该迅速返回。
- 还有个原因就是在注销中断处理函数时,因为dev_id是唯一的,所以可以通过它来判断从共享中断线上的多个中断处理程序中删除指定的一个,如果没有这个参数,那么kernel不可能知道给定的中断线上到底要删除哪一个处理程序。free_irq的函数原型
void free_irq(unsigned int irq, void *dev_id)
注册中断服务程序源码
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/interrupt.h>
#include <linux/timer.h>
#include <linux/delay.h>
#include <linux/hardirq.h>
#define DEBUG
#ifdef DEBUG
#define MSG(message, args...) printk(KERN_DEBUG "irq:" message, ##args)
#else
#define MSG(message, args...)
#endif
int irq;
char *interface;
static int count = 1;
module_param(irq, int, 0644);
module_param(interface, charp, 0644);
int irq_handle_function(int irq, void *device_id) //中断处理函数
{
if(count % 10 == 0)
{
MSG("[%d]receive the irq at %ld...
", count, jiffies);
printk("in_interupt(irq) = %d
",in_interrupt());
}
//msleep(1); dead
//ndelay(1); ok
//mdelay(1);// ok
count++; //统计中断发生了多少次
return IRQ_NONE;
}
int init_module()
{
//注册中断
if(request_irq(irq, irq_handle_function, IRQF_SHARED, interface, (void *)&irq))
{
MSG("regist irq failure...
");
return -EIO;
}
printk("in_interupt = %d
",in_interrupt());//返回0是进程上下文,返回非0是中断上下文
MSG("interface=%s and irq=%d...
", interface, irq);
MSG("register irq success...
");
return 0;
}
void cleanup_module()
{
free_irq(irq, &irq); //清除中断
MSG("unregister irq...
");
}
MODULE_LICENSE("GPL");
MODULE_AUTHOR("wang.com");
MODULE_DESCRIPTION ("irq test");
5.注册中断服务程序内核源码分析
上面我们使用了request_irq函数来注册一个中断服务程序,下面我们来分析下内核中是如何实现注册一个中断服务程序的。
首先可以发现reauest_irq函数中是封装了request_threaded_irq函数:
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
{
return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
进入这个函数:
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irqaction *action;
struct irq_desc *desc;
int retval;
/*
* handle_IRQ_event() always ignores IRQF_DISABLED except for
* the _first_ irqaction (sigh). That can cause oopsing, but
* the behavior is classified as "will not fix" so we need to
* start nudging drivers away from using that idiom.
*/
if ((irqflags & (IRQF_SHARED|IRQF_DISABLED)) ==
(IRQF_SHARED|IRQF_DISABLED)) {
pr_warning(
"IRQ %d/%s: IRQF_DISABLED is not guaranteed on shared IRQs
",
irq, devname);
}
#ifdef CONFIG_LOCKDEP
/*
* Lockdep wants atomic interrupt handlers:
*/
irqflags |= IRQF_DISABLED;
#endif
/*
* Sanity-check: shared interrupts must pass in a real dev-ID,
* otherwise we'll have trouble later trying to figure out
* which interrupt is which (messes up the interrupt freeing
* logic etc).
*/
if ((irqflags & IRQF_SHARED) && !dev_id)/*如果是共享中断的话就必须要有一个dev_id*/
return -EINVAL;
desc = irq_to_desc(irq);/*根据irq中断号获取到中断向量*/
if (!desc)
return -EINVAL;
if (desc->status & IRQ_NOREQUEST)/*表示IRQ不能申请*/
return -EINVAL;
if (!handler) {/*判断handler是否为空*/
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
/*分配irqaction结构体内存,并填充这个结构体*/
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
chip_bus_lock(irq, desc);
retval = __setup_irq(irq, desc, action);
chip_bus_sync_unlock(irq, desc);
if (retval)
kfree(action);
#ifdef CONFIG_DEBUG_SHIRQ_FIXME
if (irqflags & IRQF_SHARED) {/*如果是共享中断的话,把这条中断屏蔽,然后在执行,执行完后打开*/
/*
* It's a shared IRQ -- the driver ought to be prepared for it
* to happen immediately, so let's make sure....
* We disable the irq to make sure that a 'real' IRQ doesn't
* run in parallel with our fake.
*/
unsigned long flags;
disable_irq(irq);
local_irq_save(flags);
handler(irq, dev_id);
local_irq_restore(flags);
enable_irq(irq);
}
#endif
return retval;
}
首先分析下这个函数中的各个形参:
- irq:表示申请的中断号
- handler:表示中断服务程序。
- thread_fn:中断线程化,如果传递NULL表示没有中断线程化。
- 此参数是最新版本中才出现的。为什么要提出中断线程化?
在 Linux 中,中断具有最高的优先级。不论在任何时刻,只要产生中断事件,内核将立即执行相应的中断
处理程序,等到所有挂起的中断和软中断处理完毕后才能执行正常的任务,因此有可能造成实时任务得不
到及时的处理。中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以
有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理,即使在严重负载下仍
有实时性保证。but,并不是所有的中断都可以被线程化,比如时钟中断,主要用来维护系统时间以及定时器
等,其中定时器是操作系统的脉搏,一旦被线程化,就有可能被挂起,这样后果将不堪设想,所以不应当
被线程化。
- irqflags:表示中断标志位。
- devname:表示请求中断的设备的名称。
- dev_id:对应于request_irq()函数中所传递的第五个参数,可取任意值,但必须唯一能够代表发出中断请求的设备,通常取描述该设备的结构体。共享中断时所用。
6.总结
这里对中断的分析只是冰山一角,对中断的初始化以及一些标志位的认识还不够完善,还需要继续努力,下期,中断的下半部处理机制,我们再会。