Linux 时钟中断处理(一)

最近想研究下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

Offset Size Description
0 2 Limit - Maximum addressable byte in table
2 8 Offset - Linear (paged) base address of IDT


IDT Descriptor
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


将上述宏定义一一展开后,最终得到,我们忽略掉"trace"的部分,在我们这里不感兴趣。

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 \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 时钟中断处理(一))