广义的中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
中断描述符表是一个系统表,它与每一个中断或者异常向量相联系 每个向量在表中有相应的中断或者异常处理程序的入口地址。 每个描述符8个字节,共256项,占用空间2KB 。内核启动中断前,必须初始化IDT,然后把IDT的基地址装载到idtr寄存器中 int指令允许用户进程发出一个中断信号,其值可以是0-255的任意一个向量 。所以,为了防止用户用int指令非法模拟中断和异常,IDT的初始化时要很小心的设置特权级,可以通过把相应描述符的DPL字段设置成0来实现。 然而用户进程有时必须要能发出一个编程异常。为了做到这一点,只要把相应的中断或陷阱门描述符的特权级设置成3。
中断门
用户态的进程不能访问的一个Intel中断门(特权级为0),所有的中断都通过中断门激活,并全部在内核态 。
陷阱门
用户态的进程不能访问的一个Intel陷阱门(特权级为0),大部分linux异常处理程序通过陷阱门激活。
系统门
用户态的进程可以访问的一个Intel陷阱门(特权级为3),通过系统门来激活4个linux异常处理程序,它们的向量是3,4,5和128。因此,在用户态下可以发布int3,into,bound和int $0x80四条汇编指令。
异常
Intel公司保留0-31号中断向量用来处理异常事件:当产生一个异常时,处理机就会自动把控制转移到相应的处理程序的入口,异常的处理程序由操作系统提供。
其中2号为非屏蔽中断 ,根据异常时保存在内核堆栈中的eip的值可以进一步分为:
- 故障(fault):eip=引起故障的指令的地址 通常可以纠正,处理完异常时,该指令被重新执行 例如缺页异常 。
- 陷阱(trap):eip=随后要执行的指令的地址。
- 异常中止(abort):eip=??? 发生严重的错误。eip值无效,只有强制终止受影响的进程。
中断 (狭义)
0-31号中断向量被用作异常处理,则剩下32-255号中断向量共224个中断向量可以使用。这224个中断向量的分配方式即除了128号向量0x80 (SYSCALL_VECTOR)用作系统调用总入口之外,其他都用在外部硬件中断源上,包括可编程中断控制器8259A的15个irq(如下图所示);事实上,当没有定义CONFIG_X86_IO_APIC时,其他223(除0x80外)个中断向量,只利用了从32号开始的15个,其它208个空着未用。
中断控制器处理过程:
- 监视IRQ线,对引发信号检查
- 如果一个引发信号出现在IRQ线上 ,把此信号转换成对应的中断向量 ,并把这个向量存放在中断控制器的一个I/O端口,从而允许CPU通过数据总线读这个向量。
- 把引发信号发送到处理器的INTR引脚,即产生一个中断 ,等待CPU应答这个信号;收到应答后,清空INTR引脚 。
可以对中断控制器编程:修改起始中断向量的值,或者有选择的屏蔽/激活每条IRQ线。
屏蔽全部可屏蔽中断,使用Eflags中的IF标志: 0=关中断;1=开中断。
关中断时,CPU不响应中断控制器发布的任何中断请求,内核中使用cli和sti指令分别清除和设置IF标志。
IRQ号与I/O设备之间的对应关系是在初始化每个设备驱动程序时建立的。系统初始化时,调用init_IRQ()函数用新的中断门替换临时中断门来更新IDT。
中断程序入口操作为:
- 将中断向量入栈
- 保存所有其他寄存器
- 调用do_IRQ
- 跳转到ret_from_intr
do_IRQ(与体系结构无关)
每一个中断号具有一个中断描述符(0-224),使用action链表连接共享同一个中断号的多个设备和中断 。
irqaction的定义
struct irqaction {
irq_handler_t handler;
unsigned long flags;
cpumask_t mask;
const char *name;
void *dev_id;
struct irqaction *next;
int irq;
struct proc_dir_entry *dir;
};
irq_chip的定义 (中断控制器处理例程)
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);
void (*set_affinity)(unsigned int irq, cpumask_t 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);
/* 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;
};
do_IRQ()函数的等价代码:
int irq = ~regs->orig_ax; //取得对应的中断向量
irq_desc[irq]->handle_irq(irq, desc); //调用中断处理句柄,对8259,就是handle_level_irq
mask_ack_irq(desc, irq); //应答PIC的中断,并禁用这条IRQ线。(为串行处理同类型中断)
handle_IRQ_event(irq,®s,irq_desc[irq].action);//调用handle_IRQ_event()执行中断服务例程,例如timer_interrupt
irq_desc[irq].handler->end(irq); //句知PIC重新激活这条IRQ线,允许处理同类型中断
处理下半部分 //6
一个中断服务例程实现一种特定设备的操作, handle_IRQ_evnet()函数依次调用这些设备例程 ,这个函数本质上执行了如下核心代码:
do{
action->handler(irq,action->dev_id,regs);
action = action->next;
}while (action);
x86体系结构的异常和非屏蔽中断占用了0~19这20个中断向量。且其中仅有2号向量用于非屏蔽中断,其余19个全部分类为异常。 这20个向量的设置或初始化会在trap_init()函数中完成,通过调用set_intr_gate()、set_system_intr_gate()、set_task_gate()等一系列函数设置这20个向量。128号向量用于系统调用,在start_kernel()中的trap_init()中的set_system_trap_gate()中初始化。
硬件级中断处理过程分析
1.确定与中断或者异常关联的向量i(0~255)
2,读idtr寄存器指向的IDT表中的第i项
3,从gdtr寄存器获得GDT的基地址,并在GDT中查找,以读取IDT表项中的段选择符所标识的段描述符
4,确定中断是由授权的发生源发出的。 中断:中断处理程序的特权不能低于引起中断的程序的特权(对应GDT表项中的DPL vs CS寄存器中的CPL) 编程异常:还需比较CPL与对应IDT表项中的DPL。
5,检查是否发生了特权级的变化,一般指是否由用户态陷入了内核态。 如果是由用户态陷入了内核态,控制单元必须开始使用与新的特权级相关的堆栈 a,读tr寄存器,访问运行进程的tss段 b,用与新特权级相关的栈段和栈指针装载ss和esp寄存器。这些值可以在进程的tss段中找到 c,在新的栈中保存ss和esp以前的值,这些值指明了与旧特权级相关的栈的逻辑地址。
6,若发生的是故障,用引起异常的指令地址修改cs和eip寄存器的值,以使得这条指令在异常处理结束后能被再次执行
7,在栈中保存eflags、cs和eip的内容
8,如果异常产生一个硬件出错码,则将它保存在栈中
9,装载cs和eip寄存器,其值分别是IDT表中第i项门描述符的段选择符和偏移量字段。
10.从中断或者异常中返回。
- 用保存在栈中的值装载cs、eip和eflags寄存器。如果一个硬件出错码曾被压入栈中,那么弹出这个硬件出错码
- 检查处理程序的特权级是否等于cs中最低两位的值(这意味着进程在被中断的时候是运行在内核态还是用户态)。若是,iret终止执行;否则,进行下一步。
- .从栈中装载ss和esp寄存器。这步意味着返回到与旧特权级相关的栈
上下半部分机制
我们把中断处理切为两半。中断处理程序是上半部——接受中断,他就立即开始执行,但只有做严格时限的工作。能够被允许稍后完成的工作会推迟到下半部去,在合适的时机,下半部分会被执行。
下半部分三种方法
- 软中断
初始化:定义一个新的可延迟函数,通常在内核初始化时进行 ,open_softirq。
检查:local_softirq_pending。当do_IRQ完成了I/O中断的处理时,检查是否有软中断被挂起,若被挂起则do_softirq,软中断执行后,若发现又有新的软中断被激活,就唤醒ksoftirqd进程,来触发do_softirq的另一次执行。
- tasklet
Tasklet和高优先级的tasklet 分别存放在tasklet_vec和tasklet_hi_vec数组中。 数组的每一项针对一个CPU,代表这个CPU上的tasklet列表 ,分别由tasklet_action和tasklet_hi_action处理 找到CPU对应的那个项,遍历执行。在软中断的上下文中运行,所有的代码必须是原子的。
- 工作队列
工作队列在一个内核线程上下文运行,并且可以在延迟一段确定的时间后才执行,有更多的灵活性。