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

OffsetSizeDescription
02Limit - Maximum addressable byte in table
28Offset - Linear (paged) base address of IDT


IDT Descriptor
OffsetSizeDescription
02Offset low bits (0..15)
22Selector (Code segment selector)
41Zero
51Type and Attributes (same as before)
62Offset middle bits (16..31)
84Offset high bits (32..63)
124Zero


    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_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 这个函数。经过长途跋涉,终于找到时钟中断真正的处理函数了,篇幅太长了,下一篇再分析这个函数吧。


已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页