UNIX における現在時刻情報管理の変遷[PDP-7 UNIX(Unics) 編] - インターネット時刻同期の歴史(その7)
PDP7-UNIX(Unics) とは何か
現在入手できる最も古い UNIX のソースコードが PDP7-UNIX あるいは UNIXEditionZero と呼ばれるもの。 つまり Ken Thompson が Multics を揶揄して Unics と命名したアレなのだがそう呼ばないのは何か大人の事情でもあるんですかね(邪推)。
不鮮明なプリントしか現存してなかったようで The Unix Heritage Society で PDF形式 でしか見れなかったのだがいつの間にか OCR でソースコード復元し詳細なコメントをつけたバージョンが github/DoctorWkt/pdp7-unix から入手可能な上に実機で動くそうだ、いい時代だなぁ。
V1 から V7 までの UNIX(TM) はほぼ BSDL の Caldera Lisence だし V0 についても Caldera が権利を有してると思うのだが、リポジトリは GPLv3 である。 これは tools 以下にある perl で書かれた
- a7out … PDP-7 ユーザーモードシミュレータ 直接 PDP-7 実行ファイルが動かせる
- as7 … PDP-7 アセンブラ
- ccov7 … PDP-7 カバレッジ
- fsck7 … PDP-7 UNIX ファイルシステム整合性チェック
- mkfs7 … PDP-7 UNIX ファイルシステム作成
などのツールが GPLv3 だからと思われ、いやむしろそっち読む方が楽しそうだなオイ!?
まぁこっちのコード読むのはまた機会があればにして、この最初期の UNIX において時刻がどうやって管理されていたかを読んでいこうと思う。
まずは資料を用意する
PDP-7 に関するドキュメントは ここ に大量に転がってるので安心、いい時代だなぁ。
今回はこの 2 つをパラ見すればなんとかなるんじゃねえかなぁ(無謀)!
カーネルソースコードの構成
チェケラしたリポジトリの src/sys 以下には
$ ls src/sys/
cas.in NOTES.md s2.s s4.s s6.s s8.s sop.s trysys.s
maksys.s s1.s s3.s s5.s s7.s s9.s sysmap
というあまりに素っ気ないファイルが転がってるが、ざっと何やってるかの紹介。
sop.s
PDP-7 の命令アドレスとニーモニックを対応づけるファイル、 詳しくは前述の PDP-7 SYMBOLIC ASSEMBLER PROGRAMMING MANUAL を見るといい。
1 "** 01-s1.pdf page 62
2 " sop
3
4 dac = 0040000 " MEM: deposit AC
5 jms = 0100000 " MEM: jump to subroutine
sysmap
ラベルとメモリアドレスを対応づけるファイル。
1 "** 01-s1.pdf page 57 -- system assembly map
2 . 004671 r
3 .ac 004102 r
4 .chown 000426 r
5 .close 000725 r
ざっと中身をみるとラベルの命名規則は
.*
… システムコールs.*
… システムブロックu.*
… ユーザーブロックi.*
… インフォメーションブロックd.*
… ディレクトリブロック
となってるっぽい、カーネル弱者男性でもそれくらい判るよバカヤロウ。
i.*
だけピンと来ないかもしれんが i-node の i といえばはたと膝を鬱に違いない、鬱になってどうするいや冬はどうしてもね。
maksys.s
コンパイルしたカーネルをディスクに書込むツールのようである。 すでにマジックに a.out の文字が見える。
46 a.out:
47 <a.>;<ou>;<t 040;040040
ただし存在しない過去記事 a.out フォーマットを読み解く(その2) で解説したオブジェクトフォーマット a.out 形式とは違う事には注意。
trysys.s
コメント皆無なので困るのだが、どうやら maksys.s で書込んだカーネルをディスクから起動するためのブートローダーっぽい。 実機で PDP-7 UNIX を動かす動画の 1:10 あたりに登場するブートローダーのコードとはだいぶ異なるんだけど
10 iof // PIC: interrupts off
11 caf // CPU: clear all flags
...
26 jmp 0100 // jump kernel entry point
あたりのコードが一致してるのでそう判断した。
cas.in
Makefile を追いかけると src/cmd/cas.s で character table を生成するためのソースのようだが詳細不明。
s[連番].s
そんで今回の攻略対象がこのやる気ねえ命名のファイル群である。 こいつらこそがカーネルソースコードなのだ。
現代なら機能毎にファイル分割するのだが、当時はそれこそテープやパンチカードの時代なので、記憶媒体的にはこの方が適しているのだと思われる、適当書いてるけど。
プログラムの開始地点を探す
さっきの sysmap でゼロ番地を探してみた。
211 orig 000000 r
orig
というラベルが見つかったので、ここからプログラムが開始されるんだろうなとこれまた根拠の無いアタリをつける、連番ファイルの最初だし多分あってると思う(適当)。
3 .. = 0
4 t = 0
5 orig:
6 hlt " overwritten with interrupt return addr
7 jmp pibreak " dispatch to interrupt processing
8
9 . = orig+7 " real time (60Hz) clock
10 -1 " -1 will cause "clock overflow" on next tick
11 " Overflow is checked by "clsf" instr
12 " in PI service (pibreak) routine,
13 " reset to -1 after tick handling
14 " results in an interrupt every "jiffy"
いきなり ..
って何だよ!で挫折しそうだが大丈夫か?
マニュアル読んでもどこにも記述なくて頭抱えたのだが、前出の as7 のコメントに書かれていた。
61 # http://minnie.tuhs.org/cgi-bin/utree.pl?file=V3/man/manx/as.1
62 # ".." is the relocation constant and is added to each relocatable
63 # reference. On a PDP-11 with relocation hardware, its value is 0; on
64 # most systems without protection, its value is 40000(8).
65
66 # PLB: "relocatable" values are flagged with $RELATIVE
67
68 # start with the location counter at zero
69 # predefine syscall and opcodes as variables
70 %Var = (
71 '.' => $BASE,
72 '..' => 4096, # output base addr?
なるへどうやら DEC 謹製の assembler ではなく PDP-7 UNIX の as(1) の方言のようである、そっちかよ! 要は GOT(Global Offset Table) みたいなもんちゅうことでええんかな。
しかし .
の方に関しては sysmap にも
2 . 004671 r
と定義されとるしもう何が何だかである。
さらにorig+7
って何だよこっちも!
おまけに -1
も意味わかんねーよ!
リファレンスマニュアル流し読みしたら、おぼろげながら見えてきたんです 7
と 6
という数字が(構文)。
以下は CHAPTER 4 INPUT/OUTPUT CONTROL AND INTERFACE の
- Figure 4-2 Bit Assignment for Input-Output Transfer Instruction (iot)
- Figure 4-5 Input-Output Status Instruction - Bit Assignment
を悪魔合体させたもの。
Mnemonic Instruction Code Operation
iot 700000 input/output transfer
iors 700314 Input/Output read status, The contents of
given flags replace the contents of the as-
sined AC bit.
--- Operation Code ---
X[ 0] <- Program Interrupt On
X[ 1] <- Tape Reader Flag
X[ 2] <- Tape Punch Flag
--- Sub-Device Selection ---
X[ 3] <- Keyboard Input Flag
X[ 4] <- Type-Out Flag
X[ 5] <- Display Flag
--- Device Selection ---
X[ 6] <- Clock Overflow Flag
[ 7] <- Clock Enabled
X[ 8] <- Magnetic Tape Interrupt
X[ 9] <- Assignable
[10] <- Assignable
[11] <- Assignable
--- Sub-Device Selection ---
[12] <- Assignable
X[13] <- Assignable
---
[14] <- If Bit Is a 1: Clear AC at event time 1, Assignable
X[15] <- If Bit Is a 1: Transfer an IOT pulse at event time 3, Assignable
X[16] <- If Bit Is a 1: Transfer an IOT pulse at event time 2, Assignable
[17] <- If Bit Is a 1: Transfer an IOT pulse at event time 1
X - Program Interrupt Connected
今の orig
は 0
なので orig+7
は 0+7
だと思います だからこそ 7
だと思います(構文)。
せや! Clock Enable で 7
から -1
すれば 6
すなわち Clock Overflow Flag なんや(ガンギマリ)!
いわゆるひとつの MMIO(memory-mapped I/O) ってやつで RTC(Real-Time Clock) にアクセスしてるんかこれ、いやコメントなければ一生理解できなかっただろうが。
つまり RTC で周期的にクロック割込発生させてるコードがここなんじゃねえかな、Clock Enable 叩くと一定時間経過後に Clock Overflow Flag の割り込みがかかるって寸法よ。
RTC についてはリファレンスマニュアル 4-16 REAL TIME CLOCK に記述があるけども、1/60 秒毎に割込を発生させるもよう、つまり 60Hz ちゅうこと。
そんで pibreak
というラベルが実質クロック割込ハンドラなのでは?という推理、ロードして 10 行でこれって出会って 4 秒で合体より展開はええな!?
pibreak ラベルを読む
pi とは priority interrupt 優先割込すなわち esr 版 ジャーゴンファイル で セックス>(超えられない壁)プログラミング って揶揄されてるアレ、なお現代日本の弱男ナードすなわちデボチカとのインアウトなんて㍉も可能性無かった孤独老人の最優先度割込は SNS で三次元女性叩きである、地獄なんやな。
3 .ac 004102 r
4 pibreak: " priority interrupt break processing "chain"
5 dac .ac " save interrupt AC
6 "** CROSSED OUT....
計算を止めてより優先度の高いタスクに譲るため、AC レジスタの値を .ac ラベルの記憶領域に退避してる、でいいんかなこれ。
リファレンスマニュアル読むと
- AC … ACcumulator 積算レジスタ
と演算レジスタの一種とあるけど本当にこれだけ退避すれば足りるんすかね、スタックポインタ的なものなのなんですかね(たぶんまだそんなものは存在しない)。
時刻関連のコードを探す
ええい今回の目的はカーネル完全に理解したではないので(興味ねえし)、近道探す。
sysmap を読むと time(2) システムコールなるものを発見した。
前述のとおりシステムコールは .*
で開始するラベルなので .time
がそれである。
37 .time 000615 r
さっそく実装を読む。
185 " time system call returns line (mains) frequency ticks
186 " high order bits returned in AC, low order in MQ
187 " s.tim is located in "system" block (written to disk)
188 " so this is a running count of uptime since first boot!
189 " at 60Hz, 36 bits would last 36+ years!
190 .time:
191 lac s.tim " load high order bits
192 dac u.ac " return in AC
193 lac s.tim+1 " load low order bits
194 dac u.mq " return in MQ
195 jmp sysexit
7 lac = 0200000 " MEM: load AC
やってることは
s.tim
を AC レジスタに読み込んでu.ac
に書き込むs.tim+1
を AC レジスタに読み込んでu.mq
に書き込む
こんだけ。
PDP-7 は今ではあまり一般的ではない 18bit ワードマシンで s.time
には記憶域を 2 ワードつまり 36bit 確保している。
なのでシステムコールの戻り値は u.ac
に s.time
つまり 上位 18bit と u.mq
に s.time+1
で下位 18bit をコピーしてるわけ。
そもそも u.ac
と u.mq
についてはそれぞれ AC および MQ レジスタを退避するための記憶域のようである。
んで MQ レジスタについてはリファレンスマニュアルに
- MQ … Multiplier-Quotien 乗数・商レジスタ
とあるけど、こいつ読み書きするには
45 lacq = 0641002 " EAE: load AC with MQ
...
49 lmq = 0652000 " EAE: load MQ from AC
という命令を使って一度 AC レジスタを経由する必要があるみたい。
ただ上記のコードにおいて MQ レジスタはまったく無関係なので、単にシステムコールの戻り値にも流用されてるって感じ。
再び pibreak ラベルを読む
さて s.tim
が現在時刻情報ということが time(2) から判明した。
改めて pibreak
を追っていくと
4 pibreak: " priority interrupt break processing "chain"
...
28 1: clsf " clock overflow (60hz tick increment of loc 7 (-1))
29 jmp 1f " no
...
33 isz s.tim+1 " increment low order tick count
34 skp " no overflow, skip high order word increment
35 isz s.tim " low order overflowed, increment high order count
36 " ("never" skips: 36 bits overflows every 36 years!)
12 isz = 0440000 " MEM: increment and skip if zero
...
63 clsf = 0700001 " CLK: skip if overflow
clsf
は orig
で開始した RTC がオーバーフローしたかのチェックである。
オーバーフローしていたら isz
で s.tim+1
つまり下位 18bit を加算、こちらもオーバーフローなら s.tim
つまり上位 18bit を加算する。
ハイ、ここっすね時計の針を進めているのは。
s.time は 秒単位ではない
ここまでの出てきたコードのコメントにネタばらし書いてあったし、すでにお気づきの人もいるだろう(誰も読んでいない)。
この頃の UNIX の time(2) が返す値は 今の POSIX time(3) のような 1970 年 1 月 1 日 00:00:00 UTC いわゆる Epoch からの「秒数」で現在時刻を表したものじゃないんだよね。
そう s.time
は 1/60 秒毎にオーバーフローする RTC の割込で +1 すると思います、だからこそ s.time
は 1/60 秒毎にカウントアップすると思います(構文)。
強いて N で例えるならば tick(9) の間隔で刻まれる time_uptime(9) みたいなもんである。
そのためコメントにもある通り
- PDP-7 なら 132560 日つまり 36 年と 2 ヶ月
- PDP-11 のような 16bit アーキテクチャなら 32bit で 828 日つまり 2年と3ヶ月
で値がターンオーバーするのである。
そもそもコードのどこ探しても RTC から現在時刻を読込んでるところが存在しないっぽい。 よって PDP7-UNIX で現在時刻を知る方法は無いということ。
それならばこの時代 date(1) が存在しないのも当然である。 さっきの動画で date(1) 叩いてるからあるもんだと思ってたら、 src/other/wktdate.s は新規に書いたコードなのである、そしてマニュアルの BUGS に
NAME date ‐ print the date
SYNOPSIS date
DESCRIPTION The current date is printed to the second.
FILES ‐‐
SEE ALSO ‐‐
DIAGNOSTICS ‐‐
BUGS It is always 1970.
OWNER wkt
と書いてあり、起動したら 1970-01-01 00:00:00 UTC から開始となってる無理矢理な実装なわけですな。
そういえばこの件については N の time(3) にもちゃんと書いてあるのに最近気づいたゾ。
HISTORY
A time() function appeared in Version 2 AT&T UNIX. It returned a 32-bit
value measuring sixtieths of a second, leading to rollover every 2.26
years. In Version 6 AT&T UNIX, the precision of time() was changed to
seconds, allowing 135.6 years between rollovers.
V2 UNIX より以前からセクション 3 つまりライブラリ関数 と 2 すなわちシステムコールの違いはあれど、存在はしたので内容は不正確ですなこれ。 ただまぁ 1/60 秒単位でカウントすることと頻繁にロールオーバーが発生することについては正しい記述である。
ところで覚えてる人間は世界で 0 人の、存在しない過去記事 なぜ localtime(3) には、ポインタを渡すのか? を思い出してほしい、この記事内で time(2) が現代的な UNIX と同様に秒単位の値を返すと勘違いした記述があった事に謝罪したい(結論自体は変わらないのだが)。
えーと HARAKIRI でいいですか、痛いのヤダからスゥと意識が飛ぶ首吊りで勘弁願いたいところである、というか究極の鎮痛剤であるショットガンいいっすか。
結論
PDP7-UNIX ではそもそも現在時刻なんて管理してねーよという大変に心温まるお話である。
つーかそもそも PDP-7 の RTC ってカレンダークロック機能つまりバッテリー保護された現在時刻なんて持ってないんだなこれ。 マニュアル読んでも該当しそうな命令が存在しないし。
次回
もう完全に飽きてるのだけど、次はプログラミング幻語 C で書き直される V4 UNIX あたりの解説になると思われるが、冗長にも続けて V1 UNIX の解説がはじまり結局は結論おんなじじゃねえか!になる可能性も無きしもあらず。
NTP の話に繋げるのに前振り長過ぎるなぁと自分でも思うのだが、これくらいやらないと David L. Mills 教授から見た UNIX の現在時刻管理ってピンと来ないと思うんだよね。