UNIX 6th code reading - fork, exec, breakシステムコール

はじめに

今回は12章の続きを追います。

システムコールのfork, exec, breakを見ていきます。

Lions本ではexec, fork, breakの順に扱っていますが、fork, exec, breakの順に見ていきます。こちらの方が理解しやすいと思います。

fork & exec system call

forkとexecシステムコールはセットで使用されることが多いです。まずはそれぞれの概要を見ていきます。

forkとは

forkはプロセスを新たに生成するシステムコールです。内部でnewproc( )を呼び出します。

新しいプロセスはforkシステムコールを呼び出したプロセスのコピーと言えます。ただしpidは新しいid, ppidはforkシステムコールを呼び出したプロセスのpidになります。詳細はnewproc( )の1861-1869あたりや以前のエントリを参考にしてください。

forkシステムコールを絵で表すとこんな感じです。


execとは

execは、execシステムコールを呼び出したプロセスを「あるプログラムを実行するプロセス」に切り替えるシステムコールです。切り替えることをオーバーレイと呼ぶそうです。

絵で描くとこんな感じです。


fork & exec

forkとexecを使うことで、あるプログラムを実行することができます。

このときの流れを絵で描くとこんな感じです。

アセンブリコードで書くとこんな感じです。

  sys fork
  br hoge
  sys wait

  ...

hoge:
  sys exec; name; args
  sys exit

forkを呼び出したプロセスはsys fork実行後、brを飛び越してsys waitを呼び出して寝ます。新プロセスはswtchで選択された後、brでhogeに飛んでexec実行した後にexitでプロセスを終了します。

ここの詳細はソースコードで確認できます。

forkのソースコード

forkのソースコードを見ていきます。

forkを呼び出したプロセスと新しく生成されたプロセスの二つの観点でソースを見ていきます。

・forkシステムコールを呼び出したプロセス

  • 3326-3331 : proc配列から空きエントリ(p_stat==NULL)を探す。なければエラー
  • 3334 : newproc( )を呼び出して新しいプロセスを生成。newprocからは0が返ってくるので3344へ
  • 3344 : forkの返り値として新しいプロセスのpidを(forkシステムコールを呼び出した)ユーザプロセスのr0に格納
  • 3347 : forkシステムコールを呼び出したユーザプロセスのpcをインクリメントする。


3347でpcをインクリメントするので、先に掲載したアセンブリコードではsys fork実行後、br hogeを飛び越してsys waitが実行されます。

・新しく生成されたプロセス

forkを呼び出したプロセスは(たいていの場合?)waitシステムコールを呼び出されます。wait内ではsleepが呼び出され、プロセスは寝ます。sleep内でswtchが呼び出され、実行プロセスが切り替わります。

swtchで新しく生成されたプロセスが選択されることで、初めて新しいプロセスは動き出します。

waitシステムコールを実行したときに呼ばれるswtchで新プロセスが選択されるとは限らないです(たぶん)。

  • 3334 : swtchで選択されたときswtchから1が返ってきてここから処理が開始される。6章の時と同じ話なので以前のエントリを参考のこと
  • 3335 : 返り値として?新プロセスのr0に親プロセス(システムコールを実行したプロセス)のpidを格納する。
  • 3336-3342 : 時刻関係の値を初期化しreturn


先に掲載したアセンブリコードでは、このあとforkの次の命令であるbrが実行されhogeに飛びます。

execのソースコード

manulalで確認すると、execへの引数は以下のようになっています。

sys exec; name; args

...

name: ...\0

...

args: arg0; arg1; ...; 0
arg0: ...\0
arg1: ...\0

...

nameはプログラムファイルのパス、argsはプログラムの引数です。

name, argnは最後がnull(\0)文字の文字列です。

・プログラムファイルのinodeを取得&プログラムへの引数を詰めるためのバッファを取得する

inodeに関しては後の章で出てくる(はず)なので、今回は深く追いません。

また、プログラムへの引数はバッファに詰めてからユーザスタックに積みます。バッファの詳細は後の章で出てくる(はず)なので、inode同様深くは追いません。

  • 3034-3036 : プログラムファイルのinodeを取得する。uchar(7689)はu.u_dirp(=u.arg[0])から文字列を取得するための関数だと思われる。inodeの取得に失敗したらエラー
  • 3037-3039 : exec実行中のプロセスが(NEXEC=3)以上ならsleepを呼び出して寝る。この理由は3009からのコメント参照。引数を詰めるためのバッファ取得待ちを気にしているらしい。exec実行中のプロセスがNEXEC以下ならexecnt(exec実行中のプロセスをカウントしている変数)をインクリメントしてexec処理を開始する
  • 3040 : getblkを使って引数を詰めるためのバッファを取得する。このバッファは512byteらしい
  • 3041-3042 : 実行するプログラムのinodeが実行可能ではなく、かつ、ディレクトリや特殊キャラクタファイルならばエラー


・実行プログラムに対する引数をバッファに詰める

  • 3049 : cpにバッファの先頭アドレスを格納
  • 3050 : naを0に。naはたぶんthe number of arguments.
  • 3051 : ncを0に。ncはたぶんthe number of characters.
  • 3052-3070 : argsを一つずつ処理
    • 3053 : naをインクリメント
    • 3054-3055 : 3052のfuwordが例外で失敗したらエラー
    • 3056 : 引数のポインタをインクリメントして次の引数を指すようにする
    • 3057-3069 : 引数の文字列をバッファに詰める
      • 3058 : 1byte取得
      • 3059-3060 : 3058のfubyteが例外で失敗したらエラー
      • 3061 : バッファに1byte詰める
      • 3062 : ncをインクリメント
      • 3063-3066 : 引数がバッファサイズより大きくなったらエラー
      • 3067-3068 : 取得した文字列がnull文字なら次の引数へ
  • 3071-3074 : ncが奇数ならばバッファにnull文字を追加し、ncをインクリメント。この処理はなんのため?


・プログラムファイルの先頭8byte(4word)を取得

ファイルのヘッダのフォーマットはどこで確認できるだろう?

先頭4wordのそれぞれの意味は3079からのコメントを参考。

  • 3085-3089 : readiのためのパラメータを設定
  • 3090 : readiを使ってプログラムファイルの先頭8byteを取得
  • 3091 : u.u_segflgをリセット(なんのため?)
  • 3092-3093 : readiでエラーがあればbadへ(エラー処理)
  • 3095-3104 : ファイルの種類?によって分岐。else ifで分岐しているので、前方のif文がtrueが後方のif文は選択されない
    • 3095-3097 : W0が0407(テキストセグメントが保護されていない?)ならばデータセグメントサイズにテキストセグメントサイズを足す。テキストセグメントサイズは0にする
    • 3098-3100 : W0が0411(分離I&D?)ならばsepをインクリメント
    • 3101-3104 : W0が0410(read only?)ではないならばエラー
  • 3105-3108 : 純粋なテキストセグメントを持っていて(?)、かつ、別のプログラムがデータファイルとして開いていたらエラー


・テキストサイズとデータサイズの可能な限り拡張する

何のため?

ここの処理以降、理解が足りていません。スワップ処理やファイル処理などを理解してから復習する予定です。

  • 3116 : テキストセグメントサイズの7bit目を切り上げて、6bit右シフト。これと10bit all'1'(=1023)の小さい方をテキストセグメントサイズとして選択
  • 3117 : データセグメントサイズとbssサイズを足したものでも3116と同じ処理をする
  • 3118 : estaburを使ってユーザセグメンテーションプロトタイプを設定。失敗したらエラー


・現在のコアの割り当てをクリアし、新しいコアを割り当て

  • 3127 : たぶんxfreeのためのパラメータ設定
  • 3128 : xfree(内部でmfreeを呼ぶ)を使って共有セグメントをクリア
  • 3129 : expandを使って、データ+スタック領域をプロセス固有領域(USIZE)にまで縮小する
  • 3130 : xallocを使って共有テキストセグメントを割り当てる
  • 3131-3132 : データ+スタック領域をプロセス固有領域とデータセグメントサイズとスタックセグメントサイズを足したものに拡張
  • 3133-3134 : プロセス固有領域のみを残して、プロセスの使用領域をクリア(0クリア?)


・データセグメント読みだし

何のため?

  • 3138 : estaburを使ってユーザセグメンテーションプロトタイプを適切な設定に変更(? 3131-3134の設定に合わせている?)
  • 3139-3142 : readi用のパラメータを設定してから、readiを実行しデータセグメントを読み出す


・スタックセグメントの初期化

スタックセグメントを初期化し、スタックにプログラムに対する引数を詰めます。

manualを見ると、このように設定するらしいです。

sp value
-> the number of arguments
  arg0
  .
  .
  .
  argn
  -1
arg0: ...\0

...

argn: ...\0
  • 3148-3152 : estaburを使って、ユーザセグメンテーションプロトタイプを再設定
  • 3153 : cpにバッファの先頭アドレスを格納
  • 3154-3155 : ユーザープロセスのスタックポインタを設定。この設定で上記のようなspを指す理由は後で確認する予定
  • 3156 : スタックの先頭にnaを積む
  • 3158 : 引き数の数だけ処理
    • 3159 : スタックの後ろにcを積む
    • 3160-3162 : バッファからユーザ領域に引数をコピー
  • 3164 : スタックの後ろに-1を積む


引数の設定を絵にするとこんな感じだと思います。


引数周りはイメージしかつかめていません。今後詳細を追いたいです。

・SUID, SGID保護の設定

SUID, SGIDとは何? user id, group id? inodeを扱う章で確認できる?

  • 3170 : トレースフラグが設定されていなければ
    • 3171-3175 : inodeがISUIDでu.u_uidが値を持っているならば、inodeのuidをuser, procのuidに設定
    • 3176-3177 : inodeがISGIDならばinodeのgidをuserのgidに設定


・シグナルとレジスタをクリア

  • 3182 : inodeをコピーしておく
  • 3183-3185 : 各シグナルの設定が偶数ならば0クリアする。これの意味は13章でわかるはず
  • 3186-3188 : r0-r7をクリア。なぜr7だけ別の処理?
  • 3189-3190 : fsavを浮動小数レジスタらしい
  • 3191 : コピーしていたinodeを元に戻す。何のため?


・後処理

  • 3194 : iputを実行して、inode参照カウンタをデクリメントする
  • 3195 : バッファを解放
  • 3196-3197 : execntがNEXEC以上ならば、寝ているプロセスがいるかもしれないので起こす。その後execntをデクリメントする

sbreak

breakシステムコールを実現します。データセグメントサイズを変更します。malloc周りで使用されるらしいです。

参考 : http://twitter.com/superhoge/status/45767535307657216

  • 3364 : 引数で渡されたデータサイズをセグメントサイズに直している?
  • 3365-3366 : 分離I&Dならばテキストセグメントサイズを引く
  • 3367-3368 : nが0未満なら0に設定


この時点でnには新しいデータセグメントのサイズが入っている(たぶん)

  • 3369 : dに新しいデータセグメントサイズと現在のデータセグメントサイズの差分を格納
  • 3370 : nにプロセス固有領域とスタックセグメントのサイズを足す
  • 3371-3372 : estaburでユーザセグメンテーションプロトタイプを設定。失敗したらエラー
  • 3373 : u.u_dsizeを新データセグメントサイズに変更
  • 3374-3375 : 新データセグメントサイズが拡張なのか縮小なのかで分岐。各区町ならば3386に飛ぶ
    • 縮小
      • 3376-3382 : 以前データ領域だった箇所にスタック領域をコピーする
      • 3383 : expandを使って余剰領域を取り除く
    • 拡張
      • 3387 : 領域を拡張するためにexpandを呼び出す
      • 3388-3393 : スタックセグメントサイズだけコピー(何から何に?)
      • 3394-3395 : データ領域だった箇所をクリア?

終わりに

fork, execシステムコールを使うことで、プログラムを実行できることが確認できました。

しかし、特にexecは理解が薄い箇所があります。スワップやファイル処理を把握してから復習したいです。

さらに7章で扱ったアドレス空間(データセグメントやテキストセグメントなど)についても復習する必要がありそうです。

それにしても、今回の内容は重かったです。次章も量が多く大変そうです。