UNIX 6th システムプログラミング - init その3

はじめに

initのソースコードを、引き続き見ていきます。今回は起動時の処理であり、hangupが起きた時の再開ポイントでもある個所から始めます。

そうそう、今さらなのですが、UNIX 6th システムプログラミング(code reading)のスタイルについてです。カーネル code readingの時には、(Lions本を)自分でまとめなおすというスタイルを取っていたのですが、今回は読み進めている状況をそのまま伝える実況スタイルを取っています。そのため、進みが遅かったり、無駄に話が長いのはご容赦を。

init

http://minnie.tuhs.org/cgi-bin/utree.pl?file=V6/usr/source/s1/init.c

前回の続きの処理から見ていきます。

ファイルのクローズ
	for(i=0; i<10; i++)
		close(i);

ファイルを閉じています。i<10というのが謎です。デフォルトでは、プロセスが開くことができるファイルの数(u.u_ofile[]の要素数)は15(=NOFILE. param.hで定義)のはずなので、全てのファイルを閉じたければi<15となるはずなのですが。

端末のクローズとutmp、wtmpの更新
	switch(getcsw()) {

	case single:
	error:
		termall();
		i = fork();
		if(i == 0) {
			open(ctty, 2);
			dup(0);
			execl(shell, minus, 0);
			exit();
		}
		while(wait() != i);

	case reboot:
		termall();
		execl(init, minus, 0);
		reset();
	}

コンソールスイッチの値で分岐しています。rebootは以下のように定義されています。システムを再起動するときにも、コンソールスイッチをパチパチっと操作をするようです。

#define	reboot	0173040

まずはsingle user modeの場合を見ていきます。termall()を実行しています。

termall()
{
	register struct tab *p;

	for(all)
		term(p);
}

termall()を見ると、なんじゃこりゃ、という一文、"for(all)"があります。

実はallは以下のようにマクロ定義されていて、for(all)はitab[]の全要素を辿る処理を表します。今見ると、かなり違和感を感じる書き方ですが、当時はCの黎明期でもあり、可読性や柔軟性を高めるために色々工夫をしていたのだと思われます。(私が知らないだけで、もしかして今でもこういうやり方は使われていたりする?)

#define	all	p = &itab[0]; p < &itab[20]; p++

ちなみに、後の処理でfor(ever)という文もでてきます。

	for(ever) {

everは以下のようにマクロ定義されており、

#define	ever	;;

for(ever)はfor(;;)、つまり無限ループ(forever)を表します。なんだかお洒落な書き方ですね。

ちなみに、このallとeverはLions本読書会の時にも話題になり、会場がざわついたのを覚えています。

さて、話を本題に戻します。for(all)で操作されるitab[]は以下のように定義されています。

struct	tab
{
	int	pid;
	int	line;
	int	comn;
} itab[tabsize];

後の、端末のオープン処理を行う個所を見ると分かるのですが、itabはシステム中で使用可能であり、かつ、オープンされている端末を表すテーブルのようです。pidは端末をオープンしたプロセス(シェルプログラムを実行しているプロセスと予想)のIDのような気がします。他の二つの要素については、この後の処理を読み進めないとわからなそうです。

そのitabのエントリすべてに対しterm()を実行しています。term()はrmut()とkill()を実行し、itab[]の要素を0クリアしています。rmut()は置いておいて、kill()でSIGKILを送っているところを見ると、term()は端末(との処理を行うシェルプログラムのプロセス)のクローズ処理をしているようです。

termという単語を最初に見たときに、terminalかterminateのどちらかだろうなと予想したのですが、もしかしたらダブルミーニングなのかもしれません。

term(ap)
struct tab *ap;
{
	register struct tab *p;

	p = ap;
	if(p->pid != 0) {
		rmut(p);
		kill(p->pid, 9);
	}
	p->pid = 0;
	p->line = 0;
}

rmut()を見てみると、utmp、wtmpファイルの処理を行っています。

rmut(p)
struct tab *p;
{
	register i, f;
	static char zero[16];

	f = open(utmp, 1);
	if(f >= 0) {
		i = p->line;
		if(i >= 'a')
			i =+ '0' + 10 - 'a';
		seek(f, (i-'0')*16, 0);
		write(f, zero, 16);
		close(f);
	}
	f = open(wtmpf, 1);
	if (f >= 0) {
		wtmp.tty = p->line;
		time(wtmp.time);
		seek(f, 0, 2);
		write(f, &wtmp, 16);
		close(f);
	}
}

まずはutmpの処理から見ていきます。この処理を見るに、itabのlineは端末(のスペシャルファイル)の最後の文字に対応してそうです。そしてutmpファイルは、lineについて0-9a-zA-Z順でエントリが並んでいるのではないかと推測されます。(エミュレータでの確認要)

変数zeroはstaticなので、(スタック上ではなく、データセグメントの)データ領域に領域を確保されるのでしょうか。それならば初期値は0(のはず。Cの仕様とカーネルのexpand()あたりを確認要)です。個々の処理は、utmpファイルの該当エントリをall0クリアする処理のように見えます。

続いてwtmpファイルの処理です。前回のエントリで紹介したwtmp, utmpエントリフォーマットに対応する値を、ファイルの最後尾に書き込んでいます。(unameの設定はしていないが、問題ないのか?)

stdin, stdoutのオープン

term()実行後の処理を見ていきます。先のコードの一部を再掲します。子プロセスを生成して、何やら処理を行わせています。親プロセスは子プロセスの処理が終わるまで寝ます。

		i = fork();
		if(i == 0) {
			open(ctty, 2);
			dup(0);
			execl(shell, minus, 0);
			exit();
		}
		while(wait() != i);

cttyはシステムコンソール用端末のスペシャルファイルのパスです。

char	ctty[]	"/dev/tty8";

システムコンソール用端末をオープンし、dup()でそれを複製しています。先の処理で、オープン済みファイルの先頭10個(u.u_ofile[0-9])は全て閉じられているので、u.u_ofile[0, 1]に登録されます。これがstdin、stdoutとなります。stdin、stdoutのオープン処理はinitで行っていたのですね(まだsingle user modeの場合しか確認できていませんが)。stderrのオープン処理はどこで行っているのでしょうか。次に実行しているシェルプログラムの中のような気がします。

stdin、stdoutのオープン処理が終わったらシェルプログラムを実行しています。引数"-"の意味はシェルプログラムのソースコードを見るときに確認します。

char	minus[]	"-";

というわけで、single user modeの場合は、シェルプログラムが起動し、ユーザがシステムを操作できるようになりました。

reboot時の処理

先に載せたコードの再掲です。

	case reboot:
		termall();
		execl(init, minus, 0);
		reset();

reboot時の場合は、termall()を実行し、端末(に対応するプロセス)を全て閉じ、utmp、wtmpを更新した後に、initプログラムを再実行します。execl()(execシステムコール)に失敗すると、reset()を実行して、setexit()を実行した個所(signal(1, reset)実行直前)に戻ります。なんらかの理由でinitプログラムに対してexecを実行できない場合には、この間をぐるぐる回ることになりそうです。

今回のまとめ

長くなってきたので、ここでいったん切ります。今回読んだ内容をまとめると以下のようになります。

  • 起動時、もしくはhangupが起きると
    • u.u_ofile[]の先頭10ファイルをクローズ
    • single user modeの場合は
      • システムで使用されている端末(に対応するプロセス)を全てクローズ
      • utmpとwtmpの更新
      • システムコンソール端末をstdin、stdout用にオープン(stdin, stdoutをオープンしていたのはinitだった)
      • その後シェルプログラムを実行し、ユーザはシステムを操作できるようになる
    • rebootの場合は
      • システムで使用されている端末(に対応するプロセス)を全てクローズ
      • utmpとwtmpの更新
      • initプログラムを再実行
    • それ以外の場合(複数端末が有効の場合)は
      • 次回以降のお楽しみ
  • for()文の条件式をマクロ定義するというお洒落な書き方をしている個所がある


single user modeの場合、シェルプログラムを実行したときのプロセスの状態を表したのが下図です。


終わりに

次回も引き続きinitのソースコードを見ていきます。このペースだと、後三回くらいはinitにエントリを消費しそうな予感です。