時計とタイマー

このページの目次

周期を 100 ms に設定しているが、実際には各ラウンドで約 130 ms かかります。キャンセルから戻ると、コールバックが再び発生します。前者は時間基準であり、後者は寿命プロトコルです。

参照目標読書位置
計時とイベントのハードウェア第 1~3 節
キャンセル競合と周期ドリフト第 4~5 節
tick と timerfd の実験第 6~7 節

時計はカーネルに時間がどこまで進んだかを伝え、タイマーは将来何が起こるべきかを記録し、スケジューラは目覚めたタスクが実際にいつ実行されるかを決定します。これら 3 つのフェーズの遅延は別々に扱う必要があり、clock_getres が返すナノ秒分解能をもって、アプリケーションが必ずナノ秒単位の誤差で目覚めるとは限りません。

時間の読み取り、割り込みのスケジューリング、タスクの実行は3つのフェーズである Clocksource カウンタ → 現在時刻 タイマーキュー 期限切れポイントとコールバックを記録 Clockevent 次のハードウェア通知をスケジュール 期限切れ後にコールバックを実行するか、タスクをランナブルにする 実際の実行は、割り込み、ロック、スケジューリング、システム負荷の影響を受ける ナノ秒単位がナノ秒単位の目覚め誤差を保証するわけではない。HZ もハードウェアカウンタ周波数に等しくない。

1. Clocksource と clockevent

clocksource は読み取り可能なカウンタを提供します。カーネルはカウント差、マスク、変換パラメータに基づいて時刻を更新しますが、カウンタの周波数は必ずしも CPU の現在の実行周波数に等しいわけではありません。

clockevent は将来の通知を担当します。set_next_event() は、すべてのデバイスがナノ秒を統一して受け取るのではなく、デバイスに応じたカウント単位での delta を受け取ります。set_next_ktime() などのインターフェースは直接時刻値を使用できます。コアは変換とデバイスの最小/最大範囲などの制約を担当します。

同じハードウェアデバイスが両方の責任をサポートすることもあれば、異なるハードウェアによって分担されることもあります。例えば、読み取り可能な TSC は従来の APIC タイマーとは直接等しくありません。TSC deadline などのメカニズムは、対応するカウントに基づくイベント能力を提供します。CPU が深くアイドル状態にある場合、ローカルイベントデバイスにはブロードキャストメカニズムの支援が必要になる場合があります。timekeeping ハードウェア抽象 を参照してください。

2. Jiffies、HZ とラップアラウンド

jiffies はカーネルの粗粒度の時間カウントであり、HZ は関連する tick スケールを指定します。tickless システムでも論理時間が維持され、各 CPU が毎秒正確に HZ 回のタイマー割り込みを受け取る必要はありません。

getconf CLK_TCK はユーザーランドインターフェースの計時単位を返しますが、これを使って CONFIG_HZ を直接読み出すことはできません。HZ=250 の場合、1 jiffy は 4 ms ですが、すべての hrtimer が 4 ms 単位でしかプログラムできないわけではありません。

32 ビット符号なし long において、2^32 回のカウントは、HZ=100 で約 497 日、HZ=1000 で約 49.7 日でラップアラウンドします。期限切れの比較には time_after/time_before などのマクロを使用し、対応する距離範囲に従う必要があります。jiffies > expires を移植可能なタイムアウト判断として使用することはできません。遠い未来の区間でも、半分の範囲差の比較に無限に依存することはできません。

ミリ秒の変換には msecs_to_jiffies() などの API を使用し、独自の整数除算で短い遅延が 0 に切り捨てられたり、オーバーフローや上限を無視したりしないようにしてください。

3. Timer wheel と hrtimer が共存する理由

メカニズム組織方法と用途時間セマンティクス
timer_list / timer wheel階層化されたバケット、多くのタイムアウトがあり、その多くが事前にキャンセルされるシナリオに適しているjiffies スケール、より遠い期限切れにはより粗い粒度を採用可能
hrtimer期限切れ値に基づいて高精度の時間イベントを組織ktime/ns 表現、実際の精度はイベントハードウェアと設定による

タイマーは「正確な期限切れ時に実行される予約」ではありません。IRQ の無効化、CPU のビジー状態、スレッドの優先度、仮想マシンの一時停止などは、処理の遅延を引き起こす可能性があります。高精度は主に期限切れのスケジュール能力を改善するものであり、ハードリアルタイムの上限を直接提供するものではありません。

通常の timer callback は通常、TIMER_SOFTIRQ のセマンティクス下で実行され、任意にスリープすることはできません。hrtimer の hard/soft モードと PREEMPT_RT 設定は、実際のコールバックコンテキストに影響を与え、「hrtimer は常に HRTIMER_SOFTIRQ で実行される」または「常に hardirq で実行される」と一概には言えません。

ブロッキング作業を実行する必要がある場合は、コールバックから適切な通常の workqueue をスケジュールするか、タスクをウェイクアップできます。schedule_hrtimeout() は呼び出し元の待機関数であり、すでにアトミックコンテキストにある callback をスリープ可能な関数に変換するものではありません。hrtimer の設計 を参照してください。

4. 初期化、キャンセルと再設定の寿命問題

新しいコードでは timer_setup() を使用して timer_list を初期化します。現在のカーネルは hrtimer を初期化するために hrtimer_setup() を提供していますが、古いターゲットでは旧式の初期化インターフェースが必要になる場合があり、ターゲットのヘッダーファイルと照合する必要があります。

動作解決できること解決できないこと
保留中のタイマーを削除キュー内の期限切れイベントを取消コールバックが既に開始している可能性がある
同期削除現在のアクティブな処理の終了を待機他のパスで再設定される可能性がある
再設定の阻止将来のトリガーソースを閉じるオブジェクトは最後のアクセス者が退出するまで生存する必要がある

したがって、退出プロトコルは、キュー状態、現在のコールバック、将来のアクセス者の両方を同時に調整する必要があります。

最新の timer_list において、timer_shutdown_sync() は、同期停止と再設定の阻止を必要とする退出シナリオ、特にタイマーと work が互いに再スケジュールされる場合に使用されます。古いコードの del_timer/del_timer_sync の名前と現在の timer_delete シリーズは、ターゲットバージョンで照合する必要があり、通常の del_timer を「同期削除」とコメントすることはできません。

shutdown した場合でも、解放されたタイマーオブジェクトへの将来のパスアクセスを阻止する必要があります。「生存しているがシャットダウンされたタイマーへの再設定は無視される」ことは、「ダングリングタイマーポインタに対して API を呼び出してもよい」ことを意味しません。hrtimer_cancel も同様に、パブリッシャー/再設定者と調整する必要があります。

キャンセル時にコールバックが必要とするロックを保持しない

スレッドがロック L を保持して同期キャンセルを呼び出し、コールバックが既に開始して L を待っている場合、双方が互いを待ち合わせます。退出では、まず新しい送信パスを停止し、相互依存するロックを保持せずに同期待機を行い、その後、タイマー、work、およびそれらの共通コンテキストを解放します。

5. 周期タスクにどの基準を使用するか

周期 100 ms、各処理に 30 ms かかると仮定し、前のラウンドの開始時間を比較します。

スケジューリング方法第 1 ラウンド第 2 ラウンド第 3 ラウンド
各ラウンド終了後に 100 ms 待機0130 ms260 ms
t0 + n × 100 ms でスケジュール0100 ms200 ms

ここでは他のスケジューリング遅延は無視します。絶対基準は、作業時間を周期に繰り返し追加することを防ぎますが、実際のタイムアウト後には、補完実行、スキップ、またはマージのいずれを行うかを決定する必要があります。

hrtimer_forward などのインターフェースは、期限切れポイントを前進させ、周期の跨ぎを報告するのに役立ち、HRTIMER_RESTART を返した場合のみ、対応するルールに従って再起動します。これらは、逃した周期ごとに 1 回ずつ完全なビジネスコールバックを自動的に実行するものではありません。

ユーザーランドでも、相対スリープドリフトを減らすために clock_nanosleep(..., TIMER_ABSTIME, ...) または timerfd の絶対時間モードを使用できます。MONOTONIC、BOOTTIME、または REALTIME を選択することは、時刻同期とサスペンドが期限切れにどのように影響するかを決定します。

固定周期 P=100 ms で、コールバックの作業時間とディスパッチ遅延をそれぞれ時間軸に追加します。相対再設定では、前回の作業の終了時刻が新しい起点として扱われ、絶対期限では元の位相が保持されます。

n は 0 から始まり、P は周期、C は各ラウンドの作業時間、J はこの図に固定されたディスパッチ遅延です。ここでのパラメータ範囲は絶対周期のオーバーロードを引き起こしていません。実際のジッターも毎回完全に同じではありません。

図を準備しています
毎回余分にかかる時間は、どのように累積ドリフトになるのか?

終了後に周期を待つか、固定された絶対位相では、これら 2 つの項目が累積されることはありませんが、各ラウンドで遅れる可能性があります。

6. NO_HZ は tick を減らすものの、すべての割り込みを消滅させるわけではない

NO_HZ_IDLE は、適切なアイドル条件下で周期スケジューリング tick を停止し、次に処理が必要なイベントでウェイクアップします。NO_HZ_FULL は、選択された CPU と条件を満たす負荷の下で、実行中のタスク時の tick をさらに削減しますが、保守作業、RCU、タイマー、およびデバイス割り込みは依然として存在します。

「実行可能なタスクが 1 つだけ」は関連条件の一つですが、すべての設定で常に tick が発生しないことを保証するものではありません。housekeeping CPU とタスク/IRQ アフィニティの協調も必要です。nohz_full は完全な CPU 分離設定ではありません。

cpuidle の state*/disable は、特定のアイドル状態を許可するかどうかを示しており、現在の tick モードを直接判断することはできません。カーネル設定、起動パラメータ、および実際のトレースと組み合わせて確認する必要があります。NO_HZ ドキュメント を参照してください。

7. POSIX timer、itimer と timerfd

CLOCK_MONOTONIC/REALTIME などの壁掛け時計系 POSIX timer と CPU-time timer では計時ソースが異なり、すべての timer_create が直接同じ hrtimer に対応するとは限りません。SIGEV_SIGNAL、SIGEV_NONE、ライブラリ実装が関与する SIGEV_THREAD などの通知メカニズムも異なります。

setitimer の REAL は経過時間で計時し、VIRTUAL はユーザー CPU 時間で、PROF はユーザーとカーネルの CPU 時間で計時します。CPU 時間タイマーを通常のスリープタイムアウトとして扱うべきではありません。シグナルのマージ、overrun、および処理遅延は、アプリケーションの観察に影響を与える可能性があります。

timerfd はファイルディスクリプタの読み取り可能性で通知し、read はタイムスタンプではなく 64 ビットの累積期限切れ回数を取得し、各 poll の読み取り可能性が期限切れ 1 回に必ず対応するわけではありません。以下のプログラムは、プロセスの MONOTONIC タイマーのみを作成し、約 75 ms 待機してから、20 ms 周期の累積回数を取得します。

#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>

int main(void)
{
    int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
    if (fd < 0) { perror("timerfd_create"); return 1; }
    struct itimerspec spec = {
        .it_interval = { .tv_nsec = 20000000 },
        .it_value = { .tv_nsec = 20000000 }
    };
    if (timerfd_settime(fd, 0, &spec, NULL) < 0) {
        perror("timerfd_settime"); close(fd); return 1;
    }
    struct timespec delay = { .tv_nsec = 75000000 };
    while (nanosleep(&delay, &delay) < 0) {
        if (errno != EINTR) { perror("nanosleep"); close(fd); return 1; }
    }
    uint64_t count;
    ssize_t n;
    do { n = read(fd, &count, sizeof(count)); } while (n < 0 && errno == EINTR);
    if (n != sizeof(count)) { fprintf(stderr, "timerfd read failed\n"); close(fd); return 1; }
    printf("expirations=%" PRIu64 "\n", count);
    close(fd);
    return count >= 3 ? 0 : 1;
}
cc -std=c11 -O2 -Wall -Wextra -Werror timer-count.c -o timer-count
./timer-count

典型的な結果は 3 ですが、スケジューリング遅延によりそれ以上になる可能性があり、常に正確に 3 であると断言することはできません。fd を閉じると、そのタイマーが回収されます。この例ではシステムクロックは変更されません。詳細は timerfd を参照してください。