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