レガシーガジェット研究所

気になったことのあれこれ。

Linux Kernel ~ 時間管理とタイマ割り込み ~

概要

「詳解Linux Kernel」を参考にVersion 2.6.11のコードリーディングをしていく。CPUのアーキテクチャは書籍に沿ってIntelx86とする。

今回は時間管理及びタイマ割り込みについて見ていく。

時間管理

カーネルが行う時間管理の主な機能は以下。

  • ユーザプロセスはtime()ftime()gettimeofday()の3つのAPIを使用し日時を取得する。カーネルも当該APIを用いてネットワークパケットにタイムスタンプを付与する。
  • カーネルはタイマを用いてカーネル若しくはユーザプロセスに特定の時間が経過したことを通知する。

クロック回路とタイマ回路

クロック回路は現在時刻の取得や正確な時間計測に使用する。タイマ回路は設定可能で予め設定しておいたタイミングで割り込みを発生させることができ、ソフトウェアタイマなどに使用される。

リアルタイムクロック(Real Time Clock: RTC)

全てのPCにはRTCと(CMOS Clockとも)呼ばれる時計を搭載しており、ボタン型の電池によってPCの電源が入っていない時にも動作を継続する。TRCは2Hz~8192Hzの範囲の周波数でIRQ8に割り込みを発生させることができる。加えてRTCが特定の値になった時にIRQ8に割り込みを発生させることもできる。

バイスファイル/dev/rtcを通してRTCを設定することもでき、カーネルはI/Oポートの0x70及び0x71からアクセスする。

タイムスタンプカウンタ(Time Stamp Counter: TSC)

x86プロセッサにはCLKという入力ピンが存在し外部オシレータのクロック信号を受信する。Pentium以降のプロセッサではそのクロック信号毎に加算されるカウンタを持っており、TSCレジスタを通じてそのカウンタを使用することができる。rdtsc命令がTSCレジスタの値を読み込む。TSCレジスタの利用はクロック信号の周波数を考慮する必要があり、1GHzのクロック周波数で動作している場合、TSCは1ナノ秒毎に加算される。

正確な時間管理を行うためにLinuxはシステムの初期化時にクロック信号の周波数を決定する必要がある。CPUの周波数はシステムの起動時に求め、calibrate_tsc()で約5ミリ秒間に発生したクロック信号の数から周波数を計算する。

プログラマブルインターバルタイマ(Programmable Interval Timer: PIT)

PITはタイマ割り込みという特別な割り込みを発生させ、設定した通知時間が経過したことをカーネルに伝える。PITのタイマ割り込みは一回きりではなく、設定した特定の周期で永久にタイマ割り込みを発生させる。IBM互換機ではI/Oポートの0x40 ~ 0x43を使用する8254 CMOSチップで実装する。LinuxではPITに1000Hzの周波数で設定を行い、1ミリ秒に一回の感覚でIRQ0にタイマ割り込みを発生させる。このタイマ割り込みのインターバルを"tick"と呼ぶ。tickの値はナノ秒単位で以下の変数が保持する。

// kernel/timer.c
/*
 * Timekeeping variables
 */
unsigned long tick_usec = TICK_USEC;      /* USER_HZ period (usec) */
unsigned long tick_nsec = TICK_NSEC;      /* ACTHZ period (nsec) */

tick値が小さいほどタイマの制度は上がり、動画再生や同期I/Oの多重化などの応答性が向上する。しかしCPUがカーネルモードで動作する時間を増え、ユーザモードプロセスの動作が遅くなるというトレードオフが存在する。tick値は遅いマシンで10ミリ秒(1秒間に100回の割り込み)、速いマシンでは1ミリ秒(1秒間に1000回の割り込み)となることが多い。

Linuxではタイマの同期周期を決定するマクロとして以下のようなものがある。

// include/asm-i386/param.h
/* 1秒あたりのタイマ割り込みの回数(周波数) */
# define HZ 1000 /* Internal kernel timer frequency */

// include/asm-i386/timex.h
/* 8254チップの内部オシレータの周波数 */
#define CLOCK_TICK_RATE 1193182 /* Underlying HZ */

// include/linux/jiffies.h
/* LATCH is used in the interval timer and ftape setup. */
/* CLOCK_TICK_RATEとHZの比。PITの初期化に使用 */
#define LATCH  ((CLOCK_TICK_RATE + HZ/2) / HZ)    /* For divider */

PITはsetup_pit_timer()で初期化する。

// arch/i386/kernel/timers/timer_pit.c
void setup_pit_timer(void)
{
    extern spinlock_t i8253_lock;
    unsigned long flags;

    spin_lock_irqsave(&i8253_lock, flags); // スピンロックを取得
    outb_p(0x34,PIT_MODE); // データシートが見つからず・・・恐らく設定モードに入ってる?
    udelay(10); // ハードウェアの誤動作回避のためのスリープ
    outb_p(LATCH & 0xff , PIT_CH0); // LATCHの下位1バイトを書き込む
    udelay(10);
    outb(LATCH >> 8 , PIT_CH0); // LATCHの上位1バイトを書き込む
    spin_unlock_irqrestore(&i8253_lock, flags); // スピンロック解除
}

// include/asm-i386/mach-default/io_ports.h
/* i8253A PIT registers */
#define PIT_MODE       0x43
#define PIT_CH0        0x40
#define PIT_CH2        0x42

outb_p(0x34,PIT_MODE)で割り込み周期の変更命令を出し、outb_p(LATCH & 0xff , PIT_CH0)outb(LATCH >> 8 , PIT_CH0)0x40の8bit I/Oポートに連続して2バイトのデータを書き込むことで新たな周期を設定している。

CPUローカルタイマ

ローカルAPICは時間管理デバイスであるローカルタイマ(CPU Local Timer)を提供する。CPUローカルタイマはPITと同じく定期的な割り込みを発生させるが以下の点が異なる。

  • PITのタイマカウンタが16bitであるのに対してローカルタイマは32bitであるため、より長い割り込み間隔を設定でき発生頻度を低くすることが可能。
  • PITは全てのCPUが処理できるようなグローバルな割り込みを発生させるのに対して、CPUローカルタイマでは対象のCPUにのみ割り込みを発生させる。
  • CPUローカルタイマではバスクロック信号を基にしており、1,2,4,8,16,32,64,128のバスクロック信号毎にタイマカウンタを減らすよう設定が可能。これに対しPITは自身のクロック信号を持っており柔軟な設定が可能。

高精度イベントタイマ(High Precision Event Timer: HPET)

HPETはIntelMicrosoftが共同開発したタイマチップで、Linux2.6から使用することができる。HPETは複数のタイマを提供し、最大8個の32bitまたは64bitのカウンタが存在する。各カウンタは固有のクロック信号で動作し、クロック信号は10MHz以上の周波数、カウンタは最低でも100ナノ秒で増加する。全てのカウンタは最大32個のタイマに対応しており比較機とマッチレジスタで構成される。比較機はカウンタ値とマッチレジスタの値を比較し値が等しくなった時にハードウェア割り込みを発生させる。一部のタイマは周期的に割り込みを発生させることも可能。

HPETはメモリ空間にマップされたペリフェラルレジスタを使用し設定が可能。BIOSは起動時にそのレジスタをメモリ空間にマッピングしその先頭アドレスをカーネルに伝える。カーネルはそのアドレスを用いてメモリを読み書きすることでHPETの設定などを行う。

ACPI電源管理タイマ(ACPI PMT)

ACPI電源管理タイマ(ACPI Power Management Timer: PMT)はもう一つのクロックデバイスで、全てのACPIを持つマザーボードに搭載されており、クロック信号は約3.58MHzの固定周波数となっている。このデバイスはシンプルなカウンタでクロック毎に増加する。カーネルはI/Oポートを通してこのカウンタを値を読み出す。このI/Oポートアドレスは機動処理中にBIOSによって決定される。

時間管理の仕組み

Linuxでは定期的に以下のような処理を行う。

  • システム起動から経過した時間の更新
  • 日時の更新
  • カレントプロセスが各CPU上で動作した時間の計算及びタイムスライスを全て消費した場合のプロセス切り替え
  • 資源関連の統計情報更新
  • 各ソフトウェアタイマの経過時間の確認及び指定時間を経過した際の関数実行

Linuxの時間管理は時間の経過に関連するデータ構造及び関数の組み合わせで構成されている。

シングルプロセッサ環境とマルチプロセッサ環境では時間管理の仕組みが以下のように異なる。

  • シングルプロセッサ環境ではPITまたはHPETのグルーバルタイマ割り込みを契機に時間管理に関連する処理を行う。
  • マルチプロセッサ環境ではグローバルタイマ割り込みを契機に全体的な処理(ソフトウェアタイマ処理など)を行い、ローカルAPICタイマ割り込みなどでCPU毎の処理(カレントプロセスの実行時間の監視など)を行う。

時間管理は搭載しているハードウェアに依存しており、ローカルAPICの有無やローカルタイマ割り込みの使用できないマザーボード、各タイマの有無などで処理の内容が少々異なる物となる。

タイマオブジェクト

利用可能なタイマを共通のインターフェースで扱うためのデータ構造で、定義は以下のようにされている。

// include/asm-i386/timer.h
struct timer_opts {
    char* name; // タイマ識別文字列
    void (*mark_offset)(void); // 最後に割り込みが発生した時刻を記録する関数
    unsigned long (*get_offset)(void); // 前回のタイマ割り込みからの経過時間を返す関数
    unsigned long long (*monotonic_clock)(void); // カーネル起動からの経過時間(ナノ秒)を返す関数
    void (*delay)(unsigned long); // 指定回数ループしてウェイトする関数
};

mark_offset()はタイマ割り込みハンドラが呼び出し、タイマ割り込みの発生時刻を記録する。この記録した時間からの経過時間(マイクロ秒)を計算することでタイマ割り込み(tick)よりも正確な時刻を求めることができる。この操作を時間補間(Time Interpolation)と呼ぶ。

以下のcur_timerにはカーネルの初期化時にselect_timer()で最適なタイマオブジェクトを設定する。

// arch/i386/kernel/time.c
struct timer_opts *cur_timer = &timer_none;

タイマオブジェクトは利用可能なハードウェアタイマによって設定するタイマオブジェクトが決まっており、以下の表の上から(高性能な)順に利用可能なタイマのオブジェクトを設定する。

タイマ名 オブジェクト名 時刻補間処理 遅延処理
HPET timer_hpet HPET HPET
ACPI PMT timer_pmtmr ACPI PMT TSC
TSC timer_tsc TSC TSC
PIT timer_pit PIT 短いループ
汎用のダミータイマ timer_none 無し 短いループ

各オブジェクトは以下のように定義されている。

// arch/i386/kernel/timers/timer_hpet.c
static struct timer_opts timer_hpet = {
    .name =         "hpet",
    .mark_offset =      mark_offset_hpet,
    .get_offset =       get_offset_hpet,
    .monotonic_clock =  monotonic_clock_hpet,
    .delay =        delay_hpet,
};

// arch/i386/kernel/timers/timer_pm.c
static struct timer_opts timer_pmtmr = {
    .name           = "pmtmr",
    .mark_offset        = mark_offset_pmtmr,
    .get_offset     = get_offset_pmtmr,
    .monotonic_clock    = monotonic_clock_pmtmr,
    .delay          = delay_pmtmr,
};

// arch/i386/kernel/timers/timer_tsc.c
static struct timer_opts timer_tsc = {
    .name = "tsc",
    .mark_offset = mark_offset_tsc, 
    .get_offset = get_offset_tsc,
    .monotonic_clock = monotonic_clock_tsc,
    .delay = delay_tsc,
};

// arch/i386/kernel/timers/timer_pit.c
struct timer_opts timer_pit = {
    .name = "pit",
    .mark_offset = mark_offset_pit, 
    .get_offset = get_offset_pit,
    .monotonic_clock = monotonic_clock_pit,
    .delay = delay_pit,
};

// arch/i386/kernel/timers/timer_none.c
struct timer_opts timer_none = {
    .name =     "none",
    .mark_offset =  mark_offset_none, 
    .get_offset =   get_offset_none,
    .monotonic_clock =  monotonic_clock_none,
    .delay = delay_none,
};

jiffies変数

jiffies変数はシステム起動時からのtick回数を記録している(約50日で循環する)。jiffies変数は0xfffb6c20(-300,000)で初期化され5分後にオーバフローする。基本的にjiffies変数を扱うマクロや関数はオーバーフローに対応しているが、0xfffb6c20で初期されるのは意図的にこのオーバーフローに関連するバグをすぐに見つけられるようにするためのものである。32bitのjiffies変数は64bitのjiffies_64変数の下位32bitを使用ため、++jiffies_64jiffies変数もインクリメントする事になる。32bitで扱うのには理由があり、仮にjiffiesが64bitだった場合32bitアーキテクチャでは64bitをアトミックに処理することができず排他処理必要になるため結果的に処理が遅くなってしまうためである。

// include/linux/jiffies.h
/*
 * The 64-bit value is not volatile - you MUST NOT read it
 * without sampling the sequence number in xtime_lock.
 * get_jiffies_64() will do this for you as appropriate.
 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

jiffies_64変数には取得用の関数があり、以下のように定義されている。

// kernel/time.c
u64 get_jiffies_64(void)
{
    unsigned long seq;
    u64 ret;

    do {
        seq = read_seqbegin(&xtime_lock);
        ret = jiffies_64;
    } while (read_seqretry(&xtime_lock, seq));
    return ret;
}

xtime変数

xtime変数には現在の日時を保持しており、以下のように定義されている。

// kernel/timer.c
struct timespec xtime __attribute__ ((aligned (16)));

timespec型は以下のような構造体として定義されている。

// include/linux/time.h
struct timespec {
    time_t tv_sec;     // 1970/01/01(UTC)からの経過秒 
    long   tv_nsec;    // 現在時刻秒からの経過ナノ秒
};

一秒間に1000回発生するtick毎にxtime変数を更新する。ユーザプロセスは現在時刻と日付をxtime変数から取得する。

時間管理の仕組み

シングルプロセッサ環境

シングルプロセッサでは時間関連の処理は全てIRQ0のPIT割り込みで処理する。

カーネルの初期化時にtime_init()関数を呼び出し時間管理機能を初期化する。

// arch/i386/kernel/time.c
void __init time_init(void)
{
/* hpetが有効であった場合の処理 */
#ifdef CONFIG_HPET_TIMER
    if (is_hpet_capable()) {
        /*
        * HPET initialization needs to do memory-mapped io. So, let
        * us do a late initialization after mem_init().
        */
        late_time_init = hpet_time_init;
        return;
    }
#endif
    xtime.tv_sec = get_cmos_time(); // UTCからの時刻の取得及び設定
    xtime.tv_nsec = (INITIAL_JIFFIES % HZ) * (NSEC_PER_SEC / HZ); // 現在時刻からの経過ナノ秒を設定
    /* wall_to_monotonicの初期化。xtimeに加算するためのデータを保持する */
    set_normalized_timespec(&wall_to_monotonic,
        -xtime.tv_sec, -xtime.tv_nsec);

    cur_timer = select_timer(); // システム上に存在する最も最適なタイマを選択
    printk(KERN_INFO "Using %s for high-res timesource\n",cur_timer->name);

    time_init_hook(); // IRQ0の割り込みゲートを設定
}

select_timer()関数は以下のように定義されており、上から順にトラバースしていき存在すればそのタイマを使用するように初期化する。

// arch/i386/kernel/timers/timer.c
static struct init_timer_opts* __initdata timers[] = {
#ifdef CONFIG_X86_CYCLONE_TIMER
    &timer_cyclone_init, // cyclone ?
#endif
#ifdef CONFIG_HPET_TIMER
    &timer_hpet_init,
#endif
#ifdef CONFIG_X86_PM_TIMER
    &timer_pmtmr_init,
#endif
    &timer_tsc_init,
    &timer_pit_init,
    NULL,
};

struct timer_opts* __init select_timer(void)
{
    int i = 0;
    
    /* find most preferred working timer */
    while (timers[i]) {
        if (timers[i]->init)
            if (timers[i]->init(clock_override) == 0)
                return timers[i]->opts;
        ++i;
    }
        
    panic("select_timer: Cannot find a suitable timer\n");
    return NULL;
}

time_init_hook()関数は以下のように定義されておりIRQ0をタイマ用のIRQオブジェクトで初期化しているのがわかる。

// arch/i386/mach-default/setup.c
static struct irqaction irq0  = {
    timer_interrupt, // ハンドラで呼び出す関数
    SA_INTERRUPT,
    CPU_MASK_NONE,
    "timer",
    NULL,
    NULL
};

void __init time_init_hook(void)
{
    setup_irq(0, &irq0);
}

タイマ割り込みのハンドラであるtimer_interrupt()はPITまたはHPETのサービスルーチンで以下のように定義されている。

// arch/i386/kernel/time.c
irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    write_seqlock(&xtime_lock); // ロック
    cur_timer->mark_offset(); // タイマ割り込みを失っていないかを確認
    do_timer_interrupt(irq, NULL, regs);
    write_sequnlock(&xtime_lock); // アンロック
    return IRQ_HANDLED; // 処理が適切に処理されたことを返す。
}

// include/linux/interrupt.h
#define IRQ_NONE   (0)
#define IRQ_HANDLED    (1)
#define IRQ_RETVAL(x)  ((x) != 0)

内部で呼ばれているdo_timer_interrupt()の定義は以下。

// arch/i386/kernel/time.c
static inline void do_timer_interrupt(int irq, void *dev_id,
                    struct pt_regs *regs)
{
    do_timer_interrupt_hook(regs); // ロードアベレージやシステム時間を更新

    /* 時間を外部にある時計と同期している場合、11分間隔でRTCを調整する */
    if ((time_status & STA_UNSYNC) == 0 &&
        xtime.tv_sec > last_rtc_update + 660 &&
        (xtime.tv_nsec / 1000)
            >= USEC_AFTER - ((unsigned) TICK_SIZE) / 2 &&
        (xtime.tv_nsec / 1000)
            <= USEC_BEFORE + ((unsigned) TICK_SIZE) / 2) {
        /* horrible...FIXME */
        if (efi_enabled) {
            if (efi_set_rtc_mmss(xtime.tv_sec) == 0)
                last_rtc_update = xtime.tv_sec;
            else
                last_rtc_update = xtime.tv_sec - 600;
        } else if (set_rtc_mmss(xtime.tv_sec) == 0)
            last_rtc_update = xtime.tv_sec;
        else
            last_rtc_update = xtime.tv_sec - 600; /* do it again in 60 s */
    }

    if (MCA_bus) {
        irq = inb_p( 0x61 );   /* read the current state */
        outb_p( irq|0x80, 0x61 ); /* reset the IRQ */
    }
}

上記で呼び出されているdo_timer_interrupt_hook()は以下のように定義されている。

// include/asm-i386/mach-default/do_timer.h
static inline void do_timer_interrupt_hook(struct pt_regs *regs)
{
    do_timer(regs);
#ifndef CONFIG_SMP
    update_process_times(user_mode(regs));
#endif
/*
 * In the SMP case we use the local APIC timer interrupt to do the
 * profiling, except when we simulate SMP mode on a uniprocessor
 * system, in that case we have to call the local interrupt handler.
 */
#ifndef CONFIG_X86_LOCAL_APIC
    profile_tick(CPU_PROFILING, regs); // プロファイリング
#else
    if (!using_apic_timer)
        smp_local_timer_interrupt(regs);
#endif
}

まずdo_timer(regs);を見ていく。

// kernel/timer.c
void do_timer(struct pt_regs *regs)
{
    jiffies_64++; // jiffies変数の更新
    update_times(); // 定義は以下。
}

static inline void update_times(void)
{
    unsigned long ticks;

    ticks = jiffies - wall_jiffies;
    if (ticks) {
        wall_jiffies += ticks;
        update_wall_time(ticks); // xtimeを更新
    }
    calc_load(ticks); // システムの負荷を算出
}

unsigned long wall_jiffies = INITIAL_JIFFIES;

// include/linux/jiffies.h
/*
 * 起動後5分でオーバーフローするため、オーバフローに起因するjiffiesのバグをすぐに発見できる。
 */
#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))

次にdo_timer_interrupt_hook()の以下の部分を見ていく。

#ifndef CONFIG_SMP
    update_process_times(user_mode(regs));
#endif

上記はシングルプロセッサ環境でのみ行われる処理で、update_process_times()は以下のように定義されている。

void update_process_times(int user_tick)
{
    struct task_struct *p = current;
    int cpu = smp_processor_id();

    /* Note: this timer irq context must be accounted for as well. */
    if (user_tick)
        account_user_time(p, jiffies_to_cputime(1)); // ユーザモードでのCPU時間計測
    else
        account_system_time(p, HARDIRQ_OFFSET, jiffies_to_cputime(1)); // システムモードでのCPU時間計測
    run_local_timers(); // ソフトウェア割り込みを発生させる。
    if (rcu_pending(cpu))
        rcu_check_callbacks(cpu, user_tick);
    scheduler_tick(); // HZ(1/1000秒)で呼ばれる。
}

次にdo_timer_interrupt_hook()の以下の部分を見ていく。

#ifndef CONFIG_X86_LOCAL_APIC
    profile_tick(CPU_PROFILING, regs);
#else
    if (!using_apic_timer)
        smp_local_timer_interrupt(regs);
#endif
}

シングルプロセッサ環境の場合、上記ではprofile_tick()関数が呼び出される。これはプロファイリング処理を行なっている。

マルチプロセッサ環境

マルチプロセッサシステムでは2つのタイマ割り込み発生源を利用し、1つは"PIT"又は"HPET"、もう1つはローカルCPUタイマとなる。

"PIT"又は"HPET"が生成するグローバルタイマ割り込みではソフトウェアタイマ処理やシステム時間の更新処理などシステム全体に関連する処理を行う。ローカルCPUタイマが生成するローカルタイマ割り込みではカレントプロセスの実行時間の監視や資源使用量の統計情報を更新などのローカルCPUに関連する処理を行う。

グローバル割り込みハンドラの初期化はシングルプロセッサ環境と同様time_init()関数で行う。

ローカル割り込みは0xefがベクタとして予約されており、カーネルの初期中にapic_intr_init()関数内で初期化する。

// include/asm-i386/mach-default/irq_vectors.h
#define LOCAL_TIMER_VECTOR 0xef

// arch/i386/kernel/apic.c
void __init apic_intr_init(void)
{
#ifdef CONFIG_SMP
    smp_intr_init();
#endif
    /* self generated IPI for local APIC timer */
    set_intr_gate(LOCAL_TIMER_VECTOR, apic_timer_interrupt); // ここで初期化

    /* IPI vectors for APIC spurious and error interrupts */
    set_intr_gate(SPURIOUS_APIC_VECTOR, spurious_interrupt);
    set_intr_gate(ERROR_APIC_VECTOR, error_interrupt);

    /* thermal monitor LVT interrupt */
#ifdef CONFIG_X86_MCE_P4THERMAL
    set_intr_gate(THERMAL_APIC_VECTOR, thermal_interrupt);
#endif
}

上記からset_intr_gate()関数でLOCAL_TIMER_VECTORベクタの割り込みゲートに対してapic_timer_interrupt()関数のアドレスを登録しているのがわかる。

APICタイマ割り込み周期の設定にはcalibrate_APIC_clock()関数を呼び出し、tick(1/1000秒)間に受け取るバスクロック数を用いてtick毎にローカル割り込みが走るようAPICを初期化する。

/*
 * In this function we calibrate APIC bus clocks to the external
 * timer. Unfortunately we cannot use jiffies and the timer irq
 * to calibrate, since some later bootup code depends on getting
 * the first irq? Ugh.
 *
 * We want to do the calibration only once since we
 * want to have local timer irqs syncron. CPUs connected
 * by the same APIC bus have the very same bus frequency.
 * And we want to have irqs off anyways, no accidental
 * APIC irq that way.
 */

int __init calibrate_APIC_clock(void)
{
    :
    (割愛)

上記のコメントから複数あるCPUは共有バスで接続されているためcalibrate_APIC_clock()関数は一度しか実行されないことがわかる。

実際のローカルタイマ割り込みの設定はsetup_APIC_timer()関数で行う。setup_APIC_timer()は各CPU毎に呼び出される。

static void __init setup_APIC_timer(unsigned int clocks)
{
    unsigned long flags;

    local_irq_save(flags);

    /*
    * Wait for IRQ0's slice:
    */
    wait_timer_tick();

    __setup_APIC_LVTT(clocks);

    local_irq_restore(flags);
}

__setup_APIC_LVTT()は以下のように定義されており、初期化のための関数であるkとがわかる。

// arch/i386/kernel/apic.c
/*
 * This function sets up the local APIC timer, with a timeout of
 * 'clocks' APIC bus clock. During calibration we actually call
 * this function twice on the boot CPU, once with a bogus timeout
 * value, second time for real. The other (noncalibrating) CPUs
 * call this function only once, with the real, calibrated value.
 *
 * We do reads before writes even if unnecessary, to get around the
 * P5 APIC double write bug.
 */

#define APIC_DIVISOR 16

void __setup_APIC_LVTT(unsigned int clocks)
{

グローバル割り込みハンドラ

グローバル割り込みハンドラとして呼び出すtimer_interrupt()は基本的にはシングルプロセッサ環境時と同じだが一部異なる点が存在する。

// arch/i386/kernel/time.c
irqreturn_t timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    write_seqlock(&xtime_lock);
    cur_timer->mark_offset();
    do_timer_interrupt(irq, NULL, regs);
    write_sequnlock(&xtime_lock);
    return IRQ_HANDLED;
}

まずtimer_interrupt()内で呼び出されるdo_timer_interrupt()関数の処理の一部。

// arch/i386/kernel/time.c
static inline void do_timer_interrupt(int irq, void *dev_id,
                    struct pt_regs *regs)
{
/* APICに対する処理 */
#ifdef CONFIG_X86_IO_APIC
    if (timer_ack) {
        spin_lock(&i8259A_lock);
        outb(0x0c, PIC_MASTER_OCW3);
        inb(PIC_MASTER_POLL);
        spin_unlock(&i8259A_lock);
    }
#endif

    do_timer_interrupt_hook(regs);
    :
    (省略)
    :

上記ではAPICのI/Oポートに書き込みを行いタイマのIRQに対して割り込み応答を返す。

次にdo_timer_interrupt_hook()関数。

// include/asm-i386/mach-default/do_timer.h
static inline void do_timer_interrupt_hook(struct pt_regs *regs)
{
    do_timer(regs);
#ifndef CONFIG_SMP
    update_process_times(user_mode(regs)); // マルチプロセッサ環境ではここは呼ばれない。
#endif
/*
 * In the SMP case we use the local APIC timer interrupt to do the
 * profiling, except when we simulate SMP mode on a uniprocessor
 * system, in that case we have to call the local interrupt handler.
 */
#ifndef CONFIG_X86_LOCAL_APIC
    profile_tick(CPU_PROFILING, regs); // ここも呼ばれない。
#else
    if (!using_apic_timer)
        smp_local_timer_interrupt(regs);
#endif
}

マルチプロセッサ環境ではupdate_process_times()profile_tick()は呼び出されない。

ローカル割り込みハンドラ

ローカル割り込みハンドラは特定のCPUに関連する時間処理(プロファイリングやカレントプロセスの時間管理など)を行う。

先ほど見たローカル割り込みハンドラであるapic_timer_interrupt()は以下のように定義されている。

// arch/x86_64/kernel/entry.S
#ifdef CONFIG_X86_LOCAL_APIC   
ENTRY(apic_timer_interrupt)
    /* ベクタ番号と対応する関数をバインド */
    apicinterrupt LOCAL_TIMER_VECTOR,smp_apic_timer_interrupt

ベクタ番号にバインドされているsmp_apic_timer_interrupt()関数の定義は以下。`

// arch/i386/kernel/apic.c
fastcall void smp_apic_timer_interrupt(struct pt_regs *regs)
{
    int cpu = smp_processor_id();

    irq_stat[cpu].apic_timer_irqs++; // ウォッチドッグタイマで確認する値をインクリメント

    ack_APIC_irq(); // APICへの応答を返す。
    
    irq_enter(); // 割り込みのネスト数を加算
    smp_local_timer_interrupt(regs);
    irq_exit(); // 割り込みのネスト数を減算
}

上記呼び出されているsmp_local_timer_interrupt()関数の定義は以下。

// arch/i386/kernel/apic.c
/*
 * ローカルタイマ割り込みハンドラはプロファイリングやプロセスの統計及びリスケジューリングを行う。
 * ローカルtickでの統計とリスケジューリングは"profiling multiplier" tickの間隔で起こり、
 * "profiling multiplier"のデフォルト値は1。
 * "/proc/profile"に書き込みを行うことで任意の値を設定できる。
 */
inline void smp_local_timer_interrupt(struct pt_regs * regs)
{
    int cpu = smp_processor_id();

    /**
    * シングルプロセッサ環境ではグローバル割り込みハンドラ内で呼び出されていたが
    * マルチプロセッサ環境ではローカル割り込みで呼び出されているのがわかる
    */
    profile_tick(CPU_PROFILING, regs); // プロファイリング処理
    if (--per_cpu(prof_counter, cpu) <= 0) {
        /*
        * ユーザが/proc/profileに書き込みを行うことで
        * "profiling multiplier"が変更された場合にここで調整を行う。
        */
        per_cpu(prof_counter, cpu) = per_cpu(prof_multiplier, cpu);
        if (per_cpu(prof_counter, cpu) !=
                    per_cpu(prof_old_multiplier, cpu)) {
            __setup_APIC_LVTT(
                    calibration_result/
                    per_cpu(prof_counter, cpu));
            per_cpu(prof_old_multiplier, cpu) =
                        per_cpu(prof_counter, cpu);
        }

#ifdef CONFIG_SMP
        /**
        * シングルプロセッサ環境ではグローバル割り込みハンドラ内で呼び出されていたが
        * マルチプロセッサ環境ではローカル割り込みで呼び出されているのがわかる
        */
        update_process_times(user_mode(regs)); // CPU関連の統計情報を更新
#endif
    }
}

profile_tick()及びupdate_process_times()はシングルプロセッサ環境ではグローバル割り込みハンドラ内で実行されていたが、マルチプロセッサ環境ではローカル割り込みハンドラで呼び出されているのがわかる。

システム負荷の管理

topuptimeコマンドなどで表示されるシステム負荷(ロードアベレージ)はtick毎に呼び出される(前述の)update_times()関数内で算出している。実際に当該処理を行なっているのはcalc_load()関数で以下のように定義されている。

// kernel/timer.c
unsigned long avenrun[3];

static inline void calc_load(unsigned long ticks)
{
    unsigned long active_tasks; /* fixed-point */
    static int count = LOAD_FREQ;

    count -= ticks;
    if (count < 0) {
        count += LOAD_FREQ;
        active_tasks = count_active_tasks(); // タスク数を取得。
        CALC_LOAD(avenrun[0], EXP_1, active_tasks); // 1分単位
        CALC_LOAD(avenrun[1], EXP_5, active_tasks); // 5分単位
        CALC_LOAD(avenrun[2], EXP_15, active_tasks); // 15分単位
    }
}

// include/linux/sched.h
/* ロードアベレージの値は10bitの整数と11bitの少数で記録される。 */
#define FSHIFT     11     /* 精度 */
#define FIXED_1    (1<<FSHIFT)  /* 1<<11(2048)を1.0とする */
#define LOAD_FREQ  (5*HZ)     /* 5秒間のインターバル(5 * 1000) */
#define EXP_1      1884       /* 1/exp(5sec/1min) as fixed-point */
#define EXP_5      2014       /* 1/exp(5sec/5min) */
#define EXP_15     2037       /* 1/exp(5sec/15min) */

// include/linux/sched.h
#define CALC_LOAD(load,exp,n) \
   load *= exp; \
   load += n*(FIXED_1-exp); \
   load >>= FSHIFT;

上記でタスク数を取得しているcount_active_tasks()関数は以下のように定義されている。

// kernel/timer.c
static unsigned long count_active_tasks(void)
{
    /* TASK_RUNNING及びTASK_UNINTERRUPTIBLE状態のタスク数を取得 */
    return (nr_running() + nr_uninterruptible()) * FIXED_1;
}

unsigned long nr_running(void)
{
    unsigned long i, sum = 0;

    for_each_online_cpu(i)
        sum += cpu_rq(i)->nr_running;

    return sum;
}

unsigned long nr_uninterruptible(void)
{
    unsigned long i, sum = 0;

    for_each_cpu(i)
        sum += cpu_rq(i)->nr_uninterruptible;

    return sum;
}

nr_running()及びnr_uninterruptible()関数を見るとCPU毎に保持しているランキューがTASK_RUNNING状態のタスク数と及びTASK_UNINTERRUPTIBLE状態のタスク数をそれぞれ保持しているのがわかる。

プロファイリング

Linuxはreadprofileと呼ばれるプロファイラを持っており、カーネルの中で多く実行されるホットスポットを示す。プロファイラはタイマ割り込みの発生の際に呼び出されスタックに保存されているEIPレジスタの値から直前の処理を確認する。

コードプロファイラの有効化はカーネルパラメータを指定してLinuxを起動する必要がある。プロファイリング結果は/proc/profileを通じてアクセス可能で、当該ファイルに書き込みを行うことでサンプリング周波数を変更することもできる。

シングルプロセッサ環境では以下のようにグローバル割り込みハンドラ内で呼び出され

// include/asm-i386/mach-default/do_timer.h
static inline void do_timer_interrupt_hook(struct pt_regs *regs)
{
    do_timer(regs);
#ifndef CONFIG_SMP
    update_process_times(user_mode(regs)); // マルチプロセッサ環境ではここは呼ばれない。
#endif

#ifndef CONFIG_X86_LOCAL_APIC
    profile_tick(CPU_PROFILING, regs); // ここも呼ばれない。
#else
    :
    (省略)
    :

マルチプロセッサ環境では以下のようにローカル割り込みハンドラ内で呼び出される。

inline void smp_local_timer_interrupt(struct pt_regs * regs)
{
    int cpu = smp_processor_id();

    profile_tick(CPU_PROFILING, regs); // プロファイリング処理

profile_tick()関数自体は以下のように定義されている。

// kernel/profile.c
void profile_tick(int type, struct pt_regs *regs)
{
    if (type == CPU_PROFILING && timer_hook)
        timer_hook(regs);
    if (!user_mode(regs) && cpu_isset(smp_processor_id(), prof_cpu_mask))
        profile_hit(type, (void *)profile_pc(regs));
}

Linux 2.6ではoprofileというプロファイラもあり、柔軟性が高く微調整が可能でカーネル以外にもユーザやライブラリのホットスポットも見つけることが可能となる。

NMIウォッチドッグの確認

マルチプロセッサ環境ではウォッチドッグ機能という物を提供している。システムのストールを引き起こすようなバグを見つけるために役立つ。ウォッチドッグの有効化にはカーネルパラメータにnmi_watchdogを指定してカーネルを起動する必要がある。

ウォッチドッグはローカルAPICとI/O APICの機能を使用し、定期的にCPUに対してNMI割り込み(NonMaskable Interrupt: マクス不可割り込み)を発生させる。NMI割り込みはcli命令などでローカルCPUに対する割り込みを禁止している際にも有効である。

tick毎にNMI割り込みハンドラであるdo_nmi()関数を実行する。

// arch/i386/kernel/traps.c
fastcall void do_nmi(struct pt_regs * regs, long error_code)
{
    int cpu;

    nmi_enter();

    cpu = smp_processor_id();
    ++nmi_count(cpu);

    if (!nmi_callback(regs, cpu))
        default_do_nmi(regs); // デフォルトの処理↓

    nmi_exit();
}

// arch/i386/kernel/traps.c
static void default_do_nmi(struct pt_regs * regs)
{
    unsigned char reason = 0;

    if (!smp_processor_id())
        reason = get_nmi_reason(); // inb(0x61);
 
    if (!(reason & 0xc0)) {
        if (notify_die(DIE_NMI_IPI, "nmi_ipi", regs, reason, 0, SIGINT)
                            == NOTIFY_STOP)
            return;
#ifdef CONFIG_X86_LOCAL_APIC
        if (nmi_watchdog) {
            nmi_watchdog_tick(regs); // ウォッチドッグでの処理↓
            return;
        }
#endif
        unknown_nmi_error(reason, regs);
        return;
    }
    :
    (省略)
    :
    reassert_nmi();
}

// arch/i386/kernel/nmi.c
void nmi_watchdog_tick (struct pt_regs * regs)
{

    int sum, cpu = smp_processor_id();

    sum = irq_stat[cpu].apic_timer_irqs; // カウンタ値を確認
    
    /* 最後のチェック時の値と変化がない(異常) */
    if (last_irq_sums[cpu] == sum) {
        alert_counter[cpu]++; // アラート用のカウンタをインクリメント
        if (alert_counter[cpu] == 5*nmi_hz)
            die_nmi(regs, "NMI Watchdog detected LOCKUP"); // 異常検知
    
    /* 最後のチェック時の値と変化がある(正常) */
    } else {
        last_irq_sums[cpu] = sum; // 値を記録
        alert_counter[cpu] = 0;
    }
    :
    (省略)
    :
}

上記からわかる通り、カウンタ値を確認し前回の値から変化がない場合にはエラーカウンタをインクリメントする。当該カウンタがある一定以上の値になった際に異常検知とみなす。

カウンタ値は(前述の)tick毎に起こるローカルタイマ割り込みのハンドラ内で以下のようにインクリメントされている。

// arch/i386/kernel/apic.c
fastcall void smp_apic_timer_interrupt(struct pt_regs *regs)
{
    int cpu = smp_processor_id();

    irq_stat[cpu].apic_timer_irqs++; // ウォッチドッグタイマで確認する値をインクリメント

    ack_APIC_irq(); // APICへの応答を返す。
    
    irq_enter(); // 割り込みのネスト数を加算
    smp_local_timer_interrupt(regs);
    irq_exit(); // 割り込みのネスト数を減算
}

異常を検知した際に呼び出すdie_nmi()関数は以下のように定義されており、ログの出力やレジスタ値の表示などを行う。

// arch/i386/kernel/traps.c
void die_nmi(struct pt_regs *regs, const char *msg)
{
    spin_lock(&nmi_print_lock);
    /*
   * We are in trouble anyway, lets at least try
   * to get a message out.
   */
    bust_spinlocks(1);
    printk(msg);
    printk(" on CPU%d, eip %08lx, registers:\n",
        smp_processor_id(), regs->eip);
    show_registers(regs);
    printk("console shuts up ...\n");
    console_silent();
    spin_unlock(&nmi_print_lock);
    bust_spinlocks(0);
    do_exit(SIGSEGV);
}

参考