OpenBSD with qemu (2)
最近のTV番組はNHKに限らず、宣伝が多い。ってか、番組全体がコマーシャル。この間も、今コンビニの冷凍食品が熱いなんてタイトルで、各コンビニお勧めの冷凍品を紹介してた。
普段はコンビニなんて行かないから、事情に疎かったんだけど、何これ?高値安定のミニスーパーじゃん。
で、女房が目ざとく、 厚焼きスフレ パンケーキ なんてのをキャッチ。冷凍餃子とかピザは、スーパー品のお世話になるけど、このスフレはコンビニ限定?
早速、手に入れてこいと下命されましたよ。散歩のついでに偵察。1件目は冷凍品の棚が極小で無し。昔ながらの飲み物系が主体。2日目に行った所は、氷菓が充実してたけど、やはりお目当てな奴は無し。
女房に報告すると、有るのは東京だけかね。田舎には無いかもと諦めムード。3日目は、ちょいと遠出してみた。そしたら、置いてあったよ。売り切れ寸前で、残りが2個だった。買い占めてもよかったけど、試食と言う事で、1個だけ買った。この所の陽気で溶けるのが心配だったので、眼にも止まらぬ速さで帰還。
ご試食なさった女房曰く、驚く程旨いとは言えないと断。もう買わなくていいと購買中止宣言。鼻息荒く、これぐらいなら、自分で作れるわよとおっしゃる。
さて、どんな物を作るのやら、実験台になりますかな。
serial connection
前回は、ホストOSをOpenBSD(amd64)にして、ゲストOSをOpenBSD(i386)って環境を、qemuを使って構築した。色々やりたいんだけど、コンソール画面だとX Windowの為、切り貼りが出来ない。かと言って、ssh接続では、ちょっと心配な所が有るので、シリアル接続出来るようにしておく。
前回の起動スクリプトにちょっと、記述を加える。
qemu-system-i386 -m 128 -s -serial pty -net nic -net user,hostfwd=tcp::2022-:22 disk
何の事は無い、-serial pty を加えただけ。これで起動すると、
ob$ ./gdb-boot char device redirected to /dev/ttyp4 (label serial0)
こんな風に、ホスト側は、/dev/ttyp4 を使って見と言ってくる。
次はゲスト側の設定。/etc/ttys ファイルを編集。
tty00 "/usr/libexec/getty std.9600" vt100 on
元々は、offなってるので、それをonに変更する。ターミナルタイプは古風にvt100にした。ここがunknownだと支障をきたすので何かしら設定しておこう。そして、直ぐに使いたいなら、
doas kill -HUP 1
するだけ。
vm$ ps awx PID TT STAT TIME COMMAND : 14120 00 I+pU 0:00.26 /usr/libexec/getty std.9600 tty00
こんな風に待ち構えの体勢になってればOK。ホスト側から接続してみる。
ob$ cu -l /dev/ttyp4 Connected to /dev/ttyp4 (speed 9600) ;; 一度RETキーを叩くとバナーが出て来る OpenBSD/i386 (vm.my.domain) (tty00) login: sakae Password: Last login: Fri Feb 14 05:59:20 on ttyp0 from 10.0.2.2 OpenBSD 6.6 (DEB) #0: Sun Feb 9 14:02:01 JST 2020 Welcome to OpenBSD: The proactively secure Unix-like operating system. vm$ exit OpenBSD/i386 (vm.my.domain) (tty00) login: ;; ~. 入力でcuが終了する。 [EOT]
disk
ふと思い立ったのでdiskのパラメータを確認。最初はカーネルが知ってる構造。
vm$ disklabel wd0 # /dev/rwd0c: type: ESDI disk: ESDI/IDE disk label: QEMU HARDDISK duid: c28637de21593793 flags: bytes/sector: 512 sectors/track: 63 tracks/cylinder: 32 sectors/cylinder: 2016 cylinders: 1040 total sectors: 2097152 boundstart: 64 boundend: 2096640 drivedata: 0 16 partitions: # size offset fstype [fsize bsize cpg] a: 2061440 64 4.2BSD 2048 16384 12958 # / b: 35136 2061504 swap # none c: 2097152 0 unused
こちらは、ローダーが知ってる構造って事かな。
vm$ fdisk wd0 Disk: wd0 geometry: 520/64/63 [2097152 Sectors] Offset: 0 Signature: 0xAA55 Starting Ending LBA Info: #: id C H S - C H S [ start: size ] ------------------------------------------------------------------------------- 0: 00 0 0 0 - 0 0 0 [ 0: 0 ] unused 1: 00 0 0 0 - 0 0 0 [ 0: 0 ] unused 2: 00 0 0 0 - 0 0 0 [ 0: 0 ] unused *3: A6 0 1 2 - 519 63 63 [ 64: 2096576 ] OpenBSD
MBRが管理する4区画。インストール時にdiskを全部使うって指定すると、最後の区画が使われるのね。
timecounter
前回、時間の元に何が使われてるか調べたけど、うっかりしててホスト側を見ていたんで、改めて、ゲスト側のやつを調べてみる。
vm$ sysctl -a | grep choice kern.timecounter.choice=i8254(0) acpihpet0(1000) acpitimer0(1000) vm$ sysctl -a | grep hardware kern.timecounter.hardware=acpihpet0 vm$ dmesg | grep acpihpet0 acpihpet0 at acpi0: 100000000 Hz vm$ dmesg | grep acpitimer acpitimer0 at acpi0: 3579545 Hz, 24 bits
100MHzの高精度カウンターが使われているんだね。
ソースは何処? 前回みたいにi368の下で検索しても見つからず。ひょっとしてデバイスエリア? ビンゴでした。acpi/acpihpet.c
ACPI(4) Device Drivers Manual ACPI(4) NAME acpi - Advanced Configuration and Power Interface SYNOPSIS acpi0 at bios? DESCRIPTION The acpi driver provides basic support for ACPI including loading ACPI tables from the firmware, parsing and interpreting AML code, event handling, suspending and powering off, and attaching device drivers. Userland may access acpi by using the apm(4) device. The following devices can attach to acpi: acpiac(4) ACPI AC adapter acpials(4) ACPI ambient light sensor acpiasus(4) ASUS ACPI hotkeys acpibat(4) ACPI control method battery acpibtn(4) ACPI button acpicbkbd(4) Chromebook keyboard backlight acpicpu(4) ACPI processor power and performance state acpidock(4) ACPI docking station acpiec(4) ACPI embedded controller acpihpet(4) ACPI high precision event timer :
仕様の策定はウィンテルです。昔お世話になったapmの拡張版。石はサウスブリッジに配置されてるとか。
このドライバーで、データを読みだしている所にBPを置いてみた。
(gdb) bt #0 acpihpet_gettime (tc=0xd1103ce4 <hpet_timecounter>) at /usr/src/sys/dev/acpi/acpihpet.c:277 #1 0xd08e6618 in tc_delta (th=0xd1105db4 <th1>) at /usr/src/sys/kern/kern_tc.c:135 #2 0xd08e70a7 in tc_windup (new_boottime=0x0, new_offset=0x0, new_adjtimedelta=0x0) at /usr/src/sys/kern/kern_tc.c:493 #3 0xd08e7c64 in tc_ticktock () at /usr/src/sys/kern/kern_tc.c:673 #4 0xd0525027 in hardclock (frame=0xf1cba718) at /usr/src/sys/kern/kern_clock.c:184 #5 0xd0c66a4e in lapic_clockintr (arg=0xf1cba718) at /usr/src/sys/arch/i386/i386/lapic.c:259 #6 0xd09b241a in Xltimer_untramp () #7 0xf1cba718 in ?? () #8 0xd09b220d in Xsoftclock () #9 0x00000000 in ?? ()
このルーチンへ入ってきた時の引数。
(gdb) p *tc $2 = { 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 = 0 }
3個有ると言うカウンターの最初のやつを時計の元として使っているんだな。
inittodr
と、ここまではいいんだけど、OSがbootした時に、初期時刻をどうやって設定してるのだろう? いわゆるboottimeね。このままでは埒があかないので、man(9)を漁ってみる。 チャプター9は、kernel内部の有名な関数を解説したものだ。リナでは、ここに全く説明が無い。カーネルなんて見るものじゃありませんと、最初から拒否してますよ。
o32$ cd /usr/share/man/man9 o32$ ls | wc -l 167 o32$ ls *time* bintimeadd.9 microtime.9 rt_timer_add.9 time_second.9 timeout.9
man9のエリアに移動して、文書の数を確認。沢山有り過ぎて迷ってしまうので、キーワードで絞り込み。おお、知ってる関数 microtime が出てきたぞ。手始めに、それを見る。
o32$ man microtime : SEE ALSO settimeofday(2), hardclock(9), hz(9), inittodr(9), time_second(9)
関連の関数が列挙されたので、ざっと目を通す。
INITTODR(9) Kernel Developer's Manual INITTODR(9) NAME inittodr - initialize system time DESCRIPTION The inittodr() function determines the time and sets the system clock. It tries to pick the correct time using a set of heuristics that examine the system's battery-backed clock and the time reported by the file system, as given in base. Those heuristics include: o If the battery-backed clock has a valid time, it is used. o If the battery-backed clock does not have a valid time, and the time provided in base is within reason, base is used as the current time. o If the battery-backed clock appears invalid, and base appears nonsensical or was not provided (was given as zero), an arbitrary base (typically some time in the late 1970s) will be used. Once a system time has been determined, it is passed to the tc_setclock() function.
面白い物を発見である。timeを初期化ですって。初期化は、OS起動時に一度だけ実行されるんだよな。と言う事は、今まで避けていた、OS起動を追跡しろって事になる。
今までのqemuをキックするスクリプトに、-S(大文字)を追加して、電源ON時にgdbに制御を渡すようにする。そして、起動すると待ちに入るので、gdbを起動。
Reading symbols from bsd.gdb...done. 0x0000fff0 in ?? () (gdb) b inittodr Breakpoint 1 at 0xd05ef360: file /usr/src/sys/arch/i386/isa/clock.c, line 584. (gdb) c Continuing. Breakpoint 1, inittodr (base=-3379844726758732800) at /usr/src/sys/arch/i386/isa/clock.c:584 584 { (gdb) bt #0 inittodr (base=-3379844726758732800) at /usr/src/sys/arch/i386/isa/clock.c:584 #1 0xd0747f61 in ffs_mountroot () at /usr/src/sys/ufs/ffs/ffs_vfsops.c:193 #2 0xd07a2c1e in dk_mountroot () at /usr/src/sys/kern/subr_disk.c:1349 #3 0xd038ce81 in main (framep=0x0) at /usr/src/sys/kern/init_main.c:492
お目当てのinittodrにBPを置いて、継続。コンソールの方では、起動プロセスが実行されて行き、やっとこさBPに到達したよ。呼び出された所は、なんとファイルシステムのマウントルーチン内からだ。思ってもみない場所だな。
いや、終了時に正しくunmountされたかどうかとかの確認に、現在時刻が必要なんだろうね。そAれで、やむなく正しそうな時刻を設定したとな(多分ですが)。RTCから呼び出した時刻データ。
(gdb) p rtclk $1 = {88, 0, 1, 0, 34, 0, 7, 21, 2, 32}
このルーチンの中で、MC146818ってRTCな石から時刻を取り出している。mc146818reg.hに石の諸元が解説されてた。
* Definitions for the Motorola MC146818A Real Time Clock. * They also apply for the (compatible) Dallas Semiconductor DS1287A RTC.
モトローラは知ってるけど、ダラスセミコンダクターって聞いた事ないぞ。ひょっとしてダラスから昇格してテキサスを名乗るあの会社の事(子会社って線もあるな)。 Dallas Semiconductorによると、TIとは全く関係無いっぽい。マキシマに買収されちゃったみたいだね。
dt.dt_sec = hexdectodec(rtclk[MC_SEC]); dt.dt_min = hexdectodec(rtclk[MC_MIN]); dt.dt_hour = hexdectodec(rtclk[MC_HOUR]); dt.dt_day = hexdectodec(rtclk[MC_DOM]); dt.dt_mon = hexdectodec(rtclk[MC_MONTH]); dt.dt_year = clock_expandyear(hexdectodec(rtclk[MC_YEAR])); ts.tv_sec = clock_ymdhms_to_secs(&dt) - utc_offset; if (base < ts.tv_sec - 5*SECYR) printf("WARNING: file system time much less than clock time\n");
チップから読みだしたデータをclock_ymdhms_to_secs(struct clock_ymdhms *dt)を使って、epoc timeに変換してる。
days = 0; for (i = POSIX_BASE_YEAR; i < year; i++) days += days_in_year(i); if (leapyear(year) && dt->dt_mon > FEBRUARY) days++; /* Months */ for (i = 1; i < dt->dt_mon; i++) days += days_in_month(i); days += (dt->dt_day - 1); /* Add hours, minutes, seconds. */ secs = (time_t)((days * 24 + dt->dt_hour) * 60 + dt->dt_min) * 60 + dt->dt_sec; return (secs);
うるう年は考慮してるけど、うるう秒には無頓着ってのが仕様なんだな。この関数は、clock_subr.cに置いてあった。
それから、もう一つ興味深いのは、RTCのyearから、正確な年を割り出すルーチン。RTCなチップが保持する年は、西暦の下2桁のみだ。これをどうにかして、世紀に置き換えたい。 clock_expandyear(int clockyear)では、涙ぐましい処理が行われている。
clockcentury = (clockyear < 70) ? 20 : 19; clockyear += 100 * clockcentury;
チップから読みだしたデータが70未満なら、20xx年、それ以上なら19xx年と言う扱い。 この流れを汲んで、dateコマンドでエィヤァと日時をセットする時、下記の解釈が行われる。
ccyy Year. If yy is specified, but cc is not, a value for yy between 69 and 99 results in a cc value of 19. Otherwise, a cc value of 20 is used.
このルーチンの中で、CMOSのレイアウトチェックが行われている。標準品かそれ以外かだ。 これに引っかかると、
#ifdef DIAGNOSTIC printf("clock: unknown CMOS layout\n"); #endif
こんなメッセージをコンソールに表示する。qemuがサポートするやつは、チェックに引っかかっていた。
それから、gdbで追っていて、長い事放置してると、時刻がずれてくる。当然と言えば当然なんだけど、rebootするとこのずれが解消される。ntpdを動かしているわけでは無いので、時刻の元は、このチップ頼み。と言う事は、正しい時刻をqemuがエミュレートするチップから受け取っているって事になるな。
今さら、qemuのソースを紐解く元気は無いので、何方か検証宜しく!!
今回もgdbで流れをざっと追って、後でそれを頼りにソース内を飛び回る事をやってる。 そこで、TAGファイルが欲しくなったよ。
o32$ cd /sys o32$ find . -name '*.[ch]' | xargs etags
サイズが、80Mになったけど、気にしない。尚、archの下には、amd64とかsolarisとかのマシン独立なやつが置いてある。同名な関数が定義されてて参照する時に邪魔になるので、i386以外は、バッサリ削除しておくと良い。
ntpd
OpenBSDで時刻を合わせる方法は、唯一ntpdだけと思われる。他のOSだとntpdateなんてのが普通に有るんだけどね。そこで、ntpdを試してみた。起動時に -d を付けるとフォーグラウンドにずっと居座って、記録を表示してくれるんで、後でログを眺める必要無し。
ob$ doas ntpd -d /var/db/ntpd.drift is empty ntp engine ready constraint reply from 172.217.161.36: offset 157.468158 peer 162.159.200.123 now valid peer 162.159.200.1 now valid peer 129.250.35.250 now valid peer 162.159.200.123 now valid peer 129.250.35.251 now valid adjusting local clock by 186.814016s adjusting local clock by 186.315737s adjusting local clock by 185.985736s : adjusting local clock by 170.953536s adjusting local clock by 170.626519s adjusting local clock by 169.503546s
約1時間のランニング結果。ゆっくりと時間調整値が動いている事が確認出来る。動作が安定した時点で、/var/db/ntpd.driftに変動の癖が記憶され、監視間隔の決定とかに使われるとな。
ob$ ntpctl -s all 5/5 peers valid, constraint offset 157s, clock unsynced, clock offset is 167011.028ms peer wt tl st next poll offset delay jitter 162.159.200.123 time.cloudflare.com 1 10 3 25s 30s 0.073ms 27.964ms 2.479ms 162.159.200.1 from pool pool.ntp.org 1 10 3 18s 33s 1.362ms 29.330ms 10.831ms 129.250.35.250 from pool pool.ntp.org 1 10 2 23s 33s 2.910ms 26.681ms 2.693ms 162.159.200.123 from pool pool.ntp.org 1 10 3 31s 34s -0.549ms 27.126ms 3.460ms 129.250.35.251 from pool pool.ntp.org 1 10 2 5s 34s 3.998ms 25.934ms 2.148ms
そして、こちらは、5台の時間サーバーの状況。ジッター(バラツキ)が少なく、stが小さいやつが安心。stは、n次標準の値だ。超高精度の1次標準から時刻を頂いてきて2次標準、そこから頂いてきて3次標準と言う具合になってて、サーバーが混むのを緩和してる。福岡大学だかのサーバーが全世界から参照されて、大問題になってたのは記憶に新しい。
こんな風に緩やかに時刻を合わせていくのは、時間飛びによるファイルの不整合を防ぐ目的が有るからだ。
でも、時と場合によっては、速やかに時刻合わせをしたい事がある。そうntpdateを使えば出来るよ。でもOpenBSDには、そんなの無い。いや、ちゃんと用意されてるよ。以下はその実験。
ob$ date Mon Feb 17 06:21:26 JST 2020 ob$ doas date 0300 doas (sakae@ob.my.domain) password: Mon Feb 17 03:00:00 JST 2020 ob$ doas ntpd -s doas (sakae@ob.my.domain) password: /var/db/ntpd.drift is empty ob$ date Mon Feb 17 06:25:25 JST 2020
最初に時刻を確認、次に時刻を朝の3時に設定。次にntpdate相当の ntpd -s を実行。これで、時刻合わせが強制的に実施されてから、ntpdが裏側に回って、調整を続ける。再度時刻を確認。正しい時刻になった。
ob$ doas ntpd -d : clock is now synced constraint reply from 172.217.175.68: offset -0.535891 adjusting clock frequency by 0.253648 to 0.253648ppm peer 129.128.12.20 now invalid peer 51.159.34.173 now valid adjusting clock frequency by -4.889372 to -4.635724ppm
時刻が合った所で、再びモニター。今度は容易に同期した。
ob$ cat /var/db/ntpd.drift -4.636
ドリフトファイルにも、値が書き込まれていた。
ob$ ntpctl -s all peer wt tl st next poll offset delay jitter : 51.159.34.173 from pool pool.ntp.org * 1 10 2 28s 31s 1.791ms 247.719ms 2.648ms
ステータスを見ると、主に使われるサーバーに★印がついた。ジッターが一番小さいものが選ばれている風だ。