Linuxクロック割り込み処理(一)

13596 ワード

最近Linuxでのクロック割り込みを検討したいと思います.クロック割り込みはオペレーティングシステムで最も頻繁な割り込みイベントだからでしょう(個人的には).
4.5 x 86_で64 Linuxカーネルを例に挙げます.
膨大なコード量に直面して、手がつけられないでしょう.割り込み番号から見てみましょうLinuxソースコードには、このような定義があります(arch/x 86/include/asm/irq_vectors.h):
#define LOCAL_TIMER_VECTOR              0xef

当て間違いがなければ、Linuxでのクロック割り込みベクトルになるはずです(0 xEF=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と併せて割り込み処理関数エントリの線形アドレスを得る.
なお、AMD 64の技術マニュアルには、64ビットモードにおいて、次のような記述が記載されている.
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
", 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のヘッダアドレス:0 xFFFFFFFFFFFFFFFF 57 C 000を抽出する.なお、マルチコアシステムでは、各cpuに独自のIDTがあるため、上記のアドレスは、上記のコードを実行するそのcpuのIDTアドレスであるが、各cpu割り込み処理手順は同様であり、1つのcpuを例に挙げましょう.次に239番目のentryを読み出します.各entryが16バイトを占める場合、239番目のentryアドレスは0 xFFFFFFFF 57 CEF 0〜0 xFFFFFFFFFFFF 57 CEFFであるべきである.では、どうやって読み取るのでしょうか.簡単な文字駆動を使った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:0 x 00からOffsetを抽出し、0 xFFFFFFFFFFFFFF 815 B 6370とする.では、239番割り込み処理関数のエントリアドレスです.入り口の住所を手に入れて何に使いますか?/proc/kallsymsの中で運を試してみましょう.何か役に立つ情報を出力できるか見てみましょう.
grep -i FFFFFFFF815B6370 /proc/kallsyms
運が良ければ(クロック割り込み処理関数が導出されている)、たぶん次のような出力が見られます
ffffffff815b6370 T apic_timer_interrupt
ハハハ、関数名apic_timer_interruptの関数はクロック割り込み処理関数です.次の任務はこの関数がどのように定義されているかを見て、今度は本当におとなしくソースコードを見に行かなければなりません.の
まずarch/x 86/entry/entry_64.Sに定義があります.
apicinterrupt LOCAL_TIMER_VECTOR                apic_timer_interrupt            smp_apic_timer_interrupt
上のLOCAL_TIMER_VECTORは,文中で最初に述べた割り込みベクトルであり,0 xEF(239)と定義されている.apicinterruptはマクロ定義で、後のapic_timer_interruptとsmp_apic_timer_interruptはapicinterruptマクロで定義されたパラメータです.上の文の意味はapicを定義することです.timer_interruptは239番の割り込み処理関数で、この割り込み処理関数はapicinterruptマクロによってアセンブリ命令として定義され、アセンブリ命令の中でいくつかの簡単な操作を行った後、call命令を使ってsmp_を呼び出しますapic_timer_interrupt関数で、この関数はc関数です.マクロ定義の詳細は、arch/x 86/entry/entry_64.Sで定義されています.
.macro apicinterrupt3 num sym do_sym
ENTRY(\sym)
        ASM_CLAC
        pushq   $~(
um) .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
um trace(\sym) smp_trace(\sym) .endm #else .macro trace_apicinterrupt num sym do_sym .endm #endif .macro apicinterrupt num sym do_sym apicinterrupt3
um \sym \do_sym trace_apicinterrupt
um \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/x 86/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

このマクロ定義の最後に、「callfunc」が見えたのではないでしょうか.ここにいるよ
call smp_apic_timer_interrupt

さて、アセンブリ部分は終わりました.カーネルが時計の中断の中で何をしたのかを本当に知るには、smpを見なければなりません.apic_timer_interruptという関数ですね.でも、まあc関数です.arch/x 86/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
", 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_デバイスタイプの構造体(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;

イベント_handlerメンバー変数は前述のとおりです
evt->event_handler(evt);
によって呼び出された関数.このイベント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.やれやれ、遊べない.カーネルを再コンパイルしましょう..でもまあ、私のi 7のノートでコンパイル时间は约3~4分ですが、コンパイル时にcpuが100度の高温で燃え続け、ファンがふうふう吹いていて、痛いです.の
コンパイルが終わって、また戻ってきてやはり見つけて、検索/proc/kallsymsを通じて発見して、evt->event_handlerは hrtimer_interrupt 。 , , , 。を指しています