UNIX 6th システムプログラミング - init その4
はじめに
今回も引き続きinitのソースコードを見ていきます。今回でようやくinitも一段落です。
init
http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/source/s1/init.c
前回のエントリで、single user modeの時の起動処理を確認することができました。
今回扱うコードは以下です。複数の端末が有効な場合(以降multi user modeと呼ぶ)の処理を見ていきます。
fi = open(ifile, 0); q = &itab[0]; while(rline()) { if(line.flag == '0') continue; for(all) if(p->line==line.line || p->line==0) { if(p >= q) { i = p->pid; p->pid = q->pid; q->pid = i; p->line = q->line; p->comn = q->comn; q->line = line.line; q->coms[0] = line.comn; q++; } break; } } close(fi); if(q == &itab[0]) goto error; for(; q < &itab[tabsize]; q++) term(q); for(all) if(p->line != 0 && p->pid == 0) dfork(p); for(ever) { i = wait(); for(all) if(p->pid == i) { rmut(p); dfork(p); } }
ttysファイルの処理
fi = open(ifile, 0);
ifileはttysファイルのパス名を表します。
char ifile[] "/etc/ttys";
ttysファイルの仕様をマニュアルで確認します。
http://man.cat-v.org/unix-6th/5/ttys
1行ごとに、各端末の設定を3文字で表します。
左 | 0:その行(端末)は無効 1:その行(端末)は有効 |
中 | 端末スペシャルファイルの最後の文字(端末のスペシャルファイルの名前は/dev/ttyX(Xは任意の文字)→init解説第一回のエントリで紹介したSETTING UP UNIX - Sixth Editionを参考のこと) |
右 | gettyプログラムに渡される引数 |
実際にttysファイルの中を見てみると、こんな感じになっています。
http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/etc/ttys
000 010 020 030 040 050 060 070 18- 090 0a0 0b0 0f0 0g0 0h0 0i0 0j0 0k0 0l0 0m0 0n0 0o0 0p0 0q0 0r1 0s0 0t2 0u0
このファイルの例では、/dev/tty8のみが有効になっています。/dev/tty8はシステムコンソール用端末です。おそらくsingle user mode用の設定ですね。multi user modeだと、/dev/tty8以外にもいくつかの端末が有効になるように設定されるのだと思います。(システムコンソール用の端末/dev/tty8は常に有効としなければならなかったはず)
while(rline()) {
rline()で、ttysファイルから一行読みだしているのだろうと推測できます。実際にrline()の中身を見てみます。
rline() { static char c[4]; if(read(fi, c, 4) != 4 || c[3] != '\n') return(0); line.flag = c[0]; line.line = c[1]; line.comn = c[2]; return(1); }
ttysファイルから4文字を読み出し、それが3文字+改行(\n)でなければ0を返します。フォーマットチェックですね。
読みだしたデータが妥当だった場合、グローバル変数lineに値を代入し、1を返します。このlineのデータを使って、init()の処理が継続されます。
while文の中を見ていきます。
q = &itab[0]; while(rline()) { if(line.flag == '0') continue; for(all) if(p->line==line.line || p->line==0) { if(p >= q) { i = p->pid; p->pid = q->pid; q->pid = i; p->line = q->line; p->comn = q->comn; q->line = line.line; q->coms[0] = line.comn; q++; } break; } }
3文字の左が0ならば、そのエントリを無視します。その次に例のfor(all)=for(p = &itab[0]; p < &itab[20]; p++)が続きます。現在は起動時の処理を想定しており、itabのエントリは全て空だという前提で見ていきます。
itabの先頭からttysの(有効な)エントリを詰めていきます。pとqの交換処理(itab中のエントリの位置交換)は何を想定しているのでしょうか? 再起動時用の処理? 古いitabエントリはitabの後ろに追いやられ、ttysから読みこんだ内容がitabの前半にくるように見えます。
itab[]の設定が終わった後の処理を見ていきます。
close(fi); if(q == &itab[0]) goto error; for(; q < &itab[tabsize]; q++) term(q);
ttysファイルを閉じ、ttysファイルに有効なエントリが何もなければエラーとなります(少なくともシステムコンソール用の端末が必要だから?)。そして、ttysファイルから読み込んで登録したエントリの後ろのitabエントリをクリアします。先ほどの古いエントリを後ろに追いやる処理は、ここの処理と関係がありそうです。古いitabエントリは全てクリアされる、ということのようです。
端末に対応したプロセスの生成
for(all) if(p->line != 0 && p->pid == 0) dfork(p);
そして、itab[]に登録された有効なエントリに対し、dfork()を実行しています。dfork()の中身を見ていきます。
dfork(ap) struct tab *ap; { register i; register char *tty; register struct tab *p; p = ap; i = fork(); if(i == 0) { signal(1, 0); tty = "/dev/ttyx"; tty[8] = p->line; chown(tty, 0); chmod(tty, 0622); open(tty, 2); dup(0); execl("etc/getty", minus, p->coms, 0); exit(); } p->pid = i; }
dfork()はfork()を実行して、端末に対応したプロセスを生成する関数のようです。ここで、標準入力と標準出力をオープンしているのが確認できます。生成されたプロセスは/etc/gettyを実行するようです。次は/etc/gettyのソースコードを見ていこうと思います。
生成されたプロセスのIDはitab[]に登録されます。
initプロセス(proc[1])は、続いて下記のコードを実行します。
for(ever) { i = wait(); for(all) if(p->pid == i) { rmut(p); dfork(p); } }
ここが、カーネルのwaitシステムコールハンドラを読んだ時に出てきた「initプロセスが宙に浮いたプロセスを始末している個所」でしょう。wait()を永久に実行(for(ever)=for(;;))しているのが確認できます。(親プロセスが先に終了してしまい)中に浮いてしまったプロセスがexitシステムコールハンドラによりinitプロセスの子プロセスにされることと、waitシステムコールハンドラで子プロセスの後始末をしていることを思い出してください。
終了したプロセスが端末に対応したプロセスだった場合は、rmut()(utmpとwtmpを更新する関数)を実行した後に、dfork()を実行して再度プロセスを生成しています。再度プロセスを生成するということは、端末に対応するプロセスは(終了されたとしても)常に存在しないといけないということでしょうか。
まとめ
今回読んだ内容をまとめると以下のようになります。
- multi user modeの場合は、ttysファイルの有効なエントリに対応したプロセスが生成される
- このときに標準入出力がオープンされる(標準エラー出力はまだ)
- 生成されたプロセスは/etc/gettyを実行する
- itab[]は端末に対応するプロセスを管理するための配列
- initを実行したプロセスはプロセスの生成処理が終わると、宙に浮いたプロセスを始末するための無限ループに入る
multi user mode時に、initを実行したプロセスが無限ループに入った時の、各プロセスの状態を絵で表すと以下のようになります。
というわけで/etc/initのソースコードを一通り確認することができました。カーネルプログラムの中身を知っているので、特に難しいところはありませんでした。もしカーネルのソースコードを読んでいなければ、何をやっているのかよくわからなかったでしょう。
カーネルのソースコードを読んでいるだけでは不明だった、標準入出力をオープンする処理と、proc[1]が宙に浮いたプロセスを後始末する処理も確認できました。やはり、カーネルプログラムとカーネルが提供する機能を使用したユーザプログラムの両方を追うと理解が深まります。