最初に追加資料

今回紹介するコードには 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 は 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

.IFFif 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 ;

CLKTRPTIMTRP の中身については後回し。

うんこれなら理論上は 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

HDWCLK3 つまり 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 を読む

さっきすっ飛ばしたクロック割込みの実装を読む、条件分岐がややこしいので必要なとこだけ抜き出す。

まずは HDWCLK0(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
...

クロック割込みは TIMTRPCLOCK を前述の #INCRM1 分加算している。 INCRM2 が定義されてたら CLOCK の後ろの無名記憶域も加算しているなこれ。

ということであれはナノ秒まで拡張するための予約領域ではなく、tick を加算するものだったちゅーこと。どうりで INCRM20 のケースがあるわけだ。

次に HWDCLK2(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 であとは同じ。

最後 HWDCLK3(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
...

クロック割込みは CLKTRPCLOCK の更新はさっきの 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 話なのであるが、もう飽きたし次回は永遠に無いかも。