⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 中断.txt

📁 讲解linux内核 系统中断 部分经典讲义
💻 TXT
📖 第 1 页 / 共 5 页
字号:
if (auto_eoi)
outb_p(0x03, 0x21); /* master does Auto EOI */
else
outb_p(0x01, 0x21); /* master expects normal EOI */
outb_p(0x11, 0xA0); /* ICW1: select 8259A-2 init */
outb_p(0x20 + 8, 0xA1); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */
outb_p(0x02, 0xA1); /* 8259A-2 is a slave on master's IR2 */
outb_p(0x01, 0xA1); /* (slave's support for AEOI in flat mode is to be 
investigated) */

这样,在IDT的向量0x20-0x2f可以分别填入相应的中断处理函数的地址了。
i386中断门描述符
段选择符和偏移量决定了中断处理函数的入口地址
在这里段选择符指向内核中唯一的一个代码段描述符的地址__KERNEL_CS(=0x10),而这个描述符定义的段为0到4G:
---------------------------------------------------------------------------------
ENTRY(gdt_table) .quad 0x0000000000000000 /* NULL descriptor */
.quad 0x0000000000000000 /* not used */
.quad 0x00cf9a000000ffff /* 0x10 kernel 4GB code at 0x00000000 */
... ...
---------------------------------------------------------------------------------
而偏移量就成了绝对的偏移量了,在IDT的描述符中被拆成了两部分,分别放在头和尾。
P标志着这个代码段是否在内存中,本来是i386提供的类似缺页的机制,在Linux中这个已经不用了,都设成1(当然内核代码是永驻内存的,但即使不在内存,推测linux也只会用缺页的标志)。
DPL在这里是0级(特权级)
0D110中,D为1,表明是32位程序(这个细节见i386开发手册).110是中断门的标识,其它101是任务门的标识, 111是陷阱(trap)门标识。
Linux对中断门的设置
于是在Linux中对硬件中断的中断门的设置为:
init_IRQ(void)
---------------------------------------------------------
for (i = 0; i < NR_IRQS; i++) {
int vector = FIRST_EXTERNAL_VECTOR + i;
if (vector != SYSCALL_VECTOR)
set_intr_gate(vector, interrupt[ i]);
}
----------------------------------------------------------
其中,FIRST_EXTERNAL_VECTOR=0x20,恰好为8259芯片的IR0的中断门(见8259部分),也就是时钟中断的中断门),interrupt[ 
i]为相应处理函数的入口地址
NR_IRQS=224, =256(IDT的向量总数)-32(CPU保留的中断的个数),在这里设置了所有可设置的向量。
SYSCALL_VECTOR=0x80,在这里意思是避开系统调用这个向量。

而set_intr_gate的定义是这样的:
----------------------------------------------------
void set_intr_gate(unsigned int n, void *addr){
_set_gate(idt_table+n,14,0,addr);
}
----------------------------------------------------
其中,需要解释的是:14是标识指明这个是中断门,注意上面的0D110=01110=14;另外,0指明的是DPL.
中断入口

以8259的16个中断为例:
通过宏BUILD_16_IRQS(0x0), BI(x,y),以及
#define BUILD_IRQ(nr) \
asmlinkage void IRQ_NAME(nr); \
__asm__( \
"\n"__ALIGN_STR"\n" \
SYMBOL_NAME_STR(IRQ) #nr "_interrupt:\n\t" \
"pushl $"#nr"-256\n\t" \
"jmp common_interrupt");
得到的16个中断处理函数为:

IRQ0x00_interrupt:
push $0x00 - 256
jump common_interrupt
IRQ0x00_interrupt:
push $0x01 - 256
jump common_interrupt
... ...

IRQ0x0f_interrupt:
push $0x0f - 256
jump common_interrupt

这些处理函数简单的把中断号-256(为什么-256,也许是避免和内部中断的中断号有冲突)压到栈中,然后跳到common_interrupt

其中common_interrupt是由宏BUILD_COMMON_IRQ()展开:
#define BUILD_COMMON_IRQ() \
asmlinkage void call_do_IRQ(void); \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
SAVE_ALL \
"pushl $ret_from_intr\n\t" \
SYMBOL_NAME_STR(call_do_IRQ)":\n\t" \
"jmp "SYMBOL_NAME_STR(do_IRQ));
.align 4,0x90common_interrupt:
SAVE_ALL展开的保护现场部分
push $ret_from_intrcall
do_IRQ:
jump do_IRQ;
从上面可以看出,这16个的中断处理函数不过是把中断号-256压入栈中,然后保护现场,最后调用do_IRQ 
.在common_interrupt中,为了使do_IRQ返回到entry.S的ret_from_intr标号,所以采用的是压入返回点ret_from_intr,用jump来模拟一个从ret_from_intr上面对do_IRQ的一个调用。
和IDT的衔接
为了便于IDT的设置,在数组interrupt中填入所有中断处理函数的地址:
void (*interrupt[NR_IRQS])(void) = {
IRQ0x00_interrupt,
IRQ0x01_interrupt,
... ...
}
在中断门的设置中,可以看到是如何利用这个数组的。
硬件中断处理函数do_IRQ
do_IRQ的相关对象
在do_IRQ中,一个中断主要由三个对象来完成
其中, 
irq_desc_t对象构成的irq_desc[]数组元素分别对应了224个硬件中断(idt一共256项,cpu自己前保留了32项,256-32=224,当然这里面有些项是不用的,比如x80是系统调用).
当发生中断时,函数do_IRQ就会在irq_desc[]相应的项中提取各种信息来完成对中断的处理。
irq_desc有一个字段handler指向发出这个中断的设备的处理对象hw_irq_controller,比如在单CPU,这个对象一般就是处理芯片8259的对象。为什么要指向这个对象呢?因为当发生中断的时候,内核需要对相应的中断进行一些处理,比如屏蔽这个中断等。这个时候需要对中断设备(比如8259芯片)进行操作,于是可以通过这个指针指向的对象进行操作。
irq_desc还有一个字段action指向对象irqaction,后者是产生中断的设备的处理对象,其中的handler就是处理函数。由于一个中断可以由多个设备发出,Linux内核采用轮询的方式,将所有产生这个中断的设备的处理对象连成一个链表,一个一个执行。
例如,硬盘1,硬盘2都产生中断IRQx,在do_IRQ中首先找到irq_desc[x],通过字段handler对产生中断IRQx的设备进行处理(对8259而言,就是屏蔽以后的中断IRQx),然后通过action先后运行硬盘1和硬盘2的处理函数。

hw_irq_controller
hw_irq_controller有多种:
1.在一般单cpu的机器上,通常采用两个8259芯片,因此hw_irq_controller指的就是i8259A_irq_type
2.在多CPU的机器上,采用APIC子系统来处理芯片,APIC有3个部分组成,一个是I/O APIC模块,其作用可比做8259芯片,但是它发出的中断信号会通过 
APIC总线送到其中一个(或几个)CPU中的Local APIC模块,因此,它还起一个路由的作用;它可以接收16个中断。
中断可以采取两种方式,电平触发和边沿触发,相应的,I/O APIC模块的hw_irq_controller就有两种:
ioapic_level_irq_type
ioapic_edge_irq_type
(这里指的是intel的APIC,还有其它公司研制的APIC,我没有研究过)
3. Local APIC自己也能单独处理一些直接对CPU产生的中断,例如时钟中断(这和没有使用Local 
APIC模块的CPU不同,它们接收的时钟中断来自外围的时钟芯片),因此,它也有自己的 hw_irq_controller:
lapic_irq_type
struct hw_interrupt_type {
const char * typename;
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 (*end)(unsigned int irq);
void (*set_affinity)(unsigned int irq, unsigned long mask);
};
typedef struct hw_interrupt_type hw_irq_controller;

startup 是启动中断芯片(模块),使得它开始接收中断,一般情况下,就是将 所有被屏蔽的引脚取消屏蔽
shutdown 反之,使得芯片不再接收中断
enable 设某个引脚可以接收中断,也就是取消屏蔽
disable 屏蔽某个引脚,例如,如果屏蔽0那么时钟中断就不再发生
ack 当CPU收到来自中断芯片的中断信号,给相应的引脚的处理,这个各种情况下 (8259, APIC电平,边沿)的处理都不相同
end 在CPU处理完某个引脚产生的中断后,对中断芯片(模块)的操作。
irqaction
将一个硬件处理函数挂到相应的处理队列上去(当然首先要生成一个irqaction结构):

-----------------------------------------------------
int request_irq(unsigned int irq,
void (*handler)(int, void *, struct pt_regs *),
unsigned long irqflags,
const char * devname,
void *dev_id)
-----------------------------------------------------

参数说明在源文件里说得非常清楚。
handler是硬件处理函数,在下面的代码中可以看得很清楚:
---------------------------------------------
do {
status |= action->flags;
action->handler(irq, action->dev_id, regs);
action = action->next;
} while (action);
---------------------------------------------

第二个参数就是action的dev_id,这个参数非常灵活,可以派各种用处。而且要保证的是,这个dev_id在这个处理链中是唯一的,否则删除会遇到麻烦。
第三个参数是在entry.S中压入的各个积存器的值。
它的大致流程是:
1.在slab中分配一个irqaction,填上必需的数据
以下在函数setup_irq中。
2.找到它的irq对应的结构irq_desc
3.看它是否想对随机数做贡献
4.看这个结构上是否已经挂了其它处理函数了,如果有,则必须确保它本身和这个队列上所有的处理函数都是可共享的(由于传递性,只需判断一个就可以了)
5.挂到队列最后
6.如果这个irq_desc只有它一个irqaction,那么还要进行一些初始化工作
7在proc/下面登记 register_irq_proc(irq)(这个我不太明白)
将一个处理函数取下:
void free_irq(unsigned int irq, void *dev_id)
首先在队列里找到这个处理函数(严格的说是irqaction),主要靠dev_id来匹配,这时dev_id的唯一性就比较重要了。
将它从队列里剔除。
如果这个中断号没有处理函数了,那么禁止这个中断号上再产生中断:
if (!desc->action) {
desc->status |= IRQ_DISABLED;
desc->handler->shutdown(irq);
}
如果其它CPU在运行这个处理函数,要等到它运行完了,才释放它:
#ifdef CONFIG_SMP
/* Wait to make sure it's not being used on another CPU */
while (desc->status & IRQ_INPROGRESS)
barrier();
#endif
kfree(action);
do_IRQ
asmlinkage unsigned int do_IRQ(struct pt_regs regs)
1.首先取中断号,并且获取对应的irq_desc:
int irq = regs.orig_eax & 0xff; /* high bits used in ret_from_ code */
int cpu = smp_processor_id();
irq_desc_t *desc = irq_desc + irq;
2.对中断芯片(模块)应答:
desc->handler->ack(irq);
3.修改它的状态(注:这些状态我觉得只有在SMP下才有意义):
status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
status |= IRQ_PENDING; /* we _want_ to handle it */
IRQ_REPLAY是指如果被禁止的中断号上又产生了中断,这个中断是不会被处理的,当这个中断号被允许产生中断时,会将这个未被处理的中断转为IRQ_REPLAY。
IRQ_WAITING 
探测用,探测时,会将所有没有挂处理函数的中断号上设置IRQ_WAITING,如果这个中断号上有中断产生,就把这个状态去掉,因此,我们就可以知道哪些中断引脚上产生过中断了。
IRQ_PENDING , IRQ_INPROGRESS是为了确保:
同一个中断号的处理程序不能重入
不能丢失这个中断号的下一个处理程序
具体的说,当内核在运行某个中断号对应的处理程序(链)时,状态会设置成IRQ_INPROGRESS。如果在这期间,同一个中断号上又产生了中断,并且传给CPU,那么当内核打算再次运行这个中断号对应的处理程序(链)时,发现已经有一个实例在运行了,就将这下一个中断标注为IRQ_PENDING, 
然后返回。这个已在运行的实例结束的时候,会查看是否期间有同一中断发生了,

⌨️ 快捷键说明

复制代码 Ctrl + C
搜索代码 Ctrl + F
全屏模式 F11
切换主题 Ctrl + Shift + D
显示快捷键 ?
增大字号 Ctrl + =
减小字号 Ctrl + -