前言
前段时间根据《linux内核完全注释》一书,从linux0.12入手,逐渐了解了linux最本质的内核实现原理,并将相关笔记记录于《Linux内核学习系列(xx)》。进一步地,最近开始研读《Linux内核设计与实现》,本书以linux2.6.34进行讲解。在学习的过程中,部分实现以及结构发生了较大变动,为了更好地进行对比学习,故考虑将linux2.6.34与linux0.12的异同之处进行记录。
另外,不得不吐槽一下内核学习之困难。其难处主要在于难以调试,不好跟踪,接口太多,有时候不知道看哪个实现。不像当初探究spring源码过程中,能够十分轻易地跟踪其执行过程。不过还好,经典书籍写得十分详细,多读几次便能领悟其中奥秘
中断
中断涉及几个概念,在此简单复习一下
中断向量表
。存放中断向量到中断执行程序地址的映射。部分映射需在内核初始化开启中断前完成设置,也能够后续执行过程中动态修改与添加映射。中断执行程序
。中断向量对应的执行程序,用于执行具体中断逻辑。需用户自定义。中断执行流程
。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,÷_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,¶llel_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, ÷_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版本中断的实现,我们基本探究完成。总结流程如下
- 一小部分中断向量执行的形式,与0.12一样。一个向量号对应一个中断程序
- 另一部分中断向量调用do_irq进行执行,目的是让多个中断程序共享一个中断号。这样可以实现一个中断号触发多个中断程序
- 对于这部分中断向量,需要初始化时,为每个中断向量创建一个irq_desc,并指定其desc->handler(ps:handler可以有多种实现方式,对应了不同地执行共享中断程序的方式,这部分未考证)
- 用户调用request_irq时,可以为编号为irq_id的irq_desc的irqaction绑定一个中断程序
- 往同一个irq_desc绑定多个中断程序后,每个irqaction会组成链表
- 触发中断时,do_irq会执行到irq_desc的handler,遍历irqaction链表,执行其handler(ps:irq_desc的handler与irqaction的handler是不一样的)。
芜湖,中断部分完结。啃起来虽然吃力,但还是能啃动的,内核的设计确实很巧妙,无处不在的数据结构!