首先说明一点,在看到的资料和教材上,中断应该分为“中断”和“异常”两种。
这里的“中断”指的是外部中断,“异常”说的是内部中断。在我的总结里面,为了让自己的思路能够清晰一点,
没有将内部中断称为“异常”(正确的应该叫做异常),也没有把外部中断成为“中断”。
第五章讲的是linux的中断机制,接下来我想从这么几个方面来说说我看到的linux中断机制:
一:中断的类型和一些中断相关的知识
二:中断处理过程
三:关于中断的几个重要的函数分析
接下来开始:
一、中断的类型简介
1. 首先简单说一下中断有哪几个类型:
中断给人的感觉就是由外部设备所引起的,比如由打印机,键盘,鼠标等。然而这些由外部设备所引起
的中断只能称为“外部中断”。既然有“外部中断”,那么与之对应的肯定有“内部中断”。
外部中断是由外部硬件设备引起的,而外部中断又可以分为两种,一种叫做“非屏蔽中断”(由硬件故障等所引起的紧急事件),
另一种叫做“可屏蔽中断”(I/O设备产生的正常的中断请求)。从命名就可以看出来,前者是不能被屏的;后者
是可以屏蔽的,也就是说计算机内部可以不响应这种中断。
内部中断也可以分为两类(其实是三类,有一种叫做“终止”,不作介绍):一种叫做“故障”,另一种叫做“陷阱”。
关于“故障”和“陷阱”的区别,简单来讲故障的处理后要重新执行产生故障的那条指令,而陷阱被处理后,执行的是产生
陷阱的那条指令的下一条指令。
以上就是对于中断的简单分类。为了区分这几类中断,x86用了一个8位的无符号整型数据来对这几类中断进行区分。
下表是对以上内容的简单概括。(从word里面复制过来的表就成这个样子了,不知道怎么改好)
中断类型 |
|
对应8位中断类型码 (十进制表示) |
外部中断 |
可屏蔽中断 |
32~47 |
非屏蔽中断 |
0~31 |
|
内部中断 |
故障 |
|
陷阱 |
从表中可以发现并没有48到255对应的中断,这是因为x86将剩余的这些数字对应到了软中断上面,
至于什么是软中断,这里简单说一下:软中断是软件实现的中断,也就是程序运行时其他程序对它的中断 ,
CPU或接收进程在适当的时机自动进行中断处理或完成软中断信号对应的功能。
2. 与中断相关的一些名词
中断控制器:就是控制15个可屏蔽中断的两片8259A。那么就简单说一下可屏蔽中断是如何进行屏蔽的。
有两种方式屏蔽可屏蔽中断:第一种是CPU通过清除EFLAG位为0,此时就屏蔽了所有的可屏蔽中断,也就是
说禁止了所有的I/O中断请求。第二种方式是,中断控制器中有一个中断屏蔽寄存器,通过把中断屏蔽寄存器的
相关位置置为1,而屏蔽与之相对应的某一条中断请求线。
中断处理程序:处理中断的程序。
门描述符:所谓门的意思就是,门的外面是中断,门的里面的中断处理程序。中断必须通过这个门,
才能得到中断处理程序的处理。所以门就应该包含以下信息,一个是中断处理程序的地址,还有就是对于
中断检验的以及中断相关的一些信息。
中断描述符表 :存放门描述符的表。
二、中断机制简介以及一个中断的处理过程
1. 简单说一下中断描述符表的填充和中断处理程序的形成
中断描述符表的填充和中断处理程序的形成都是要进行中断处理之前的工作。
上面说过,中断描述符表里面存储的每一个表项都是一个门描述符,那么如何将门描述符插入到中断
描述符表之中的。内核通过函数_set_gate() 来实现的。具体的实现函数如下:
_set_gate(idt_table+n, 14, 0, addr); --> 插入一个中断门
_set_gate(idt_table+n, 15, 0, addr); --> 插入一个陷阱门
_set_gate(idt_table+n, 15, 3, addr); --> 插入一个系统门
可以看上面三个函数只是参数不同:
其中第一个参数是描述插入的门描述符在中断描述表中的位置,idt_table表示中断描述符表,
n就是门描述符的位置。最后一个参数addr表示的是中断处理程序的地址。
三个函数的第二个参数是14或者15,用二进制表示分别是1110和1111。将这个二进制位拆分成两部分,
第一位为第一部分,1表示32位,0表示16位。后面三位为第二部分,110表示这是一个中断门,111表示这是
一个陷阱门。那么,就会发现系统门也是一个陷阱门,那么它们的区别在哪。区别就在第三个参数。
第三个参数表示DPL域的值,学过第二章我们都知道DPL域描述一个描述符的特权级。0表示内核态,
3表示用户态。所以系统门就是一个用户可以访问的陷阱门。而这个门就是专门为系统调用从用户态进入内核态而准备的。
从三个函数的形式上就可以看到,这三个函数一定是有个其他函数来传值的。具体的话就是通过下面三个函数来将上面的三个函数包装起来。
static void __init set_intr_gate(unsigned int n, void * addr){
_set_gate(idt_table+n, 14, 0, addr);
}
static void __init set_trap_gate(unsigned int n, void * addr){
_set_gate(idt_table+n, 15, 0, addr);
}
static void __initset_system_gate(unsigned int n, void *addr){
_set_gate(idt_table+n, 15, 3, addr);
}
有了外面的包装函数后,那么具体给它们传什么样的值。在这里,中断门和其他两个门(陷阱门和系统门)不同。
1) 先说简单一点的系统门和陷阱门。
系统门只有一句set_system_gate(SYSTEM_VECTOR, &system_call); 其中SYSCALL_VECTOR= 0x80 (128);
&system_call表示这个中断处理程序的名字是system_call,在arch/x86/kernel/entry_32.S里可以找到符号system_call,
是用汇编来写的,具体可以去目录下查看。
CPU保留了19个陷阱门,是通过下面的方式插入到中断描述符表中的。
set_trap_gate0(0, ÷_error);
set_trap_gate0(1, &debug);
...
set_trap_gate0(19, &simd_coprocessor_erro);
通过上面19个语句就可以向中断描述符表中插入19个陷阱门。加上上面提到的系统门,这20个门描述符的插入是通过
调用一个叫做trap_init()的函数来实现的。trap_init 定义在arch/x86/kernel/traps.c中。
2) 再说一下中断门的插入。
从上面可以看到对于系统门和陷阱门的插入是通过调用trap_init()函数来实现的,那么调用哪个函数来实现对中断门
的插入。在这里,调用的是init_IRQ()这个函数。这个函数的位置在arch/x86/kernel/irqinit.c 中。通过查看源码,我们会发现其中有下面的这段代码:
for(i =0; i<(NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++){
int vector = FRIST_EXTERNAL_VECTOR + i;
if(i >=NR_IRQS){
break;
}
if(vector !=SYSCALL_VECTOR){
set_intr_gate(vector, interrupt[i]);
}
}
一句句的看上面的代码,第一行中的NR_VECTORS = 255,表示0~255个中断号。FRIST_EXTERNAL_VECTOR= 31,
表示外面的32个中断号,也就是我们在最上面的表中看到的的陷阱,故障和非屏蔽中断。用 NR_VECTORS减去
FRIST_EXTERNAL_VECTOR等于224。因此也就知道了vector = 32,代码继续往下,如果i大于等于224那么就跳出循环,
也就说说当i=223时,是最后一次循环,也就将全部的224个中断门插入到了中断描述符表中。代码继续往下,如果vector不等于
128的时候,调用set_intr_gate()函数,从上文中我们可以知道,128号位置插入的是系统门,这个系统门已经在插入陷阱门的时
候将它插入到了中断描述符表中,所以在这里要跳过这个位置。最后看一下set_intr_gate()函数的第二个参数,就是那个interrupt[i]
数组。这个数组里面保存的就是每个中断处理程序的入口地址。
总结一下上面中断描述符表的初始化过程。
Linux内核初始化中断是从init/main.c中的start_kernel()调用trap_init()和init_IRQ()开始的。从上面我们知道trap_init()就是初始化
陷阱门和系统门的,而init_IRQ()就是初始化中断门的。trap_init()调用set_trap_gete()和set_system_gate(),这两个函数又分别执行
_set_gate()函数将对应的门描述符插入到中断描述符表之中。同样的,trap_init()函数调用set_intr_gate()函数,set_intr_gate()再调
用_set_gate()函数实现对中断门的插入。这样就完成了对中断描述符表的初始化。
2. 再简单说一下中断的处理过程
当系统完成对中断描述符表的初始化之后,就可以进行中断的处理等工作。下面是中断处理的一个流程图:
这个流程图大概说明了一下中断发生后到达中断处理程序的过程。
以上内容简单说了中断的类型以及中断描述符表的填充,然后再说了下中断处理的前半部分,也就是硬件部分的处理。
接下来,简单说一下中断处理的后半部分,也就说从上图中的最后一步“执行中断处理程序”之后的内容给。
三、中断相关的函数介绍
首先我们所说的中断处理程序,其实只是指一个总处理程序,因为对于外部中断来说,只有15条中断请求线可用,而如今外设的增加,使得多个设备必须要共享同一条中断请求线。但是因为不同设备的中断处理程序是不同的,而我们通过中断描述符表所找到的中断处理程序的起始地址只能是一个程序的地址。基于此,其实真正处理中断的并不是中断处理程序,而是中断服务程序。也就是说,中断处理程序只是一个总把关的,通过它,同一中断线上的不同中断请求就会得到自己的中断服务程序去处理。因为共享中断线的原因,所以linux下设置了一个中断请求队列来处理这个问题。
接下来,先简单说几个关于中断服务程序的函数:
1. 中断服务程序注册程序
int request_irq (unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char*devname, void *dev_id){
structirqaction *action; //存放着一个中断子程序的入口地址及相关信息
int retval;
#ifdefCONFIG_LOCKDEP
/*
*禁止中断嵌套,所有中断都关中断运行
*/
irqflags |= SA_INTERRUPT;
#endif
if ((irqflags & IRQF_SHARED) && !dev_id) //使用共享中断但没有提供非NULL的dev_id则返回错误
return -EINVAL;
if (irq >= NR_IRQS) //中断号超出最大值
return -EINVAL;
if (irq_desc[irq].status & IRQ_NOREQUEST) //该中断号已被使用并且未共享
return -EINVAL;
if (!handler)
return -EINVAL;
action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC); // 动态创建一个irqaction
if (!action) //创建失败
return -ENOMEM;
/* 下面几行是根据request_irq 传进来的参数对irqaction结构体赋值 */
action->handler = handler; //我们定义的中断处理子程序
action->flags = irqflags;
cpus_clear(action->mask); //中断的属性
action->name = devname; //与该中断相关联的名称,在/proc/interrupt中可看到。
action->next = NULL; //next域赋值为NULL
action->dev_id = dev_id; //设备号
select_smp_affinity(irq);
retval = setup_irq(irq, action); // 调用setup_irq注册该中断的irqaction结构体
if (retval)
kfree(action);
return retval;
}
函数参数简析:
irq,是要注册的硬件中断号;
handler,是向系统注册的中断处理函数,它是一个回调函数,在相应的中断线发生中断时,系统会调用这个函数;
irqflags,中断类型标志,IRQF_*,是中断处理的属性,若设置了IRQF_DISABLED ,则表示中断处理程序是快速
处理程序,快速处理程序被调用时屏蔽所有中断,慢速处理程序不屏蔽;若设置了IRQF_SHARED,则表示是多个设备
共享同一个中断;若设置了IRQF_SAMPLE_RANDOM,表示将对系统熵有贡献,对系统获取随机数有好处,还可以用
IRQF_TRIGGER_*标志来指定中断的触发方式。
devname,一个声明的设备的ascii 名字,与中断号相关联的名称,在/proc/interrupts文件中可以看到此名称。
dev_id,I/O设备的私有数据字段,典型情况下,它标识I/O设备本身(例如,它可能等于其主设备号和此设备号),
或者它指向设备驱动程序的数据,这个参数会被传回给handler函数。在中断共享时会用到,一般设置为这个设备的驱动
程序中任何有效的地址值或者NULL
thread_fn,由irq handler线程调用的函数,如果为NULL,则不会创建线程。
2. 中断线共享的数据结构
struct irqaction {
irq_handler_t handler; //指向具体的I/O设备的中断服务程序
unsigned long flags; //描述中断线与I/O设备之间的关系
cpumask_t mask; //在x86上不会用到
const char * name; // I/O设备名
void * dev_id; //指定I/O 设备的主设备号和此设备号
struct irqaction * next; //如果flags=SA_SHIRQ,那么这就是指向对列中下一个struct irqaction结构体的指针,否则为空。
int irq; //中断号
struct proc_dir_entry * dir;
};
上面列出来的两个函数,只是在中断机制里面的冰山一角,而之前说的将上面的流程图扩展。现在
假设终端服务程序已经挂在了中断请求队列中了。那么流程图的扩展如下图所示:
然而,中断处理并没有到此结束,因为在中断服务程序处理中断的过程中,我们是关中断的。假设
在此过程中,在同一中断请求线上又有中断请求来了,而因为关中断的原因,这个后来的中断请求
被屏蔽掉了,当然这是不被用户所接受的。因此呢,中断机制通过将中断处理分为上半部分和下半
部分来解决这个问题。上半部分基本上就是我们上面所谈到的,而下半部分的处理和小任务机制,
在以后有时间的话再好好看一下。
以上内容借鉴了网上一些大神的成果,大部分也只是我的片面只言,如需引用,请慎重。