UNIX における現在時刻情報管理の変遷[Third/Fourth Edition UNIX 編] - インターネット時刻同期の歴史(その9)
なぜ Second Edition(V2 UNIX) を飛ばしたのか
残念なことに V2 はカーネルのソース(二度漬け禁止)の現存が確認されていない。 逆に V1 はユーザーランドのソースが一部しか残っていない、前回動かした unix-jun72 というやつは 両者のツギハギチャンポンだったのは前回書いた通りである。
このバージョンでプログラミング厳語 C が登場しアセンブラや B からの移行が始まってるはずなのだけど V2 のカーネルがどこまで書換えられてたかは、ken(Ken Thompson) や dmr(Dennis Ritchie) らの記憶に頼るしかない。
dmr が残した V3 に関する メモ によると彼らは 1969 年から 1973 年の間に毎年のように UNIX Epoch すなわちシステム時刻の開始日を変更したようである。 前回解説した V2 UNIX 由来の date(1) コマンドに年を変更する方法が存在しなかった事からして、年が明けるたびに UNIX Epoch を一年ズラしていたであろうことは想像に難くない。
そして当然の事ながらこれは災害をもたらした、UNIX Epoch 一年ズラすと一年前にとった テープアーカイブ中のファイルタイムスタンプが今年のものに変わってしまうのである、これはおハーブ生えますわ。
それによってバックアップファイルの世代管理が破綻し、当時も新しいファイルが古いファイルに置き換わる事故を起こすわ 現在に至っても新旧判らんようになるわで、正確な日付はもはや同定する術無し実装されてる機能で新旧を判断する他無いそうである。
いい話だなあ、いやよくねえよ。
Third Edition UNIX(V3 UNIX) あるいは Fourth Edition UNIX(V4 UNIX)
前述の問題が影響したのかは知らないが、現存するコードは一部のみである。
また V3 と V4 の中間時点のスナップショットなので dmr は V3 としているけども Unix Heritage Society では V4 として公開しているのでややこしい、当記事では V3/V4 と称することにする。
見解の相違は V4 リリース前のテープだから V3 とする dmr と このスナップショット C で構造体が使えるンゴゴゴゴォと V4 の新機能が実装済であることを重視した Unix Heritage Society の違いでしかない。
こいつもソース修正の上 Fifth Edition(V5 UNIX) 上でビルドすることで PDP-11/45 上で動作するカーネルを作れるのだがさすがにめんどくせえのでパス。 挑戦したいジョン・レノン(ヒマジン)は modified_nsys.tar.gz を取得して中のドキュメントを読んでくだち。
カーネルソースコードの構成
一部を除いてほぼアセンブラは姿を消し C による実装に置換えられているので PDP-11 の知識が無くてもほぼ理解できると思う。
$ tree nsys
nsys
├── buf.h
├── conf.h
├── dmr
│ ├── bio.c
│ ├── cat.c
│ ├── dc.c
│ ├── dn.c
│ ├── dp.c
│ ├── draa.c
│ ├── gput.s
│ ├── kl.c
│ ├── malloc.c
│ ├── pc.c
│ ├── rf.c
│ ├── rk.c
│ ├── tc.c
│ ├── tdir
│ │ └── tiu.c
│ ├── tm.c
│ ├── tty.c
│ ├── vs.c
│ └── vt.c
├── file.h
├── filsys.h
├── inode.h
├── junk
│ ├── nps.s
│ ├── prot.s
│ ├── ustr
│ └── ustr.s
├── ken
│ ├── 11-45
│ ├── 45.s
│ ├── alloc.c
│ ├── clock.c
│ ├── conf.c
│ ├── fio.c
│ ├── iget.c
│ ├── incl
│ ├── low.s
│ ├── main.c
│ ├── mem.c
│ ├── nami.c
│ ├── prf.c
│ ├── prproc.c
│ ├── rc
│ ├── rdwri.c
│ ├── sig.c
│ ├── slp.c
│ ├── subr.c
│ ├── sys1.c
│ ├── sys2.c
│ ├── sys3.c
│ ├── sys4.c
│ ├── sysent.c
│ ├── text.c
│ └── trap.c
├── param.h
├── proc.h
├── reg.h
├── systm.h
├── tables.c
├── text.h
├── tty.h
├── u
└── user.h
4 directories, 62 files
移植先が PDP-11/20 から PDP-11/45 に変わって MMU(Memory Management Unit) が使えるはずだけど仮想記憶は無い。
アセンブラで書かれてた時代はファイル名から中身が想像できなかった。 しかし C で書かれるようになりファイル名から機能の推測がし易くなっている。 まぁまだ sys[連番].c なんてのが残ってるはいるけど。
ディレクトリ構成は担当者単位 +junk なのだが
- ken は OS のありとあらゆる抽象化機能を
- dmr はひたすらデバイスドライバを
それぞれ一手に引き受けてるせいで結果として機能毎に別れてるのが面白い。 s/ken/sys/;s/dmr/dev/ するだけでさらに現代の UNIX ソースツリーに近くなる。
ルートディレクトリには現代の UNIX 系 OS にもその名残を残す名前を持つヘッダファイル名がみえる。 これらは前回 V1 にあった ux.s が C に書き直されたものだ。
例えば inode.h に転生する前の ux.s の該当部分で書き直しの前後を比較してみようか。
15 inode:
16 i.flgs: .=.+2
17 i.nlks: .=.+1
18 i.uid: .=.+1
19 i.size: .=.+2
20 i.dskp: .=.+16.
21 i.ctim: .=.+4
22 i.mtim: .=.+4
23 . = inode+32.
2 #define NINODE 100
1 struct inode {
2 char i_flag;
3 char i_count;
4 int i_dev;
5 int i_number;
6 int i_mode;
7 char i_nlink;
8 char i_uid;
9 char i_gid;
10 char i_size0;
11 char *i_size1;
12 int i_addr[8];
13 } inode[NINODE];
はい、おなじ inode
と名付けられた記憶域を定義するのに前者だと i.flgs: .=.+2
つまり
inode:
ラベルの先頭.
から- 2 バイトを
i.flgs
に割り当てて .
の位置を+2
インクリメントする
なんてコードをひたすら書いてく苦行である。
しかし C では型を導入したおかげでオフセットはコンパイラ任せで自動計算なわけ。 さらに V4 UNIX から構造体も導入されて記憶域をグループ分けしてたラベルをヘッダファイル名だけでなく構造体名として変換できるようにもなったわけ、超便利!
ちなみにこの時代のコンパイラだと構造体のフィールドはラベル名に変換されるから他と重複できない。
なので i_*
ってプリフィクスをいちいち付けているけど現代においては不要な習慣である。
しかし
- 検索しやすい
- 初見で何の構造体かわかりやすい
- libc なんかの構造体がそうしてるから(これは後方互換のため)なんとなくそうするものかなって
などの理由で今でもつける人が後を絶たないのである。
他も ux.s にあったラベルは
- systm ラベルは systm.h へ
- inode ラベルは inode.h へ
- proc ラベルは proc.h へ
- tty ラベルは tty.h へ
- user ラベルは user.h へ
- 灰から灰へ
- ファンクはファンキーへ
- トム大佐はジャンキーだったって知ってた?
それぞれ転生している。
ヘッダファイルの元が ux.s のような記憶域(変数)の定義ファイルなんだからそりゃ過去回の Tentative Definition もそりゃ合法というかむしろ正しい使い方だったのである。
プログラムの開始地点を探す
もうファイル名みただけで ken/main.c に main()
があると確信できるこの安心感。
なのでそれを呼出す箇所を探すと
242 start:
...
297 / set up previous mode and call main
298
299 mov $30000,PS
300 jsr pc,_main
301 mov $170000,-(sp)
302 clr -(sp)
303 rti
start
では PDP-11/45 の MD(Machine Dependent) つまり機種依存な処理(主に初期化)がアセンブラで書かれている。
そして最後に main()
を呼出し MI(Machine Independent) つまり機種非依存の C の世界へ飛び込む。
ただどうもこの MD な初期化処理内では LKS(=RTC) 関係の処理はやってないもよう。
余談ではあるが main
ではなく _main
となる理由は存在しない過去記事「
sys/cdefs.hとは何ですか? (その9)
」で念入りに解説済なのだが、a.out の時代に C で書かれたコードは
ラベルがアセンブラと衝突しないように name mangling つまり
まんぐり返し
名前修飾を行うので常に _*
が自動でプリフィクスにつくのだ。
過去記事と王様の着る服がどうしても見えない賢者は、ふんわりした解説だけど Linkers & Loaders にもそのへんの事情が書かれてるのでそっち参照してくれ。
本題に戻る、main()
の方を読んだらそっちで LKS 触ってたわ。
23 #define LKS 0177546
15 struct {
16 int integ;
17 };
...
37 main()
38 {
...
70 LKS->integ = 0115;
...
95 }
LKS の I/O をキャストも無しにしかも無名構造体としてアクセスできてて笑う。 どんだけ当時の C はユルユルなんだか。
謎なのが 8 進で 0115
は 2 進で 0b0000000001001101
なので LKS だと未使用ビットにも1が立ってることである。
前回マニュアルから引用した図を再掲する。
[15] <- UNUSED
...
[ 7] <- LINE CLOCK MONITOR
[ 6] <- LINE CLOCK INTERRUPT EN(ABLE)
...
[ 0] <- UNUSED
まぁでもクロック割込はスタートするから別にいいか…細かいことはスルーしよう。
クロック割込ハンドラの登録はどこで?
ken/45.s にも ken/main.c にも無かった、ただクロック割込みハンドラ自体は
ken/clock.c の clock()
であると名前で一目瞭然なので grep(1) して探したら以下にあった。
1 / low core
2
3 fpp = 1
4 br4 = 200
5 br5 = 240
6 br6 = 300
7 br7 = 340
8 .globl start
9
10 . = 0^.
11 4
12 br 1f
13
...
34 . = 100^.
35 kwlp; br6
36 kwlp; br6
...
116 //////////////////////////////////////////////////////
117 / interface code to C
118
119 .globl _clock
120 kwlp:
121 jsr r0,call; _clock
grep(1) はいいよね、インターネット検索エンジンのようにいかかでしたか返してこないし。 しかしいずれは grep(1) をビッグテックがゴミしかヒットしない腐れ AI ツールに改変する日が来るのだろう。 そうこの Windows 10 の虫眼鏡アイコンのように(全部オフ)。
話戻して low.s って低レベルって事か、低レベルプログラミングにも二種類
- レイヤが低いプログラマ
- 技能が低レベルなプログラマ
私がいわれてたのは明らかに…後者…!!
やはり Nirvana/Smells Like Teen Spirit の歌詞
Hello, Hello. How Low? (やあやあ、どれくらい深みに嵌ってる?)
を口ずさみながらオピオイドより強力な究極の鎮静剤であるショットガンの鉛玉を服用すべきである。
low の文字から勘のいい人はお気づきになったかもしれないが 386BSDカーネルソースコードの秘密 で開幕早々にカーネル弱者男性の頭をピンポイントで破壊しにくる locore.s のご先祖様である。
いやマジであの本を最初の BSD の参考書に買ったことで絶対に sys の下は読まねえぞクソがになったので入門書はだいじ。 そりゃシリーズ第一巻のまま最終巻にもなりますわ。
んーと V1 UNIX だと 340
つまり br7
の優先順位世界最高ランク 7 だったクロック割込は br6
に下げられてるんだなこれ。
そんで kwlp
は C の _clock
呼ぶラッパー(パブリックエナミー)。
それにしてもますます何の略だか判らん kwlp
って、アリゾナの FM 局くらいしか出てこねえ。
クロック割込ハンドラを読む
はいはい clock()
の中身をいつものように追ってくよ。
5 int lbolt;
6 int time[2];
9 struct {
10 int integ;
11 };
12
13 clock(dev, sp, r4, r3, r2, r1, nps, r0, pc, ps)
14 {
...
20 /*
21 * restart clock
22 */
23
24 LKS->integ = 0115;
...
80 /*
81 * lightning bolt time-out
82 * and time of day
83 */
84
85 out:
...
89 if(++lbolt >= 60) {
...
92 lbolt =- 60;
93 if(++time[1] == 0)
94 ++time[0];
...
127 }
128 }
129 }
クロック割込ハンドラが担うお仕事も増えたのでかなり込み入ったコードになってるが 現在時刻管理に関しては相変わらずこんだけである。
V1 UNIX では現在時刻 systm: s.time
を 1/60 秒毎に加算してたけども、これを
lbolt
… 16bit で クロック割込の度、すなわち 1/60 毎に加算され 60 に達したらリセットtime[2]
… 32bit でlbolt
が 60 に達する、すなわち 1 秒毎に加算される
という方式に改めていることにお気づきだろうか。 そう現代の UNIX Epoch すなわち 1970-01-01 00:00:00 UTC からの経過秒数というのは V4 UNIX で誕生したわけだ。
returns the time since 00:00:00 GMT, Jan. 1, 1970, measured in seconds.
これで 828 日つまり 2 年と 3 ヶ月ではなく
- 31bit (符号つきだからね)
- 2,147,483,647 秒
- およそ 24,855 日
- 68 年とおよそ 1 ヶ月
- 2038-01-19 03:14:07 UTC (閏秒は廃止されました)
でターンオーバーすることとなった訳だ、ガハハその頃には UNIX なんて使ってないからどうでもええわ(2038 年問題)。
これでようやく冒頭で触れたベル研のテープアーカイブがバックアップとして破綻することなく正常に動くようになった、いい話だなぁ。 実際 Fifth Edition(V5 UNIX) 以降はちゃんとソースが残っているのである。
time(2) の実装は gtime() になった
22 gtime()
23 {
24
25 u.u_ar0[R0] = time[0];
26 u.u_ar0[R1] = time[1];
27 }
28
29 stime()
30 {
31
32 if(suser()) {
33 time[0] = u.u_ar0[R0];
34 time[1] = u.u_ar0[R1];
...
36 }
37 }
あれ?っと思うかもしれないけど libc からは time(2) なので安心してほしい。
これはシステムコール番号(配列インデックス)と関数ポインタをペアにしたテーブル管理による管理になったから。 N の settimeofday(2) の解説でちょろっと話したけど。
1 int sysent[]
2 {
...
16 0, >ime, /* 13 = time */
...
28 0, &stime, /* 25 = stime */
...
67 };
それと V1 でのシステムコールの sys*
プリフィクスも前述の名前修飾の関係か消えたようである。
話は脱線するけど、ここで過去のシステムコールの実装を振り返ってほしい。
まず PDP7-UNIX ではユーザーブロックの u.ac
と u.mq
というレジスタ退避用の記憶域を使ってた。
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
しかし V1 では sp(=r6) レジスタ、つまりスタックポインタ経由だったんだよね。
408 systime: / get time of year
409 mov s.time,4(sp)
410 mov s.time+2,2(sp) / put the present time on the stack
411 br sysret4
412
413 sysstime: / set time
414 tstb u.uid / is user the super user
415 bne error4 / no, error
416 mov 4(sp),s.time
417 mov 2(sp),s.time+2 / set the system time
418 br sysret4
ユーザーの sp
レジスタは user: u.sp
が退避領域のようである。
75 user:
76 u.sp: .=.+2
ところが V3/V4 ではまた PDP-7 UNIX のように u
を使うように戻っている。
つか記憶力に優れる読者(ここでゼロ除算が発生する)なら、 adjtime(2) の実装を読む[前編] で 4.3BSD でも同様だったのを覚えているだろう。
1 struct user {
...
36 int *u_ar0;
37 } u; /* u = 140000 */
まぁ本筋とは関係ないし、システムコール実装の変遷については将来(56億7千万年後)の解説記事に譲るとしよう。
時刻の保存できるようになりました!
main()
の中でもうひとつ現在時刻に関する処理をやっているのだ、実際には iinit()
内だけれども。
37 main()
38 {
...
73 iinit();
...
95 }
じゃあ iinit()
の実装読むか。
24 #define ROOTDEV (0)
25 #define NODEV (-1)
8 iinit()
9 {
10 int *cp, *bp;
11 int i;
12
13 bp = bread(ROOTDEV, 1);
14 cp = getblk(NODEV);
...
20 mount[0].m_bufp = cp;
21 mount[0].m_dev = ROOTDEV;
22 cp = cp->b_addr;
...
26 time[0] = cp->s_time[0];
27 time[1] = cp->s_time[1];
28 }
iinit()
内で dmr 担当のデバイスドライバ操作関数を呼んでる。
この記事はデバイスドライバ解説でもファイルシステム解説でもメモリ管理解説でもないので詳細は省く。
ただルートデバイスの先頭ブロックのタイムスタンプを現在時刻の初期化に使ってるくらいは読み取れたかと思う。 そもそもハードに変化は無いので RTC に保存できるようになったわけではないのだ。
この先頭ブロックのタイムスタンプは update()
ですべてのマウントポイントに対して更新が行われる。
155 update()
156 {
...
163 for(i=0; i<NMOUNT; i++)
164 if(mount[i].m_bufp != NULL) {
165 p = mount[i].m_bufp->b_addr;
...
170 p->s_time[0] = time[0];
171 p->s_time[1] = time[1];
...
176 }
...
189 }
この関数は sync(2) や umount(2) が呼ばれると内部で実行される。
1 int sysent[]
2 {
...
25 1, &sumount, /* 22 = umount */
...
39 0, &sync, /* 36 = sync */
...
67 };
69 sync()
70 {
71
72 update();
73 }
...
261 sumount()
262 {
...
267 update();
...
295 }
ダーティーハックではあるけど、再起動かけた後に時刻合わせしなくてもだいたい合ってる状態にはなった。 マシン落ちてる間はひたすら時刻ズレていくけどね。
まぁ無いよりはマシの精神である、いいですよね足らぬ足らぬは工夫が足らぬ。
結論
ニュージャージーでもう一人、仲間らしい奴が乗り込んできてその二人に声をかけた。
「お!kenさんとdmrさん!奇遇ですね!」
「おお!そういう君は****(聞き取れず。何か蟹ご飯ぽい名前)ではないか!エポック!」
「エポック!出た!エポック出た!得意技!エポック出た!エポック!これ!エポック出たよ~~!」
俺は限界だと思った(2038年問題)。
うーんネタもヘリテージ。
次回
ほぼこれで UNIX の現在時刻管理は完成し、Seventh Edition(V7 UNIX) まで変化ないんだよね。
ちゅーことで次回は V7 UNIX の話になるはず。