本文为原创,转载请注明:http://www.cnblogs.com/tolimit/

本篇文章主要讲述源码中是如何对中断进行一系列的初始化的。

回顾

  在上一篇概述中,介绍了几个对于中断来说非常重要的数据结构,分别是:中断描述符表,中断描述符数组,中断描述符,中断控制器描述符,中断服务例程。可以说这几个结构组成了整个内核中断框架主体,所以内核对整个中断的初始化工作大多集中在了这几个结构上。

  在系统中,当一个中断产生时,首先CPU会从中断描述符表中获取相应的中断向量,并根据中断向量的权限位判断是否处于该权限,之后跳转至中断处理函数,在中断处理函数中会根据中断向量号获取中断描述符,并通过中断描述符获取此中断对应的中断控制器描述符,然后对中断控制器执行应答操作,最后执行此中断描述符中的中断服务例程链表,最后执行软中断。

  而整个初始化的过程与中断处理过程相应,首先先初始化中断描述符表,再初始化中断描述符数组和中断描述符。中断控制器描述符是系统预定编写好的静态变量,如i8259A中断控制器对应的变量就是i8259A_chip。这时一个中断已经初始化完毕,之后驱动需要使用此中断时系统会将驱动中的中断处理加入到该中断的中断服务例程链表中。如下图

初始化中断向量

  虽然称之为中断描述符表,其实对于CPU来说只是一个起始地址,此地址开始每向上9个字节为一个中断向量。我们的CPU上有一个idtr寄存器,它专门用于保存中断描述符表地址,当产生一个中断时,CPU会自动从idtr寄存器保存的中断描述符表地址处获取相应的中断向量,然后判断权限并跳转至中断处理函数。当计算机刚启动时,首先会启动引导程序(BIOS),在BIOS中会把中断描述符表存放在内存开始位置(0x00000000)。BIOS会有自己的一些默认中断处理函数,而当BIOS处理完后,会将计算机控制器转交给linux,而linux会在使用BIOS的中断描述符表的同时重新设置新的中断描述符表(新的地址保存在配置中的CONFIG_VECTORS_BASE),之后会完全使用新的中断描述符表。

  一般的,我们也把中断描述符表中的中断向量称为门描述符,其大小为64位,其主要保存了段选择符、权限位和中断处理程序入口地址。CPU主要将门分为三种:任务门,中断门,陷阱门。虽然CPU把门描述符分为了三种,但是linux为了处理更多种情况,把门描述符分为了五种,分别为中断门,系统门,系统中断门,陷阱门,任务门;但其存储结构与CPU定义的门不变。结构如下:

  在一个门描述符中:

  • P:代表的是段是否处于内存中,因为linux从不把整个段交换的硬盘上,所以P都被置为1。
  • DPL:代表的是权限,用于限制对这个段的存取,当其为0时,只有CPL=0(内核态)才能够访问这个段,当其为3时,任何等级的CPL(用户态及内核态)都可以访问。
  • 段选择符:除了任务门设置为TSS段,陷阱门和中断门都设置为__KERNER_CS(内核代码段)。
  • 偏移量:就是中断处理程序入口地址。

  门描述符的初始化主要分为两部分,我们知道,中断描述符表中保存的是中断和异常,所以整个中断描述符的初始化需要分为中断初始化和异常初始化。而中断描述符表的初始化情况是,第一部分是经过一段汇编代码对整个中断描述符表进行初始化,第二部分是在系统进入start_kernel()函数后分别对异常和中断进行初始化。在linux中,中断描述符表用idt_table[NR_VECTORS]数组进行描述,中断向量(门描述符)在系统中用struct desc_struct结构表示,具体我们可以往下看。

第一部分 - 汇编代码(arch/x86/kernel/head_32.S):

/*
* setup_once
*
* The setup work we only want to run on the BSP.
*
* Warning: %esi is live across this function.
*/
__INIT
setup_once: movl $idt_table,%edi # idt_table就是中断描述符表,地址保存到edi中
movl $early_idt_handlers,%eax # early_idt_handlers地址保存到eax中,early_idt_handlers是二维数组,每行9个字符
movl $NUM_EXCEPTION_VECTORS,%ecx # NUM_EXCEPTION_VECTORS地址保存到ecx中,ecx用于循环,NUM_EXCEPTION_VECTORS为32
:
movl %eax,(%edi) # 将eax的值保存到edi保存的地址中
movl %eax,(%edi) # 将eax的值保存到edi保存的地址+4中
/* interrupt gate, dpl=, present */
movl $(0x8E000000 + __KERNEL_CS),(%edi) # 将(0x8E000000 + __KERNEL_CS)一共4个字节保存到edi保存的地址+2的位置中
addl $,%eax # eax += ,指向early_idt_handlers数组下一列
addl $,%edi # edi += ,就是下一个门描述符地址
loop 1b # 根据ecx是否为0进行循环
# 前32个中断向量初始化结果:
# | | | | |
# |early_idt_handlers[i](高16位)| 0x8E00 | __KERNEL_CS |early_idt_handlers[i](低16位)| movl $ - NUM_EXCEPTION_VECTORS,%ecx # - 保存到ecx,进行新一轮的循环
movl $ignore_int,%edx # ignore_int保存到edx
movl $(__KERNEL_CS << ),%eax # (__KERNEL_CS << )保存到eax
movw %dx,%ax
movw $0x8E00,%dx :
movl %eax,(%edi)
movl %edx,(%edi)
addl $,%edi # edi += ,就是下一个门描述符地址
loop 2b
# 其他中断向量初始化结果:
# | | | | |
# | ignore_int(高16位) | 0x8E00 | __KERNEL_CS | ignore_int(低16位) |

  

  如果CPU是486,之后会通过 lidt  idt_descr 命令将中断描述符表(idt_descr)地址放入idtr寄存器;如果不是,则暂时不会将idt_descr放入idtr寄存器(在trap_init()函数再执行这步操作)。idtr寄存器一共是48位,低16位保存的是中断描述符表长度,高32位保存的是中断描述符表基地址。我们可以看看idt_descr的形式,如下:

idt_descr:
.word IDT_ENTRIES*- # 这里放入的是表长度, * -
.long idt_table # idt_table地址放在这,idt_table定义在/arch/x86/kernel/trap.h中 /* 我们再看看 idt_table 是怎么定义的,idt_table代表的就是中断描述符表 */
/* 代码地址:arch/x86/kernel/Traps.c */
gate_desc idt_table[NR_VECTORS] __page_aligned_bss; /* 继续,看看 gate_desc ,用于描述一个中断向量 */
#ifdef CONFIG_X86_64
typedef struct gate_struct64 gate_desc;
#else
typedef struct desc_struct gate_desc;
#endif /* 我们看看32位下的 struct desc_struct,此结构就是一个中断向量(门描述符) */
struct desc_struct {
union {
struct {
unsigned int a;
unsigned int b;
};
struct {
u16 limit0;
u16 base0;
unsigned base1: , type: , s: , dpl: , p: ;
unsigned limit: , avl: , l: , d: , g: , base2: ;
};
};
} __attribute__((packed));

  

  可以看出,在汇编代码初始化部分,所有的门描述符的DPL权限位都设置为0(用户态不可访问),段选择符设置为__KERNEL_CS内核代码段。而对于中断处理函数设置则不同,前32个门描述符的中断处理函数为early_idt_handlers,之后的门描述符的中断处理函数为ignore_int。而在linux中,0~19的中断向量是用于异常和陷阱。20~31的中断向量是intel保留使用的。

初始化异常向量

  异常向量作为在中断描述符表中的前20个向量(0~19),在汇编代码中已经将其的处理函数设置为early_idt_handlers,而进入start_kernel()函数后,系统会在trap_init()函数中重新设置它们的处理函数,由于异常和陷阱的特殊性,它们并没有像中断这样复杂的数据结构,单纯的,每个异常和陷阱有它们自己的中断处理函数,系统只是简单地把中断处理函数放入异常和陷阱的门描述符中。在了解trap_init()函数之前,我们需要先了解如下几个函数:

 /* 设置一个中断门
* n:中断号
* addr:中断处理程序入口地址
*/
#define set_intr_gate(n, addr) \
do { \
BUG_ON((unsigned)n > 0xFF); \
_set_gate(n, GATE_INTERRUPT, (void *)addr, , , \
__KERNEL_CS); \
_trace_set_gate(n, GATE_INTERRUPT, (void *)trace_##addr,\
, , __KERNEL_CS); \
} while ()

/* 设置一个系统中断门 */
static inline void set_system_intr_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_INTERRUPT, addr, 0x3, , __KERNEL_CS);
}

/* 设置一个系统门 */
static inline void set_system_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, 0x3, , __KERNEL_CS);
}

/* 设置一个陷阱门 */
static inline void set_trap_gate(unsigned int n, void *addr)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TRAP, addr, , , __KERNEL_CS);
}

/* 设置一个任务门 */
static inline void set_task_gate(unsigned int n, unsigned int gdt_entry)
{
BUG_ON((unsigned)n > 0xFF);
_set_gate(n, GATE_TASK, (void *), , , (gdt_entry<<));
}

  

  这几个函数用于设置不同门的API函数,他们的参数n都为中断号,而他们都会调用_set_gate()函数,只是参数不同,_set_gate()函数如下:

/* 设置一个门描述符,并写入中断描述符表
* gate: 中断号
* type: 门类型
* addr: 中断处理程序入口
* dpl: 权限位
* ist: 64位系统才使用
* seg: 段选择符
*/
static inline void _set_gate(int gate, unsigned type, void *addr,
unsigned dpl, unsigned ist, unsigned seg)
{
gate_desc s; /* 生成一个门描述符 */
pack_gate(&s, type, (unsigned long)addr, dpl, ist, seg);
/*
* does not need to be atomic because it is only done once at
* setup time
*/
/* 将新的门描述符写入中断描述符表中的gate项,使用memcpy进行写入 */
write_idt_entry(idt_table, gate, &s);
/* 用于跟踪? 暂时还不清楚这个 trace_idt_table 的用途 */
write_trace_idt_entry(gate, &s);
}

  

  了解了以上的设置门描述符的函数,我们再看看trap_init()函数:

 void __init trap_init(void)
{
int i; /* 使用了EISA总线 */
#ifdef CONFIG_EISA
void __iomem *p = early_ioremap(0x0FFFD9, ); if (readl(p) == 'E' + ('I'<<) + ('S'<<) + ('A'<<))
EISA_bus = ;
early_iounmap(p, );
#endif /* Interrupts/Exceptions */
//enum {
// X86_TRAP_DE = 0, /* 0, 除0操作 Divide-by-zero */
// X86_TRAP_DB, /* 1, 调试使用 Debug */
// X86_TRAP_NMI, /* 2, 非屏蔽中断 Non-maskable Interrupt */
// X86_TRAP_BP, /* 3, 断点 Breakpoint */
// X86_TRAP_OF, /* 4, 溢出 Overflow */
// X86_TRAP_BR, /* 5, 越界异常 Bound Range Exceeded */
// X86_TRAP_UD, /* 6, 无效操作码 Invalid Opcode */
// X86_TRAP_NM, /* 7, 无效设备 Device Not Available */
// X86_TRAP_DF, /* 8, 双重故障 Double Fault */
// X86_TRAP_OLD_MF, /* 9, 协处理器段超限 Coprocessor Segment Overrun */
// X86_TRAP_TS, /* 10, 无效任务状态段(TSS) Invalid TSS */
// X86_TRAP_NP, /* 11, 段不存在 Segment Not Present */
// X86_TRAP_SS, /* 12, 栈段错误 Stack Segment Fault */
// X86_TRAP_GP, /* 13, 保护错误 General Protection Fault */
// X86_TRAP_PF, /* 14, 页错误 Page Fault */
// X86_TRAP_SPURIOUS, /* 15, 欺骗性中断 Spurious Interrupt */
// X86_TRAP_MF, /* 16, X87 浮点数异常 Floating-Point Exception */
// X86_TRAP_AC, /* 17, 对齐检查 Alignment Check */
// X86_TRAP_MC, /* 18, 设备检查 Machine Check */
// X86_TRAP_XF, /* 19, SIMD 浮点数异常 Floating-Point Exception */
// X86_TRAP_IRET = 32, /* 32, 汇编指令异常 IRET Exception */
//}; set_intr_gate(X86_TRAP_DE, divide_error);
/* 在32位系统上其效果等同于 set_intr_gate */
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
/* int4 can be called from all */
set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, invalid_TSS);
set_intr_gate(X86_TRAP_NP, segment_not_present);
set_intr_gate(X86_TRAP_SS, stack_segment);
set_intr_gate(X86_TRAP_GP, general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, coprocessor_error);
set_intr_gate(X86_TRAP_AC, alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
#endif
set_intr_gate(X86_TRAP_XF, simd_coprocessor_error); /* 将前32个中断号都设置为已使用状态 */
for (i = ; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors); #ifdef CONFIG_IA32_EMULATION
/* 设置0x80系统调用的系统中断门 */
set_system_intr_gate(IA32_SYSCALL_VECTOR, ia32_syscall);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif #ifdef CONFIG_X86_32
/* 设置0x80系统调用的系统门 */
set_system_trap_gate(SYSCALL_VECTOR, &system_call);
set_bit(SYSCALL_VECTOR, used_vectors);
#endif /*
* Set the IDT descriptor to a fixed read-only location, so that the
* "sidt" instruction will not leak the location of the kernel, and
* to defend the IDT against arbitrary memory write vulnerabilities.
* It will be reloaded in cpu_init() */
/* 将中断描述符表设置在一个固定的只读的位置,以便“sidt”指令不会泄漏内核的位置,和保护中断描述符表可以处于任意内存写的漏洞。它将会在 cpu_init() 中被加载到idtr寄存器 */
__set_fixmap(FIX_RO_IDT, __pa_symbol(idt_table), PAGE_KERNEL_RO);
idt_descr.address = fix_to_virt(FIX_RO_IDT); /* 执行CPU的初始化,对于中断而言,在 cpu_init() 中主要是将 idt_descr 放入idtr寄存器中 */
cpu_init(); /* x86_init是一个定义了很多x86体系上的初始化操作,这里执行的另一个trap_init()函数为空函数,什么都不做 */
x86_init.irqs.trap_init(); #ifdef CONFIG_X86_64
/* 64位操作 */
/* 将 idt_table 复制到 debug_idt_table 中 */
memcpy(&debug_idt_table, &idt_table, IDT_ENTRIES * );
set_nmi_gate(X86_TRAP_DB, &debug);
set_nmi_gate(X86_TRAP_BP, &int3);
#endif
}

  

  在代码中,used_vectors变量是一个bitmap,它用于记录中断描述符表中哪些中断已经被系统注册和使用,哪些未被注册使用。trap_init()已经完成了异常和陷阱的初始化。对于linux而言,中断号0~19是专门用于陷阱和故障使用的,以上代码也表明了这一点,而20~31一般是intel用于保留的。而我们的外部IRQ线使用的中断为32~255(代码中32号中断被用作汇编指令异常中断)。所以,在trap_init()代码中,专门对0~19号中断的门描述符进行了初始化,最后将新的中断描述符表起始地址放入idtr寄存器中。在trap_init()中我们看到每个异常和陷阱都有他们自己的处理函数,不过它们的处理函数的处理方式都大同小异,如下:

#代码地址:arch/x86/kernel/entry_32.S

# 11号异常处理函数入口
ENTRY(segment_not_present)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_segment_not_present
jmp error_code
CFI_ENDPROC
END(segment_not_present) # 12号异常处理函数入口
ENTRY(stack_segment)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_stack_segment
jmp error_code
CFI_ENDPROC
END(stack_segment) # 17号异常处理函数入口
ENTRY(alignment_check)
RING0_EC_FRAME
ASM_CLAC
pushl_cfi $do_alignment_check
jmp error_code
CFI_ENDPROC
END(alignment_check) # 0号异常处理函数入口
ENTRY(divide_error)
RING0_INT_FRAME
ASM_CLAC
pushl_cfi $ # no error code
pushl_cfi $do_divide_error
jmp error_code
CFI_ENDPROC
END(divide_error)

  

  这些函数具体细节我们下篇文章分析。

  在trap_init()函数中调用了cpu_init()函数,在此函数中会将新的中断描述符表地址放入idtr寄存器中,而具体内核是如何实现的呢,之前已经说明,idtr寄存器的低16位保存的是中断描述符表长度,高32位保存的是中断描述符表基地址,相对于的,内核定义了一个struct desc_ptr结构专门用于保存idtr寄存器内容,其如下:

/* 代码地址:arch/x86/include/asm/Desc_defs.h */
struct desc_ptr {
unsigned short size;
unsigned long address;
} __attribute__((packed)) ; /* 代码地址:arch/x86/kernel/cpu/Common.c */
/* 专门用于保存需要写入idtr寄存器值的变量,这里可以看出,中断描述符表长度为256 * 16 - 1,地址为idt_table */
struct desc_ptr idt_descr = { NR_VECTORS * - , (unsigned long) idt_table };

  在cpu_init()中,会调用load_current_idt()函数进行写入,如下:

static inline void load_current_idt(void)
{
if (is_debug_idt_enabled())
/* 开启了中断调试,用的是 debug_idt_descr 和 debug_idt_table */
load_debug_idt();
else if (is_trace_idt_enabled())
/* 开启了中断跟踪,用的是 trace_idt_descr 和 trace_idt_table */
load_trace_idt();
else
/* 普通情况,用的是 idt_descr 和 idt_table */
load_idt((const struct desc_ptr *)&idt_descr);
} /* load_idt()的定义 */
#define load_idt(dtr) native_load_idt(dtr) /* native_load_idt()的定义 */
static inline void native_load_idt(const struct desc_ptr *dtr)
{
asm volatile("lidt %0"::"m" (*dtr));
}

  

  到这,异常和陷阱已经初始化完毕,内核也已经开始使用新的中断描述符表了,BIOS的中断描述符表就已经遗弃,不再使用了。

初始化中断

  内核是在异常和陷阱初始化完成的情况下才会进行中断的初始化,中断的初始化也是处于start_kernel()函数中,分为两个部分,分别是early_irq_init()和init_IRQ()。early_irq_init()是第一步的初始化,其工作主要是跟硬件无关的一些初始化,比如一些变量的初始化,分配必要的内存等。init_IRQ()是第二步,其主要就是关于硬件部分的初始化了。

  首先我们先看看中断描述符数组irq_desc[NR_IRQS]:

/* 中断描述符数组 */
struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[ ... NR_IRQS-] = {
.handle_irq = handle_bad_irq,
.depth = ,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};

  

  可以看到,irq_desc数组有NR_IRQS个元素,NR_IRQS并不是256-32,实际上,虽然中断描述符表中一共有256项(前32项用作异常和intel保留),但并不是所有中断向量都会使用到,所以中断描述符数组也不一定是256-32项,CPU可以使用多少个中断是由中断控制器(PIC、APIC)或者内核配置决定的,我们看看NR_IRQS的定义:

/* IOAPIC为外部中断控制器 */
#ifdef CONFIG_X86_IO_APIC
#define CPU_VECTOR_LIMIT (64 * NR_CPUS)
#define NR_IRQS \
(CPU_VECTOR_LIMIT > IO_APIC_VECTOR_LIMIT ? \
(NR_VECTORS + CPU_VECTOR_LIMIT) : \
(NR_VECTORS + IO_APIC_VECTOR_LIMIT))
#else /* !CONFIG_X86_IO_APIC: NR_IRQS_LEGACY = 16 */
#define NR_IRQS NR_IRQS_LEGACY
#endif

  

  这时我们可以先看看early_irq_init()函数:

int __init early_irq_init(void)
{
int count, i, node = first_online_node;
struct irq_desc *desc; /* 初始化irq_default_affinity变量,此变量用于设置中断默认的CPU亲和力 */
init_irq_default_affinity(); printk(KERN_INFO "NR_IRQS:%d\n", NR_IRQS); /* 指向中断描述符数组irq_desc */
desc = irq_desc;
/* 获取中断描述符数组长度 */
count = ARRAY_SIZE(irq_desc); for (i = ; i < count; i++) {
/* 为kstat_irqs分配内存,每个CPU有自己独有的kstat_irqs数据,此数据用于统计 */
desc[i].kstat_irqs = alloc_percpu(unsigned int);
/* 为 desc->irq_data.affinity 和 desc->pending_mask 分配内存 */
alloc_masks(&desc[i], GFP_KERNEL, node);
/* 初始化中断描述符的锁 */
raw_spin_lock_init(&desc[i].lock);
/* 设置中断描述符的锁所属的类,此类用于防止死锁 */
lockdep_set_class(&desc[i].lock, &irq_desc_lock_class);
/* 一些变量的初始化 */
desc_set_defaults(i, &desc[i], node, NULL);
} return arch_early_irq_init();
}

  

  更多的初始化在desc_set_defaults()函数中:

static void desc_set_defaults(unsigned int irq, struct irq_desc *desc, int node,
struct module *owner)
{
int cpu; /* 中断号 */
desc->irq_data.irq = irq;
/* 中断描述符的中断控制器芯片为 no_irq_chip */
desc->irq_data.chip = &no_irq_chip;
/* 中断控制器的私有数据为空 */
desc->irq_data.chip_data = NULL;
desc->irq_data.handler_data = NULL;
desc->irq_data.msi_desc = NULL;
/* 设置中断状态 desc->status_use_accessors 为初始化状态_IRQ_DEFAULT_INIT_FLAGS */
irq_settings_clr_and_set(desc, ~, _IRQ_DEFAULT_INIT_FLAGS);
/* 中断默认被禁止,设置 desc->irq_data->state_use_accessors = IRQD_IRQ_DISABLED */
irqd_set(&desc->irq_data, IRQD_IRQ_DISABLED);
/* 设置中断处理回调函数为 handle_bad_irq,handle_bad_irq作为默认的回调函数,此函数中基本上不做什么处理,就是在屏幕上打印此中断信息,并且desc->kstat_irqs++ */
desc->handle_irq = handle_bad_irq;
/* 嵌套深度为1,表示被禁止1次 */
desc->depth = ;
/* 初始化此中断发送次数为0 */
desc->irq_count = ;
/* 无法处理的中断次数为0 */
desc->irqs_unhandled = ;
/* 在/proc/interrupts所显名字为空 */
desc->name = NULL;
/* owner为空 */
desc->owner = owner; /* 初始化kstat_irqs中每个CPU项都为0 */
for_each_possible_cpu(cpu)
*per_cpu_ptr(desc->kstat_irqs, cpu) = ; /* SMP系统才使用的初始化,设置
* desc->irq_data.node = first_online_node
* desc->irq_data.affinity = irq_default_affinity
* 清除desc->pending_mask
*/
desc_smp_init(desc, node);
}

  

  整个early_irq_init()在这里就初始化完毕了,相对来说比较简单,可以说early_irq_init()只是初始化了中断描述符数组中的所有元素。

  在看init_IRQ()前需要看看legacy_pic这个变量,它其实就是CPU内部的中断控制器i8259A,定义了与i8259A相关的一些处理函数和中断数量,如下:

struct legacy_pic default_legacy_pic = {
.nr_legacy_irqs = NR_IRQS_LEGACY,
.chip = &i8259A_chip,
.mask = mask_8259A_irq,
.unmask = unmask_8259A_irq,
.mask_all = mask_8259A,
.restore_mask = unmask_8259A,
.init = init_8259A,
.irq_pending = i8259A_irq_pending,
.make_irq = make_8259A_irq,
}; struct legacy_pic *legacy_pic = &default_legacy_pic;

  在X86体系下,CPU使用的内部中断控制器是i8259A,内核就定义了这个变量进行使用,在init_IRQ()中会将所有的中断描述符的中断控制器芯片指向i8259A,具体我们先看看init_IRQ()代码:

void __init init_IRQ(void)
{
int i; /*
* On cpu 0, Assign IRQ0_VECTOR..IRQ15_VECTOR's to IRQ 0..15.
* If these IRQ's are handled by legacy interrupt-controllers like PIC,
* then this configuration will likely be static after the boot. If
* these IRQ's are handled by more mordern controllers like IO-APIC,
* then this vector space can be freed and re-used dynamically as the
* irq's migrate etc.
*/
/* nr_legacy_irqs() 返回 legacy_pic->nr_legacy_irqs,为16
* vector_irq是一个int型的数组,长度为中断描述符表长,其保存的是中断向量对应的中断号(如果中断向量是异常则没有中断号)
* i8259A中断控制器使用IRQ0~IRQ15这16个中断号,这里将这16个中断号设置到CPU0的vector_irq数组的0x30~0x3f上。
*/
for (i = ; i < nr_legacy_irqs(); i++)
per_cpu(vector_irq, )[IRQ0_VECTOR + i] = i; /* x86_init是一个结构体,里面定义了一组X86体系下的初始化函数 */
x86_init.irqs.intr_init();
}

  

  x86_init.irqs.intr_init()是一个函数指针,其指向native_init_IRQ(),我们可以直接看看native_init_IRQ():

void __init native_init_IRQ(void)
{
int i; /* Execute any quirks before the call gates are initialised: */
/* 这里又是执行x86_init结构中的初始化函数,pre_vector_init()指向 init_ISA_irqs */
x86_init.irqs.pre_vector_init(); /* 初始化中断描述符表中的中断控制器中默认的一些中断门初始化 */
apic_intr_init(); /*
* Cover the whole vector space, no vector can escape
* us. (some of these will be overridden and become
* 'special' SMP interrupts)
*/
/* 第一个外部中断,默认是32 */
i = FIRST_EXTERNAL_VECTOR;
/* 在used_vectors变量中找出所有没有置位的中断向量,我们知道,在trap_init()中对所有异常和陷阱和系统调用中断都置位了used_vectors,没有置位的都为中断
* 这里就是对所有中断设置门描述符
*/
for_each_clear_bit_from(i, used_vectors, NR_VECTORS) {
/* IA32_SYSCALL_VECTOR could be used in trap_init already. */
/* interrupt[]数组保存的是外部中断的中断门信息
* 这里将中断描述符表中空闲的中断向量设置为中断门,interrupt是一个函数指针数组,其将31~255数组元素指向interrupt[i]函数
*/
set_intr_gate(i, interrupt[i - FIRST_EXTERNAL_VECTOR]);
} /* 如果外部中断控制器需要,则安装一个中断处理例程irq2到中断IRQ2上 */
if (!acpi_ioapic && !of_ioapic && nr_legacy_irqs())
setup_irq(, &irq2); #ifdef CONFIG_X86_32
/* 在x86_32模式下,会为当前CPU分配一个中断使用的栈空间 */
irq_ctx_init(smp_processor_id());
#endif
}

  在native_init_IRQ()中,又使用了x86_init变量中的pre_vector_init函数指针,其指向init_ISA_irqs()函数:

void __init init_ISA_irqs(void)
{
/* CHIP默认是i8259A_chip */
struct irq_chip *chip = legacy_pic->chip;
int i; #if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
/* 使用了CPU本地中断控制器 */
/* 开启virtual wire mode */
init_bsp_APIC();
#endif
/* 其实就是调用init_8259A(),进行8259A硬件的初始化 */
legacy_pic->init(); for (i = ; i < nr_legacy_irqs(); i++)
/* i为中断号,chip是irq_chip结构,最后是中断回调函数
* 设置了中断号i的中断描述符的irq_data.irq_chip = i8259A_chip
* 设置了中断回调函数为handle_level_irq
*/
irq_set_chip_and_handler(i, chip, handle_level_irq);
}

  

  在init_ISA_irqs()函数中,最主要的就是将内核使用的外部中断的中断描述符的中断控制器设置为i8259A_chip,中断回调函数为handle_level_irq。

  回到native_init_IRQ()函数,当执行完x86_init.irqs.pre_vector_init()之后,会执行apic_initr_init()函数,这个函数中会初始化一些中断控制器特定的中断函数(这些中断游离于之前描述的中断体系中,它们没有自己的中断描述符,中断向量中直接保存它们自己的中断处理函数,类似于异常与陷阱的调用情况),具体我们看看:

static void __init apic_intr_init(void)
{
smp_intr_init(); #ifdef CONFIG_X86_THERMAL_VECTOR
/* 中断号为: 0xfa,处理函数为: thermal_interrupt */
alloc_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
#ifdef CONFIG_X86_MCE_THRESHOLD
alloc_intr_gate(THRESHOLD_APIC_VECTOR, threshold_interrupt);
#endif #if defined(CONFIG_X86_64) || defined(CONFIG_X86_LOCAL_APIC)
/* self generated IPI for local APIC timer */
alloc_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt); /* IPI for X86 platform specific use */
alloc_intr_gate(X86_PLATFORM_IPI_VECTOR, x86_platform_ipi);
#ifdef CONFIG_HAVE_KVM
/* IPI for KVM to deliver posted interrupt */
alloc_intr_gate(POSTED_INTR_VECTOR, kvm_posted_intr_ipi);
#endif /* IPI vectors for APIC spurious and error interrupts */
alloc_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
alloc_intr_gate(ERROR_APIC_VECTOR, error_interrupt); /* IRQ work interrupts: */
# ifdef CONFIG_IRQ_WORK
alloc_intr_gate(IRQ_WORK_VECTOR, irq_work_interrupt);
# endif #endif
}

  在apic_intr_init()函数中,使用了alloc_intr_gate()函数进行处理,这个函数的处理也很简单,置位该中断号所处used_vectors位置,调用set_intr_gate()设置一个中断门描述符。

  到这里整个中断及异常都已经初始化完成了。

  

总结

  整篇文章代码有点多,又有点乱,这里我们总结一下。

  • 在linux系统中,中断一共有256个,0~19主要用于异常与陷阱,20~31是intel保留,未使用。32~255作为外部中断进行使用。特别的,0x80中断用于系统调用。
  • 机器上电时,BIOS会初始化一个中断描述符表,当交接给linux内核后,内核会自己新建立一个中断描述符表,之后完全使用自己的中断描述符表,舍弃BIOS的中断描述符表。
  • 在x86上系统默认使用的中断控制器为i8259A。
  • 中断描述符的初始化过程中,内核会将中断描述符的默认中断控制器设置为i8259A,中断处理回调函数为handle_level_irq()。
  • 外部中断的门描述的中断处理函数都为interrupt[i]。

  中断的初始化大体上分为两个部分,第一个部分为汇编代码的中断描述符表的初次初始化,第二部分为C语言代码,其又分为异常与陷阱的初始化和中断的初始化。如图:

  在汇编的中断描述符表初始化过中,其主要对整个中断描述符表进行了初始化,其主要工作是:

  • 所有的门描述符的权限位为0;
  • 所有的门描述符的段选择符为__KERNEL_CS;
  • 0~31的门描述符的中断处理程序为early_idt_handlers[i](0 <= i <= 31);
  • 其他的门描述符的中断处理程序为ignore_int;

  而trap_init()所做的异常与陷阱初始化,就是修改中断描述符表的前19项(异常和中断),主要修改他们的中断处理函数入口和权限位,特殊的如任务门还会设置它们的段选择符。在trap_init()中就已经把所有的异常和陷阱都初始化完成了,并会把新的中断描述符表地址放入idtr寄存器,开始使用新的中断描述符表。

  在early_irq_init()中,主要工作是初始化整个中断描述符数组,将数组中的每个中断描述符中的必要变量进行初始化。

  最后在init_IRQ()中,主要工作是初始化中断描述符表中的所有中断门描述符,对于一般的中断,内核将它们的中断处理函数入口设置为interrupt[i],而一些特殊的中断会在apic_intr_init()中进行设置。之后,init_IRQ()会初始化内部和外部的中断控制器,最后将一般的中断使用的中断控制器设置为i8259A,中断处理函数为handle_level_irq(电平触发)。

  

linux中断源码分析 - 初始化(二)的更多相关文章

  1. linux中断源码分析 - 中断发生(三)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 回顾 上篇文章linux中断源码分析 - 初始化(二)已经描述了中断描述符表和中断描述符数组的初始化,由于在初始 ...

  2. linux调度器源码分析 - 初始化(二)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 引言 上期文章linux调度器源码分析 - 概述(一)已经把调度器相关的数据结构介绍了一遍,本篇着重通过代码说明 ...

  3. linux中断源码分析 - 软中断(四)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 在上一篇文章中,我们看到中断实际分为了两个部分,俗称就是一部分是硬中断,一部分是软中断.软中断是专门用于处理中断 ...

  4. linux中断源码分析 - 概述(一)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 关于中断和异常 一般在书中都会把中断和异常一起说明,因为它们具有相同的特点,同时也有不同的地方.在CPU里,中断 ...

  5. linux内存源码分析 - 伙伴系统(初始化和申请页框)

    本文为原创,转载请注明:http://www.cnblogs.com/tolimit/ 之前的文章已经介绍了伙伴系统,这篇我们主要看看源码中是如何初始化伙伴系统.从伙伴系统中分配页框,返回页框于伙伴系 ...

  6. Linux内核源码分析之setup_arch (二)

    1. 概述 接着上一篇<Linux内核源码分析之setup_arch (一)>继续分析,本文首先分析arm_memblock_init函数,然后分析内核启动阶段的是如何进行内存管理的. 2 ...

  7. Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3.0 ARMv7)

    http://blog.chinaunix.net/uid-20543672-id-3157283.html Linux内核源码分析--内核启动之(3)Image内核启动(C语言部分)(Linux-3 ...

  8. 一个普通的 Zepto 源码分析(二) - ajax 模块

    一个普通的 Zepto 源码分析(二) - ajax 模块 普通的路人,普通地瞧.分析时使用的是目前最新 1.2.0 版本. Zepto 可以由许多模块组成,默认包含的模块有 zepto 核心模块,以 ...

  9. Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7)【转】

    原文地址:Linux内核源码分析--内核启动之(6)Image内核启动(do_basic_setup函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://bl ...

随机推荐

  1. canvas实现验证码

    在通常的登录界面我们都可以看到验证码,验证码的作用是检测是不是人在操作,防止机器等非人操作,防止数据库被轻而易举的攻破. 验证码一般用PHP和java等后端语言编写. 但是在前端,用canva或者SV ...

  2. 2018-01-17 Antlr4实现简单语言之整数比较表达式

    续上文Antlr4: 修改语法规则更接近普通BNF格式. 例程 为先=1 为先 为2 => 返回false '为'作为关键词, 与数字可以连写, 但必须与变量名用空格间隔: 变量一=1 变量二= ...

  3. Ansible--原理

    什么是Ansible Ansible是一种IT自动化运维工具,它可以配置系统,部署软件以及协调更高级的IT任务,例如持续部署或者是零停机滚动更新Ansible是新出现的自动化运维工具,基于Python ...

  4. AJAX的优点 个人理解记录

    1:对网站性能的提高.例如我只需要刷新页面中购物车的数据,使用ajax时不需要请求整个页面的数据,对于客户端和服务器的压力都会降低, 减少了ISP的负担,服务器的空间和带宽压力都会降低. 2:用户体验 ...

  5. python第四十一天---作业:简单FTP

      作业要示: 开发简单的FTP:1. 用户登陆2. 上传/下载文件3. 不同用户家目录不同4. 查看当前目录下文件5. 充分使用面向对象知识 REDMAE 用户登陆 1.查看用户目录文件 2.上传文 ...

  6. 扩展BootstapTable支持TreeGrid

    (function ($) { 'use strict'; var sprintf = function (str) { var args = arguments, flag = true, i = ...

  7. python遍历本地文件系统 按文件大小排序

    在这个例子中,主要会用到python内置的和OS模块的几个函数: os.walk() : 该方法用来遍历指定的文件目录,返回一个三元tuple(dirpath, dirnames, filenames ...

  8. php处理手机号中间的四位为星号****

    在显示用户列表的场景中,一般用到手机号的显示时都需要对手机号进行处理,一般是把中间的四位换成星号****,我本人用php处理的思路是进行替换,用****替换手机号的中间四位 代码如下: $all_lo ...

  9. Beta冲刺! Day4 - 砍柴

    Beta冲刺! Day4 - 砍柴 今日已完成 晨瑶:追进度 昭锡:改主页UI(还在 永盛:完成大部分接口和接口文档,上线代码 立强:文章去广告,适配手机屏幕.第三方编辑器整合到记录模块. 炜鸿:完成 ...

  10. arcgis api for javascript中使用proxy.jsp

    当我们使用arcgis api for javascript 查询arcgis服务时,如果查询的参数很长时,通过get方式提交会查询不到结果,因为get方式提交的参数有长度限制,需要通过代理的方式使用 ...