どんなときにpanicで落ちる?UNIX V6の話(Lions本合宿のLTネタ)

これが下記を基にしたプレゼン資料です。

https://docs.google.com/present/edit?id=0AdSF0S1K6MqxZGNwNjVqOWZfMHM4cno4bWQz&hl=en_US


他の方々の資料はここで見られます。

https://sites.google.com/site/lionscommentaryonunixreading/2011autumncanp/result

はじめに

UNIX V6のコードを読み始めて思ったこと。

「んー、結構簡単にpanic( )を呼んで落ちるんだなぁ」

これを読書会で話したところ

「panicは普通起こらないエラーや設定ミスなどでしか呼ばれないよ」

とのコメントをいただきました。


それじゃざっとpanic周りの箇所を見直してみるか!と思ったのが今回の始まりです。

流れ

  • panicの中身を見直してみた
  • panicで落ちる箇所の一覧を並べてみた
  • 上記の分類をしてみた
  • panicをsimh上で起こそうとした

panicの中身を見直してみた

panic( )の中身はこうなっています。

panic(s)
char *s;
{
	panicstr = s;
	update();
	printf("panic: %s\n", s);
	for(;;)
		idle();
}

流れはこんな感じです。

  1. update( )を呼んで、まだデバイスに書き込んでいないデータをフラッシュ
  2. printf(これはKernelのprintf. libcのものではないので注意)を呼んで印字
  3. idle( )を永遠に呼び続ける


次にidle( )の中を見てみます。idle( )はアセンブリで書かれています。

.globl	_idle
_idle:
	mov	PS,-(sp)
	bic	$340,PS
	wait
	mov	(sp)+,PS
	rts	pc

流れはこんな感じです。

  1. PSをスタックに積む
  2. プロセッサ優先度を0に <- 重要!!
  3. waitを呼んで他プロセスに切り替え
  4. 再度このプロセスが選ばれたらスタックに積んでおいたPSを元に戻す
  5. return


プロセッサ優先度を0にしているので割り込みが可能です。

なのでpanicが呼ばれると以下の状態になります。

  • もう操作することができなくなる
  • 実行中だった入出力処理を完了することができる
  • クロックが時を刻み続けることができる

panicを呼ぶ箇所の一覧を並べてみた

panicを呼んで落ちる箇所は以下の14種類。


1605行目 : main( )

起動時にclockが見つからないとpanic.

	UISA->r[7] = ka6[1]; /* io segment */
	UISD->r[7] = 077406;
	lks = CLOCK1;
	if(fuiword(lks) == -1) {
		lks = CLOCK2;
		if(fuiword(lks) == -1)
			panic("no clock");
	}


1853行目 : newproc( )

新しくprocessを生成するときに、proc[ ]に空きエントリがなければpanic.

newproc( )は起動時にinit processを作成するとき、forkでprocessを生成するときにそれぞれ呼ばれる。

	for(rpp = &proc[0]; rpp < &proc[NPROC]; rpp++) {
		if(rpp->p_stat == NULL && p==NULL)
			p = rpp;
		if (rpp->p_pid==mpid)
			goto retry;
	}
	if ((rpp = p)==NULL)
		panic("no procs");


2051行目 : sched( )

swap inに失敗するとpanic.

	if((rp=p1->p_textp) != NULL) {
		if(rp->x_ccount == 0) {
			if(swap(rp->x_daddr, a, rp->x_size, B_READ))
				goto swaper;
			rp->x_caddr = a;
			a =+ rp->x_size;
		}
		rp->x_ccount++;
	}
	rp = p1;
	if(swap(rp->p_addr, a, rp->p_size, B_READ))
		goto swaper;
	mfree(swapmap, (rp->p_size+7)/8, rp->p_addr);
	rp->p_addr = a;
	rp->p_flag =| SLOAD;
	rp->p_time = 0;
	goto loop;

swaper:
	panic("swap error");


2719行目 : trap( )

Kernel mode中に浮動小数点エラー以外のtrapが起きるとpanic.

	default:
		printf("ka6 = %o\n", *ka6);
		printf("aps = %o\n", &ps);
		printf("trap type %o\n", dev);
		panic("trap");


3236行目 : exit( )

malloc( )でswap領域が確保できないとpanic.

	a = malloc(swapmap, 1);
	if(a == NULL)
		panic("out of swap");


3521行目 : unlink( )

iget( )でinodeの取得に失敗するとpanic.

	ip = iget(pp->i_dev, u.u_dent.u_ino);
	if(ip == NULL)
		panic("unlink -- iget");


4377行目 : xswap( )

malloc( )でswap領域の確保に失敗するとpanic.

	a = malloc(swapmap, (rp->p_size+7)/8);
	if(a == NULL)
		panic("out of swap space");


4451行目 : xalloc( )

text[ ]領域に空きエントリがないとpanic.

	rp = NULL;
	for(xp = &text[0]; xp < &text[NTEXT]; xp++)
		if(xp->x_iptr == NULL) {
			if(rp == NULL)
				rp = xp;
		} else
			if(xp->x_iptr == ip) {
				xp->x_count++;
				u.u_procp->p_textp = xp;
				goto out;
			}
	if((xp=rp) == NULL)
		panic("out of text");


4458行目 : xalloc( )

malloc( )でswap領域の確保に失敗するとpanic.

	if((xp->x_daddr = malloc(swapmap, (ts+7)/8)) == NULL)
		panic("out of swap space");


4928行目 : getblk( )

取得しようとしているブロックのデバイス種が大きすぎたらpanic.

	if(dev.d_major >= nblkdev)
		panic("blkdev");


4936行目 : getblk( )

bdevse[dev.major].d_tabがNULLだとpanic.

		dp = bdevsw[dev.d_major].d_tab;
		if(dp == NULL)
			panic("devtab");


6930行目 : iinit( )

起動時にroot devのsuperblockが読めなかったらpanic.

	(*bdevsw[rootdev.d_major].d_open)(rootdev, 1);
	bp = bread(rootdev, 1);
	cp = getblk(NODEV);
	if(u.u_error)
		panic("iinit");


7184行目 : getfs( )

バイスがマウントされていなかったらpanic.

	for(p = &mount[0]; p < &mount[NMOUNT]; p++)
	if(p->m_bufp != NULL && p->m_dev == dev) {
		p = p->m_bufp->b_addr;
		n1 = p->s_nfree;
		n2 = p->s_ninode;
		if(n1 > 100 || n2 > 100) {
			prdev("bad count", dev);
			p->s_nfree = 0;
			p->s_ninode = 0;
		}
		return(p);
	}
	panic("no fs");


7300行目 : iget( )

mountされたfile systemがmount[ ]に存在しない場合panic.

	for(p = &inode[0]; p < &inode[NINODE]; p++) {
		if(dev==p->i_dev && ino==p->i_number) {
			if((p->i_flag&ILOCK) != 0) {
				p->i_flag =| IWANT;
				sleep(p, PINOD);
				goto loop;
			}
			if((p->i_flag&IMOUNT) != 0) {
				for(ip = &mount[0]; ip < &mount[NMOUNT]; ip++)
				if(ip->m_inodp == p) {
					dev = ip->m_dev;
					ino = ROOTINO;
					goto loop;
				}
				panic("no imt");
			}
			p->i_count++;
			p->i_flag =| ILOCK;
			return(p);
		}
		if(ip==NULL && p->i_count==0)
			ip = p;
	}

panicが呼ばれるケースを分類してみた

大まかにですが、こんな感じにわけられると思います。

  1. 設定のミス
  2. 資源の枯渇


1.は、例えばmkconf.cで吐かれるconf.cでbdevswのrktabをNULLに書き換えたりするとpanicで落ちるはずです。後は、clockが機能していなかったりなど、システムとして正しく動作しないであろう状態なので、panicでさっさと落としてしまって特に問題ない気がします。

問題なのが2. proc[ ], text[ ]の空きがなくなったり、swap領域がなくなってmalloc( )で取得できなかったりした場合の話。

例えばproc[ ]の空きがなくなったとしても、少し待っていれば他のuser processが完了して空きができるかもしれません。

panicではなく「待つ」という選択肢も出てくると思うのですが、ではどうやって待つのか。proc[ ]に空きがないとわかるのはKernelに入ってからなので、この状態で待ってしまうと他のuser processがまともに動きません。

それを避けるためには設計を変える必要が有るでしょう。そうなるとMultixの失敗を踏まえシンプルな設計を心がけているUNIXの思想に反するのでしょう。Lions本でもそのような言及が有ります。
(個人的にはMultixに興味深々ですよ)

資源が枯渇した場合、最近のOSではどのような対処をしているのでしょうか。

panicをsimh上で起こそうとしてみた

せっかくなのでsimh上でpanicを起こしてみようと思いました。

どうやってpanicを起こすか。

目をつけたのはnewproc( ).

processをたくさん作ればproc[ ]が埋まってpanicが呼ばれるはずです。

そんなわけで以下のコードをedで書いて実行してみました。

#ed hoge.c
?
a
int main( ) {
  while( 1 ) {
    fork( ) ;
  }
  return ;
}
.
w
62
q
# cc hoge.c
# a.out

あ、あれ? 何も起こらない……

fork( )を見直し、そこでハタと気がつく。

	p1 = u.u_procp;
	for(p2 = &proc[0]; p2 < &proc[NPROC]; p2++)
		if(p2->p_stat == NULL)
			goto found;
	u.u_error = EAGAIN;
	goto out;

found:
	if(newproc()) {
... snip ...

newproc( )を呼ぶ前に、proc[ ]に空きエントリがあるかどうかを確認しています。

なので以下の二行を削除しました。

	u.u_error = EAGAIN;
	goto out;

カーネルを再コンパイルし、ログアウト->再ログインし再度上記を実行。

# cc hoge.c
# a.out

あ、あれ? やはり何も起きない……



と、いうわけで、ここで頓挫してしまいました。

自分の理解が間違っているのか、simhの仕様に依存した話なのか……


試しにカーネルの色んなところに

panic( "hoge" ) ;

なんてコードを埋め込んでみたのですが、何も起こらず。

simhの仕様な気がします。

(合宿では「お前のオペミスだろう」という突っ込みを多々受けました)

終わりに

実験が失敗したために、なんとも中途半端な感じがしますが、LTのネタはこれでお終いです。