ラベル UNIX の投稿を表示しています。 すべての投稿を表示
ラベル UNIX の投稿を表示しています。 すべての投稿を表示

2013年11月2日土曜日

vmware-mount

vmwareの中で動かしている仮想マシン、うっかり/etc/hostsを消してしまって。
特にカスタマイズもしてなかったのでリポジトリ管理もしてないファイルだったんだけど、元通りじゃないと気持ち悪いので、仮想ディスクvmdkのバックアップから発掘しようかな、と思って作業した時のメモ。
バックアップから別の仮想マシンを起動すれば簡単なんだけど、面倒だからvmdkを直接マウントしようとして、結論から言えばもっと面倒だった、という話。

まず、普通に生活していればvmware-mountが便利に使えたはず。だったんだけど、中で動かしていた仮想マシンがNetBSDだったので、そのままだとうまく動いてくれなかった。ので、vmware-mountが中でやってると思われるmountコマンドを引数を適切に指定して呼ぶ必要がありました。
% sudo mount disk.vmdk /mnt/tmp -t ufs
-o ro,loop=/dev/loop1,offset=2719232,ufstype=44bsd
最近じゃ違うのかもしれないけど、僕は昔からNetBSDでは奥ゆかしきufsを使い続けてるので-t ufsをつけて、-oの方にはufstype=44bsdを指定。ufstypeを指定しないとoldが選ばれて、マウントはできるけど一切読めない状態になるので注意。あとufsはread onlyでしかサポートされてないのでroも必要。ここまではmanを読めばすぐわかる話。で、問題なのはoffsetの探し方。

ここで指定するoffsetはvmdkのヘッダサイズと読みたいパーティションの位置を足したもの。とりあえずvmdkのヘッダサイズがよくわからなかったので適当に推測。以下、仮想マシンの中での作業。
% disklabel /dev/wd0
(snip)
#        size    offset     fstype [fsize bsize cpg/sgs]
 a:   2097900        63     4.2BSD   2048 16384 21880  # (Cyl.      0*-   2220*)
 b:   1048950   2097963       swap                     # (Cyl.   2220*-   3330*)
 c:  41942977        63     unused      0     0        # (Cyl.      0*-  44384*)
 d:  41943040         0     unused      0     0        # (Cyl.      0 -  44384*)
(snip)
って感じの状態なので、とりあえずディスクの先頭に何が書いてあるか確認。
% hexdump -C /dev/wd0d | head -n 20
って感じで見れば、先頭から0x100付近にNetBSDのMBRのエラーメッセージが見えるので、これをキーにディスクイメージ側を探索。以下はホストでの作業。
% hexdump -C disk.vmdk|less
"BSD"をキーに探せばすぐに該当箇所が見つかる。僕の場合0x290120付近。ここからちょっと遡ると0x290000がディスクの先頭と一致することがわかる。

ここまでわかれば簡単。読みたいパーティションはoffset 63からなので
% expr 63 \* 512 + `printf %d 0x290000`
2719232

ふぅ。ちなみに最初の段階でvmdkが複数のファイルに別れてたりする場合はvmware-vdiskmanagerを使って単一ファイルのフォーマットに変換してから作業する必要があるはず。

2012年6月3日日曜日

ネットワーク関連の本、下から上まで


最近、面白そうなネットワーク本を見つけたので、それを含めて蔵書の紹介。どれも名著と呼ばれるものばかりのはずです。ただ、書いてみて気づいたんだけど、ここで一番上のレイヤーって言ってる本ですら、今時のほとんどの人にとっては目に触れる事のない最下層レイヤーかも・・・。それでも気になるって人だけ読んでください。

Principles and Practices of Interconnection Networks

京のネットワークコントローラを設計する際に散々お世話になった教科書。ルーティング、フロー制御、トポロジについて論理からハードウェア設計まで丁寧に説明されています。シミュレーションによる性能評価のノウハウも書かれてるので、この分野(インターコネクトアーキテクチャ)で研究しようと思ったら、これ一冊読むだけでスタート地点まで行けます。

Interconnections

古本が安く出てたので気になって購入。まだ読んでないんだけど、ハードウェアとIPの間を埋めてくれそう。ようするに、ひたすらL2中心の分厚い書籍(笑)。この本でようやく上から下まで繋がる気がします。

詳細TCP/IP (TCP/IP Illustrated, Volume 1: The Protocols)

日本語だと初版しか出てないのかな。SOFTBANK BOOKS版とピアソンの新装版があって、僕は古いSOFTBANK BOOKS版の頃に買って読みました。TCP/IPの基礎がすべて詰まっていて、ソフトの人が必要になるであろう知識としては一番下のレイヤーから書かれてます。たぶん、自作ウェブブラウザーを開発しようと思って買ったんじゃなかったかなぁ? 1997年です・・・。
有名なプロトコルについては簡単に説明が書いてあるのでRFC読むのが面倒な時とか、時々振り返って読んでます。会社の人に2版を進められてるので、そのうち買うかも。現在僕にとって一番勉強しておかないといけないレイヤー。

マスタリングTCP/IP SSL/TLS編

仕事でWebSocketに携わることになって。SPDYとの関係もあったのでSSLの理解は避けて通れない予感がして覚悟を決めて読んだ一冊。すべてを理解したとは言い難いかもしれないけど、だいぶ見通しは良くなったかな。なんだかんだでSSL周りのコードをいじる事もあるんですよね。上記TCP/IP本のセキュリティに関する補遺的な位置づけで読むべき本です。

UNIXネットワークプログラミング第2版 Vol.1 ネットワークAPI: ソケットとXTI

10年以上前に働いてた会社で初版を読みました。当時は会社で唯一のWindowsプログラマだった事もあり、WinSock2の本も読んでましたけど・・・あまり覚えてないなぁ。で、会社を辞めて大学に戻ってから、手元にも一冊ないと不便だなぁ・・・と買ったのが第2版。なので、結構読んでないところもあります。
ソフトウェア屋さんにとっては上の2冊が理論で、この本が実践。やっぱりソフトの方がハードより勉強する事多いのかなぁ。

2012年5月29日火曜日

いまどきのUNIXプログラミング

久々にblog更新しようとしたら、UIが一新されててビックリ。

さて、しばらく前の話になりますが、やや若い世代の人と集中的に開発を行う機会がありまして。「epoll使っていいですか、selectってあまり使った事ないので」と言われて愕然。当たり前と言えば当たり前なんだけど、90年代に身に着けたUNIXの知識もいまや年代物。少しはアップデートしないとなぁ・・・という事で本を読んで勉強したので、そのメモ。もしろん昔からあるけど知らなかったって事も沢山ありました。

読んだのは「LINUXシステムプログラミング」というO'REILLY本。400ページ弱という(この手の本にしては)薄い本なのだけど、興味深い話題が多く楽しんで読めました。以下、この本によってアップデートされた私の知識の項目一覧と概説。これを見て「おぉ」と思った人は仲間なので買って損はないと思う。

ファイル・I/O


ノンブロッキングI/O

O_NONBLOCKによるノンブロッキングI/O。知らなかったわけじゃないけど、自分のアプリでは使った事なかった。

同期I/O、ダイレクトI/O

fsync()とfdatasync()。あと、O_SYNC、O_DSYNC、O_RSYNCとか。O_DIRECTも。


ファイルサイズを超えたシーク

あぁ、これやるだけでsparse file作れたんだ・・・知らなかった。


ファイルポジション指定I/O

pread()、pwrite()とか。これはqemuの移植やってた時に見かけたのでちょっと前から知ってた。PepperのFileIOってpread()/pwrite()系列のAPIになってますね。


I/Oの多重化

select()ループがクールだった時代はとっくに終わってた。シグナルハンドラでできるのはpipeに書いてselectを起こす事くらい・・・と信じてた人はpselect()を調べるべし。あと知らなかったけどSystem V系ではpoll()とかppoll()というのがある。でも、今時Linuxでサーバ書くなら大量のデスクリプタに対してもスケールするepoll_*系関数群を使うべき。期待した通りのインタフェースを持ってる。Win32のWaitForMultipleObjectsより賢い多重化が可能になったと思う。


scatter-gather I/O

readv()とwritev()。リンクアレイチェーン転送的なI/Oインタフェースと言えば一部の人には分かりやすいかも。


ページサイズ取得

普段みかけるのはgetpagesize()ばかりなんだけど、こっちはLinux固有で、POSIX的にはsysconf(_SC_PAGESIZE)が正しいらしい。確かにNative Clientもsysconf()は持ってる。

I/Oスケジューラ

Deadline I/Oスケジューラ、Anticipatory I/Oスケジューラ、CFQ I/Oスケジューラ、Noop I/Oスケジューラ。4.6節でいきなりOSの教科書かと思うような細かいスケジューラの解説があります。なんでかと思ったら、Linuxは手軽にI/Oスケジューラが差し替えられるんですね。この辺は純粋なOS好きにとっても楽しめる内容かと。


拡張属性

xattr。時代はレジストリ、もといBFS。


ファイルイベント

inotify_*系のインタフェース。さらば、SIGHUP。


プロセス管理


プロセス階層

あんまり真面目に見て来なかったプロセスグループの話。


ゾンビプロセス

そういえば、なんでプロセス終了時に子プロセスを回収する処理をOSレベルで走らせないんだろ。ゾンビとして生かしておく根拠を知らないなぁ・・・。wait系は直接の子供しか待てないという理解なのだけど。


ユーザとグループ

プロセスのユーザIDは、実ユーザID、実効ユーザID、savedユーザIDの3種類ある・・・知らなかったし、一度読んだだけでは覚えられない・・・。


セッションとプロセスグループ

シェル書いてジョブコントロールとかしようとすると必要になる知識なんだろうけど。そう言えば、ちょっと前にLinuxに入ったスケジューラの改善って、この辺をうまく考慮してスケジューリングするとかなかったっけ?
※記憶と照合するとStaircase Schedulerの事だったのかなぁ・・・。でも、それももうobsoleteっぽい。ちなみに「Linux カーネル 2.6 Completely Fair Scheduler の内側」はスケジューラに関する良い記事な気がする。


デーモン

日頃お世話になっているdaemonさん。実は然るべき正規の手順が存在していたのですね・・・。新規プロセスグループのリーダープロセスにした上でchdir('/')して標準入出力は全て/dev/nullにする。わりと面倒な処理が必要みたいだけど、実はdaemon()ってライブラリ関数がある・・・というのも知らなかった。


I/Oの優先度

プロセス優先度だけでなく、ioprio_get()/ioprio_set()というインタフェースでプロセスのI/O優先度を指定できるらしい。ただしCFQ I/Oスケジューラ選択時に限る、か。


プロセッサアフィニティ

sched_getaffinity()とsched_setaffinity()。なるほど、SunOS+Oracle以外でも今や当たり前のインタフェース。


名前空間

Linuxでは(ファイルシステムの)名前空間はシステム固有ではなくプロセス固有。古典的なUNIXではシステム固有。Plan 9由来ですね。逆に、これがないとchrootってどうしてるんだろ。単にアクセス範囲をサブツリーに限定してるだけ?


メモリ管理


アラインメント

posix_memalign()。


/dev/zeroのmmap

そう言えば、そんなテクニックもあった。


高度なメモリ割り当て

mallopt()、malloc_usable_size()、malloc_trim()、mallinfo()あたりは知っていると使う機会もありそう。


スタック上への文字列コピー

strdupa() = alloca() + strdup()。C言語だけで開発する機会ってのも無くなってきてるので、知ってて役に立つかは微妙だけど。


ピンダウン

mlock()、mlockall()、munlock()、munlockall()。mincore()でスワップ判定。


オーバコミットとOOM

OOM Killerとの付き合い方。そういえば青山Killer物語の舞台って青山キラー通り


シグナル

「古い実装ではシグナルを喪失することがありました。」いえ、それは私が知っているシグナルの実装、そのものです・・・。古い実装ではシグナル送信はプロセスにただ1つ存在するバッファにシグナル情報を保存して戻るだけの単純な実装で、プロセスは起きる前にバッファを確認し、存在していれば最新のただ1つのシグナルを処理してました。ところが、今時のシグナルを考えると、他にも考えなければならない問題が。
例えば、この本にも載ってなかったけど、マルチスレッド環境でシグナルを処理するのは誰(どのスレッド)か。答えは任意のスレッド。具体的な実装を見たわけではないけど、シグナルが積まれた後、最初に起こすスレッドが処理するのが一番ナイーブな実装か。特定のスレッドで受けたければ、他のスレッドはシグナルマスクを設定するのが作法らしい。


時間


時刻表現

micro秒のtimevalとnano秒のtimespec。最近はnano系が標準化されてるので、timespec系に一本化しとくと間違いなさそう。


POSIXクロック

clock_gettime()。Native Client内でも動いてるので重宝してます。


スリープ

秒単位のsleep()、micro秒単位のselect()ってのが古典の世界。今はmicro秒単位のusleep()もあるけど、nano秒単位のnanosleep()、clock_nanosleep()が標準化の観点からもオススメらしい。


タイマー

POSIXクロックの一部。time_*系関数群。


と、まぁ、こんな感じです。この本には出て来なかったけど、ttyとかも深い闇なんだよなぁ。未だに理解しきれてないプロセスグループやネットワークスタックなんかの話も含めて、いずれBSD本あたりを熟読したい次第。
 

2011年4月12日火曜日

Lions' Commentary (11) - 読書会#6 -

再び読書会の記録です。
今回は13章・・・の途中まででした。signalの処理全般とexit、waitの関係あたりなので内容としては重たかった。ので、仕方ないかなぁ、という感じですね。トレースの部分が終わらずに次回へ繰越です。

ssig
Lions本では3625行について明確な答えが得られていない。自分の理解を読書会で話したんだけど、たぶん同意してもらえたと思う。つまり、あるシグナルハンドラを設定する際、その設定よりも前に該当シグナルが発生し、処理待ち状態になっていたならば、新たなハンドラはその過去に発生したシグナルに関して興味を持っていないはず。よって、なかった事にして良い。もちろん丁寧に処理するのであれば、古いハンドラを呼び出してあげれば良いわけだが、例によってコンテキストスイッチのネストは複雑なので、処理せずに済むのであれば処理しないのがベター。もともとKILL以外は最新のシグナル以外は上書きロストする仕様なので、ここは手を抜くのが落とし所として適切。

kill
pidに0を指定して呼び出した時の動作が特殊。今時のUNIXではこの時、呼び出し元プロセスと同group IDを持つプロセス全てにsignalをbroadcastするのが仕様。当時はまだgroup IDの概念がなく、同じttyにぶら下がっているプロセス全てにbroadcastしている。端末でコントロールコードを入力してsignalを飛ばすとき、この機構を使うのが一般的と思われる。

psignal
シグナルハンドラの起動。kill経由で呼び出されるがLions本で指摘?の通り、バグ持ちのようだ。signal番号として負の値を入れて呼び出すと、psigで配列の範囲外を読み出し、ゴミデータをハンドラのアドレスだと思ってハンドラ呼び出しをしてしまう。たぶんOSごと死んでしまうのではなかろうか。
さらに言えば、シグナル番号が範囲外だった時にエラーを返さない。本来ならssig同様に範囲チェックをしてエラーを返すべき。

issig
トレースが有効になっていたらstopを呼び出す。さらにstopでは親プロセスIDが1だった場合(initの子供だった場合)にプロセスを終了する。ここだけ読んでもわからないが、これはプロセスのデバッグ中に親のデバッガプロセスが不正終了した時のためのコード。親が先に死んだ場合、子プロセスはinitの子プロセスとして登録されるというのがUNIXの風習。initがデバッガの代理処理を行うわけにもいかないので、諦めてデバッガプロセスと道連れでデバッグ対象プロセスもexit・・・、というのはやむを得ない処置か。

exit
user構造体の複製をswap領域に作成し、終了コードを保存。全シグナルハンドラを無視に設定して、プロセス領域を開放、ゾンビプロセスになる。その後の処理が読書会の中でも時間をかけて議論された部分。
まず、親プロセスが存在しないと、自分自身をinitプロセスの子プロセスとして登録する。続けて、initプロセスと親プロセスを起こす。最後に未回収の子プロセスをinitプロセスにぶら下げ直して、コンテキストスイッチ。initプロセスを起こすのは、initにぶら下げた子プロセスが既にexitしてwait待ちになっている可能性があるため。親プロセスを起こすのは、自身の終了をwaitで待ってブロックしている時のため。全者の理由を解読するのに時間がかかった。
プロセスを起こす処理はwakeup(&proc[1])とwakeup(p)。waitで寝るとき、自身のproc構造体のポインタが要因として利用されるため、このような記述でそれぞれwait中のinitプロセス、親プロセスを起こすことになる。

wait
ゾンビ化したプロセスを最終的に回収して上げるのがwait。UNIXではfork、execで生成されたプロセスは、exitで実行イメージを開放し、終了コードを含むuser構造体をswap領域に退避する。最後に親がwaitで回収する事でuser構造体を含む全てのプロセスリソースが開放可能となる。逆に言えば親のwaitで回収されないプロセスはゾンビプロセスとして永久にswap領域内に居残り続ける。PDP-7 UNIXの頃はこのあたりの終了コードのやり時も、もう少し汎用性の高いプロセス間通信で行われていた模様。ところが、実際には決まりきった使い方しかされていなかったので、マルチプログラミングの改善時に、終了コードの受け渡しだけに特化したexit、waitの実装になった模様。このあたりの様子はUNIX原典に書かれてます。

7shiさんのデモ
いや、これは熱い。デモを見せてもらったんだけど、Silverlightのファンになりました(言い過ぎ)。しかし、良く作りこまれていて。Ken Thompsonとかが見たら泣いて喜ぶんじゃないだろうか。コマンドライン版はbinfmt_miscに登録すると素敵かもしれない。あとqemuにマージされたりすると、もっと素敵。
そうそう、simh上の動作は標準出力が遅い〜みたいな話があったけど、たぶんコンソールから出力するにあたり、kl.cのドライバ経由でtty.cのコードが動き、デバイスのBUSY状態を見ながら出力している&BUSY状態がデバイスの実時間をシミュレートしているためではないかと思います。

2011年3月28日月曜日

Lions' Commentary (10) - 読書会#5 -

読書会、#1と#2に参加したけど、#2は怠慢により日記更新なし。#3と#4は都合により不参加・・・という事で、久々の参加記録になります。綺麗にまとめようとして更新できなくなるのも残念なので、乱文ですが更新させて頂きます。

さて、今回は第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で使われてるよ、とかいう話がありました。
アセンブラで書くとこんな感じ?
.text
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
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相当で切り詰めてから子プロセス起動、ってやってた気がします。なんか共通の根っこでもあるのだろうか?

という事で、新たに発見した事をまとめたつもりだったのですが、改めて読むと分からなかった事リスト、宿題リストになってますね。。。まぁ、それだけ難しかったという事で。

2010年12月26日日曜日

UNIX 6th Editionにシステムコールを追加してみる

という事で、カーネル/VM Advent Calendarの担当日なので、表題の件で書いてみようと思います。カーネル修行中の身という事で、Lions Commentaryで勉強中のUNIX 6th Editionをちょっとだけhackしてみます。

目標
Linuxなどで「man select」としてみると「The select() function call appeared in 4.2BSD.」などと書かれています。こいつを6th Editionに実装してみましょう。
Ancient UNIXではブロックする時には原因となる変数のポインタを引数としてsleep()を呼び出します。逆にブロック要因が解けた時、同じく変数のポインタを引数としてwakeup()を呼び出します。この時、全プロセスの情報をなめ、同じポインタ値を要因としてブロックしている全プロセスを起こすという古典的な手法をとっています。要因が1つしか持てず、リストも1つだけで管理されている事からも、select()が存在しない、というのは納得な話に思えます(と、言いつつselectの実装は理解していないのですが)。
そこで、完璧なものは難しいですが、pipeを多重化して入力待ちするのに必要最低限な機能を持たせた限定版システムコールselectを追加してみる事にします。

アセンブラからシステムコールを呼び出してみる
追加したシステムコールを呼び出すにはアセンブラを書く必要があるため、まずはシステムコールを呼び出すアセンブラの確認です。適当な作業領域に移りましょう。
% ed hello.s
?
a
main:

        mov $1,r0
        sys write; hello; 6
        sys exit
hello:
        <hello\n>

.
w
66
q
% as hello.s
% a.out
hello
ユーザ入力を赤、または青字、ソフト側からの出力を黒字で分けてあります。
ユーザ入力については、まとまったソース部分を青字にしました。
大丈夫だ、問題ない。

次は簡単なシステムコールを追加してみる
まずはエントリの追加から。
# chdir /usr/sys/ken
# ed sysent.c
2131
62
        0, &nosys,                      /* 49 = x */
s/nosys/hello/
s/x/hello/
p
        0, &hello,                      /* 49 = hello */
w
2135
q
この構造体に引数の数とハンドラを登録する事でシステムコールを追加できるはず。そして、実際のシステムコールの実装。ここでは簡単のためconsoleに直接helloと表示することにします。ここも同じくken以下で作業します。
# ed hello.c
?
a
hello()
{
        printf("hello\n");
}
w
32
q
最後にカーネル再コンパイル。
ところがそこには罠が。/usr/sys以下で「# sh run」とすれば、苦労せずにカーネル再コンパイルができる・・・と随所に書かれているのですが、実はこのままではlib1、lib2が更新されず、すなわちken以下での先程の修正が反映されません。そこで、以下の手順でrunスクリプトを修正します。
# chdir /usr/sys
# ed run
3
ar r ../lib1
s/1/1 *.o/
p
ar r ../lib1 *.o
8
ar r ../lib2
s/2/2 *.o/
p
ar r ../lib2 *.o
w
908
q
修正適用後は「# sh run」でカーネル再コンパイル可能。lib1とlib2を完全に削除しちゃうと解決できないシンボルが出てきてしまうので注意。ここは細かく追ってません。また、ken以下の拡張子cを全てコンパイルするように記述されているため、追加したファイルについて気にする必要はありません。

追加したシステムコールの確認
先ほど追加したhelloで呼んでるprintfはC言語の標準関数ではなく、ken/prf.cの中で定義されているconsoleへ直接出力するpanic用の関数。素の状態だと出力禁止になってるので、デバッグのために出力を開放する必要があります。simhを利用している場合にはCtrl-Eでモニタ?に落ちて
Simulation stopped, PC: 002502 (MOV (SP)+,177776)
sim> d sr 1
sim> cont
とすればOK(前回のLions読書会でoracchaさんに教えてもらいました、さんくす!)。
続けて、追加したシステムコールを呼び出すだけの簡単なユーザランドプログラムを書いてみます。適当な作業領域で作業しましょう。
% ed sys_hello.s
?
a
main:
        sys 61
        sys exit
w
24
q
49番で追加したシステムコールですが、アセンブラは8進数で記述する必要があるため61と書く必要があります。ちなみに、hello.sではsys writeなどと書きましたが、実はこのシステムコール名、マクロなどではなく、アセンブラの中に直接「名称→コール番号」の変換テーブルを持ってるようです。なので、アセンブラを修正しない限りは数値直書きしかありません。
という事で、実行して動作を確認。
% as sys_hello.s
% a.out
hello
%
・・・とは、ならず・・・
% as sys_hello.s
hellout


%
こんな風になりました。最後の改行についてCRだけ表示されたタイミングで、カーネル内のprintfがLFを追い越して表示しているようです。ここも細かくは追っていません。まぁ、とりあえずシステムコールの追加はうまくいっているようなので一安心。


システムコールの引数と返り値
システムコールの引数は、レジスタr0とtrap命令に続くテキスト領域内の最大5つまでのデータ列として渡せます。また、返り値はレジスタr0です。
システムコールのハンドラ側では、それぞれu.u_ar0[R0]、u.u_arg[n]として値を受け取ることができます。返り値はu.u_ar0[R0]にセットすれば、システムコールから返るときにユーザランドのコンテキストに書き戻されます。
これらの挙動を確かめるため、やはり簡単なテストコードで実験しましたが、ここでは割愛。

selectの実装
まずはシステムコールとCの関数をブリッジする部分を作ります。適当な作業領域に移動して以下のファイルを作ります。
% ed syssel.s
?
a
.globl  _select


_select:
        mov 2(sp), _nfds
        mov 4(sp), _rfds
        mov 6(sp), _wfds
        mov 10(sp), _efds
        mov 12(sp), _timo
        sys 62
_nfds:  0
_rfds:  0
_wfds:  0
_efds:  0
_timo:  0
        rts pc
w
178
q
システムコールの番号はさっきより1つ大きな値。引数をsys命令の直後にコピーしてシステムコール呼び出し&リターンするだけの簡単なラッパーで、今回R0は返り値だけに利用しました。spからのオフセットも8進数で書くことに注意。これでC言語から「int select(int nfds, int *rfds, int *wfds, int *efds, int *timo);」の形でシステムコールを呼び出せます。本来ならrfds, wfds, efdsはfd_set *ですが、typedefがないのとプロセスが持てる最大ファイルディスクリプタ数が15なのでint *としました。気持ち的には「typedef struct fd_set { int fds_bits[(NOFILE + 7) >> 3] } fd_set」です。timeoutは難しいので今回は無視。というか、nfdsとrfds以外は今回は無視します(笑)。
さて、いよいよシステムコール側の実装です。本来は指定されたデスクリプタのいずれかが変化するか、signalが割り込むまでブロックするのが正しい動作ですが、簡単のためノンブロックで返るような動作にしましょう。
# chdir /usr/sys/ken
# ed sysent.c
2135
62,63p
        0, &hello,                      /* 49 = hello */
        0, &nosys,                      /* 50 = x */
s/nosys/select/
s/x/select/
s/0,/5,/
62,63p
        0, &hello,                      /* 49 = hello */
        5, &select,                     /* 50 = select */
w
2141
q
# ed select.c

?
a
\#
\#include "../param.h"
\#include "../user.h"
\#include "../file.h"
\#include "../inode.h"
\#include "../reg.h"


select()
{
        int nfds, rfds;
        int out_rfds;
        int rc;
        register *fp, *ip, i;


        nfds = u.u_arg[0];
        rfds = fuword(u.u_arg[1]);
        out_rfds = 0;

        rc = 0;

        for (i = 0; i < nfds; ++i) {
                if (!(rfds&(1<<i)))
                        continue;
                fp = getf(i);
                if (fp == NULL)
                        continue;
                if (!(fp->f_flag&FPIPE))
                        continue;
                ip = fp->f_inode;
                plock(ip);
                if ((fp->f_flag&FREAD) && (fp->f_offset[1] != ip->i_size1)) {
                        out_rfds =| (1<<i);
                        ++rc;
                }
                prele(ip);
        }
        suword(u.u_arg[1], out_rfds);
        u.u_ar0[R0] = rc;
        u.u_error = 0;
}
.
w
627
q
#
sysent.cにselectのエントリを追加します。引数の数が5となるため、構造体の先頭の0を書き換える必要がある点に注意。selectの実装は見ての通りです。いくつか注意点を書くと、まずは引数の処理。u.u_arg[1]にはint *rfdsが入っていますが、この値はユーザ空間でのrfdsへのポインタ値です。なので、中の値を見るためにはfuword()を使ってユーザ空間から読みだしてやる必要があります。値の書き戻しも同様でsuword()を使います。それぞれfetch user-space word、store user-space wordですね。もう1点忘れがちなのがu.u_errorの処置。ループ内でgetf()を呼び出しています。こいつはファイルディスクリプタ値を渡して、指定したデスクリプタが開いていたら構造体を返してくれる関数ですが、開いていなかった場合にはNULLが返るばかりでなく、u.u_errorにEBADFがセットされます。このエラーをこのまま放置すると、trapから返るときにu.u_ar0[R0]にu.u_errorを上書きします。必ず0にリセットしてあげましょう。カーネルの再構築も「# sh run」で忘れずに行います。

動作確認
selectの確認はちょっと面倒ですね。pipeで子プロセスを2つ作り、それらの入力を多重化して待ちながら逐次表示してあげるようなプログラムを書いてみます。まずはselectを使う上で必要になるいくつかの関数を用意します。syssel.sを作った作業領域で続けましょう。
% ed select.c
?
a
FD_SET(fd, fds)
int fd;
int *fds;
{
        *fds =| (1<<fd);
}


FD_ISSET(fd, fds)
int fd;
int *fds;
{
        return (*fds & (1<<fd));
}


FD_ZERO(fds)
int *fds;
{
        *fds = 0;
}
.
w
162
q
%
本来ならマクロで書きたいところですが、たぶん当時のプリプロセッサでは引数の置き換えとかできなそうなので、トラブルを避けて関数で書きました。grepするとわかりますが、当時のdefineは定数定義しかしてません。
続けてテストプログラム。
ed main.c
?
a
main(argc, argv)
int argc;
char **argv;
{
        int fds0[2];
        int fds1[2];
        char buffer[32];
        int rfds;
        int rc;


        rc = pipe(fds0);
        rc = pipe(fds1);
        rc = fork();
        if (0 == rc) {
                for (;;) {
                        write(fds0[1], "this is child 0\n", 16);
                        sleep(2);
                }
        }
        rc = fork();
        if (0 == rc) {
                for (;;) {
                        sleep(1);
                        write(fds1[1], "this is child 1\n", 16);
                        sleep(1);
                }
        }
        for (;;) {
                FD_ZERO(&rfds);
                FD_SET(fds0[0], &rfds);
                FD_SET(fds1[0], &rfds);
                rc = select(16, &rfds, 0, 0, 0);
                if (rc <= 0) continue;
                if (FD_ISSET(fds0[0], &rfds)) {
                        rc = read(fds0[0], buffer, 32);
                        write(1, buffer, rc);
                }
                if (FD_ISSET(fds1[0], &rfds)) {
                        rc = read(fds1[0], buffer, 32);
                        write(1, buffer, rc);
                }
        }
        return 0;
}
w
729
q
%
コンパイルして実行してみます。と、その前に。カーネルを作り直したので新しいカーネルでの再起動をお忘れなく。「# sync」してCtrl-Eから「sim> exit」でエミュレーション終了。再度エミュレータを起動してrkunixで立ち上げます。
% as syssel.s
% mv a.out syssel.o
% cc main.c select.c syssel.o
main.c:
select.c:
% a.out
this is child 0
this is child 1
this is child 0
this is child1
.
.
成功です!永久ループなので、適当なところでDELキー(Ctrl-C相当)を押します。という事で、最低限のミッションは達成しました。
応用編としてはselectシステムコール内で読めるデスクリプタがなかったらsleepするように変更すると良いかと思います。適当な新規要因を決めてsleepしてあげて、sleepから返ってきたら再度デスクリプタの走査へgoto。wakeup側では毎回、問答無用でselectを起こしてあげるようにしとけば、最低限それっぽく動くはず。真面目にやるならproc構造体を拡張してp_wchan相当のエントリ、p_wfdset[NOFILE]的な物を追加してあげて、p_wchanがselectならp_wfdsetも調べる〜とかするだけでOKなはず。

・・・なんか簡単だったので実装してみました。今までの形式だと分かりにくいので以下は差分を赤文字にしてソースを転載します。

[select.c]
#include "../param.h"
#include "../user.h"
#include "../file.h"
#include "../inode.h"
#include "../reg.h"
#include "../proc.h"


select()
{
        int nfds, rfds;
        int out_rfds;
        int rc;
        int fdbits;
        int i;
        register *fp, *ip, *rp;


        nfds = u.u_arg[0];
        rfds = fuword(u.u_arg[1]);
        out_rfds = 0;

        rc = 0;
        rp = u.u_procp;

retry:
        fdbits = 0;
        for (i = 0; i < nfds; ++i) {
                if (!(rfds&(1<<i)))
                        continue;
                fp = getf(i);
                if (fp == NULL)
                        continue;
                if (!(fp->f_flag&FPIPE))
                        continue;
                ip = fp->f_inode;
                rp->p_wfdset[fdbits] = ip+2;
                ++fdbits;
                plock(ip);
                if ((fp->f_flag&FREAD) && (fp->f_offset[1] != ip->i_size1)) {
                        out_rfds =| (1<<i);
                        ++rc;
                } else {
                        ip->i_mode =| IREAD;
                }
                prele(ip);
        }
        if (rc == 0) {
                sleep(fdbits, PPIPE);
                goto retry;
        }
        suword(u.u_arg[1], out_rfds);
        u.u_ar0[R0] = rc;
        u.u_error = 0;
}
[proc.h](差分周辺のみ)
        int     p_wchan;        /* event process is awaiting */
        int     p_wfdset[NOFILE];
        int     *p_textp;       /* pointer to text structure */
[slp.c](差分周辺のみ)
wakeup(chan)
{
        register struct proc *p;

        register c, i;

        int fd;


        c = chan;
        p = &proc[0];
        i = NPROC;
        do {
                if(p->p_wchan == c) {
                        setrun(p);
                } else if (p->p_wchan <= NOFILE) {
                        for (fd = 0; fd < p->p_wchan; ++fd) {
                                if (p->p_wfdset[fd] == c) {
                                        setrun(p);
                                        break;
                                }
                        }
                }
                p++;
        } while(--i);
}
という事で、長文に最後までお付き合いいただき、ありがとうございました。

2010年12月12日日曜日

UNIX原典(3) - ラスト

続けてUNIX移植話に関連したトピックを3件ほど取り上げてみます。他にも性能やスケジューラの話なんかも面白かったのですが、UNIX原典の読書記録としては今回の記事を最後にしようと思います。

Fair Share Schedulerについてはこの資料が一番詳しいかな、って感じだったのですが、他のスケジューラとか勉強した時に、また比較で考えてみようと思います。

マルチプロセッサUNIXオペレーティング・システム
「UNIXカーネルの設計」の著者、M.J.Bachによるマルチプロセッサ環境への移植話。ここではMP(multiprocessor)、AP(Associated Processor)を取り上げていて、前者がいわゆる今時のSMP、CMPの話。後者はI/OはマスターCPUに括りつけて、他のCPUはユーザランドの実行だけを行うような単純な方式によるマルチプロセッサ化の話になります。この方式ではカーネル自体、特に排他制御などは不要で、スレーブCPU上でユーザがシステムコールを発行した際、マスターCPUにコンテキストを移し、システムコールは常にマスターCPU上で実行するようにするだけで対応できます。よって、この論文では主にMP上に移植する際の問題について書かれています。

Lions本などでも書かれているように、UNIXではatomicな処理が必要となるリスト探索などは、一時的に割り込みレベルを上げて、リスト探索中に割り込みによる中断と他の文脈からのコードの実行を禁止し、探索が終了したら割り込みレベルを通常に戻す、といった処理(spl6やspl0)をしています。ところがマルチプロセッサでは割り込みレベルを上げたところで、他方のCPUが競合するコードを実行する事は可能なわけで、意味をなしていません。よってMP版UNIXでは、これらの問題に対処するためにセマフォ(*1)が導入されました。

また、sleepとwakeupについてもセマフォの取得と開放(psemaとvsema)に置き換えられました。

セマフォを使う際に問題となるデッドロックについても説明されています。これは一般的なデッドロック回避方法「依存ループを作らない」の言い換えでもありますが、資源に順序付けを行い、ロックは順序付けにしたがって行う、という単純なルールに従うだけでデッドロックは回避できます。

排他制御を行う際には、細かな粒度でロックをかけることが性能の上で重要になってきます。連結リストをハッシュに置き換えてロックの粒度を小さくする方法が紹介されているが、一番競合が起きやすいバッファプールのフローリストについては、うまい分割方法が知られていない、と書かれていました。今時の最新事情でどうなっているのか、知っている人がいたら教えてください(笑)。


IBMシステム/370のUNIXシステムの実現
こちらはIBMのメインフレームへ移植したお話。この移植は370のハードへ移植した、というよりは370上で動くTSS/370をターゲットとして移植したようです。mach上で動くように移植されたLinux、MkLinuxに近いもの、と読みました。理由としては370の仕様がハードごとに細かく異なり、個別の移植が困難だったことが挙げられるようです。360系は長期に渡る使用に耐えた拡張性のあるアーキテクチャ、という事になっていますが、メインフレームの文化を考えると、ファームレベルではワークアラウンドやRASの処理がごった返し、相当なカオスなのでは、と想像されます。

この版のUNIXで特徴的なのはページングの実装方法です。1つの物理ページに対して、リファレンスカウントを用意して、複数の仮想ページに割りつけられるようになっています。当時のforkは、親のメモリを丸々コピーして子プロセスを生成する非常にコストの高い処理でした。そのためBSDではfork後すぐにexecを呼ぶプロセスのために、メモリをコピーしないvforkを用意しましたが、これは、親子でメモリを共有する危険な実装で、vfork後にexecせずにメモリ操作をした場合の動作は保証されていませんでした。この版のUNIXの実装では、fork時にはページのコピーをするのではなく、ページテーブルのコピーと物理ページ側の参照カウントをインクリメントするだけで済んでいます。また子プロセスで書き込みが発生した場合には親と共有している物理メモリの参照をデクリメントし、新しいページにメモリをコピーし、子供のページテーブルに登録するという、いわゆるcopy-on-writeを実装していたようです(*2)。

ディスクブロックサイズも、System III以前が512B、System Vや4.1BSDが1024Bだったのに対して、370版では4096Bと大きかったようです。


UNIX移植の経験
Intel 8086、AT&T 3B20S、3B5、UNIVAC 1100シリーズへの移植について書かれています。今までの2章に比べると目新しい話題はないのですが、AT&T UNIXが8086へ移植されていたとは、ちょっと驚きです。

ただ、この8086 UNIX、単純なソフトの移植というわけではなく、それ専用にPDP-11/70風のオフチップMMUを開発し、それを載せたシステムへの移植だったようです。

またエンディアンの違いの話も出ていたりしますが、今の人から見ればPDP-11のエンディアンが変態(*3)すぎるだけなわけですが。


(*1): 本の中ではDijkstraのセマフォ、と書かれている。いまやあまりにも当たり前の道具として使われているため、発明者について言及されることもなくなってしまいました。
(*2): 勉強不足のため、いわゆるcopy-on-writeの原点がこの実装なのかどうか、知りません。ご存じの方はぜひコメントを。
(*3): PDP-11のエンディアンはリトルエンディアンでもビッグエンディアンでもなく、ミドルエンディアン。リトルもビッグもそれぞれの言い分は理解できるのですが、ミドルだけは意味不明。アドホックにしか思えないのですが。。。これも意義をしっている方がいましたら、ぜひコメントを。

UNIX原典(2)

前回の投稿からだいぶ空いてしまいましたが、読んでみて特に面白かったBlitの章についてコメントしてみます。

Blit - 多重化されたグラフィクス端末
Rob Pikeによる、X以前の世界でもグラフィクス端末のお話。UNIX 7th Edition〜8th Editionの頃の話のようです。何と言っても面白いのが、グラフィクス端末側にもプログラムをロードして実行できる、という考え方。ご存知のようにXなんかの世界では、ユーザプログラムはXクライアントとして動き、Xサーバへは描画命令の単位で依頼を出します。これって、今時のアプリケーションによっては、通信の多い切り口でレイヤーを区切っちゃってるんですよね。VNCやRDPなんかのリモートデスクトップともなるとなおさら。更新のあった描画区画単位でBitmap情報をやり取りするので、通信はすごく多い。

一方で、ウェブの世界を考えてみると、実はもう少し賢い。MVCモデルで言うと、Modelをサーバサイドでセッション管理をしながら実行。Viewについては素材をサーバサイドで提供しつつ、クライアントサイドのブラウザが最終的なViewを構築。Controlについてもクライアントサイドのブラウザが提供するわけですが、Web2.0以降になってくると、この辺はサーバから送られてブラウザ上で実行されるJavaScriptが担っており、気の利いたUIや滑らかな画面更新が実現されています。

Blitの作りはこのWeb2.0に似てたりします。Blitが初めに目指すところは端末の多重化。今で言えば、画面に複数のターミナルを開きたい、というただそれだけの欲求です。サーバ側ではmpxというプログラムが走り、その中で複数のシェルが実行されます。今で言えばGNU screenみたいなものでしょうか。ディスプレイサイドではmtxtermというプログラムが走り、サーバとの間はRS-232(19,200bps!!!)で接続されます。mtxtermは多重化されたシェルの入出力を復元し、複数のウィンドウ上で動くシェル環境を再生。原始的なXに近い外観を提供しています。ただXと本質的に違うのは、ディスプレイ側でも直接アプリケーションプログラムが実行できる点。例えばエディタなどでは、画面を表示して、ユーザのカット&ペーストなどを制御するアプリケーションコードがBlit側にロードされ、このサブプログラムにより編集されたデータが定期的にUNIXサーバ側に渡される、といった具合です。

描画に関連するオブジェクトはシリアライズしてRDPクライアント側に送りつけ、ユーザ側に近い場所で実行。セキュリティ的に難しいところはあるかもしれませんが、今風に味付けすれば、色々と研究しがいがありそうなテーマにも思えます。WindowsなんかではDirectXのコードがクライアント側のGPUを使って動く、なんて話もあるみたい(*1)なので、これから面白くなってくるのかなぁ、なんて思ってます。クラウドを考える際の切り口の1つとしても面白いのではないでしょうか。

百聞は一見にしかず、という事でYouTubeで見つけた紹介動画を埋め込んでおきます。


(*1): Windows 7のRDP 7ではDirect 2D/3Dがクライアント側でレンダリングされる話があったのですが、製品レベルに到達しなかったのか、RCからリリース版に移る際に外れてしまったようです。

2010年11月4日木曜日

Lions' Commentary (9) - 読書会#1 -

なんと、Lions' Commentary on UNIX 読書会が開かれる、という事で参加してきました。
しばらく独りで読み進めていましたが、まさか読書会に巡り会えるとは!
って、改めて読み返してみると、日記のエントリに1年近いブランクがありますね。
やばいやばい・・・時間が経つのが早過ぎる。

読書会ってのははじめてだったんですが、この会では1章ずつ読み進めます。
まずは時間が与えられて個々に内容を理解。その後、みんなで内容について議論する、と。
初回だったので進め方を決めて、さわりを・・・って感じでしたが、次回からは
月1回のペースで2章ずつ進める事になりました。途中参加もOKだと思うので
興味のある方がいたらぜひ。

1章〜4章は飛ばして実際の内容に入る5章からやりました。
pre K&RなC言語がわけわからんので、やっぱり3章も見とけば良かった・・・
みたいな話もありますが、まぁ必要になったら戻るという感じで。

5章はメモリ管理とpanic用の簡易版printfのところです。
mallocとかprintfとかいう名前だけど、libcの同名関数とは別物なので注意。
内容自体は簡単ですが、癖のある当時の文法が一番の悩みどころでした。

・型について
intとポインタはともに16bit。unsignedという考え方は存在しない。
  unsignedが必要な場合はポインタで代用する!!!
  このためstructのm_sizeの型がchar *になっている。
  ちなみにv7ではm_sizeはsigned shortだったりするけど。。。

・キャストが存在しない
  キャストがないため「*((int *)アドレス直値) = 書き込み値」みたいに書けない。
  そのかわり「->」を使うと型に関係なく構造体だと思ってメンバを探しにいく仕様で、
  構造体の名前空間も単一。これを利用して例えばintのメンバintegを持つ構造体を定義し、
  「アドレス直値->integ」と書く事で「*((int *)アドレス直値)」と等価になる。
  現代人からすると超キモイ仕様。

・大域変数にextern宣言をしない
  これは今でも通用するみたいなので、きちんとC言語の仕様をあたればそういう物かも。
  大域変数はあるソース(.c)に実体を置き、他のソースからはヘッダ(.h)にextern宣言
  された物をincludeして使う、というのが今時の描き方。
[foo.c]
 int global_variable;
 ...

[foo,h]
 extern int global_variable;
 ...

[bar.c]
 #include "foo.h"

 void bar (void) {
  global_variable = 1;
  ...
 }
  なのだが、実はexternは省略しても良い。リンク時に型や初期値の不整合がなければ
  1つにまとめてくれる。実際、coremapとswapmapはsystem.hで宣言されており、
  extern修飾もなければソース側での実体定義も存在しない。

・ldiv/lremはなぜアセンブラ?
  当時のC言語は乗除算は使えなかった? とか効率の良いコードが吐けなかった?
  とか憶測が飛び交ったけど、結局は妥当な理由が見当たらない。
  6th Cでのサポートはoracchaさんが試して通る事を確認しているし、吐かれるコードも
  悪くはない(関数にしなければinlineで展開されるのでprolog/epilogも不要なはず)。
  単純に前の版までアセンブラで書いてあったのでそのまま流用?とも思える。
  あるいはKernel書いてる頃にはコンパイラがサポートしておらず、その後ユーザランドを
  作ってるうちに面倒になってコンパイラサポートが入った、とか。
  v7ではアセンブラは廃止されてCの演算子で書かれてたりするので、この線が濃厚か。
  ちなみに、printnの最後はv6では「putchar(lrem(n, b) + '0');」という全うな書き方だが、
  v7では「putchar("0123456789ABCDEF"[(int)(n%b)]);」という微妙な書き方に。。。
  どう考えても遅いしメモリも食う書き方なので、こう書けるようになったのが嬉しくて
  つい書いてしまったコードに違いない。

・改行後のDEL
  改行がCRとLFにわかれているのと同様、歴史的な配慮?
  consoleがプリンタだった場合、描画位置を左に戻して、紙を送って・・・
  といった時間がかかるので、少し時間稼ぎ。
  プリンタがreadyを返さなければXSTのbusy loopで待つ気もするけど。

・8進数
  DECはPDP1の頃に8進数を使う文化が。。。
  詳細はこの辺とか。

・可変長引数
  stdargみたいな書き方は当然確立されていない。
  もちろん、やってる事はva_startとかva_argの中身と一緒なんだけど、
  実装を隠蔽せずに生でスタックを指すアドレスをずらして使ってる。
  なので、無理矢理ダミーの引数を並べて書いてあるけど、実は意味がない。
  引数を無駄に並べるのはv7で廃止されてるけど、実装はそのまま。

いくつか参考サイトを紹介します。
  1. 2238クラブ または 私は如何にして心配するのを止めてUNIXカーネルを愛するようになったか ... 貴重な日本語情報サイト
  2. Commentary on the Sixth Edition UNIX Operating System ... PDF版Commentaryあり
  3. Dennis Ritchie Home Page ... 6th Edition UNIXのC言語リファレンスマニュアルあり
  4. Operating System Engineering / xv6 ... x86/ANSI版
  5. xv6詳説 ... xv6のソースをひらメソッドで読む
  6. Plan9日記 2010-11-01 ... 読書会参加者のレポート
  7. やる気のないはてだ 2010-10-31 ... 同じく参加者のレポート
  8. PDP1 emulator on flash ... おまけ

2010年10月2日土曜日

デーモン君のソース探検

中古で買った読み物シリーズ。

ずっとカーネルの手引書だと勘違いしてたんだけど、実はユーザーランドのハックの話。難易度的には低いので「Cの基本は理解していて、読み書きは普通にできます」くらいの人でも大丈夫。UNIXの伝統的なユーザーランドプログラムやライブラリなどを題材に、ソースの探り方、UNIXのお作法、などを指南。

さっと一読したけど、自分にとっては探検12 scriptで出てきたptty周りのお話、探検16 malloc()で出てきたPHK mallocのお話あたりが勉強になった。というか、もう一度読まないと、理解しきれていない。

パパ:え? 最近は中学校でもmmap()を習うのか?


とか、そんな親子の会話。私もしてみたいわー。


2010年9月9日木曜日

UNIX原典(1)

ベル研メンバーによる開発秘話本って事で、読み物として買ってみました。オリジナルはBell System Technical Journal 1984年8月号第2部UNIX特集号らしい。

技術書というよりは歴史書として買ったつもりだったんだけど、結構面白い。ライオン本だと「どう実装しているのか」は理解できても、「どうしてそう実装したのか」までは理解しきれない点も多々ありますが、この本読んでると、バグの話なんかも出てきて、意外なところで実装の意図が読み取れるかも?まだ読み始めたところなんで最初だけかもしれませんが。とりあえず面白いなぁ、という話があったので、それについて書いておきます。

file descriptorの実体については"Lions' Commentary (7) - closeとunlink -"で書いたとおりで、プロセスに直接ぶら下がっているのは実体ではなく参照です。プロセス内でユニークな番号が振られているので、プロセス固有のリソースのような気がするわけですが、実はシステム固有のリソースとして実装されている。forkの話もあるので、なんとなく「そうすべき」というのは直感としてあるのですが、いまいち必然性までは理解できていませんでした。

初期のUNIX@PDP-7ではShellがCP/MのCCPに近い実装になっており、ユーザ・プログラムではなくOSの一部として動いていました。その後、forkとexecが実装され、shellがユーザ・プログラムとして分離。今風のプロセス制御も導入されて、バックグラウンド実行なんかもできるようになりました。この頃はfile descriptorの実体がプロセスにあり、以下のようなコマンド(今風に書きなおしています)が正しく動作しなかったそうです。
$ (echo foo; echo bar) > log

期待される結果は
foo
bar

ですが、実際には
bar

になってしまう。ははぁ、確かにforkとexecの挙動を考えると、file descriptorがプロセスにぶら下がっていると、そういう事になりますね。このバグに気づき、file descriptorの実体はプロセスの外に追い出された、との事です。なるほど。
また、当初はchdirも外部コマンドとして実装されていたそうです。プロセス制御がなかった頃はカレントディレクトリはttyに括りつけで1つで良かったわけで、chdirはこの括りつけのカレントディレクトリを変更して親のShellに戻っていました。ところがプロセス制御の導入によりカレントディレクトリはプロセスごとに個別に持たせる必要が生じ、その結果として外部プロセスとしてのchdirを実装できなくなりました。最近のUNIXから入ってると、むしろ初歩的なトラブルって感じがしちゃいますが、当時はUNIXの生みの親たちですら理解に苦しんだバグのようです。コロンブスの卵的なところがあるのかな。

2010年8月28日土曜日

The Singularity System

ここのところ、まったく時間がなかったのですが、出張帰りの飛行機で時間があったので、Communications of the ACMに寄稿されていたSingularityの記事を読んでみました。

SingularityとはMicrosoft Researchによる研究段階のOperating Systemです。micro kernelを採用しており、コストの高いプロセス間通信を解決する方法としてSIPと呼ばれる機構と、高位言語による記述を採用している点が特徴かと思います。

大雑把な説明はWikipediaの解説が手っ取り早いでしょうか。

Singularityでは、OSを含めて高位言語で記述するのが基本です。kernel本体はSing#と呼ばれるC#の拡張言語で記述され、他にはC#、F#、Visual Basicあたりがサポートされています。

Wikipediaを見るとSIPはmicro kernelと各サーバ群との通信を最適化するための仕組みのようにも見えますが、記事によるともっと一般的なプロセス分離の仕組みのようです。

Singularityにおけるプロセス保護は多層で実現(multiple layers of protection)され、基本的な方式がSIP、二次的な方式が仮想メモリ空間による分離(近代的なOSが一般的に採用している方法)になります。

SIPによる分離では、同一メモリ空間内にページ単位で複数プロセスを貼り付け、仮想メモリ空間切り替えによるプロセス切り替えコストを除去する事が目的となっています。ページ保護は利用しているようなので、SIPで分離されたプロセス間の切り替えでは、ページのアクセス権限だけ書き換えるのかな?この辺は詳しく書かれていないので想像です。このへんの保護は言語レベルでのサポートや形式検証なども使えるみたいです。

一般的にプロセス単位で仮想メモリ空間を切り替えるメリットは、メモリ保護だけでなくメモリ管理にもあります。プロセスにつきヒープとスタックという二種類の可変長作業域が必要となります。一般的にはヒープは下位アドレスからプラス方向に、スタックは上位アドレスからマイナス方向に伸びていくことで実現していますが、同じアドレス空間に複数のプロセスを配置するとなると、ヒープを拡張しようとしたら、別のプロセス空間が邪魔で・・・なんて状況が起き得ます。

680x0時代のMac OSでは、MMUが使えなかったため、ハンドルとコンパクションという考え方で対応してきました。メモリはハンドルで管理し、必要なときだけロックをかけてハンドルからポインタに変換します。使い終わったメモリはすぐにアンロックすることで、ポインタは無効になります。このお約束を守ることで、連続したメモリ領域がなくなった時、コンパクションによってロックされていないメモリを移動し、メモリ再配置による最適化を行うことができるようになります。

Singularityでこの辺りをどう対処しているのかは読み取れませんでした。Garbage Collectionによるメモリ管理をOS含めて導入しており、runtimeによるポインタチェックも行っているようなので、その気になればコンパクションもできるのかもしれません。今時のメモリサイズであれば、コンパクションがなくて実用上問題にならないのかなぁ。

あるいは、記事の例を見ると、そうは言ってもSIPによる分離は共有ライブラリやコンポーネント、Plug-insなどのレベルでメモリ保護を実現するための機構として利用しているようにも見え、実際には仮想メモリ空間による分離もかなり積極的に使っているのかもしれません。

SIPを導入するためには、Javaのような実行時のポインタチェック、配列領域チェックなどが必要になります。この記事の結論では、これらのruntimeのコストは5%程度の効率低下に留まり、仮想メモリ分離をやめたことによるパフォーマンスゲインが38%もあるため、総じて安い、という事のようです。

SIPを使うとプログラムはリロケータブルにする必要があるので、元々共有ライブラリで実装されていたような物は差がないんでしょうが、プロセス分離に適用しちゃうとローダがリロケートするコストが馬鹿にならないような気はしました。

SIP間通信の仕組みとしては、強く型付けされたパイプを使っているようです。実データは同一メモリ空間内のExchange Heapと呼ばれるところに格納され、ゼロコピーでデータ交換しているように読めます。このあたりが効いて、low latency、high bandwidthなプロセス間通信が実現できる、という事のようです。また、通信には"contract"と呼ばれる仕組みが使われており、この辺も突っ込んで調べてみると面白そう。通信データを入出力としてステートマシンを記述して制御しているようなので、形式検証を使ってプロトコルチェックとかできるのかも。

最後の方では、kernelにGarbage Collectionを適用する事の是非について触れられています。kernel objectの多くはlifetimeが非常に長く、プロセスの開始から終了まで張り付いています。この種のobjectを大量にGCで管理したところで、メモリ回収のwalkingにかかるコストが大きくなるだけで、あまり旨みがないのでは、といった議論があるようです。

という事で、最近は懐古趣味ばかりでしたが、めずらしく先端研究についても調べてみました。

2010年5月9日日曜日

Kerne/VM探検隊

Kernel/VM探検隊#4に参加してきました。
今回も色々と刺激的なお話が聞けました。
技術的にも、年齢層的にも、けっこう幅広いのが良いです。

yojiroさんのUSBデバイス解析の話は面白かった。
「俺の電線に流れる電気信号を観測して何が悪い」って、なるほど。
電子工作のデバッグでプロトコルアナライザ使ったら敗北感がありますが、
ソフト開発では漢のツールですね。

kobaさんのqemu+chrootの話もgood。
クロス環境作成にはいつも苦労するので。
こういう発想はなかったな。。。
linuxの/proc/sys/fs/binfmt_miscも今回はじめて知った。
そういえば、UNIXの「#!」の習慣はどこから始まったんだろう。
Ancient UNIXではこの習慣はなかった。
実行ファイルでなければシェルがそのまま実行してますよね。

ucqさんはバイナリエディタでプログラミング。
Plan 9を例にしてたけど、やっぱり一番簡単なのはX680x0の.r形式だな。
ヘッダ一切なしで、relocatableな実行列の塊。
自分は開発環境が入ってなかった部室のマシンで、しかたなく
TYPEコマンドでリセットコマンドを作った記憶がある。。。

shudoさん参加してたのか。
しかも発表側。
末尾再帰で最適化かかってるんじゃない?的なコメントもあったけど、
その時はスタックは呼び出し元と整合するように補正してからジャンプ
ってなるはずだし、やはり元作者のケアレスミスでは。

oracchaさんの発表でsocketはshutdownかけると相手側のreadが0で返り、
それをEOFとして処理するアプリが多いという事を知った。
signalが来た時もsystem call中断してhandlerに処理を移すけど、
あの時は-1か。確かにman 2 readに書いてある。
BeOSのsshがEOF検出できずに固まる事があったけど、あれはshutdownが
きちんと動いていなかったからなのだな。。。と今更気づいた。


そうそう、今朝、目覚めてふと疑問に思いました。
ファイルシステムのジャーナルってどの層でやってるんだろう。
普通UNIX系のシステムだと、デバイスの上にブロックデバイス
ドライバがあって、その上にバッファキャッシュが載っていて、
さらにその上にファイルシステムが載ってるはず。
トランザクションって意味ではファイルシステムの最下層で
ジャーナル録りたい気がするけど、その下のバッファキャッシュが
LRUで動いているので、それでは不意の終了に耐えられない。
かといってディスク直上でジャーナル録ってたら、
◯△fsのジャーナル機能が云々〜って話にはならないはずだし。