イヌツムリのメモ

学習したことのメモである。しかし、他の人が読んでもわかるように書こう。

linux kernelドライバでタイマー割り込みを実現するのにhrtimerを使ったのでメモする

はじめに

hrtimerはhigh resolusion timerの略。 従来のtimer_listより高解像度なタイマーを提供する。

もとのドライバソフトウェアでは、timer_list を用いたブロックデバイスを使用していた。
timer_list では割り込みできる最小単位がmili secであったため、
nano secで割り込み可能なhrtimerを使用するコードに変更した。
変更する過程で調べたことを下記にまとめる。

16666666nano secで割り込むようにして、printkで割り込んだ時刻を記録したが
誤差が発生することがわかった。
誤差が発生する要因の1つに仮想マシン上で実行していることが挙げられる。
実機でもそのうち評価できたらいいな。

環境

使用できるか確認する

下記で調べられる。
後で追記する。

cat /proc/timer_list

.resolution: 1 nsecs
event_handler: hrtimer_interrupt

初期化

調べたことを記録するに当たり、要素だけを抜き出したコードもどきを下記に示す。
(計測に使ったコードそのままではないという意味)

ドライバの初期化関数で、wait_qおよびhrtimerの初期化を行う。
hrtimer_initの引数は下記の通り。
void hrtimer_init(struct hrtimer *timer, clockid_t which_clock, enum hrtimer_mode mode);

  1. struct hrtimer : タイマー
  2. clockid_t which_clock: CLOCK_MONOTONICCLOCK_REALTIMECLOCK_BOOTTIMECLOCK_TAIを選べる
  3. enum hrtimer_mode mode: HRTIMER_MODE_ABSHRTIMER_MODE_RELを選べる

hrtimerのメンバであるfunctionにタイマーが期限切れになったときに発火する関数を与える。
hrtimer_start ()で、タイマーの計測を開始する。
int hrtimer_start(struct hrtimer *timer, ktime_t tim, const enum hrtimer_mode mode)
ktime_t には、ktime_get()で現在時刻を取得し、そこにnanosecだけ進めた時刻をktime_add_ns()で計算して設定する。
ktime_t で設定した時刻に到達すると、functionに与えた関数が発火する。

static struct mymod_device{

  struct hrtimer timer;
  wait_queue_head_t waitq;

}mymod_dev;

int __init mymod_init(void){
    init_waitqueue_head(&vsync_dev.waitq);
    hrtimer_init(&mymod_dev.timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
    mymod_dev.timer.function = &mymod_interrupt;

    long nanosec = 1000;
    hrtimer_start(&mymod_dev.timer,
                          ktime_add_ns(ktime_get(), nanosec), HRTIMER_MODE_ABS);

}

割り込み

初期化関数内でタイマーが期限切れになったときにmymod_interrupt()を発火するように設定した。
mymod_interrupt()を下記に示す。
wake_up_interruptible_all()でwaitqで待っている関数を発火する(後述)。
hrtimer_forward()で次にtimer.functionに与えられている関数(mymod_interrupt())を発火する時刻を設定する。
u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval)
ktime_t now に現在時刻を、ktime_t intervalに待機する時間を指定する。
ここで、発火してから現在時刻を取得する処理時間分の遅延が誤差になっている?
return HRTIMER_RESTART; で再度タイマーをスタートしている。
1回だけ実行したい場合は、 return HRTIMER_NORESTART;とする。

以上により、wait_event_interruptible()で待機している関数を、intervalごとに起床する。

enum hrtimer_restart mymod_interrupt(struct hrtimer *timer){

    wake_up_interruptible_all(&mymod_dev.waitq);

    long nanosec = 16666666;
    ktime_t  currtime, interval;
    currtime  = ktime_get();
    interval = ktime_set(0,nanosec);

    interval = ktime_set(0,nanosec);
    hrtimer_forward(timer, currtime , interval);

    return HRTIMER_RESTART;
}

割り込み待ち

mymod_interrupt()で割り込むまで、待機する関数の説明をする。
wait_event_interruptible()で待機する。
mymod_interrupt()内のwake_up_interruptible_all()で起床する。
起床すると、copy_to_user()でユーザランドのデバイスファイルへへ書き込む。

ssize_t mymod_read(struct posix_clock *pc,
    uint rdflags, char __user *buf, size_t count){

    wait_event_interruptible(mymod_dev.waitq, true);
    copy_to_user((void *)buf, "hello ", len);
    return (len);
}

計測

上記ではhelloを送るように書いているが、実際はgetnstimeofday()で時刻を取得して送っている)。 カーネルレイヤの結果と、ユーザレイヤの結果をそれぞれ示す。

printkで出力した、割り込み時刻と前回からの差を下記に示す。
差の期待値は016666usecであったが、最大約1.5msecの誤差がある。
一方、ほぼ設定したタイマーどおりに動作しているパターンも存在する。

[  143.477075] dddd 1617174009.033413, 033413
[  143.493679] dddd 1617174009.050083, 016670
[  143.510381] dddd 1617174009.066785, 016702
[  143.527836] dddd 1617174009.084235, 017450
[  143.543590] dddd 1617174009.099994, 015759
[  143.560254] dddd 1617174009.116658, 016664
[  143.576927] dddd 1617174009.133331, 016673
[  143.593592] dddd 1617174009.149996, 016665
[  143.611618] dddd 1617174009.168155, 018159
[  143.626930] dddd 1617174009.183335, 015180
[  143.643595] dddd 1617174009.200000, 016665
[  143.660191] dddd 1617174009.216595, 016595
[  143.678135] dddd 1617174009.234729, 018134
[  143.693585] dddd 1617174009.249989, 015260
[  143.710252] dddd 1617174009.266657, 016668
[  143.726976] dddd 1617174009.283381, 016724
[  143.743596] dddd 1617174009.300001, 016620

ユーザランドで出力した値を下記に示す。
kernelから受信した時刻と、前回からの差を出力している。
差の期待値は016666usecであったが、最大約1.5msecの誤差がある。

sec 1617174009, usec 033413,    7:0:9.33 ,sa 033413
sec 1617174009, usec 050083,    7:0:9.50 ,sa 016670
sec 1617174009, usec 066785,    7:0:9.66 ,sa 016702
sec 1617174009, usec 084235,    7:0:9.84 ,sa 017450
sec 1617174009, usec 099994,    7:0:9.99 ,sa 015759
sec 1617174009, usec 116658,    7:0:9.116 ,sa 016664
sec 1617174009, usec 133331,    7:0:9.133 ,sa 016673
sec 1617174009, usec 149996,    7:0:9.149 ,sa 016665
sec 1617174009, usec 168155,    7:0:9.168 ,sa 018159
sec 1617174009, usec 183335,    7:0:9.183 ,sa 015180
sec 1617174009, usec 200000,    7:0:9.200 ,sa 016665
sec 1617174009, usec 216595,    7:0:9.216 ,sa 016595
sec 1617174009, usec 234729,    7:0:9.234 ,sa 018134
sec 1617174009, usec 249989,    7:0:9.249 ,sa 015260
sec 1617174009, usec 266657,    7:0:9.266 ,sa 016668
sec 1617174009, usec 283381,    7:0:9.283 ,sa 016724
sec 1617174009, usec 300001,    7:0:9.300 ,sa 016620

おわりに

hrtimerを導入してnano sec単位で割り込みタイマーを仕掛けられるようにした。
しかし、誤差が大きく、安定してnano secで割り込みできない。
今後は実機で試して精度を確認する。
また、hrtimer_initのclockid_t やhrtimer_mode を他の値に変えて精度がどのように変わるか調査する。

参考

  1. High Resolution Timers - eLinux.org
  2. 時刻と時間の管理
  3. カーネルにおけるタイマー事情 | 技術文書 | 技術情報 | VA Linux Systems Japan株式会社