めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

Ticklessカーネルとクロックソースに関するお話

「詳解 Linuxカーネル 第3版」や「Linuxカーネル2.6解読室」などのLinuxカーネル本では、Linuxカーネルの時刻管理について、超絶に要約すると次のように説明されています。

  • 一定の時間間隔(1000Hz)でタイマ割り込みが入る。
  • タイマ割り込みごとにjiffies変数を1増やす(つまり、jiffiesは、システム起動後にタイマ割り込みが入った回数を表す。)
  • jiffiesの増加分に合わせて、システム時刻(変数xtime)をアップデートする。

しかしながら、RHEL6では、定期的なタイマ割り込みを行わない「Ticklessカーネル」が採用されており、jiffiesやxtimeが変更される仕組みがごっそり変わっています。

※ Ticklessカーネルの何が嬉しいのかというと。。。。
これまでのカーネルでは、定期的にタイマ割り込み処理を行う必要があったので、実行するプロセスが無いアイドル状態であっても、それなりに電力を消費していました。Ticklessカーネルでは、アイドル状態のCPUは、本当に何もせずに深い眠りにつくことができるので、消費電力削減に貢献することができるわけです。

Ticklessカーネルでは、時刻の更新は、定期的な割り込みに頼るのではなく、「クロックソース」と呼ばれる外部のHW機能に頼ります。クロックソースはいろいろなものが選択可能ですが、昔からあるTSCなども利用可能です。

利用可能なクロックソースと現在選択されているクロックソースの確認は、次の通り。

$ cd /sys/devices/system/clocksource/clocksource0/
$ cat available_clocksource 
kvm-clock tsc acpi_pm 
$ cat current_clocksource 
kvm-clock

これは、KVMゲスト環境なので、KVMハイパーバイザが提供する時刻情報を参照するkvm-clockが選択されています。

このようなクロックソースを利用して、jiffiesとxtimeを更新していきます。以前は、jiffiesを基準にxtimeを更新していましたが、現在では、共通のクロックソースを利用して、jiffiesとxtimeは独立して更新されていきます。

以下、ソースを読んだ時のメモ。

クロックソースに合わせてxtimeを更新する関数はこちら。

kernel/time/timekeeping.c

static void timekeeping_forward_now(void)	
{
        cycle_t cycle_now, cycle_delta;
        struct clocksource *clock;
        s64 nsec;

        clock = timekeeper.clock;
        cycle_now = clock->read(clock);  // クロックソースから現在時刻を取得
        cycle_delta = (cycle_now - clock->cycle_last) & clock->mask;  // 前回設定した時刻との差分をとる
        clock->cycle_last = cycle_now;    // 現在時刻を前回設定時刻(cycle_last)に再代入
        nsec = clocksource_cyc2ns(cycle_delta, timekeeper.mult,  // 差分をnsecに変換
                                  timekeeper.shift);

        /* If arch requires, add in gettimeoffset() */
        nsec += arch_gettimeoffset();

        timespec_add_ns(&xtime, nsec); // xtimeにnsecを追加

        nsec = clocksource_cyc2ns(cycle_delta, clock->mult, clock->shift);
        timespec_add_ns(&raw_time, nsec);
}

また、クロックソースが示す現在時刻を直接取得する関数はこちら。
(xtimeの更新が遅れている場合でも正しい現在時刻が得られる。)

kernel/time/timekeeping.c

ktime_t ktime_get(void)
{
        unsigned int seq;
        s64 secs, nsecs;

        WARN_ON(timekeeping_suspended);

        do {
                seq = read_seqbegin(&xtime_lock);
                secs = xtime.tv_sec + wall_to_monotonic.tv_sec; // まずは、xtimeから時刻を取得
                nsecs = xtime.tv_nsec + wall_to_monotonic.tv_nsec;
                nsecs += timekeeping_get_ns();  // ここでクロックソースの示す現在時刻に補正する

        } while (read_seqretry(&xtime_lock, seq));
        /*
         * Use ktime_set/ktime_add_ns to create a proper ktime on
         * 32-bit architectures without CONFIG_KTIME_SCALAR.
         */
        return ktime_add_ns(ktime_set(secs, 0), nsecs);
}
EXPORT_SYMBOL_GPL(ktime_get);

static inline s64 timekeeping_get_ns(void)
{
        cycle_t cycle_now, cycle_delta;
        struct clocksource *clock;

        /* read clocksource: */
        clock = timekeeper.clock;
        cycle_now = clock->read(clock);

        /* calculate the delta since the last update_wall_time: */
        cycle_delta = (cycle_now - clock->cycle_last) & clock->mask; 
       // クロックソースの最新時刻と前回xtimeを更新した際のクロックソース時刻の差分を取得して・・・

        /* return delta convert to nanoseconds using ntp adjusted mult. */
        return clocksource_cyc2ns(cycle_delta, timekeeper.mult,  // その差分を補正した値を返す。
                                  timekeeper.shift);
}

という道具立てを踏まえて、実際にjiffiesとxtimeの更新が呼び出される流れは次の通り。

アイドル状態のCPUに実行するべき処理が入って、起き上がったタイミングで、ktime_get()で取得したクロックソースによる現在時刻をnowに入れて、下記のtick_do_update_jiffies64()が呼ばれれます。

kernel/time/tick-sched.c

static void tick_do_update_jiffies64(ktime_t now)
{
        unsigned long ticks = 0;
        ktime_t delta;

        /*
         * Do a quick check without holding xtime_lock:
         */
        delta = ktime_sub(now, last_jiffies_update);
        if (delta.tv64 < tick_period.tv64)
                return;

        /* Reevalute with xtime_lock held */
        write_seqlock(&xtime_lock);

        delta = ktime_sub(now, last_jiffies_update);  // 前回jiffiesを更新したからの経過時間を計算
        if (delta.tv64 >= tick_period.tv64) {

                delta = ktime_sub(delta, tick_period);
                last_jiffies_update = ktime_add(last_jiffies_update,
                                                tick_period);

                /* Slow path for long timeouts */
                if (unlikely(delta.tv64 >= tick_period.tv64)) {
                        s64 incr = ktime_to_ns(tick_period);

                        ticks = ktime_divns(delta, incr); // それを元にjiffiesをいくつ増やすか決定

                        last_jiffies_update = ktime_add_ns(last_jiffies_update,
                                                           incr * ticks);
                }
                do_timer(++ticks); // ここで、jiffiesとxtimeの更新が行われる。

                /* Keep the tick_next_period variable up to date */
                tick_next_period = ktime_add(last_jiffies_update, tick_period);
        }
        write_sequnlock(&xtime_lock);
}

kernel/timer.c

void do_timer(unsigned long ticks)
{
        jiffies_64 += ticks;  // jiffiesを更新
        update_wall_time();   // xtimeを更新
        calc_global_load();
}

kernel/time/timekeeping.c

void update_wall_time(void)
{
        struct clocksource *clock;
        cycle_t offset;
        u64 nsecs;
        int shift = 0, maxshift;

        /* Make sure we're fully resumed: */
        if (unlikely(timekeeping_suspended))
                return;

        clock = timekeeper.clock;
#ifdef CONFIG_GENERIC_TIME
        offset = (clock->read(clock) - clock->cycle_last) & clock->mask;
            // クロックソースの現在時刻と、前回xtimeを更新した時の時刻を差分を計算
#else
        offset = timekeeper.cycle_interval;
#endif
        timekeeper.xtime_nsec = (s64)xtime.tv_nsec << timekeeper.shift;

// 以下長いので、ちょっと省略。上で計算した差分を元にxtimeを最新時刻に更新する。