outside ntpd (6)

以前Windows7が入っていたThinkPad SL510 ある時updateが終了しなくなってしまい、業をきたしてDebian(32Bit)に変更しちゃった。それ以来数年使ってるけど無トラブルですこぶる調子が良い。

でもWindows7時代にBackup用として使ってたUSB接続のDISKなんだけど、こやつもWindows7との組み合わせで書き込みが失敗する事が有ったんだ。んな訳でDebianに変更してからは、ずっとほったらかしにしてたんだ。

このDiskには個人情報も豊富に入っているので、クリアしておきたいぞ。いわゆるディジタル終活ね。萌えないゴミで出されるとか遺産整理で、どんな運命を辿るか全く分かりませんから。

Windows式DISKフォーマット(NTFS)なやつをリナでそのまま扱えるのか? やってみる。だめなら、リナ用にフォーマットするだけですから。

USBケーブルを刺して電源入れたら、マウントするかって聞いてきた。okしたら普通に使えた。これって産婆さんのおかげかな?

debian:~$ df -h
/dev/sdb1       150G   57G   93G  38% /media/sakae/HD-160U2

57Gって、自分のエリアのバックアップだな。まずはこれをゴミ箱に放り込んで、素人には見えないようにしよう。これだと悪意のある人に、簡単に復元されてしまう。んなら、ベタにデータを全エリアに書き込んじゃえ。

こういう場合は、/dev/zeroをデータ源としてddすれば良い。これ普通の考え。OpenBSDな小僧なら、木は森に隠せ ってのを思い出すぞ。

ZEROデータだと、もし未書き込みのエリアが有れば、簡単に発見されてしまう。そこで、いかにもそれらしいデータですよってのを書き込んで、目くらましするのさ。忍法、隠れ身の術。

出鱈目なデータの発生源として、/dev/random と /dev/urandom の2種類が用意されてる。 前者は高級な乱数なんで、余り採掘出来ない。後者はえせの乱数なんでザクザクと採掘出来る。

今回の用途なら偽物乱数(計算式で発生、初期値のみ高級乱数を利用)で十分。ちなみに高級乱数は真の乱数とも呼ばれる。マウスのクリック間隔やらdiskのアクセス状況やらの、不確定要素を元に収集したものだ。余り取れないので、システムが定期的に採集して保存してる。

for i in `seq 100 300`
do
    echo $i
    dd if=/dev/urandom of=$i bs=1M count=1000
done

1Mの乱数列を1000個まとめて、一つのファイル(100から始まる通番)にしてる。要するに1Gのファイルをエラーになるまで書き込めってやつ。 後は、これを実行するだけね。

100
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB, 1000 MiB) copied, 43.3694 s, 24.2 MB/s
101
1000+0 records in
1000+0 records out
1048576000 bytes (1.0 GB, 1000 MiB) copied, 46.1816 s, 22.7 MB/s

VFO or VXO

inside macのむこうをはって、タイトルを上記に設定しました。オイラーのページは、話題があっちに飛んだり、こっちに戻ったりと取り留めがないので、毎回ネーミングには苦労します。

普通は記事を書き終わった後、もっとも話題が膨らんだ物をタイトルにしてます。 でも、今回は、まずタイトルを決め、それに従って筆を進める事にしました。

無線界でVFOと言えば、ロートルはL(コイル)とC(コンデンサ)による共振回路を使った発振器の事を言う。現代ではデジタル式が普通だけどね。

このVFOは種々の要因によりQRH(周波数変動)を起こす。それが悩みの種。 一方水晶発振器は、発振周波数が固定(水晶の物理的な特性で決まる)な代わりに、安定度は良い。

アマチュア無線は、アマチュアゆえのチャレンジ精神で、両者の良い所取りに挑戦した。 それが、

永遠の課題VXO

と呼ばれるものだ。記事にも有るように、無理すると水晶発振器の特性を乱してしまい、元の木阿弥になる。そのさじ加減と言ったら。。。

思わず昔の事を書いちゃったけど、まさかパソコンにVFOが組み込まれている訳もなかろうし、VXOなんてもっての他だろう。

したら、ntpdで発振周波数を調整してるって表現は何よ? いい加減な事言うなって思っちゃう、元アマチュア無線家がここに居ます。

こうしたVFOをどうやって実現してる。電子回路を勉強した人なら、インバーター回路(例、SN7404、古くてすみません)を、奇数個リング状に接続すれば、デジタル式の発振器になるよ。

じゃ、周波数はどうやって調整するねん? そんなの簡単。電源電圧を変えれば、発振周波数は可変出来ると。電源電圧によりインバーター回路のTpd(プロパゲーション・ディレー)伝搬遅延時間が変わると言う性質が有るからだ。

でも、にわかにこんな方法でVFOと表現された機能を実現してるとは思えない。じゃ、PLL回路か? とか言い出すと、話がどんどんズレて行く(ドリフトとか言いますね)ので、これぐらいにしておく。

気になるファイル

ドリフトと名付けられたファイルが有る。

ob$ cat /var/db/ntpd.drift
18.199

OpenBSDなやつ

sakae@pen:~$ cat /usr/local/var/db/ntpd.drift
9.007

Debian(64Bit)なやつ

debian:~$ cat /usr/local/var/db/ntpd.drift
-16.358

Debian(32Bit)リアルマシンなやつ

ntpd.c/readfreq とかで使われている。勿論、直交するwritefreqも有るよ。

       fd = open(DRIFTFILE, O_RDWR);

        freqfp = fdopen(fd, "r+");

        /* if we're adjusting frequency already, don't override */
        if (adjfreq(NULL, &current) == -1)
                log_warn("adjfreq failed");
        else if (current == 0 && freqfp) {
                if (fscanf(freqfp, "%lf", &d) == 1) {
                        d /= 1e6;       /* scale from ppm */
                        ntpd_adjfreq(d, 0);

ファイルの中身はppmスケールで表された補正値とな。実際のシステムへの適用は、ntpd_adjfreqの中で行われるのだけど、やってる事は、adjfreq(&curfreq, NULL)だ。読み込んだデータの単位を調整して、それがcurfreqにセットされてから実行されてる。

adjfreqが肝っぽい。manすると、スーパーユーザーだけが実行出来る特権付きのやつ。何故って、目的が、システムクロックのレートを修正する ですからね。下々の者が勝手に実行したら困る訳よ。

レートを修正するってのは、平たく言うと、時間が進む速度を調整するって事ね。国家が国家たるには、税金を貢がせる事、逮捕とか死刑とかの暴力を振るえる事、暦を修正出来る事って言うように、強大な権力の一つですよ。

この機構の裏側を探るには、ntpdの外側に出る必要がある。ntpdはユーザー空間での実行。その外側ったらカーネル側ね。カーネル側は特別閲覧コースとして取っておこう。

ntpd_adjfreqと並置して、ntpd_adjtimeってのもある。こちらは、内部でやはりシステムコールのadjtimeってのが使われている。

adjtime(const struct timeval *delta, struct timeval *olddelta);

gettimeofday(2)によって返されるシステム時間を微調整し、timeval deltaで指定された
時間だけシステム時間を 進めたり遅らせたりします 。
デルタが負の場合、修正が完了するまでクロックを通常よりもゆっくりとインクリメントする
ことにより、クロックが遅くなります。
デルタが正の場合、通常よりも大きな増分が使用されます。補正を実行するために使用される
スキューは、通常1パーセントの割合です。したがって、時間は常に単調に増加する関数です。

補正が完了する前に再度呼び出されると、未達成のデータがolddeltaに返されるそうな。 この機能を利用して、syncが完了したか確認してる。

        if (adjtime(&tv, &olddelta) == -1)
                log_warn("adjtime failed");
        else if (!firstadj && olddelta.tv_sec == 0 && olddelta.tv_usec == 0)
                synced = 1;

この他に、特権システムコールのsettimeofdayって言う、身も蓋もなく時刻調整するやつも利用されてる。

いざ、裏側へ。

into kernel

いよいよカーネル側に突入かと思うと、脱線します。パソコンの中で時を刻む水晶発振器の簡単な資料を探してみた。

振動子の精度

パソコン 基準クロック用

通常の音叉型水晶振動子の温度特性カーブ

安定に発振すると言っても、10ppmぐらいが関の山。1ppmは、100万分の1の事。仮に1MHzの発振器と言っても、±10Hzぐらいはずれてて当たり前。しかも、周辺の温度によってずれが変わる。更に、長年使用してると、(発振の)振動により水晶片が擦り減ってしまい、周波数が変わってしまう(経年変化)と言う問題がある。

1ppmずれていたとすると、1秒につき1usと言う割合になるんで、10日も経てば0.9秒近くずれる事になる。これじゃ、ある時点でぴったり時刻合わせしたって、ずれていってしまう。

だから、この発振器の特性を調べておいて補正をかけてしまおうってのが、adjfreqってシステムコールだ。

vm$ cat /var/db/ntpd.drift
19.394

この補正データは、OpenBSD上のqumuでゲストOSとしてOpenBSDを動かしておいて、ntpdした時のものだ。

man(9)は宝庫です。ここを経由で、 Timecounters: Efficient and precise timekeeping in SMP kernels.素敵な資料にたどり着けたぞ。

後、ヒントを得て

ob$ sysctl -a | grep clock
kern.clockrate=tick = 10000, tickadj = 40, hz = 100, profhz = 100, stathz = 100
kern.timeout_stats=added = 441387, cancelled = 41889, deleted = 96521, late = 120, pending = 25, readded = 846, rescheduled = 399593, run_softclock = 388679, run_thread = 9948, softclocks = 124005, thread_wakeups = 8701

sys/time.h

/*
 * clock information structure for sysctl({CTL_KERN, KERN_CLOCKRATE})
 */
struct clockinfo {
        int     hz;             /* clock frequency */
        int     tick;           /* micro-seconds per hz tick */
        int     tickadj;        /* clock skew rate for adjtime() */
        int     stathz;         /* statistics clock frequency */
        int     profhz;         /* profiling clock frequency */
};

tc_adjfreq

sys_adjfreqの肝はtc_adjfreqなんで、そこにBPを置く。その前に、どんなカウンターが使われているか確認。

vm$ sysctl -a | grep timecounter
kern.timecounter.tick=1
kern.timecounter.timestepwarnings=0
kern.timecounter.hardware=acpihpet0
kern.timecounter.choice=i8254(0) acpihpet0(1000) acpitimer0(1000)

BPして、tc_adjfreqを抜ける時の、カウンター構造体。

(gdb) p *timecounter
$5 = {
  tc_get_timecount = 0xd079cb40 <acpihpet_gettime>,
  tc_poll_pps = 0x0,
  tc_counter_mask = 4294967295,
  tc_frequency = 100000000,
  tc_name = 0xd17fe414 "acpihpet0",
  tc_quality = 1000,
  tc_priv = 0xd17fe400,
  tc_next = {
    sle_next = 0xd10fc014 <acpi_timecounter>
  },
  tc_freq_adj = 83296595738623
}

上の構造体の内容から、カウンター値を読み取る場所が判明したので、BPを貼る。 即座にBPした。

(gdb) b acpihpet_gettime
Breakpoint 2 at 0xd079cb4a: file /usr/src/sys/dev/acpi/acpihpet.c, line 277.
(gdb) c
Continuing.

止まった所はこんな感じ。step実行すると

  acpihpet_gettime(struct timecounter *tc)
  {
B         struct acpihpet_softc *sc = tc->tc_priv;

  =>      return (bus_space_read_4(sc->sc_iot, sc->sc_ioh, HPET_MAIN_COUNTER));
  }

kern_tc.cに呼び戻された。後は地道に見て行けとな。

  tc_delta(struct timehands *th)
  {
          struct timecounter *tc;

          tc = th->th_counter;
          return ((tc->tc_get_timecount(tc) - th->th_offset_count) &
  =>          tc->tc_counter_mask);
  }

お遊びで、 timeカウンターを昔ながらのi8254に切り替えてみた。

(gdb) p *timecounter
$2 = {
  tc_get_timecount = 0xd0e07a70 <i8254_simple_get_timecount>,
  tc_poll_pps = 0x0,
  tc_counter_mask = 32767,
  tc_frequency = 1193182,
  tc_name = 0xd0fa2d0d "i8254",
  tc_quality = 0,
  tc_priv = 0x0,
  tc_next = {
    sle_next = 0xd1103ce4 <hpet_timecounter>
  },
  tc_freq_adj = 0
}

一秒毎に更新されるntp秒の呼び出し状況。ここでadjfreqの値が使われている。

(gdb) bt 5
#0  ntp_update_second (th=0xd1105d54 <th0>) at /usr/src/sys/kern/kern_tc.c:740
#1  0xd08e7207 in tc_windup (new_boottime=0x0, new_offset=0x0, new_adjtimedelta=0x0) at /usr/src/sys/kern/kern_tc.c:539
#2  0xd08e7c64 in tc_ticktock () at /usr/src/sys/kern/kern_tc.c:673
#3  0xd0525027 in hardclock (frame=0xf1cba99c) at /usr/src/sys/kern/kern_clock.c:184
#4  0xd0c66a4e in lapic_clockintr (arg=0xf1cba99c) at /usr/src/sys/arch/i386/i386/lapic.c:259
(More stack frames follow...)

補正値は、th_adjustmentの中に格納されている。

(gdb) p *th
$13 = {
  th_counter = 0xd1103ce4 <hpet_timecounter>,
  th_adjtimedelta = 0,
  th_adjustment = 83296595738623,
  th_scale = 184471018260,
  th_offset_count = 131832062,
  th_boottime = {
    sec = 1584162557,
    frac = 11037716929446611105
  },
  th_offset = {
    sec = 4701,
    frac = 7616474744532179515
  },
  th_microtime = {
    tv_sec = 1584167258,
    tv_usec = 976142
  },
  th_nanotime = {
    tv_sec = 1584167258,
    tv_nsec = 976142636
  },
  th_generation = 0,
  th_next = 0xd1105db4 <th1>
}

call adjfreq

ntpdばかりに頼っていてはアレなんで、簡単な呼び出しプログラムを書いてみた。一つの必須引数が必要。ppmの単位で指定する。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/time.h>

int main(int argc, char *argv[]){
  double fin;
  int64_t freq,old;

  fin = atof( argv[1] ) * 1e-6;
  freq = fin * 1e9 * (1LL << 32);

  if (adjfreq(&freq, &old) == -1){
    printf("Error\n");
    return 1;
  }
  printf("%lld  %lld\n", freq, old);
  return 0;
}

下記は実行例。 これから設定する値と、以前に設定されてた値が表示される。

ob$ doas ./a.out -12.345
doas (sakae@ob.localdomain) password:
-53021371269120  72009421684736

modify

ntpdで、どんなパラメータが変更されたか確認。まずはntpd起動前

(gdb) p *th
$3 = {
  th_counter = 0xd1103ce4 <hpet_timecounter>,
  th_adjtimedelta = 0,
  th_adjustment = 0,
  th_scale = 184467440736,
  th_offset_count = 986958941,
   :

そして、こちらはntpdの起動後

(gdb) p *th
$10 = {
  th_counter = 0xd1103ce4 <hpet_timecounter>,
  th_adjtimedelta = 0,
  th_adjustment = 83296595738623,
  th_scale = 184471018260,
  th_offset_count = 3845837627,
   :

th_adjustmentの値は、adjfreqの内部表現

In [1]: 83296595738623 / 1e3 / (1<<32)
Out[1]: 19.393999999999767

この19.394って値は、ntpd.driftってファイルに記憶されてた。また、th_scaleの値は、リアルなカウンター値から、正しい(時間)値に読み替えるための文字通りスケールだ。

tickadj

前の方でtickadjって言うグローバル変数が出てきた。どこに表われているか、全文検索

ob$ find . -name '*.c' | xargs grep tickadj
./arch/mips64/mips64/mips64_machdep.c:  tickadj = 240000 / (60 * hz);  /* can adjust 240ms in 60s */
./conf/param.c:int      tickadj = 240000 / (60 * HZ);  /* can adjust 240ms in 60s */
./kern/kern_clock.c:    if (tickadj == 0)
./kern/kern_clock.c:            tickadj = 1;
./kern/kern_clock.c:    clkinfo.tickadj = tickadj;

設定してるだけで、使ってる所が見当たらない。もしやと思って

ob$ find . -name '*.h' | xargs grep tickadj
./sys/kernel.h:extern int tickadj;      /* "standard" clock skew, us./tick */
./sys/time.h:   int     tickadj;        /* clock skew rate for adjtime() */

調べてみるも、使ってる風は無い。でも、それらしいコメントが有るしね。もう使っていないのかな。

もしやと思って、思いっきり時刻をずらしておいてから、ntpdを起動。この時に-sで、サーバーとの時刻同期をさせない設定が味噌かな。

(gdb) p *th
$11 = {
  th_counter = 0xd1103ce4 <hpet_timecounter>,
  th_adjtimedelta = 3013244709,

これが、adjtimeのためのデルタではなかろうか?

四の五の言ってないで、実測してみるのが、正しい科学の方法です。

ob$ cat setoffset.c
#include <sys/time.h>
#include <stdio.h>

int main(){
  struct timeval  tv,olddelta;

  tv.tv_sec = 2;
  tv.tv_usec = 0;
  if (adjtime(&tv, NULL) == -1){
    puts("Err Set offset");
    return 1;
  }
  return 0;
}

2秒のオフセットが有る事にした。

ob$ cat mon.c
#include <sys/time.h>
#include <stdio.h>

int main(){
  struct timeval  tv,olddelta;

  if (adjtime(NULL, &olddelta) == -1){
    puts("Err monitor");
    return 1;
  }
  printf("%lld %ld\n", olddelta.tv_sec, olddelta.tv_usec);
  return 0;
}

監視装置。監視するだけなら、一般ユーザーでも可能。

ob$ cat watch.sh
#!/bin/sh
./setoffset
while :
do
    ./mon
    sleep 10
done

10秒毎に、オフセットをモニター。

2 0
1 950000
1 900000
1 850000
:

1秒間当たり5msのスピードで、時刻を変更してくのね。

ntpd server by fpga

さくらインターネット、福岡大学と協力し 世界最速クラスのハードウェア 時刻同期(NTP)サーバーを自社開発 ~FPGAベースの公開NTPサービスをトライアル提供~

FPGA ベース・ハードウェアNTPサーバ(Stratum1)特設実験サイト

応答してくれるか、簡易検査。

ob$ python3 ntp.py fpga-ntp-trial.elab.sakura.ad.jp
1584512351.6905785

wiresharkで、パケット分析

Network Time Protocol (NTP Version 3, server)
    Flags: 0x1c, Leap Indicator: no warning, Version number: NTP Version 3, Mode: server
    [Request In: 3]
    [Delta Time: 0.033985000 seconds]
    Peer Clock Stratum: primary reference (1)
    Peer Polling Interval: 10 (1024 seconds)
    Peer Clock Precision: 0.000000 seconds
    Root Delay: 0.000000 seconds
    Root Dispersion: 0.000000 seconds
    Reference ID: Unidentified reference source 'SKR'
    Reference Timestamp: Mar 18, 2020 06:22:14.000000000 UTC
    Origin Timestamp: Feb  7, 2036 06:28:16.000000000 UTC
    Receive Timestamp: Mar 18, 2020 06:22:14.389175832 UTC
    Transmit Timestamp: Mar 18, 2020 06:22:14.389176070 UTC

サクラさんて略すとSKRを名乗るのね。そして、確かに1次サーバーになってる。福岡大学のリベンジだな。助太刀はサクラさんだ。

サーバーの応答時間は、(389176070 - 389175832) = 238ns って事で、めちゃめちゃ速い。これがFPGAの威力!!

OpenBSDのntpdに組み込んでみた。

ob$ ntpctl -sa
1/1 peers valid, constraint offset -1s, clock synced, stratum 2

peer
   wt tl st  next  poll          offset       delay      jitter
133.242.40.220 fpga-ntp-trial.elab.sakura.ad.jp
 *  1 10  1   28s   34s        -1.773ms    38.588ms     8.654ms

ラウンドトリップ時間が38msと露わになってるけど、pingには応答無しなんで参考値だな。