Linux 时钟中断处理(一)

时间:2021-07-20 04:33:13

最近想研究下Linux下的时钟中断,因为时钟中断算是一个操作系统下最频繁的中断事件了吧(个人认为)。

以4.5 x86_64 Linux内核为例。

面对庞大的代码量,无从下手啊。不如从中断号看起吧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的几句代码Linux 时钟中断处理(一)

#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 的函数就是时钟中断处理函数了。接下来的任务就是看看这个函数是怎么定义的了,这回真得老老实实的去看源码了 Linux 时钟中断处理(一)。。。

首先在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" Linux 时钟中断处理(一)?在这里,就是

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