「詳解 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を最新時刻に更新する。