adjtime(2) の実装を読む[前編] - インターネット時刻同期の歴史(その5)
先に settimeofday(2) のおさらい
N ではざっくりいうと
- libc のシステムコール
foo()
を呼ぶと - kernel の
sys_foo()
が実行される
事になっている、いや本当は SYS_foo
というシステムコール番号とペアになる関数ポインタをテーブルから引くのだがそこまで知らんでもいい、話が長くなるし。
なので settimeofday()
は sys_settimeofday()
となるのだが、 N5 で time_t
を 64bit 化した関係でちょっと事情が異なる。
time_t
のサイズが変わると libc の後方互換性が失われるので soname というものを変更しなくてはならない。
N における運用では libc.so.12.*
を libc.so.13.*
とする、つまりメジャーバージョン変更が必要だったのだ。
しかしあまりにインパクトが大きいので、ちょっとしたトリックを使ってバイナリの後方互換性を保ち再コンパイル不要なまま 64bit time_t
化を成し遂げたのである。
方法については存在しない過去回「
どのようにしてlibcは後方互換を保つのか?(その3) 最終回
」で解説済なのでそっちを読んでもらえばいい。
要約すれば __RENAME
マクロという邪悪なプリプロセッサの魔法でシステムコール名を強制変更しているということ。
この魔法により settimeofday()
はプログラマが一切意識することなく __settimeofday50()
に置換えられている。
なので実際にカーネル側で呼ばれているのは sys___settimeofday50()
である事に注意。
526 /* ARGSUSED */
527 int
528 sys___settimeofday50(struct lwp *l, const struct sys___settimeofday50_args *uap,
529 register_t *retval)
530 {
531 /* {
532 syscallarg(const struct timeval *) tv;
533 syscallarg(const void *) tzp; really "const struct timezone *";
534 } */
535
536 return settimeofday1(SCARG(uap, tv), true, SCARG(uap, tzp), l, true);
537 }
538
539 int
540 settimeofday1(const struct timeval *utv, bool userspace,
541 const void *utzp, struct lwp *l, bool check_kauth)
542 {
...
570 return settime1(l->l_proc, &ts, check_kauth);
571 }
sys___settimeofday50()
は settimeofday1()
を呼出しているが、これは後方互換のために残されている compat_50_sys_settimeofday()
と実装を共有するためである。
237 /* ARGSUSED */
238 int
239 compat_50_sys_settimeofday(struct lwp *l,
240 const struct compat_50_sys_settimeofday_args *uap, register_t *retval)
241 {
242 /* {
243 syscallarg(const struct timeval50 *) tv;
244 syscallarg(const void *) tzp; really "const struct timezone *";
245 } */
...
252 return settimeofday1(&atv, false, SCARG(uap, tzp), l, true);
253 }
そして settimeofday1()
は settime1()
を呼出している。
これは kernel 内では不必要なエラーチェックを分離することで最適化している。
176 /*
177 * Time of day and interval timer support.
178 *
179 * These routines provide the kernel entry points to get and set
180 * the time-of-day and per-process interval timers. Subroutines
181 * here provide support for adding and subtracting timeval structures
182 * and decrementing interval timers, optionally reloading the interval
183 * timers when they expire.
184 */
185
186 /* This function is used by clock_settime and settimeofday */
187 static int
188 settime1(struct proc *p, const struct timespec *ts, bool check_kauth)
189 {
...
214 tc_setclock(ts);
215
216 resettodr();
...
236 }
156 static struct {
157 struct bintime bin;
158 volatile unsigned gen; /* even when stable, odd when changing */
159 } timebase __cacheline_aligned;
...
874 /*
875 * Step our concept of UTC. This is done by modifying our estimate of
876 * when we booted.
877 */
878 void
879 tc_setclock(const struct timespec *ts)
880 {
...
890 timebase.gen |= 1; /* change in progress */
...
892 timebase.bin = bt;
...
894 timebase.gen++; /* commit change */
...
905 }
N の vmt(4) による時刻同期
はここで再登場した tc_setclock()
という 関数で時刻同期を行っていたのを思い出してほしい。
さらに
RFC868 Time Protocol 回
で解説した resettodr()
も再登場している。
もうおわかりですね、tc_setclock()
の正体は 4.3BSD の settimeofday(2) が行っていた現在時刻 struct timeval time
に相当する timebase.bin
「だけ」を更新し resettodr()
を呼ばないので RTC(RealTime-Clock) は更新されないという事。
VMware Tools では普通に settimeofday(2) で RTC 更新するんだけど、実マシンと同じイメージで RTC 更新は重い処理って回避したのだろうかね。 仮想マシンなんだから無意味と思うんだよなこれおそらく。
adjtime(2) を読む
ということで settimeofday(2) と同じように sys___adjtime50()
を探せとなるが、最新の実装は読むのクソかったるいので、またまた 4.3BSD へと回帰する。
86 adjtime()
87 {
88 register struct a {
89 struct timeval *delta;
90 struct timeval *olddelta;
91 } *uap = (struct a *)u.u_ap;
...
98 u.u_error = copyin((caddr_t)uap->delta, (caddr_t)&atv,
99 sizeof (struct timeval));
...
120 (void) copyout((caddr_t)&oatv, (caddr_t)uap->olddelta,
121 sizeof (struct timeval));
122 }
この時代は libc 側も kernel 側も同じ名前であるが引数がない。
なので 引数 delta
と olddelta
は u.u_ap
経由でアクセスする。
このu
という変数は以下で定義されている。
30 struct user {
...
36 /* syscall parameters, results and catches */
37 int u_arg[8]; /* arguments to current system call */
38 int *u_ap; /* pointer to arglist */
...
50 char u_error; /* return error code */
125 };
...
140 extern struct user u;
user
構造体の u_ap
フィールドで引数リストのポインタとしてアクセスできるって寸法。
引数がポインタの場合アドレスだけもらっても困るので、ユーザースペースとカーネルの間でコピー処理が必要。 よって copyin(2) を使え。
現代のカーネルの sys_* プリフィクス付システムコールだともうちょっとめんどくさい。
グローバル変数 u
ではなくシステムコール自身の第二引数からさらに SCARG
というマクロを使う。
575 /* ARGSUSED */
576 int
577 sys___adjtime50(struct lwp *l, const struct sys___adjtime50_args *uap,
578 register_t *retval)
579 {
580 /* {
581 syscallarg(const struct timeval *) delta;
582 syscallarg(struct timeval *) olddelta;
583 } */
584 int error;
585 struct timeval atv, oldatv;
...
592 error = copyin(SCARG(uap, delta), &atv,
593 sizeof(*SCARG(uap, delta)));
...
600 error = copyout(&oldatv, SCARG(uap, olddelta),
601 sizeof(*SCARG(uap, olddelta)));
602 return error;
603 }
138 #if BYTE_ORDER == BIG_ENDIAN
139 #define SCARG(p,k) ((p)->k.be.datum) /* get arg from args pointer */
140 #elif BYTE_ORDER == LITTLE_ENDIAN
141 #define SCARG(p,k) ((p)->k.le.datum) /* get arg from args pointer */
142 #else
143 #error "what byte order is this machine?"
144 #endif
この辺については大阪人の行けたら行くくらいの実現性でいずれまた解説記事を書きたいと思う。
そんで以下が adjtime(2) が参照あるいは書換を行うグローバル変数である。
81 extern int tickadj; /* "standard" clock skew, us./tick */
82 int tickdelta; /* current clock skew, us. per tick */
83 long timedelta; /* unapplied time correction, us. */
84 long bigadj = 1000000; /* use 10x skew above bigadj us. */
クロックスキューとは何ぞや、あの一時期やたらと流行した合成樹脂のサンダルの事かな?
電気回路屋なら知ってて当然なんだろうが俺はチャラい大学の文系卒で経済学部で西洋音楽史を研究していたオッサンだ。 ギターの可変抵抗を右に回したら抵抗が上がるのか下がるかレベルの電気回路知識すら無いぞ俺。
たぶん音がデカくなるんだから抵抗もどーんとデカくなるんやろロックってのは世間様への精一杯の抵抗やガハハ。 David Bowie 神も TO BE PLAYED AT MAXIMUM VOLUME とおっしゃられとるわ。
通常クロックスキューって遅延で生じたズレの差のことだと思うんだが、tickadj
や tickdelta
はズレを補正するために一回のクロック割り込みでどれだけ調整するかの値だからニュアンス違うよな。
うーんズレを打ち消すためにズラすのもクロックスキューと呼んでいいのか電気回路屋じゃないし英語ネイティブでもないしわからん。
まあいいやクロック割込みすなわち hardclock(9) の解説については
3.4 時計割込み
3.4 クロック割込み
をどうぞ、俺は hardclock(9) とハードロックの違いすら判らないのだ(流れる Led Zeppelin の Im My Time Of Dying)。
この記事読む上で知っとけばいい事はクロック割込みは実時間タイマーともいい、例えば
過去回
で書いた time_uptime(9) やさっき登場した timebase.bin
をカウントアップするのもこいつのお仕事であるくらいだな。
とはいえ本読め本だとこの後の話進まなくなるのでこっちも軽く実装みてみよか。
40 #define HZ 100
41 int hz = HZ;
42 int tick = 1000000 / HZ;
クロック割込みが 1 秒間に 100 回発生する場合
- 周波数で表現すると 100 Hz となり
hz
は 100 となる - マイクロ秒で表現すると 10000 マイクロ秒 となり
tick
は 10000 となる
わけですな。
しかしクロックジェネレーターはたいして精度が高くない。 原子時計ですら数億年に 1 秒くらいズレるし、いわんやクオーツの精度などお察しである。 このクロックがズレていく現象がクロックドリフト、テンポがズレて気持ちいいのは Steve Reich の音楽くらいなので、補正が必要になる。
その補正は 60 秒間に最大 240 ミリ秒まで可能、それを一回のクロック割込分にした値が tickadk
である。
43 int tickadj = 240000 / (60 * HZ); /* can adjust 240ms in 60s */
以上の事をふまえて ajdtime(2) の残り部分読んでこか。
ノイズとなるシステムコール作法や排他制御を取り除くと以下のようになる。
93 register long ndelta;
...
102 ndelta = atv.tv_sec * 1000000 + atv.tv_usec;
103 if (timedelta == 0)
104 if (ndelta > bigadj)
105 tickdelta = 10 * tickadj;
106 else
107 tickdelta = tickadj;
108 if (ndelta % tickdelta)
109 ndelta = ndelta / tickadj * tickadj;
...
112 if (uap->olddelta) {
113 oatv.tv_sec = timedelta / 1000000;
114 oatv.tv_usec = timedelta % 1000000;
115 }
116 timedelta = ndelta;
ndelta
は第一引数で指定された補正が必要な時間をマイクロ秒に変換したもの。
timedelta
ゼロは現在 hardclock(9) で補正が行われてないということ。
補正が行われてる間は tickdelta
は変更しない(理由がよくわからない)。
tickdelta
は一回のクロック割込みで補正する時間。
通常は tickadj
となるが ndelta
が 1 秒を超える場合はその 10 倍として補正を急ぐ。
ndelta
を tickdelta
で割って余りがある場合は ndelta
を tickadj
の倍数で揃える
(これ tickdelta
は tickadj
の倍数ではあるけどここで剰余求めるのは tickadj
だよなぁ)。
timedelta
を ndelta
で更新(以前の値は第二引数で返す)
次回以降のクロック割込みで timedelta
で指定した時間分のクロック補正が開始される。
まぁこんなかんじ。
次回
次回は hardclock(9) で timedelta
および tickdelta
がどう扱われるかを確認する。
それと adjtime(2) の 4.3BSD 以降の進化について書けたら書く。