The Fuzzball (最古の NTP リファレンス実装) を読む[後編] - インターネット時刻同期の歴史(その13)
最初に追加資料
今回紹介するコードには C のプリプロセッサ的なマクロが登場するので
を必要ならば参照してくれ。
NTPSRV.MAC を読む
いよいよ NTP の実装を読んでいくわけだが今回はプロトコルそのものの解説はやらない。 なぜなら PDP-11/RT-11 のアセンブラ及びマクロ使って解説するのは書く方も読む方も不幸にしかならないから。 漢文古文ラテン語で長文読解問題作るバカはいねえので、次回以降に現代文すなわちプログラミング言語 C によるリファレンス実装にて改めて解説するでおじゃるぞ。
前回まで読んでた SETCLK.MAC では RT-11 の現在時刻を取得する .GTIM
というシステムコールが登場したけれども、NTPSRV.MAC ではこいつらは出現しない。
代わりにオレオレマクロ
.GDAT
… 現在の日付を取得する.GCLK
… 現在の時刻を取得する
が使われている。
149 ;
150 ; Time request
151 ; R0 = udp length, r1 = packet pointer, r2 = udp header pointer
152 ;
153 NTPREQ: MOV R0,PKTLNG ;save length
...
156 .GDAT ;yes. save receive timestamp
...
440 ;
441 ; Transmit procedure
442 ; r4 = neighbor pointer (preserves only r4)
443 ;
444 NTPSND: MOV NG.DST(R4),UDPDST ;restore address fields
...
474 .GDAT ;get current date
...
481 .GCLK ;get current time
こいつらは SUP.MAC に定義されている。
SUP.MAC を読む
SUP とは System sUPer visor の意味のようであるがその意味はというと、コメントに Walt Kelly/Pogo というスラップスティック漫画から Deck Us All with Boston Charlie という劇中歌が引用されているだけ、うーん you’re not expected to understand this お前に理解するのは無理って万国共通クソオタクのチー仕草やのう。
そんで前述の .GDAT
および .GCLK
は以下のとおり。
1691 ;
1692 ; .gdat (gdt) get system date
1693 ; Returns r0 = date
1694 ;
1695 .GDAT: JSR PC,GTCLK ;make sure date/time are consistent
1696 MOV DATE,@R4 ;fetch date
1697 RTS PC
1698 ;
1699 ; .gclk (gck) get system clock
1700 ; Returns r0-r1 = time
1701 ;
1702 .GCLK: JSR PC,GTCLK ;read local clock
1703 MOV R0,@R4
1704 MOV R1,REGR1(R4)
1705 RTS PC
GTCLK
が本体で、それぞれ日付部と時刻部を取り出すだけ。
GTCLK を読む前に
ところでこのファイルにはなんと RT-11 まかせではない自前のハードウェアクロック実装があるのである、おおついに 1000Hz ってやつとご対面か?
ハードウェアクロックとなるクロックジェネレーターには以下の増設ボードが使えるもよう。
44 ;
45 ; Hardware clocks
46 ;
47 ; For UNIBUS systems the interval timer and system clock functions
48 ; can be provided by the KW11-L or KW11-P. For Q-BUS systems these
49 ; functions can be provided by the integral LTC or KWV11-A/C. For the
50 ; highest accuracy and lowest overhead, the combination of LTC as the
51 ; interval timer plus KWV11-A/C as the system clock is recommended.
52 ;
53 ; HDWCLK Timer Clock
54 ; --------------------------------------------------------------
55 ; 0 KW11-L/LTC KW11-L/LTC UNIBUS and Q-BUS
56 ; 1 KW11-P KW11-P UNIBUS only
57 ; 2 KWV11-A/C KWV11-A/C Q-BUS only
58 ; 3 LTC KWV11-A/C Q-BUS only
59 ;
60 .IIF NDF,HDWCLK HDWCLK == 0 ;hardware clock
61 .IF EQ,HDWCLK-3 ;conditional assembly for clock type
62 .IIF NDF,KWVCLK KWVCLK == 40145 ;clock register (KWV11)
63 .ENDC
- KW11-L line time clock manual
- KW11-P Programmable real-time clock manual
- ADV11-A, KWV11-A, AAV11-A, DRV11 user’s manual
KW11-L は 50/60Hz だけど KW11-P は 100kHz/10kHz そして KWV11-A/C は 10MHz/1MHz/100kHz/1kHz/100Hz に対応してるもよう。
64 ;
65 ; The LINFRQ symbol establishes the clock interrupt frequency. If this
66 ; symbol is not defined the INCRM1/2 symbols can be defined for an
67 ; arbitrary clock period. If none of these three symbols are defined the
68 ; default is 60 Hz.
69 ;
70 .IF DF,LINFRQ ;include for specified frequency
71 .IF EQ,LINFRQ-60. ;include for 60-Hz
72 INCRM1 == 16. ;(mod(1000/60))
73 INCRM2 == 43691. ;(rem(1000/60)/60*65536)
74 .IFF ;include for other frequencies
75 INCRM1 == 1000./LINFRQ ;(mod(1000/linfrq))
76 INCRM2 == 0 ;(rem(1000/linfrq)/linfrq*65536)
77 .ENDC
78 .IFF ;include for unspecified frequency
79 .IF DF,INCRM1 ;include for specified period
80 .IIF NDF,INCRM2 INCRM2 == 0 ;default even milliseconds
81 .IFF ;include for default (60 Hz)
82 INCRM1 == 16. ;(mod(1000/60))
83 INCRM2 == 43691. ;(rem(1000/60)/60*65536)
84 .ENDC
85 .ENDC
.IFF
は if and only if
かと思ったがさにあらず if tests false
なのに一瞬戸惑ったがまあいい。
理解しづらいので C プリプロセッサで書き直すとそれぞれ
#if !defined(HDWCLK)
# define HDWCLK 0
#endif
#if HDWCLK == 3
# if !defined(KWVCLK)
# define KWVCLK 40145
# endif
#endif
#if defined(LINFRQ)
# if LINFRQ == 60
# define INCRM1 16
# define INCRM2 43691
# else
# define INCRM1 1000/LINFRQ
# define INCRM2 0
# endif
#else
# if defined(INCRM1)
# if !defined(INCRM2)
# define INCRM2 0
# else
# define INCRM1 16
# define INCRM2 43691
# endif
#endif
ちゅう感じになるはずである。
HDWCLK(KWVCLK)
や LINFRQ
あるいは INCRM1(INCRM2)
を上記のクロックジェネレーターの設定に合わせて定義するんすかね。
初期化は以下でやっている。
226 ;
227 ; Clock control/status registers
228 ;
229 KW11L = 177546 ;kw11-l line-frequency clock
230 KW11P = 172540 ;kw11-p programmable clock (unibus only)
231 KWV11 = 170420 ;kwv11-a/c programmable clock (q-bus only)
...
694 ;
695 ; Complete initialization
696 ;
697 INI7: CLR R1 ;initialize semaphores
...
739 .IF EQ,HDWCLK-0 ;conditional assembly for clock type
740 MOV #100,@#KW11L ;start kw11-l timer/clock
741 .ENDC
742 .IF EQ,HDWCLK-1 ;conditional assembly for clock type
743 MOV #1,@#KW11P+2 ;start kw11-p timer/clock
744 MOV #115,@#KW11P ;(line frequency)
745 .ENDC
746 .IF EQ,HDWCLK-2 ;conditional assembly for clock type
747 MOV #-<INCRM1*1000.>,@#KWV11+2 ;set clock rate
748 MOV #113,@#KWV11 ;start kwv11-a/c timer/clock (1 MHz mode 1)
749 .ENDC
750 .IF GE,HDWCLK-3 ;conditional assembly for clock type
751 MOV #100,@#KW11L ;start kw11-l timer
752 MOV #KWVCLK,@#KWV11 ;start kwv11-a/c clock (1 kHz mode 2)
753 .ENDC
割込みベクタは以下の通り。
2297 ;
2298 ; Cpu interrupt vectors
2299 ;
2300 COPBGN: JMP @#.INIT ;000 system startup
...
2307 VECEND = .-COPBGN ;end of cpu interrupt vectors
...
2310 . = COPBGN+100 ;kw11-l clock
2311 .WORD TIMTRP,PR7 ;timer interrupt
2312 ;
2313 . = COPBGN+104 ;kw11-p clock
2314 .WORD TIMTRP,PR7 ;timer interrupt
2315 ;
...
2325 .IF GE,HDWCLK-2 ;conditional assembly for clock type
2326 . = COPBGN+440 ;kwv11-a/c clock
2327 .WORD CLKTRP,PR7 ;clock overflow interrupt
2328 .WORD CLKTRP,PR7 ;clock on-time interrupt
2329 .ENDC
2330 COPEND = . ;end of rt-11 fiddle
2331 ;
CLKTRP
と TIMTRP
の中身については後回し。
うんこれなら理論上は 1000Hz でクロック割込みを処理可能なコードである気がしてきた。
しかし実際の設定例 (SUP*.MAC) をみるとそんな高クロックにしてないどころか単に商用電源周波数を北米の 60Hz から欧州の 50Hz に落とす用途にしか使ってないっぽい。
1 .TITLE SUP13 System conditionals - timer.unik.no
2 ;
3 ; Pdp11/dcn - System conditionals - timer.unik.no
4 ;
5 CPU == 3 ;sup cpu/bus type (22-bit bus)
6 HDWCLK == 3 ;sup hardware clock (kwv11-a/c)
7 ATOM == 1 ;atomic clock onboard
8 LINFRQ == 50. ;line frequency (Hz)
9 TS.APX == -10. ;reset aperture (+-512 ms)
結局のところ The Fuzzball の紹介にも 1000Hz は「論理クロック」とあったし、現実的にはそんな高速なクロック割込なんて処理しきれねえてことなんすかね。
昭和の置時計もクォーツ無しに商用電源の 50/60Hz で計時してたし、この時代はそれが一番安く信頼性もあったという事なんだろうか。
GTCLK を読む
まぁいいや、ちょっと長くなるけど大したコードではない。
まず記憶域は
2033 DATE: .WORD 0 ;system date
2034 CLOCK:: .WORD 0,0,0 ;system clock
DATE
… システム日付 epoch (たぶん RT-11 と同じ 1972 年 1 月 1 日)からの日数を 32bit で保持CLOCK
… システム時刻 00:00:00.000 のミリ秒を 32bit で保持
となっている、CLOCK
のうしろ 16bit ぶん無名で確保されてるのは将来的に精度をナノ秒に拡張するための予備領域かなこれ。
1 日 = 24 時間 = 1,440 分 = 86,400 秒 = 86,400,000 ミリ秒 = 86,400,000,000 マイクロ秒 = 86,400,000,000,000 ナノ秒だから 48bit あれば十分だし。
まぁ読んでけば判るでしょそのうち。
1725 ;
1726 ; Subroutine to latch local clock
1727 ; Returns r0-r1 = time (milliseconds)
1728 ;
1729 GTCLK:
1730 .IF LT,HDWCLK-3 ;conditional assembly for clock type
1731 MOV CLOCK,R0 ;fetch latest clock value
1732 MOV CLOCK+2,R1
1733 .IFF
1734 TST @#KWV11 ;is clock buffer already latched
1735 BMI 2$ ;branch if yes
1736 BIS #001000,@#KWV11 ;no. request counter transfer to buffer
1737 1$: TST @#KWV11 ;wait for response
1738 BPL 1$
1739 2$: MOV @#KWV11+2,R1 ;read clock buffer
1740 BIC #100000,@#KWV11
1741 MOV CLOCK,R0 ;resolve latest clock value
1742 ADD CLOCK+2,R1
1743 ADC R0
1744 TSTB @#KWV11 ;is overflow pending
1745 BPL 3$ ;branch if no
1746 BIC #000200,@#KWV11 ;yes. process overflow
1747 INC CLOCK ;up the tick by 65.556 sec
1748 BR GTCLK
1749 ;
1750 3$:
1751 .ENDC
HDWCLK
が 3
つまり LTC + KWV11-A/C の組合わせであれば
- 低精度の LTC がハードウェアクロック
- クロック割込みは
CLKTRP
- 精度は
LINFRQ
つまり 1/50 あるいは 1/60 秒
なので KWV11-A/C のタイマーの値によって CLOCK
の値を補正している。
ここでは INCRM1/INCRM2
は使わない、なぜなら初期化のところにあった通り 1000Hz 前提なので。
ところで 1744-1748 行目のコードが謎である。
@#KWV11
は 32bit 幅なのですべてを一度に読めないから整合性保つためにループしてるんだけど、そもそも 1/50 あるいは 1/60 秒間隔なんだから下位 16bit だけ読んで上位なんか捨ておけばいいはずなんだが
up the tick by 65.556 sec
という謎の秒数は何なのか、そもそもここで INC CLOCK
する必要無いよなぁ。
それにそもそもこの補正処理って CLKTRP
でやるべきじゃねえのかな…
まあいいや、それ以外のケースでは
- 高精度のハードウェアクロックが使用可能
- クロック割込みは
TIMTRP
- 精度は
INCRM1/INCRM2
よって CLOCK
の値をそのまま使うちゅーこと。
1752 TST R0 ;did clock underflow
1753 BPL 4$ ;branch if no
1754 ADD MIDNIT+2,CLOCK+2 ;yes. reduce modulo 2400 hours
1755 ADC CLOCK
1756 ADD MIDNIT,CLOCK
1757 DEC DATE ;roll back date
1758 RTS PC ;let the user beware...
1759 ;
1760 4$: CMP R0,MIDNIT ;(mod(86400*1000/65536)) did clock overflow
1761 BLO CLKRTN ;branch if no
1762 CMP R1,MIDNIT+2 ;(rem(86400*1000/65536)*65536)
1763 BLO CLKRTN ;branch if no
1764 SUB MIDNIT+2,CLOCK+2 ;yes. reduce modulo 2400 hours
1765 SBC CLOCK
1766 SUB MIDNIT,CLOCK
1767 INC DATE ;roll forward date
...
さっきの補正により日を跨いでしまった場合
CLOCK
を 24時間戻し(コメントが 2400 時間になっとる!)DATE
を プラス 1 日
する必要があるのでそれを実行してるのだが、補正をしないケースでも無駄にこの処理通るのは何故なんだぜ。
この後もフラグ類をクリアする処理があるけどそこは省略。
CLKTRP と TIMTRP を読む
さっきすっ飛ばしたクロック割込みの実装を読む、条件分岐がややこしいので必要なとこだけ抜き出す。
まずは HDWCLK
が 0
(KW11-L/LTC) あるいは 1
(KW11-P) のケース。
1524 TIMTRP:
...
1526 .IF NE,INCRM2 ;conditional assembly for timer interval
1527 ADD #INCRM2,CLOCK+4 ;up the tick
1528 ADC CLOCK+2
1529 ADC CLOCK
1530 .ENDC
1531 ADD #INCRM1,CLOCK+2
1532 ADC CLOCK
...
クロック割込みは TIMTRP
で CLOCK
を前述の #INCRM1
分加算している。
INCRM2
が定義されてたら CLOCK
の後ろの無名記憶域も加算しているなこれ。
ということであれはナノ秒まで拡張するための予約領域ではなく、tick を加算するものだったちゅーこと。どうりで INCRM2
が 0
のケースがあるわけだ。
次に HWDCLK
が 2
(KWV11-A/C) のケース。
1513 TIMTRP: RTI ;ignore if it can't be shut off
1514 ;
1515 CLKTRP: BIC #000200,@#KWV11 ;process overflow
...
1526 .IF NE,INCRM2 ;conditional assembly for timer interval
1527 ADD #INCRM2,CLOCK+4 ;up the tick
1528 ADC CLOCK+2
1529 ADC CLOCK
1530 .ENDC
1531 ADD #INCRM1,CLOCK+2
1532 ADC CLOCK
...
クロック割込みは CLKTRP
であとは同じ。
最後 HWDCLK
が 3
(LTC + KWV11-A/C) のケース。
1487 ;
1488 ; Update time-of-day clock
1489 ;
1490 CLKTRP: MOV R0,-(SP) ;save
1491 MOV R1,-(SP)
1492 MOV @#KWV11,-(SP) ;save interrupt flag
1493 JSR PC,GTCLK ;update local clock
...
クロック割込みは CLKTRP
で CLOCK
の更新はさっきの GTCLK
内での補正まかせである。
うーんやっぱりどう考えてもこっちでやるべき処理だよなぁ。
そしてお気づきかと思いますが TIMTRP
CLKTRP
いずれでも日跨ぎ処理やってないのよね。
さっき GTCLK
で補正をしないケースでも無駄に日跨ぎ処理を行ってた理由は、そもそもやってなかったからという酷いオチなのだった。
たぶんコードの継ぎ足しでこういうピタゴラスイッチコードになったんだろう。 リファクタリングする勇気の重要さである。
STCLK は読まんのか?
RT-11 の .GTIM
つまり現在時刻取得に相当する GTCLK
ちゅー実装があるなら .SDTTM
つまり時刻設定もあるでしょって話なのだが、もちろん存在してそれが表題の STCLK
である。
1778 ;
1779 ; stclk (sck) increment system clock
1780 ; R0-r1 = increment (milliseconds)
1781 ;
1782 STCLK: MOV DLTPTR,R2 ;save increment in median filter
...
しかし冒頭の繰り返しになるのだけど NTP の仕様について話をしないとならなくなるので、アセンブラでそれやりたくねえのでパスである、読みたいマゾは勝手に読んで。
結論
The Fuzzball ではハッタリでなく本当に現在時刻をミリ秒単位での精度で管理していたことがご理解いただけたであろうか、まぁ実用してたかはちょっと疑問があるけど。
これまで見てきた通りこのルーター OS は RT-11/LSI-11 べったりの実装で 90 年初頭にもなるともはや延命することすらままならない状態だった。
その結果 NTP Version 3 のリファレンス実装は移植性に優れるプログラミング言語 C で実装するために UNIX 上で動作するデーモン xntpd として開発が行われることになる。
だが過去回で解説したように UNIX の現在時刻取得および設定のシステムコールなんて RT-11 以下でミリ秒どころか秒数単位の精度しかないし、内部実装もせいぜい 1/60 秒の tick を数えてる程度である。
これでは xntpd は移植性は改善すれど機能的には The Fuzzball 以下の出来になってしまうのは自明である、その時 Mills 教授は動いた。
同時期に Rob Pike は「UNIXはただ死んだだけでなく、本当にひどい臭いを放ち始めている」と揶揄しているのだが、Mills 教授が Plan9 を選ばずに本当によかったですね。
次回
Mills 教授追悼編のラストを飾る xntpd 話なのであるが、もう飽きたし次回は永遠に無いかも。