全部博文(573)
分类: LINUX
2016-04-12 20:05:07
x86系统中,当CPU处于实模式和保护模式时,中断机制是不同的。由于Linux只工作在保护模式下,因此只说明保护模式下的中断机制。
在保护模式下,CPU根据中断描述符表(IDT)来决定中断和异常的处理。当中断或异常发生时,CPU通过查中断向量号对应IDT的表项决定动作。IDT最多有256项。根据Intel的分配,IDT的前32项是由Intel定义的异常和保留项。
IDT的每一项为一个门描述符,记录了该下标中断发生时要运行的程序的入口和权限信息。在x86 CPU中,门被分为多种类型,且权限的检查十分繁琐。Linux主体部分只使用这个机制很简单的一部分。
另外,传统的x86系统使用8259A来作为中断控制器,因此还需要对8259A进行编程,才能组成完整的x86中断系统。
下面从几个方面来说明Linux对上述机制的利用。
IDT在内核进入保护模式时就已经初始化,不过真正的初始化在init/main.c::start_kernel调用的 arch/x86/kernel/traps_32.c::trap_init和arch/x86/kernel/i8259_32.c::init_IRQ中:
void __init trap_init(void) { .... set_trap_gate(0, ÷_error); set_intr_gate(1, &debug); set_intr_gate(2, &nmi); set_system_intr_gate(3, &int3); /* int3/4 can be called from all */ set_system_gate(4, &overflow); set_trap_gate(5, &bounds); set_trap_gate(6, &invalid_op); set_trap_gate(7, &device_not_available); set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS); set_trap_gate(9, &coprocessor_segment_overrun); set_trap_gate(10, &invalid_TSS); set_trap_gate(11, &segment_not_present); set_trap_gate(12, &stack_segment); set_trap_gate(13, &general_protection); set_intr_gate(14, &page_fault); set_trap_gate(15, &spurious_interrupt_bug); set_trap_gate(16, &coprocessor_error); set_trap_gate(17, &alignment_check); #ifdef CONFIG_X86_MCE set_trap_gate(18, &machine_check); #endif set_trap_gate(19, &simd_coprocessor_error); .... set_system_gate(SYSCALL_VECTOR, &system_call); .... }
void init_IRQ(void) __attribute__((weak, alias("native_init_IRQ"))); void __init native_init_IRQ(void) { .... /* all the set up before the call gates are initialised */ pre_intr_init_hook(); .... for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) { int vector = FIRST_EXTERNAL_VECTOR + i; if (i >= NR_IRQS) break; /* SYSCALL_VECTOR was reserved in trap_init. */ if (!test_bit(vector, used_vectors)) set_intr_gate(vector, interrupt[i]); } .... }
这两段程序设置好了中断、异常和系统调用的表项。
8259A初始化流程初始调用是上面native_init_IRQ的pre_intr_init_hook()函数,辗转到达arch/x86/kernel/i8259_32.c::init_ISA_irqs和init_8259A中:
void __init init_ISA_irqs (void) { int i; #ifdef CONFIG_X86_LOCAL_APIC init_bsp_APIC(); #endif init_8259A(0); /* * 16 old-style INTA-cycle interrupts: */ for (i = 0; i < 16; i++) { set_irq_chip_and_handler_name(i, &i8259A_chip, handle_level_irq, "XT"); } }
void init_8259A(int auto_eoi) { unsigned long flags; i8259A_auto_eoi = auto_eoi; spin_lock_irqsave(&i8259A_lock, flags); outb(0xff, PIC_MASTER_IMR); /* mask all of 8259A-1 */ outb(0xff, PIC_SLAVE_IMR); /* mask all of 8259A-2 */ /* * outb_pic - this has to work on a wide range of PC hardware. */ outb_pic(0x11, PIC_MASTER_CMD); /* ICW1: select 8259A-1 init */ outb_pic(0x20 + 0, PIC_MASTER_IMR); /* ICW2: 8259A-1 IR0-7 mapped to 0x20-0x27 */ outb_pic(1U << PIC_CASCADE_IR, PIC_MASTER_IMR); /* 8259A-1 (the master) has a slave on IR2 */ if (auto_eoi) /* master does Auto EOI */ outb_pic(MASTER_ICW4_DEFAULT | PIC_ICW4_AEOI, PIC_MASTER_IMR); else /* master expects normal EOI */ outb_pic(MASTER_ICW4_DEFAULT, PIC_MASTER_IMR); outb_pic(0x11, PIC_SLAVE_CMD); /* ICW1: select 8259A-2 init */ outb_pic(0x20 + 8, PIC_SLAVE_IMR); /* ICW2: 8259A-2 IR0-7 mapped to 0x28-0x2f */ outb_pic(PIC_CASCADE_IR, PIC_SLAVE_IMR); /* 8259A-2 is a slave on master's IR2 */ outb_pic(SLAVE_ICW4_DEFAULT, PIC_SLAVE_IMR); /* (slave's support for AEOI in flat mode is to be investigated) */ if (auto_eoi) /* * In AEOI mode we just have to mask the interrupt * when acking. */ i8259A_chip.mask_ack = disable_8259A_irq; else i8259A_chip.mask_ack = mask_and_ack_8259A; udelay(100); /* wait for 8259A to initialize */ outb(cached_master_mask, PIC_MASTER_IMR); /* restore master IRQ mask */ outb(cached_slave_mask, PIC_SLAVE_IMR); /* restore slave IRQ mask */ spin_unlock_irqrestore(&i8259A_lock, flags); }
上面一段程序用set_irq_chip_and_handler_name向上注册了8259A芯片;下面的程序重新对8259A编程,将中断向量设为0x20~0x27(master)、0x28~0x2f(slave)。
在i8259_32.c中,还有一些基本操作代码,最终形成了如下结构:
static struct irq_chip i8259A_chip = { .name = "XT-PIC", .mask = disable_8259A_irq, .disable = disable_8259A_irq, .unmask = enable_8259A_irq, .mask_ack = mask_and_ack_8259A, };
这三者的入口都在arch/x86/entry_32.S中。其中异常和系统调用的入口可以直接找到,而中断入口interrupt数组是用下列宏语句生成的:
.section .rodata,"a" ENTRY(interrupt) .text ENTRY(irq_entries_start) RING0_INT_FRAME vector=0 .rept NR_IRQS ALIGN .if vector CFI_ADJUST_CFA_OFFSET -4 .endif 1: pushl $~(vector) CFI_ADJUST_CFA_OFFSET 4 jmp common_interrupt .previous .long 1b .text vector=vector+1 .endr END(irq_entries_start) .previous END(interrupt)
可见每个中断向量的处理程序都是在压栈~vector后,就转到了common_interrupt处,最终进入do_IRQ。
在进入common_interrupt后,代码如下:
common_interrupt: SAVE_ALL TRACE_IRQS_OFF movl %esp,%eax call do_IRQ jmp ret_from_intr ... ret_from_intr: GET_THREAD_INFO(%ebp) check_userspace: movl PT_EFLAGS(%esp), %eax # mix EFLAGS and CS movb PT_CS(%esp), %al andl $(X86_EFLAGS_VM | SEGMENT_RPL_MASK), %eax cmpl $USER_RPL, %eax jb resume_kernel # not returning to v8086 or userspace ENTRY(resume_userspace) ... jmp restore_all END(ret_from_exception) ... ENTRY(resume_kernel) ...
可见向量号压栈后,首先调用了SAVE_ALL宏来保存所有的通用寄存器,然后进入do_IRQ函数执行。在执行完毕后,跳到ret_from_intr中,最终到restore_all中恢复保存的上下文并返回。
Linux通过上述代码利用x86中断机制进行了最底层的操作,为在上层使用体系结构无关的描述提供了基础。
Linux中断处理的体系结构无关层是由irqaction、irq_desc和irq_chip几个结构向上向下联系起来,从而达到了隔离体系结构相关和无关代码的目的。
irq_chip是无关层向下的接口,它提供了对中断控制器的抽象。前面已经看到,在传统x86系统上,8259A将进行注册。
irqaction是无关层向上的接口,它记录了上方代码对某IRQ所进行的响应的信息。在include/linux/interrupt.h中,如下:
typedef irqreturn_t (*irq_handler_t)(int, void *); 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的irqaction通过next形成链表,从而实现了IRQ复用。
irq_desc是一个软件层面上的IRQ描述符(类似于IDT,但是体系结构无关的),它记录了每个IRQ号底部的控制器和对应的irqaction等内容。定义在include/linux/irq.h中,如下:
struct irq_desc { 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 int irqs_unhandled; unsigned long last_unhandled; /* Aging timer for unhandled count */ spinlock_t lock; #ifdef CONFIG_SMP cpumask_t affinity; unsigned int cpu; #endif #if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE) cpumask_t pending_mask; #endif #ifdef CONFIG_PROC_FS struct proc_dir_entry *dir; #endif const char *name; } ____cacheline_internodealigned_in_smp; extern struct irq_desc irq_desc[NR_IRQS];
do_IRQ是由硬件中断驱动上层动作的核心函数。
do_IRQ()函数的等价代码(摘自课件):
int irq = ~regs->orig_ax; //1 irq_desc[irq]->handle_irq(irq, desc); //2 mask_ack_irq(desc, irq); //3 handle_IRQ_event(irq,®s,irq_desc[irq].action);//4 irq_desc[irq].handler->end(irq); //5 irq_exit(); //其中invoke_softirq //6其中:
由此可以看出Linux在处理中断时的策略。
下面以时钟中断为例分析中断的注册与运行。
时钟中断IRQ为0,在arch/x86/mach-default/setup.c注册:
static struct irqaction irq0 = { .handler = timer_interrupt, .flags = IRQF_DISABLED | IRQF_NOBALANCING | IRQF_IRQPOLL, .mask = CPU_MASK_NONE, .name = "timer" }; /** * time_init_hook - do any specific initialisations for the system timer. * * Description: * Must plug the system timer interrupt source at HZ into the IRQ listed * in irq_vectors.h:TIMER_IRQ **/ void __init time_init_hook(void) { irq0.mask = cpumask_of_cpu(0); setup_irq(0, &irq0); }
可见kernel/irq/manage.c::setup_irq函数负责中断处理程序的注册,主干如下:
int setup_irq(unsigned int irq, struct irqaction *new) { .... spin_lock_irqsave(&desc->lock, flags); p = &desc->action; old = *p; if (old) { .... /* add new interrupt at end of irq queue */ do { p = &old->next; old = *p; } while (old); shared = 1; } *p = new; .... spin_unlock_irqrestore(&desc->lock, flags); new->irq = irq; register_irq_proc(irq); new->dir = NULL; register_handler_proc(irq, new); return 0; .... }可见主要工作就是把irqaction插入到irq_desc::action的最后。
不过,更加常见的中断处理程序注册是通过request_irq来进行的,如drivers/net/phy.c::phy_start_interrupts,requset_irq最终也调用setup_irq。
当时钟中断到来时,CPU查询IDT,并由记录转到interrupt[0]处执行,之后流程为:
这三种机制是内核对可延迟中断处理的支持。这三者特点为:
可延迟函数上可以执行4种操作初始化:定义一个新的可延迟函数,通常在内核初始化时进行激活:设置可延迟函数在下一轮处理中执行屏蔽:有选择的屏蔽一个可延迟函数,这样即使被激活也不会被运行执行:在特定的时间执行可延迟函数
以TIMER_SOFTIRQ为例。
kernel/timer.c::init_timers中,有:
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
open_softirq在kernel/softirq.c中,它做的事情很简单:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data) { softirq_vec[nr].data = data; softirq_vec[nr].action = action; }
而run_timer_softirq就在kernel/timer.c中。当这个软中断激活时,就会被执行。
激活过程颇为曲折。时间中断来临时,会依次走如下流程:
raise_softirq最终会将local_softirq_pending对应位置置位,这就完成了激活。
注:
event_handler最终注册为kernel/time/tick-common.c::tick_periodic,分析见http://blog.chinaunix.net/space.php?uid=20729605&do=blog&id=1884329
前面在分析do_IRQ函数时,说到最后调用irq_exit。这个函数如下:
void irq_exit(void) { account_system_vtime(current); trace_hardirq_exit(); sub_preempt_count(IRQ_EXIT_OFFSET); if (!in_interrupt() && local_softirq_pending()) invoke_softirq(); ....
此函数先用sub_preempt_count退出一层中断上下文。若中断没有嵌套,且local_softirq_pending有置位(假设已经raise_softirq),则会运行invoke_softirq,最终进入__do_softirq:
asmlinkage void __do_softirq(void) { .... pending = local_softirq_pending(); .... restart: /* Reset the pending bitmask before enabling irqs */ set_softirq_pending(0); local_irq_enable(); h = softirq_vec; do { if (pending & 1) { h->action(h); rcu_bh_qsctr_inc(cpu); } h++; pending >>= 1; } while (pending); local_irq_disable(); pending = local_softirq_pending(); if (pending && --max_restart) goto restart; if (pending) wakeup_softirqd(); .... }
这里一个while循环在允许中断的环境下将每个pending的软中断执行一遍。然后重新查看local_softirq_pending,若再次非空,则重启循环继续执行。但是若负荷特别重,以致重启次数超过max_restart的初值次,则唤醒ksoftirqd内核线程,交由它继续执行。而自己退出,以免正常任务饿死。
这里,kernel/timer.c::run_timer_softirq会被执行。
在kernel/softirq.c::softirq_init中,有:
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL); open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
因此tasklet是使用softirq机制实现的。
这里以键盘驱动为例(drivers/char/keyboard.c),说明tasklet的接口。tasklet_hi类似,只不过优先级高。
在drivers/char/keyboard.c的1030行有:
DECLARE_TASKLET_DISABLED(keyboard_tasklet, kbd_bh, 0);
这就定义了一个叫keyboard_tasklet的tasklet_struct结构,延迟函数为kbd_bh。
在这个文件中,有多处使能和屏蔽,分布在kbd_start和kbd_init中。
使能用的是tasklet_enable(&keyboard_tasklet),屏蔽用的是tasklet_disable(&keyboard_tasklet)。
在kbd_event和kbd_init中,各有一次激活操作,用的都是tasklet_schedule(&keyboard_tasklet)。
在include/linux/interrupt.h中,有:
static inline void tasklet_schedule(struct tasklet_struct *t) { if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state)) __tasklet_schedule(t); }
kernel/softirq.c中:
void __tasklet_schedule(struct tasklet_struct *t) { unsigned long flags; local_irq_save(flags); t->next = NULL; *__get_cpu_var(tasklet_vec).tail = t; __get_cpu_var(tasklet_vec).tail = &(t->next); raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_restore(flags); }
因此,对tasklet的激活操作是:将tasklet插入tasklet_vec队列尾,并raise_softirq。并且由tasklet_schedule中的if判断,若此tasklet已经schedule但还没执行,激活操作将被忽略。这里体现了tasklet机制与softirq的区别。
由于套用softirq机制,前半段执行与softirq相同。softirq机制最终调用tasklet_action函数:
static void tasklet_action(struct softirq_action *a) { struct tasklet_struct *list; local_irq_disable(); list = __get_cpu_var(tasklet_vec).head; __get_cpu_var(tasklet_vec).head = NULL; __get_cpu_var(tasklet_vec).tail = &__get_cpu_var(tasklet_vec).head; local_irq_enable(); while (list) { struct tasklet_struct *t = list; list = list->next; if (tasklet_trylock(t)) { if (!atomic_read(&t->count)) { if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state)) BUG(); t->func(t->data); tasklet_unlock(t); continue; } tasklet_unlock(t); } local_irq_disable(); t->next = NULL; *__get_cpu_var(tasklet_vec).tail = t; __get_cpu_var(tasklet_vec).tail = &(t->next); __raise_softirq_irqoff(TASKLET_SOFTIRQ); local_irq_enable(); } }
这个函数做的事情为:从tasklet_vec取下链表,并依次执行。但若某个tasklet正在别的CPU上面执行(tasklet_trylock失败),则将放回tasklet_vec链,并再次raise_softirq。
这里说明,tasklet是严格串行执行的。
在这个例子中,kbd_bh函数得到执行。
这里以PS/2鼠标驱动为例(drivers/input/mouse/psmouse-base.c)。
在此文件psmouse_init有:
kpsmoused_wq = create_singlethread_workqueue("kpsmoused");
这里创建了一个kpsmoused的工作队列。
这句调用最终执行kernel/workqueue.c::__create_workqueue_key。最终创建一个名为kpsmoused的内核线程。线程函数为worker_thread。
在psmouse_interrupt,有两次激活操作,用的都是queue_work(kpsmoused_wq, &psmouse->resync_work),加入了一个psmouse->resync_work工作。而此工作是运行psmouse_resync函数。
queue_work阅读起来和tasklet_schedule差不多,都是将工作插入一个链表。然后,使用工作队列的more_work这个类似信号量的东西,唤醒对应内核线程。
在某一时刻,kpsmoused线程得到调度,在worker_thread函数中运行:
static int worker_thread(void *__cwq) { .... for (;;) { prepare_to_wait(&cwq->more_work, &wait, TASK_INTERRUPTIBLE); if (!freezing(current) && !kthread_should_stop() && list_empty(&cwq->worklist)) schedule(); finish_wait(&cwq->more_work, &wait); .... run_workqueue(cwq); } .... }
由于前面以及激活,死循环中的finish_wait顺利通过,向下执行run_workqueue函数。
run_workqueue函数与tasklet_action也类似,也是将工作逐一取出执行,不再具体分析。
这时,psmouse_resync函数得到运行。
键盘IRQ是1。思路是用写一个内核模块,用request_irq注册处理程序。如下:
#include#include #include /* Specifically, a module */ MODULE_LICENSE("GPL"); #define DEVICE_NAME "kbd_intr_hook" irqreturn_t hook_handler(int irq, void *dev_id) { printk("kbd_hook: intr!\n"); return IRQ_HANDLED; } static int hook_init() { int retval; retval = request_irq(1, hook_handler, IRQF_SHARED | IRQF_DISABLED, "kbd_hook", 0x55aa00ff); printk("kbd_hook: retval=%d\n", retval); return retval; } static void hook_exit() { free_irq(1, 0x55aa00ff); printk("kbd_hook: exit\n"); } module_init(hook_init); module_exit(hook_exit);
最后,套用模板编译并insmod。结果如下:
[52232.201128] kbd_hook: retval=0 [52232.256559] kbd_hook: intr! [52233.324266] kbd_hook: intr! [52233.395786] kbd_hook: intr! [52233.589061] kbd_hook: intr! [52233.655376] kbd_hook: intr! [52233.667734] kbd_hook: intr! [52233.712986] kbd_hook: intr! ....
最后rmmod卸载。