最近想研究下Linux下的时钟中断,因为时钟中断算是一个操作系统下最频繁的中断事件了吧(个人认为)。
以4.5 x86_64 Linux内核为例。
面对庞大的代码量,无从下手啊。不如从中断号看起吧Linux 源码中有这样的定义(arch/x86/include/asm/irq_vectors.h):
#define LOCAL_TIMER_VECTOR 0xef
1)先通过idtr寄存器,找到IDT(中断描述符表)的地址(线性地址),然后读取该描述符表的第239个entry。
idtr和中断描述符表IDT中entry的格式分别如下:
IDTR
Offset | Size | Description |
0 | 2 | Limit - Maximum addressable byte in table |
2 | 8 | Offset - Linear (paged) base address of IDT |
Offset | Size | Description |
0 | 2 | Offset low bits (0..15) |
2 | 2 | Selector (Code segment selector) |
4 | 1 | Zero |
5 | 1 | Type and Attributes (same as before) |
6 | 2 | Offset middle bits (16..31) |
8 | 4 | Offset high bits (32..63) |
12 | 4 | Zero |
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
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_sym
ENTRY(\sym)
ASM_CLAC
pushq $~(\num)
.Lcommon_\sym:
interrupt \do_sym
jmp ret_from_intr
END(\sym)
.endm
#ifdef CONFIG_TRACING
#define trace(sym) trace_##sym
#define smp_trace(sym) smp_trace_##sym
.macro trace_apicinterrupt num sym
apicinterrupt3 \num trace(\sym) smp_trace(\sym)
.endm
#else
.macro trace_apicinterrupt num sym do_sym
.endm
#endif
.macro apicinterrupt num sym do_sym
apicinterrupt3 \num \sym \do_sym
trace_apicinterrupt \num \sym
.endm
ENTRY(apic_timer_interrupt)
ASM_CLAC
pushq $~(0xef)
.Lcommon_apic_timer_interrupt:
interrupt smp_apic_timer_interrupt
jmp ret_from_intr
END(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 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;
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 这个函数。经过长途跋涉,终于找到时钟中断真正的处理函数了,篇幅太长了,下一篇再分析这个函数吧。