Linux内核对比学习系列(1)——中断

前言

前段时间根据《linux内核完全注释》一书,从linux0.12入手,逐渐了解了linux最本质的内核实现原理,并将相关笔记记录于《Linux内核学习系列(xx)》。进一步地,最近开始研读《Linux内核设计与实现》,本书以linux2.6.34进行讲解。在学习的过程中,部分实现以及结构发生了较大变动,为了更好地进行对比学习,故考虑将linux2.6.34与linux0.12的异同之处进行记录。

另外,不得不吐槽一下内核学习之困难。其难处主要在于难以调试,不好跟踪,接口太多,有时候不知道看哪个实现。不像当初探究spring源码过程中,能够十分轻易地跟踪其执行过程。不过还好,经典书籍写得十分详细,多读几次便能领悟其中奥秘

中断

中断涉及几个概念,在此简单复习一下

  1. 中断向量表。存放中断向量到中断执行程序地址的映射。部分映射需在内核初始化开启中断前完成设置,也能够后续执行过程中动态修改与添加映射。
  2. 中断执行程序。中断向量对应的执行程序,用于执行具体中断逻辑。需用户自定义。
  3. 中断执行流程。CPU在执行过程中,触发中断,而后CPU根据触发中断携带的中断向量,从中断向量表中找出中断执行程序,跳转执行中断执行程序。执行完成再跳回中断前程序执行。

Linux 0.12

代码相关

1 . 在boot/head.s中完成中断向量表idt初始化。即将所有中断程序设为ignore_int

setup_idt:
	lea ignore_int,%edx
	movl $0x00080000,%eax
	movw %dx,%ax		/* selector = 0x0008 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea _idt,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt
	lidt idt_descr
	ret
ignore_int:
	pushl %eax
	pushl %ecx
	pushl %edx
	push %ds
	push %es
	push %fs
	movl $0x10,%eax
	mov %ax,%ds
	mov %ax,%es
	mov %ax,%fs
	pushl $int_msg
	call _printk
	popl %eax
	pop %fs
	pop %es
	pop %ds
	popl %edx
	popl %ecx
	popl %eax
	iret
_idt:	.fill 256,8,0	

2 . 可以通过kernel/trap_init()动态修改idt,该函数在init/main.c初始化时会被调用。即可以通过set_trap_gate(id,addr)的方式修改上述idt内容。id为向量号,addr为中断程序地址

void trap_init(void)
{
	int i;

	set_trap_gate(0,&divide_error);
	set_trap_gate(1,&debug);
	set_trap_gate(2,&nmi);
	set_system_gate(3,&int3);	/* int3-5 can be called from all */
	set_system_gate(4,&overflow);
	set_system_gate(5,&bounds);
	set_trap_gate(6,&invalid_op);
	set_trap_gate(7,&device_not_available);
	set_trap_gate(8,&double_fault);
	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_trap_gate(14,&page_fault);
	set_trap_gate(15,&reserved);
	set_trap_gate(16,&coprocessor_error);
	set_trap_gate(17,&alignment_check);
	for (i=18;i<48;i++)
		set_trap_gate(i,&reserved);
	set_trap_gate(45,&irq13);
	outb_p(inb_p(0x21)&0xfb,0x21);
	outb(inb_p(0xA1)&0xdf,0xA1);
	set_trap_gate(39,&parallel_interrupt);
}

3 . 同样地,可以看到系统调用在sched_init时被设置

void sched_init(void)
{
	int i;
	struct desc_struct * p;

	if (sizeof(struct sigaction) != 16)
		panic("Struct sigaction MUST be 16 bytes");
	set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
	set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
	p = gdt+2+FIRST_TSS_ENTRY;
	for(i=1;i<NR_TASKS;i++) {
		task[i] = NULL;
		p->a=p->b=0;
		p++;
		p->a=p->b=0;
		p++;
	}
/* Clear NT, so that we won't have troubles with that later on */
	__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
	ltr(0);
	lldt(0);
	outb_p(0x36,0x43);		/* binary, mode 3, LSB/MSB, ch 0 */
	outb_p(LATCH & 0xff , 0x40);	/* LSB */
	outb(LATCH >> 8 , 0x40);	/* MSB */
	set_intr_gate(0x20,&timer_interrupt);
	outb(inb_p(0x21)&~0x01,0x21);
	set_system_gate(0x80,&system_call);
}

小结

关于linux0.12的中断内容其实就这么多,因为实现的功能比较简单,没有考虑扩展的需求,比如怎么注册中断等问题。但这体现的是中断的本质。具体CPU如何执行中断流程的无需自定义代码控制。这对理解后续linux2.6.34的实现也十分关键

Linux 2.6.34

该版本的内核体量已经变大,并且需要讲究扩展性,很多实现都体现了这一点。由于本人刚开始研读该版本,笔记深度会较浅,后续随着理解的深入会对此进行重修。

代码相关

从对比学习的角度,我希望在该版本代码中,试图找到linux 0.12的影子

1 . arm/x86/kernel/head_32.s 与0.12类似,进行了idt的初始化

setup_idt:
	lea ignore_int,%edx
	movl $(__KERNEL_CS << 16),%eax
	movw %dx,%ax		/* selector = 0x0010 = cs */
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */

	lea idt_table,%edi
	mov $256,%ecx
rp_sidt:
	movl %eax,(%edi)
	movl %edx,4(%edi)
	addl $8,%edi
	dec %ecx
	jne rp_sidt

.macro	set_early_handler handler,trapno
	lea \handler,%edx
	movl $(__KERNEL_CS << 16),%eax
	movw %dx,%ax
	movw $0x8E00,%dx	/* interrupt gate - dpl=0, present */
	lea idt_table,%edi
	movl %eax,8*\trapno(%edi)
	movl %edx,8*\trapno+4(%edi)
.endm

	set_early_handler handler=early_divide_err,trapno=0
	set_early_handler handler=early_illegal_opcode,trapno=6
	set_early_handler handler=early_protection_fault,trapno=13
	set_early_handler handler=early_page_fault,trapno=14

	ret

2 . 同样地,在内核初始化时,会调用trap_init()进行idt初始化设置。同样也是调用set_intr_gate()及其类似函数
arch/x86/kernel/trap.c

// 省略了很多配置相关代码
void __init trap_init(void)
{
	int i;

	set_intr_gate(0, &divide_error);
	set_intr_gate_ist(1, &debug, DEBUG_STACK);
	set_intr_gate_ist(2, &nmi, NMI_STACK);
	/* int3 can be called from all */
	set_system_intr_gate_ist(3, &int3, DEBUG_STACK);
	/* int4 can be called from all */
	set_system_intr_gate(4, &overflow);
	set_intr_gate(5, &bounds);
	set_intr_gate(6, &invalid_op);
	set_intr_gate(7, &device_not_available);
#ifdef CONFIG_X86_32
	set_task_gate(8, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
	set_intr_gate_ist(8, &double_fault, DOUBLEFAULT_STACK);
#endif
	set_intr_gate(9, &coprocessor_segment_overrun);
	set_intr_gate(10, &invalid_TSS);
	set_intr_gate(11, &segment_not_present);
	set_intr_gate_ist(12, &stack_segment, STACKFAULT_STACK);
	set_intr_gate(13, &general_protection);
	set_intr_gate(14, &page_fault);
	set_intr_gate(15, &spurious_interrupt_bug);
	set_intr_gate(16, &coprocessor_error);
	set_intr_gate(17, &alignment_check);

	/*
	 * Should be a barrier for any external CPU state:
	 */
	cpu_init();

	x86_init.irqs.trap_init();
}

3 . init_IRQ()。在《linux内核设计与实现》第7章中,关于中断入口的描述,直接提及do_IRQ(),并给出如下所示流程。当时就纳闷,怎么跟 0.12 的不一样。处理器不是能够自动完成中断程序寻址并跳转执行的吗?只需要提供idt地址即可。怎么这会又需要进入do_IRQ0了。
在这里插入图片描述
参考这篇博客,https://www.cnblogs.com/bittorrent/p/3376134.html,我们可以知晓原因。前1,2步初始化了前20个中断向量,这20个中断向量仍然由CPU直接跳转执行。后续的中断向量,由init_IRQ进行初始化,在main.c中该函数将会在trap_init后执行
arch\x86\kernel\irqinit.c

void __init init_IRQ(void)
{
	int i;
	
	for (i = 0; i < legacy_pic->nr_legacy_irqs; i++)
		per_cpu(vector_irq, 0)[IRQ0_VECTOR + i] = i;

	x86_init.irqs.intr_init();
}

具体地,我们进一步跟踪intr_init()实现,这个函数是在arch/x86/kernel/x86_init.c中设定的。我们可以看到intr_init其实就是native_init_IRQ,它是在irqinit.c中定义的。该程序主要做了两件事:一是调用pre_vector_init实际上就是init_ISA_irqs;二是将所有的中断门设为interrupt数组里的函数。

interrupt是在entry_32.S中定义的。这里实际是上是生成NR_VECTORS-FIRST_EXTERNAL_VECTOR个函数入口,每个函数都在入口处压入一个中断号,然后jmp common_interrupt。init_ISA_irqs是在irqinit.c中定义的,它主要初始化8259芯片为非AEOI模式,并将中断起始向量设为0×20。接着将中断描述符的硬件芯片设为i8259A_chip.其实这里8259的中断号与中断描述符的数组是一一对应的,只不过是0×20号对应irq_desc中的0,依此类推。

现在可以大致总结一下,凡是小于0×20的中断号都由traps.c中的init_trap所初始化的函数接管。而这之后内核将IDT中0×20以后的项的入口都初始化为不同的函数,这些函数都做相同的一件事情就是压入中断号,注意这个中断号是实际的中断号减去0×20得到的逻辑中断号,然后再跳转到common_interrupt中执行真正的中断处理程序。

arch\x86\kernel\entry_32.S

.section .init.rodata,"a"
ENTRY(interrupt)
.text
	.p2align 5
	.p2align CONFIG_X86_L1_CACHE_SHIFT
ENTRY(irq_entries_start)
	RING0_INT_FRAME
vector=FIRST_EXTERNAL_VECTOR
.rept (NR_VECTORS-FIRST_EXTERNAL_VECTOR+6)/7
	.balign 32
  .rept	7
    .if vector < NR_VECTORS
      .if vector <> FIRST_EXTERNAL_VECTOR
	CFI_ADJUST_CFA_OFFSET -4
      .endif
1:	pushl $(~vector+0x80)	/* Note: always in signed byte range */
	CFI_ADJUST_CFA_OFFSET 4
      .if ((vector-FIRST_EXTERNAL_VECTOR)%7) <> 6
	jmp 2f
      .endif
      .previous
	.long 1b
      .text
vector=vector+1
    .endif
  .endr
2:	jmp common_interrupt
.endr
END(irq_entries_start)

.previous
END(interrupt)
.previous

common_interrupt:
	addl $-0x80,(%esp)	/* Adjust vector into the [-256,-1] range */
	SAVE_ALL
	TRACE_IRQS_OFF
	movl %esp,%eax
	call do_IRQ
	jmp ret_from_intr
ENDPROC(common_interrupt)

可以看到common_interrupt会跳转执行do_IRQ,这时候再去看书中分析就很清楚了。该方法中,通过__get_cpu_var根据向量号获取具体地址,handle_irq执行。ps:其实这有点system_call那味的。
arch/x86/kernel/irq.c

unsigned int __irq_entry do_IRQ(struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);

	/* high bit used in ret_from_ code  */
	unsigned vector = ~regs->orig_ax;
	unsigned irq;

	exit_idle();
	irq_enter();

	irq = __get_cpu_var(vector_irq)[vector];

	if (!handle_irq(irq, regs)) {
		ack_APIC_irq();

		if (printk_ratelimit())
			pr_emerg("%s: %d.%d No irq handler for vector (irq %d)\n",
				__func__, smp_processor_id(), vector, irq);
	}

	irq_exit();

	set_irq_regs(old_regs);
	return 1;
}

进入handle_irq就能够看到通过irq实际上是获取了一个中断描述符结构体,通过调用结构体的handle_irq函数完成中断程序执行。到这里,我们有理由怀疑,注册中断的原理实际上只需要将中断执行程序设置为desc->hadle_irq即可。

arch\x86\kernel\irq_32.c

bool handle_irq(unsigned irq, struct pt_regs *regs)
{
	struct irq_desc *desc;
	int overflow;

	overflow = check_stack_overflow();

	desc = irq_to_desc(irq);
	if (unlikely(!desc))
		return false;

	if (!execute_on_irq_stack(overflow, desc, irq)) {
		if (unlikely(overflow))
			print_stack_overflow();
		desc->handle_irq(irq, desc);
	}

	return true;
}

4 . 注册中断。上述我们一路跟踪,理清了两个内核版本实现中断的区别。2.6版本不同之处在于通过类似统一分发的机制,让一部分中断由do_IRQ入口进入,再根据irq进行分发。这可以一定程度的解耦,便于用户自定义中断。因此,我们进一步看看2.6中如何注册一个中断。

根据书中内容,我们知道request_irq可以向内核注册一个中断。由代码可知,实际上是往irqaction中注册了中断执行程序handler
arch\sparc\kernel\irq_32.c

int request_irq(unsigned int irq,
		irq_handler_t handler,
		unsigned long irqflags, const char * devname, void *dev_id)
{
	struct irqaction * action, **actionp;
	unsigned long flags;
	unsigned int cpu_irq;
	int ret;
	
	if (sparc_cpu_model == sun4d) {
		extern int sun4d_request_irq(unsigned int, 
					     irq_handler_t ,
					     unsigned long, const char *, void *);
		return sun4d_request_irq(irq, handler, irqflags, devname, dev_id);
	}
	cpu_irq = irq & (NR_IRQS - 1);
	if(cpu_irq > 14) {
		ret = -EINVAL;
		goto out;
	}
	if (!handler) {
		ret = -EINVAL;
		goto out;
	}
	    
	spin_lock_irqsave(&irq_action_lock, flags);

	actionp = &sparc_irq[cpu_irq].action;
	action = *actionp;
	if (action) {
		if (!(action->flags & IRQF_SHARED) || !(irqflags & IRQF_SHARED)) {
			ret = -EBUSY;
			goto out_unlock;
		}
		if ((action->flags & IRQF_DISABLED) != (irqflags & IRQF_DISABLED)) {
			printk("Attempt to mix fast and slow interrupts on IRQ%d denied\n", irq);
			ret = -EBUSY;
			goto out_unlock;
		}
		for ( ; action; action = *actionp)
			actionp = &action->next;
	}

	/* If this is flagged as statically allocated then we use our
	 * private struct which is never freed.
	 */
	if (irqflags & SA_STATIC_ALLOC) {
		if (static_irq_count < MAX_STATIC_ALLOC)
			action = &static_irqaction[static_irq_count++];
		else
			printk("Request for IRQ%d (%s) SA_STATIC_ALLOC failed using kmalloc\n", irq, devname);
	}
	
	if (action == NULL)
		action = kmalloc(sizeof(struct irqaction),
						     GFP_ATOMIC);
	
	if (!action) { 
		ret = -ENOMEM;
		goto out_unlock;
	}

	action->handler = handler;
	action->flags = irqflags;
	action->name = devname;
	action->next = NULL;
	action->dev_id = dev_id;

	*actionp = action;

	__enable_irq(irq);

	ret = 0;
out_unlock:
	spin_unlock_irqrestore(&irq_action_lock, flags);
out:
	return ret;
}
struct irqaction {
	irq_handler_t handler;
	unsigned long flags;
	const char *name;
	void *dev_id;
	struct irqaction *next;
	int irq;
	struct proc_dir_entry *dir;
	irq_handler_t thread_fn;
	struct task_struct *thread;
	unsigned long thread_flags;
};

到此,我们知道了,do_IRQ实际会调用 irq_desc.handle_irq() 执行中断向量号irq对应的中断程序。但 request_irq 注册过程中,只将中断程序 handler 与 irqaction 进行关联。因此,理清 irqaction 与 irq_desc 的关系是理解 2.6 版本中断的最后一堵墙!!

irqaction与irq_desc

这部分内容参考了,https://www.cnblogs.com/lifexy/p/7506504.html
在这里插入图片描述
一张图说明两者的关系。在理解这部分内容之前,需要理解共享中断线,书中7.5.1的内容。简单地说,就是可以用同一个ireq触发不同的中断程序,这样一个中断号可以对应多个中断处理程序,使得用户可以注册更多的中断。而 request_irq(irq_id,handler,flag,dev_id) 目的是将 handler 注册到编号为 irq_id 的 irq_desc(一个irq_id只有一个irq_desc) 的 irqaction 上。如上图描述,irqaction是个链表。意味着一个中断号对应一个irq_desc,但对应多个irqaction,每个irqaction绑定一个中断程序,irqaction通过dev_id进行标识。由此便可以实现共享中断线的功能。因为当使用同一个irq触发中断时,根据dev_id再进一步搜索实际的中断处理程序即可。

进一步地,我们回到上述的handle_irq,可以进一步地跟踪,是set_irq_handler方法为desc设置handler。具体地,由如下调用

set_irq_handler(irq, handle_edge_irq)
set_irq_handler(irq, handle_level_irq);

进入handle_edge_irq。可以发现,该函数获取irq_desc所关联的action,并进行遍历,调用handle_IRQ_event(irq, action)进行执行(这就能回到书中提到的内容了-0-)

void
handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
	raw_spin_lock(&desc->lock);

	desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);

	/*
	 * If we're currently running this IRQ, or its disabled,
	 * we shouldn't process the IRQ. Mark it pending, handle
	 * the necessary masking and go out
	 */
	if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||
		    !desc->action)) {
		desc->status |= (IRQ_PENDING | IRQ_MASKED);
		mask_ack_irq(desc, irq);
		goto out_unlock;
	}
	kstat_incr_irqs_this_cpu(irq, desc);

	/* Start handling the irq */
	if (desc->chip->ack)
		desc->chip->ack(irq);

	/* Mark the IRQ currently in progress.*/
	desc->status |= IRQ_INPROGRESS;

	do {
		struct irqaction *action = desc->action;
		irqreturn_t action_ret;

		if (unlikely(!action)) {
			mask_irq(desc, irq);
			goto out_unlock;
		}

		/*
		 * When another irq arrived while we were handling
		 * one, we could have masked the irq.
		 * Renable it, if it was not disabled in meantime.
		 */
		if (unlikely((desc->status &
			       (IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==
			      (IRQ_PENDING | IRQ_MASKED))) {
			unmask_irq(desc, irq);
		}

		desc->status &= ~IRQ_PENDING;
		raw_spin_unlock(&desc->lock);
		action_ret = handle_IRQ_event(irq, action);
		if (!noirqdebug)
			note_interrupt(irq, desc, action_ret);
		raw_spin_lock(&desc->lock);

	} while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);

	desc->status &= ~IRQ_INPROGRESS;
out_unlock:
	raw_spin_unlock(&desc->lock);
}

handle_IRQ_event的功能就是遍历action链表,执行action的handler

irqreturn_t handle_IRQ_event(unsigned int irq, struct irqaction *action)
{
	irqreturn_t ret, retval = IRQ_NONE;
	unsigned int status = 0;

	if (!(action->flags & IRQF_DISABLED))
		local_irq_enable_in_hardirq();

	do {
		trace_irq_handler_entry(irq, action);
		ret = action->handler(irq, action->dev_id);
		trace_irq_handler_exit(irq, action, ret);

		switch (ret) {
		case IRQ_WAKE_THREAD:
			/*
			 * Set result to handled so the spurious check
			 * does not trigger.
			 */
			ret = IRQ_HANDLED;

			/*
			 * Catch drivers which return WAKE_THREAD but
			 * did not set up a thread function
			 */
			if (unlikely(!action->thread_fn)) {
				warn_no_thread(irq, action);
				break;
			}

			/*
			 * Wake up the handler thread for this
			 * action. In case the thread crashed and was
			 * killed we just pretend that we handled the
			 * interrupt. The hardirq handler above has
			 * disabled the device interrupt, so no irq
			 * storm is lurking.
			 */
			if (likely(!test_bit(IRQTF_DIED,
					     &action->thread_flags))) {
				set_bit(IRQTF_RUNTHREAD, &action->thread_flags);
				wake_up_process(action->thread);
			}

			/* Fall through to add to randomness */
		case IRQ_HANDLED:
			status |= action->flags;
			break;

		default:
			break;
		}

		retval |= ret;
		action = action->next;
	} while (action);

	if (status & IRQF_SAMPLE_RANDOM)
		add_interrupt_randomness(irq);
	local_irq_disable();

	return retval;
}

总结

至此,对于2.6版本中断的实现,我们基本探究完成。总结流程如下

  1. 一小部分中断向量执行的形式,与0.12一样。一个向量号对应一个中断程序
  2. 另一部分中断向量调用do_irq进行执行,目的是让多个中断程序共享一个中断号。这样可以实现一个中断号触发多个中断程序
  3. 对于这部分中断向量,需要初始化时,为每个中断向量创建一个irq_desc,并指定其desc->handler(ps:handler可以有多种实现方式,对应了不同地执行共享中断程序的方式,这部分未考证)
  4. 用户调用request_irq时,可以为编号为irq_id的irq_desc的irqaction绑定一个中断程序
  5. 往同一个irq_desc绑定多个中断程序后,每个irqaction会组成链表
  6. 触发中断时,do_irq会执行到irq_desc的handler,遍历irqaction链表,执行其handler(ps:irq_desc的handler与irqaction的handler是不一样的)。

芜湖,中断部分完结。啃起来虽然吃力,但还是能啃动的,内核的设计确实很巧妙,无处不在的数据结构!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值