どんなときに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(); }
流れはこんな感じです。
- update( )を呼んで、まだデバイスに書き込んでいないデータをフラッシュ
- printf(これはKernelのprintf. libcのものではないので注意)を呼んで印字
- idle( )を永遠に呼び続ける
次にidle( )の中を見てみます。idle( )はアセンブリで書かれています。
.globl _idle _idle: mov PS,-(sp) bic $340,PS wait mov (sp)+,PS rts pc
流れはこんな感じです。
- PSをスタックに積む
- プロセッサ優先度を0に <- 重要!!
- waitを呼んで他プロセスに切り替え
- 再度このプロセスが選ばれたらスタックに積んでおいたPSを元に戻す
- 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.は、例えば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のネタはこれでお終いです。