さて、今回は第12章と第13章がテーマの予定だったのですが・・・内容が重かったため、第12章だけをみっちり半日議論、って感じになりました。第12章だけと言っても、trap内でハードウェア例外からsignalを設定、システムコール呼び出しの場合はエントリテーブルを引いて各システムコールの実装の呼び出し、といった重たい内容からスタート。続けてシステムコールの実装の中から、execとfork、sbreak。UNIXの魂とも言えるAPI群を読み解くことになるので、かなり難解。章の最後にはさらっと「ここは自分で読んどいてね(はーと)」ってノリで12のシスターならぬシステムコールが。。。とりあえず、自力で読んだ時には気づかなかっけど、読書会でみんなで議論したら気づいた、理解できた、って事を重点的に書き出そうと思います。
trap
SETD(擬似?)命令を踏んでIllegal Instructionが発生した場合は無視となっているが、カーネルだけ読んでいるとこの辺のユースケースが理解できず。FPU周りでCコンパイラが自動挿入するんだろう事はコメントから想像できますが、何事もなかったかのように処理されるため、例外起きたか否かでFPUの何らかの機能の有無を判定、ってわけでもなさそう。この部分は今回解明できなかった点の1つ。
次はLions本でも指摘されてるcase 8とcase 8+USERでのfloating exceptionの処理の違い。8(kernel)だとsignalセットしたら該当プロセスを起こすだけでuser modeにreturnしているけど、8+USERの場合にはsignalセット後にハンドラを起動している。system call呼び出しやsignalハンドラ起動用のr5,r6保存場所が、スタック構造ではなく固定箇所への退避になっており、多重呼出しに対応できないから、という解釈。つまり、kernelからtrapが呼ばれてsignalをセットしてハンドラ起動〜ってパスで動作させようとすると、戻る時にkernelより前に正しく戻せないパタンが出てくると思っています。
ちなみにkernelモードでfloating exceptionが起きるのは、kernelでFPU使ってるわけではなく、PDP-11が正確な例外をサポートできておらず、例外が数命令滑ってからトラップされるため。backup()付近を読むと、PDP-11は正確な例外(precise exception)が実現できていなかった事がわかり、今時のCPU屋さんとしてはアンモナイトを見たような衝撃を受けるわけです。
system call((*f)())直前にu_qsavにr5,r6を退避している点の理解もやや不完全。u_qsavからの復帰はsleep内だけ、というのは簡単に調べられるので、sleep呼ぼうとした時に、わきからsignalが飛んできた時の対策かな・・・とは想像されるのですが。sslepのwhile文から大域脱出したいためだけに使ってる?
間接呼び出しについても話題になりました。通常の仕組みだと引数を含めたシステムコール呼び出しのコード全体が静的にテキスト領域に埋め込まれる形になるので、動的なシステムコール呼び出しができない。このために間接呼び出しの仕組みが用意されていて、システムコール0番を呼び、第一引数にポインタを設定すると、ポインタの先のデータ領域をシステムコール(trap命令+コール番号)と引数の構造だと解釈してシステムコールを間接実行する事ができる。実際にv6のlibcのwriteで使われてるよ、とかいう話がありました。
アセンブラで書くとこんな感じ?
.textexec
direct_call:
sys write
hello
6
hello:
<hello¥n>
.text
indirect_call:
sys indir
entry
.data
entry
sys write
hello
6
hello:
<hello¥n>
execは先の知識がないとblack boxとして読み飛ばさないといけない関数がぼちぼちあって気持ち悪い。nameiはファイル名からinode構造体へのポインタを求める関数、getblkは、もともとI/Oのブロックバッファ(ディスクキャッシュ)を検索してバッファを引っ張ってくる関数だけど、ここではNODEVを対象とした特殊処理で、空の新規ブロックバッファ512Bを取得するために使っている。
nameiとuchar周りが気持ち悪くて、system callから渡されたファイル名がどうnameiに渡っているのかを理解するのが結構大変。というか実装が汚い。ファイル名はr0決め打ちでsystem callに渡される事になっていて、trap()、u.u_dirp = u.u_arg[0]の形でu_dirpにr0のポインタがコピーされる。uchar()ではこのu_dirpを決め打ち引数にして文字列をuser空間からkernel空間にコピーしており、これがnameiに渡るファイル名になっていく・・・というのが大まかな流れ。
a.outのフォーマットについては410形式ではテキスト領域がページ単位でパディングされており、read onlyで複数のプロセスで共有できるようになっている、との事でした。
u.u_prof[3] = 0;は宿題。profil()でプロファイラを有効にしていると、ここが非0になっていてclock()でプロファイラが動くことまでは追えている。けど、なぜテキスト領域開放直前に0クリアしてるのかまでは理解できていない。
データセグメントのロード部分はややトリッキー。MMUを書き換えて0番地にデータセグメントをマップ、readiで0番地以降に直接転送をして、終了後にMMUを元に戻してる。機能的、あるいは性能的に意味のあるトリックなのかが理解できていない。
ISUID関連も少し勉強不足。非rootプロセスだった場合でISUIDが立ってた場合はファイルのuidに権限を移行してから実行開始。非root→rootは遷移できるのに、root→非rootは遷移できない。そういうもの?
最後にちょっと衝撃だったのがシグナルハンドラ周り。シグナルハンドラはu_signal[]にsignal毎に格納されており、0がSIG_DFL相当、奇数(主に1)がSIG_IGN相当で、偶数(validなポインタ)が通常のハンドラ、という割り当て。exec字には偶数のエントリだけ0に初期化、という事をやっているので、どうやらSIG_IGNの設定は子プロセスにも継承される仕組みらしい。これは現在のPOSIXでもこういう仕様?
fork
system callレベルでは親に子プロセスIDが返るだけじゃなく、子プロセスにも親プロセスIDが返る(少なくともいまどきのC言語レベルでは子プロセスには0が返る)というのが、ちょっと新鮮だったわけですが、自分の理解はそこまでで、最後のu_ar0[R7] += 2; つまりプログラムカウンタの更新の意味は理解できていませんでした。他のsystem callについてはtrap内でエントリ構造体を引いて引数の数だけカウンタを回していたので、forkだけ特殊なのが理解できませんでした。が、ここで青柳くんのヘルプ。どうやら、forkから返ると親プロセス側では後続の1命令をスキップする、という仕様らしい!!!これはわりと衝撃。というか、醜い実装? 何か歴史的な背景があるのかなぁ。forkだけにfork内でパスが分岐して返ってくるほうが当時の人の直感と合ってたんでしょうか。
sbreak
セグメントごとのサイズを足し引きして、データセグメント部のサイズを変更するだけの機能。面倒なだけで、あまり難解なところはないのですが・・・自分的にはsbreakという名前がUNIX触り始めた頃からの長年の疑問でした。sbrkという関数名を先に知っていて、brkってなんだろう?breakじゃないよなぁ・・・と思ってたんですが、やっぱりbreakでした。コメントにもbad planningとか書いてあるし、命名に失敗したようです(Cのbreakが失敗なのか、sbreakが失敗なのかは不明)。ここで参加メンバーからmanの紹介があり、manを読んでいて名前の由来に気づきました。どうやら、スタック先頭(=プロセスが使っているメモリ空間の末尾)をbreak pointと呼んでいるらしい。sbreakはset break pointの意味? メモリ空間の中でここまでvalid、という意味でいうなら、このbreak pointという意味合いもなんとなく納得できますし、ひょっとしたらMMU以前から使われている名前なのかなぁ、という気もしてきます。MMUのない世界ではプロセス境界ですよね、このbreak pointの場所って。DOS(というかhuman68kの頃の記憶)だとプロセス起動時に空きメモリ全てが渡されるので、子プロセスを呼ぶ際には必要最低限にsbreak相当で切り詰めてから子プロセス起動、ってやってた気がします。なんか共通の根っこでもあるのだろうか?
という事で、新たに発見した事をまとめたつもりだったのですが、改めて読むと分からなかった事リスト、宿題リストになってますね。。。まぁ、それだけ難しかったという事で。
0 件のコメント:
コメントを投稿