Linux 时钟中断处理(一)
来源:互联网 发布:javascript 反转义 编辑:程序博客网 时间:2024/05/17 07:52
最近想研究下Linux下的时钟中断,因为时钟中断算是一个操作系统下最频繁的中断事件了吧(个人认为)。
以4.5 x86_64 Linux内核为例。
面对庞大的代码量,无从下手啊。不如从中断号看起吧Linux 源码中有这样的定义(arch/x86/include/asm/irq_vectors.h):
#define LOCAL_TIMER_VECTOR 0xef
如果没猜错的话,应该就是Linux下的时钟中断向量了(0xEF=239)。为了保险起见,验证一下吧,不过该怎么验证呢?参考CPU硬件的中断处理过程,可按如下方法找到239号中断的处理函数入口地址:
1)先通过idtr寄存器,找到IDT(中断描述符表)的地址(线性地址),然后读取该描述符表的第239个entry。
idtr和中断描述符表IDT中entry的格式分别如下:
IDTR
2)从 IDT Descriptor 中提取出 Segment selector和Offset。
3)根据gdtr寄存器找到GDT的地址,再结合第2步中的段选择符,找到相应的段描述符。
4)从段描述符中提取基地址,再结合第2步中的Offset,便得到中断处理函数入口的线性地址。
需要注意的是,在64位模式中,AMD64的技术手册上有这样的描述:
Segmentation is disabled in 64-bit mode, and code segments span all of virtual memory. In this mode, code-segment base addresses are ignored. For the purpose of
virtual-address calculations, the base address is treated as if it has a value of zero.
原来,在64位系统中,早就不使用代码段和数据段的概念了(不过有些段还是在用的,例如TSS段),逻辑地址直接等于线性地址。因此以上步骤中的3、 4都是不必要的。只要从IDT descriptor中提取出 Offset,这便是中断处理函数的入口地址了(线性地址)。
下面来看看实际是怎么操作的吧:
1)读取idtr寄存器。额。。。得需要内嵌汇编了,本人不是很熟,写了下面很ugly的几句代码:
#include <stdio.h>struct idtr{ unsigned char byte[10];};int main(int argc, char* argv[]){ struct idtr idtr; int i; __asm__ __volatile__ ("SIDT %0" : "=m"(idtr) ); for (i = 0; i < 10; i++) printf("byte %02d: 0x%hhx\n", i, idtr.byte[i]); return 0;}结果如下:
byte 00: 0xff
byte 01: 0xf
byte 02: 0x0
byte 03: 0xc0
byte 04: 0x57
byte 05: 0xff
byte 06: 0xff
byte 07: 0xff
byte 08: 0xff
byte 09: 0xff
根据以上信息提取IDT的首地址:0xFFFFFFFFFF57C000。需要注意的是,由于多核系统下,每个cpu都有自己的IDT,因此上述地址是运行上面代码的那个cpu的IDT地址,不过每个cpu中断处理过程都一样,就以一个cpu为例吧。然后读取第239个entry。每个entry占16个字节,那么第239个entry应该地址是 0xFFFFFFFFFF57CEF0~0xFFFFFFFFFF57CEFF。那么该怎么读取呢?在使用简单字符驱动来做Kernel Hacking中已经介绍过啦。
3)读取该段内存数据为:
result@0xffffffffff57cef0: 0x70
result@0xffffffffff57cef1: 0x63
result@0xffffffffff57cef2: 0x10
result@0xffffffffff57cef3: 0x00
result@0xffffffffff57cef4: 0x00
result@0xffffffffff57cef5: 0x8e
result@0xffffffffff57cef6: 0x5b
result@0xffffffffff57cef7: 0x81
result@0xffffffffff57cef8: 0xff
result@0xffffffffff57cef9: 0xff
result@0xffffffffff57cefa: 0xff
result@0xffffffffff57cefb: 0xff
result@0xffffffffff57cefc: 0x00
result@0xffffffffff57cefd: 0x00
result@0xffffffffff57cefe: 0x00
result@0xffffffffff57ceff: 0x00
从中提取Offset,为0xFFFFFFFF815B6370。那么这就是239号中断处理函数的入口地址了。拿到入口地址用来做什么呢?到 /proc/kallsyms 里面碰碰运气吧,看看能输出点什么有用信息不?
grep -i FFFFFFFF815B6370 /proc/kallsyms如果幸运的话(时钟中断处理函数被导出),大概能看到下面的输出
ffffffff815b6370 T apic_timer_interrupt哈哈,看来函数名 apic_timer_interrupt 的函数就是时钟中断处理函数了。接下来的任务就是看看这个函数是怎么定义的了,这回真得老老实实的去看源码了。。。
首先在arch/x86/entry/entry_64.S中有定义:
apicinterrupt LOCAL_TIMER_VECTOR apic_timer_interrupt smp_apic_timer_interrupt上面的 LOCAL_TIMER_VECTOR 就是文中最开始提到的中断向量,定义为0xEF(239)。而apicinterrupt 是宏定义,后面的apic_timer_interrupt和smp_apic_timer_interrupt是apicinterrupt 宏定义的参数。上面整句话的意思就是定义 apic_timer_interrupt 为 239号中断处理函数,而该中断处理函数被 apicinterrupt 宏定义成了汇编指令,在汇编指令里面进行一些简单操作后,会使用 call 指令调用 smp_apic_timer_interrupt 函数,而该函数就是c函数了。具体有关宏定义分别如下(都在arch/x86/entry/entry_64.S中定义):
.macro apicinterrupt3 num sym do_symENTRY(\sym) ASM_CLAC pushq $~(\num).Lcommon_\sym: interrupt \do_sym jmp ret_from_intrEND(\sym).endm#ifdef CONFIG_TRACING#define trace(sym) trace_##sym#define smp_trace(sym) smp_trace_##sym.macro trace_apicinterrupt num symapicinterrupt3 \num trace(\sym) smp_trace(\sym).endm#else.macro trace_apicinterrupt num sym do_sym.endm#endif.macro apicinterrupt num sym do_symapicinterrupt3 \num \sym \do_symtrace_apicinterrupt \num \sym.endm
将上述宏定义一一展开后,最终得到,我们忽略掉"trace"的部分,在我们这里不感兴趣。
ENTRY(apic_timer_interrupt) ASM_CLAC pushq $~(0xef).Lcommon_apic_timer_interrupt: interrupt smp_apic_timer_interrupt jmp ret_from_intrEND(apic_timer_interrupt)
上述语句里面其实还有很多宏定义,我们不打算一一展开,我们只看其中的 "interrupt" 宏定义(在arch/x86/entry/entry_64.S中定义):
.macro interrupt func cld ALLOC_PT_GPREGS_ON_STACK SAVE_C_REGS SAVE_EXTRA_REGS testb $3, CS(%rsp) jz 1f /* * IRQ from user mode. Switch to kernel gsbase and inform context * tracking that we're in kernel mode. */ SWAPGS /* * We need to tell lockdep that IRQs are off. We can't do this until * we fix gsbase, and we should do it before enter_from_user_mode * (which can take locks). Since TRACE_IRQS_OFF idempotent, * the simplest way to handle it is to just call it twice if * we enter from user mode. There's no reason to optimize this since * TRACE_IRQS_OFF is a no-op if lockdep is off. */ TRACE_IRQS_OFF CALL_enter_from_user_mode 1: /* * Save previous stack pointer, optionally switch to interrupt stack. * irq_count is used to check if a CPU is already on an interrupt stack * or not. While this is essentially redundant with preempt_count it is * a little cheaper to use a separate counter in the PDA (short of * moving irq_enter into assembly, which would be too much work) */ movq %rsp, %rdi incl PER_CPU_VAR(irq_count) cmovzq PER_CPU_VAR(irq_stack_ptr), %rsp pushq %rdi /* We entered an interrupt context - irqs are off: */ TRACE_IRQS_OFF call \func /* rdi points to pt_regs */ .endm
在这个宏定义的最后,是不是看到了 "call \func" ?在这里,就是
call smp_apic_timer_interrupt
好了,汇编部分结束了,要想真正知道内核在时钟中断里面做了些什么,得要看 smp_apic_timer_interrupt 这个函数咯,不过还好是c函数。在arch/x86/kernel/apic/apic.c中有如下函数定义:
static void local_apic_timer_interrupt(void){ int cpu = smp_processor_id(); struct clock_event_device *evt = &per_cpu(lapic_events, cpu); /* * Normally we should not be here till LAPIC has been initialized but * in some cases like kdump, its possible that there is a pending LAPIC * timer interrupt from previous kernel's context and is delivered in * new kernel the moment interrupts are enabled. * * Interrupts are enabled early and LAPIC is setup much later, hence * its possible that when we get here evt->event_handler is NULL. * Check for event_handler being NULL and discard the interrupt as * spurious. */ if (!evt->event_handler) { pr_warning("Spurious LAPIC timer interrupt on cpu %d\n", cpu); /* Switch it off */ lapic_timer_shutdown(evt); return; } /* * the NMI deadlock-detector uses this. */ inc_irq_stat(apic_timer_irqs); evt->event_handler(evt);}__visible void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs){ struct pt_regs *old_regs = set_irq_regs(regs); /* * NOTE! We'd better ACK the irq immediately, * because timer handling can be slow. * * update_process_times() expects us to have done irq_enter(). * Besides, if we don't timer interrupts ignore the global * interrupt lock, which is the WrongThing (tm) to do. */ entering_ack_irq(); local_apic_timer_interrupt(); exiting_irq(); set_irq_regs(old_regs);}可见,在 smp_apic_timer_interrupt 函数中调用了 local_apic_timer_interrupt 函数,而在local_apic_timer_interrupt 函数中真正的处理函数是这句话:
...evt->event_handler(evt);...而evt是 struct clock_event_device 类型的结构体,该结构体定义为(在include/linux/clockchips.h中):
struct clock_event_device { void (*event_handler)(struct clock_event_device *); int (*set_next_event)(unsigned long evt, struct clock_event_device *); int (*set_next_ktime)(ktime_t expires, struct clock_event_device *); ktime_t next_event; u64 max_delta_ns; u64 min_delta_ns; u32 mult; u32 shift; enum clock_event_state state_use_accessors; unsigned int features; unsigned long retries; int (*set_state_periodic)(struct clock_event_device *); int (*set_state_oneshot)(struct clock_event_device *); int (*set_state_oneshot_stopped)(struct clock_event_device *); int (*set_state_shutdown)(struct clock_event_device *); int (*tick_resume)(struct clock_event_device *); void (*broadcast)(const struct cpumask *mask); void (*suspend)(struct clock_event_device *); void (*resume)(struct clock_event_device *); unsigned long min_delta_ticks; unsigned long max_delta_ticks; const char *name; int rating; int irq; int bound_on; const struct cpumask *cpumask; struct list_head list; struct module *owner;} ____cacheline_aligned;
其中event_handler成员变量就是前面提到的
evt->event_handler(evt);所调用的函数。这个 event_handler 只是个函数指针,如何找到它所指向的函数呢?不如先把这个函数指针的值(所指向的地址)读出里瞧瞧吧。那么得先找到结构体 evt 了(其实 event_handler 是结构体 evt 的第一个成员变量,因此找到了 结构体evt 的地址,其实就是函数指针 event_handler 的地址了)。在 local_apic_timer_interrupt 函数中,evt 变量是通过下面语句赋值的:
...struct clock_event_device *evt = &per_cpu(lapic_events, cpu);...
关于 per_cpu 在include/linux/percpu-defs.h中有如下定义(只考虑 CONFIG_SMP=y的情况):
#define SHIFT_PERCPU_PTR(__p, __offset) \ RELOC_HIDE((typeof(*(__p)) __kernel __force *)(__p), (__offset))#define __verify_pcpu_ptr(ptr) \do { \ const void __percpu *__vpp_verify = (typeof((ptr) + 0))NULL; \ (void)__vpp_verify; \} while (0)#define per_cpu_ptr(ptr, cpu) \({ \ __verify_pcpu_ptr(ptr); \ SHIFT_PERCPU_PTR((ptr), per_cpu_offset((cpu))); \})#define per_cpu(var, cpu) (*per_cpu_ptr(&(var), cpu))其中RELOC_HIDE和per_cpu_offset分别在include/linux/compiler-gcc.h和include/asm-generic/percpu.h中定义:
extern unsigned long __per_cpu_offset[NR_CPUS];#define per_cpu_offset(x) (__per_cpu_offset[x])#define RELOC_HIDE(ptr, off) \({ \ unsigned long __ptr; \ __asm__ ("" : "=r"(__ptr) : ""(ptr)); \ (typeof(ptr)) (__ptr + (off)); \})至此,将所有相关宏定义展开后,可以看出结构体 evt 的赋值语句
struct clock_event_device *evt = &per_cpu(lapic_events, cpu);其实就等效于下面这句话了:
struct clock_event_device *evt = (struct_event_device *)(((unsigned long)&lapic_events) + __per_cpu_offset[cpu]);
看来要找到这个 结构体evt 指针所指向的地址,只需要找到 lapic_events 的地址和 __per_cpu_offset[cpu] 的值就行了。到 /proc/kallsyms 去找找吧,悲催的发现什么都找不着。原来我的内核编译选项中有这么一句话:# CONFIG_KALLSYMS_ALL is not set。哎,没法玩了。重新编译内核吧。。。不过还好,在我i7的本子上编译时间大约3~4分钟,只是编译时cpu在100度的高温下持续燃烧,风扇呼呼的吹啊,好心疼。。。
编译完,再回来果然找到了,通过查找 /proc/kallsyms 发现,evt->event_handler 指向的是 hrtimer_interrupt 这个函数。经过长途跋涉,终于找到时钟中断真正的处理函数了,篇幅太长了,下一篇再分析这个函数吧。
- Linux 时钟中断处理(一)
- Linux中断处理之时钟中断--X86
- Linux时钟处理-时钟的软中断处理
- Linux的时间与时钟中断处理
- Linux的时间与时钟中断处理
- Linux的时间与时钟中断处理
- Linux的时间与时钟中断处理
- linux的时间与时钟中断处理
- Linux的时间与时钟中断处理
- Linux的时间与时钟中断处理
- linux时钟处理机制(一)
- Linux中断处理体系结构分析(一)
- Linux中断处理体系结构分析(一)
- Linux中断处理体系结构分析(一)
- 【Linux 驱动】中断处理(一)上半部
- 时钟中断处理程序
- 时钟中断处理
- 时钟中断处理函数
- svn分支管理的使用与经验
- 读薄《Linux 内核设计与实现》(4) - 中断与同步
- Jquery noConfict()方法
- C#学习问题记录
- 结构体在内存中的对齐规则
- Linux 时钟中断处理(一)
- Apache+PHP windows
- jquery总结
- Retrofit2 使用记录
- Android应用开发中的进程名,包名,applicationId
- 如何循环获取html的值,并拼写到json/json,求value所对应key的值
- Spring MVC入门第1天--框架说明与快速入门
- http协议三次握手四次挥手
- 2016年中总结