2011年2月27日日曜日

第11回 V7から始めるUNIX講座 復習とまとめ(IO割り込みとDISKドライバ)

第11回 IO割り込みとDISKドライバ

●callout
前回、何に使うのか(functionに登録するのは、何か)という話題がありました。
特定の時間が経過したら実行する処理として、何を思い浮かべるでしょうか?

そう、sleep(3)です。
カーネルの内部で使う、sleep,wakeupではなくsleep(3)です。
Section3ですので、ライブラリ(システムコールではない)です。

man 3 sleepを実行します。

最後に
SEE ALSO
alarm(2), pause(2)

と表示されます。
これは、sleep(3)がalarm(2), pause(2)の素材から出来ていることを示します。
alarm(2), pause(2)はシステムコールです。

sleep(3)とsleep(2)は全く別物なので注意が必要です。

alarm, pauseシステムコールのソースファイルは以下です。
http://tamacom.com/tour/kernel/unix/S/101.html#L357


354 /*
355 * alarm clock signal
356 */
357 alarm()
/* */
358 {
359 register struct proc *p;
360 register c;
361 register struct a {
362 int deltat;
363 } *uap;
364
365 uap = (struct a *)u.u_ap;
366 p = u.u_procp;
367 c = p->p_clktim;
368 p->p_clktim = uap->deltat;
369 u.u_r.r_val1 = c;
370 }
371
372 /*
373 * indefinite wait.
374 * no one should wakeup(&u)
375 */
376 pause()
/* */
377 {
378
379 for(;;)
380 sleep((caddr_t)&u, PSLEP);
381 }


sleep(3)のソースを見てみます
http://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/src/libc/gen/sleep.c


#include <signal.h>
#include <setjmp.h>

static jmp_buf jmp;

sleep(n)
unsigned n;
{
int sleepx();
unsigned altime;
int (*alsig)() = SIG_DFL;

if (n==0)
return;
altime = alarm(1000); /* time to maneuver */
if (setjmp(jmp)) {
signal(SIGALRM, alsig);
alarm(altime);
return;
}
if (altime) {
if (altime > n)
altime -= n;
else {
n = altime;
altime = 1;
}
}
alsig = signal(SIGALRM, sleepx);
alarm(n);
for(;;)
pause();
/*NOTREACHED*/
}

static
sleepx()
{
longjmp(jmp, 1);
}


ざっくり言いますと、sleepはpauseで眠っています。
calloutで、N秒後にSIGALRMを発生させます。
これでsleepから復帰するはずです。

(備考)
alarm(n)は、自分自身にN秒後にSIGALRMを通知する機能です。
http://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B0%E3%83%8A%E3%83%AB_(%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2)

alarmについては、ちょっと不明なので次回再度取り上げます。



●DISKドライバ

1、磁気DISK
まずは、磁気DISKをイメージして下さい。
磁気DISKは円板(プラッタ)が複数毎構成になっており、磁気ヘッドでデータアクセスします。以下のような図です。
画像


Cylinder head sector(CHS シリンダ・ヘッド・セクタ)で磁気ディスクにアクセスします。
(参考)
http://www.it-license.com/memory/harddisc.html
基本情報技術者の資料、そういや昔やった覚えが

http://ja.wikipedia.org/wiki/Cylinder_head_sector

小から大に説明すると
・セクタ:トラックを分割したもの

・トラック:セクタの集まったもの、データの記録単位で円の1周分
⇒陸上のトラックを思い浮かべると理解しやすいかな?

・シリンダ:同じトラックの集合、同じトラックを縦に見ると、筒のように見えることからシリンダと呼ぶようです。
同じシリンダ上のデータは磁気ヘッド動かずにデータの読み書きが出来ます。

よって、特定のデータを読み書きするには、
・シリンダにより磁気ヘッドの移動
・ヘッドによりどのプラッタにアクセスするか
・セクタによって、トラック内のどの部分か
により、実現します。

(備考)LBA
初期のハードディスクもこの方式でアクセスを行っていたが、容量が増大するにつれ、内側と外側のトラックでセクタ数が異なる等の理由で内部で適当な値に変換してアクセスするようになった。
どうせ変換するであれば、先頭から何番目のセクタであるかのみを指定してアクセスできた方があらゆる点で便利であるため、現在ではLogical Block Addressing(LBA)によるアクセスが主に用いられている。

2、DISKアクセスの実現
Lions本をお持ちの方は第16章RKディスクドライバを参考にして下さい。

DISKアクセスも割り込みです。
DISKドライバに読み込み、書き込みを依頼します。処理が終わると割り込みで教えてくれます。
割り込みですからベクターテーブルに記述があるはずです。
Lions本のP336に割り込みベクタの一覧があります。





ベクタ位置周辺デバイス割り込み優先度プロセス優先度
220RKディスクドライブ55


では、お馴染みのベクターテーブルの220を見てみます。
http://tamacom.com/tour/kernel/unix/S/7.html


41 . = ZERO+220
42 rkio; br5


rkioは


68 .globl _rkintr
69 rkio: jsr r0,call; jmp _rkintr


callで色々準備してから、最終的に_rkintrにジャンプします。
callでの詳細処理は前回のまとめを参照して下さい。

_rkintr(C言語のrkintr)を見てみましょう。
http://tamacom.com/tour/kernel/unix/S/40.html#L96


96 rkintr()
/* */
97 {
98 register struct buf *bp;
99
100 if (rktab.b_active == NULL)
101 return;
102 dk_busy &= ~(1 << DK_N);
103 bp = rktab.b_actf;
104 rktab.b_active = NULL;
105 if (RKADDR->rkcs < 0) { /* error bit */
106 deverror(bp, RKADDR->rker, RKADDR->rkds);
107 RKADDR->rkcs = RESET|GO;
108 while((RKADDR->rkcs&CTLRDY) == 0)
109 ;
110 if (++rktab.b_errcnt <= 10) {
111 rkstart();
112 return;
113 }
114 bp->b_flags |= B_ERROR;
115 }
116 rktab.b_errcnt = 0;
117 rktab.b_actf = bp->av_forw;
118 bp->b_resid = 0;
119 iodone(bp);
120 rkstart();
121 }


100 if (rktab.b_active == NULL)
b_activeはb_bcountです(#defineしてる)ので転送カウント?がNULLならリターンします(通常はここは通らないはず)

105から115まではエラー処理です。
105 if (RKADDR->rkcs < 0) { /* error bit */
エラービットが立っていないかのチェックです。
110から113は10回のリトライ処理です。10回やっても駄目ならb_flagsにB_ERRORを設定します。
ここで実行している、 rkstart()がデバイスに対してIOを発行する部分になります。

●ディスクコントローラのレジスタアドレス定義
ディスクコントローラのレジスタのアドレスがRKADDR(0177400)でdefineされていて、レジスタのdevice構造体にマッピングされています。
このマッピングされているものはrkstartのところで出てきます(後述)

●疑問
ディスクコントローラのレジスタアドレスに対して処理を行うことでディスクIOを行っているようですが、
これは、いわゆる「メモリマップドIO」ということでしょうか?
ディスクコントローラのレジスタアドレスがメモリ空間上にマッピングされているので、ディスクコントローラの仕様に従って操作することでディスクIOが出来る?

メモリマップドIO
http://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%A2%E3%83%AA%E3%83%9E%E3%83%83%E3%83%97%E3%83%89I/O


RKADDR
12 #define RKADDR ((struct device *)0177400)

27 /*
28 * Monitoring device bit
29 */
30 #define DK_N 1
31
32 struct device
33 {
34 int rkds;
35 int rker;
36 int rkcs;
37 int rkwc;
38 caddr_t rkba;
39 int rkda;
40 };


正常ならば
116以降、buf構造体に値を設定して、iodone、rkstartを実行
以下にコードを示します。


●iodone
http://tamacom.com/tour/kernel/unix/S/18.html#L374

・b_flagsをB_DONE(転送完了)に設定
・b_flagsにB_ASYNC(非同期:IOの完了を待たない)が立っていたらだったらbpを開放
・B_ASYNCが立っていない場合はB_WANTED(誰かが使っているので欲しいという印)を落として、wakeupでこのbpが開放されるのを待っているプロセスを起こす。

以前第8回のキャッシュバッファのところで出てきました。
http://xiangcai.at.webry.info/201102/article_1.html

2、キャッシュヒットしたが誰かが使っている。
getblkで、そのバッファが欲しいという印を付けて、スリープして、待つ。

この待っているのを起こす作業になると思います。


370 /*
371 * Mark I/O complete on a buffer, release it if I/O is asynchronous,
372 * and wake up anyone waiting for it.
373 */
374 iodone(bp)
/* */
375 register struct buf *bp;
376 {
377
378 if(bp->b_flags&B_MAP)
379 mapfree(bp);
380 bp->b_flags |= B_DONE;
381 if (bp->b_flags&B_ASYNC)
382 brelse(bp);
383 else {
384 bp->b_flags &= ~B_WANTED;
385 wakeup((caddr_t)bp);
386 }
387 }




●rkstart
http://tamacom.com/tour/kernel/unix/S/40.html#L68


68 rkstart()
/* */
69 {
70 register struct buf *bp;
71 register com;
72 daddr_t bn;
73 int dn, cn, sn;
74
75 if ((bp = rktab.b_actf) == NULL)
76 return;
77 rktab.b_active++;
78 bn = bp->b_blkno;
79 dn = minor(bp->b_dev);
80 cn = bn/12;
81 sn = bn%12;
82 RKADDR->rkda = (dn << 13) | (cn << 4) | sn;
83 RKADDR->rkba = bp->b_un.b_addr;
84 RKADDR->rkwc = -(bp->b_bcount>>1);
85 com = ((bp->b_xmem&3) << 4) | IENABLE | GO;
86 if(bp->b_flags & B_READ)
87 com |= RCOM; else
88 com |= WCOM;
89 RKADDR->rkcs = com;
90 dk_busy |= 1 << DK_N;
91 dk_numb[DK_N] += 1;
92 com = bp->b_bcount>>6;
93 dk_wds[DK_N] += com;
94 }


・転送カウントをインクリメント
・bn(block number)を取得
・dn(デバイスのマイナー番号)を取得
(補足)
bp->b_devはメジャー番号(8ビット)+マイナー番号(8ビット)が入っています。
⇒同じく第8回で出てきました。

Wikipediaのスペシャルファイルの解説
http://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%82%B7%E3%83%A3%E3%83%AB%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB

一般にメジャー番号がデバイスドライバの識別に使われ、マイナー番号がそのドライバが制御する個々の機器の識別に使われる。この場合、システムはドライバに対して引数としてマイナー番号を渡す。

まさにこれのためにマイナー番号を取得していると思います。

・cn(cylinder number)はbnを12で割った値
・sn(sector number)はbnを12で割った余り
1トラック当たりのセクタが12ということだと思います。
Lions本のP382に
セクタ/トラック 12
と記載があります。

Lions本のP383には、データ転送を開始する場合、RKDA、RKBA、RKWCを設定し、その後RKCSを設定する。とあります。

●RKDA、RKBA、RKWC


82 RKADDR->rkda = (dn << 13) | (cn << 4) | sn;
83 RKADDR->rkba = bp->b_un.b_addr;
84 RKADDR->rkwc = -(bp->b_bcount>>1);


・RKDAにマイナー番号、シリンダ番号、セクタ番号を設定しているようです。
・RKBAにbp->b_un.b_addrを設定。読み書きするバッファのアドレス?
⇒buf構造体はヘッダみたいなものでバッファの実体は別にあるはず?
・RKWCには-(bp->b_bcount>>1)を設定。転送カウントを二分の一してマイナスを付けた値?

●RKCS


85 com = ((bp->b_xmem&3) << 4) | IENABLE | GO;
86 if(bp->b_flags & B_READ)
87 com |= RCOM; else
88 com |= WCOM;
89 RKADDR->rkcs = com;


RKCSにはコマンドを記述するようです。
ReadならRCOM、WriteならWCOM
これで、実際にデバイスに対してReadあるいはWriteが実行されるはずです。

ということでrkstartをざっくりまとめると
RKDAで指定したブロック(メジャー番号、シリンダ、トラックに変換してアクセス)に対して、RKBAで指定したバッファで読み書きするってことになるかと思います。

●疑問点

rkintrは、依頼した読み書きが終わった段階で割り込みとして呼ばれるという認識です。
しかしrkintrの最後にrkstartを実行しています。
rkstartは実際にDISKに対してコマンドを送信している処理だと思うのですが、ここで実行している理由は?

readを例にして、流れを書いて見ました。(バッファキャッシュ上にデータがない場合)
この認識で合っていますか?
画像

2011年2月20日日曜日

第10回 V7から始めるUNIX講座 復習とまとめ(割り込みPart2)

第10回 クロック割り込み
今回で10回目です。あっという間でしたね。

今回はクロック割り込みです。

汎用OSでは、どんなプログラムが動くか分かりません。
中には無限ループ(while(1), for(;;))するような、プログラムがいるかもしれません。
そういうプログラムがいてもUNIXは他のプロセスの実行が出来ます。

システムコールの時には自発的に「割り込み」を発生させるわけですが、一定時間ごとに割り込みを発生させる機能もあります=インターバルタイマ割り込み。
Lions本の第11章にはクロック割り込みと記述があります。以降「クロック割り込み」と記述します。

クロック割り込みは「clock」で実装されています。
Lions本のP346に電源周波数クロックかプログラマブルリアルタイムクロックのどちらからの割り込みを処理するとあります。
クロックの呼び出しはベクターテーブルに記載があります。
http://tamacom.com/tour/kernel/unix/S/7.html

34 . = ZERO+100
35 kwlp; br6
36 kwlp; br6


ベクタアドレスが100(34行目)がベクタテーブルです。
Lions本のP336の表を見ると、





ベクタ位置周辺デバイス割り込み優先度プロセス優先度
100電源周波数クロック66
104プログラマブルクロック66


となっています。
どちらも割り込みも、PCはkwlpを実行します。
PSWはbr6(300)、8進数の300なので、11000000
5,6,7がプロセッサ優先度を示すので、110⇒6となります。上記のP336の割り込み優先度と一致しています。
kwlpはもう少し下に定義されています。


64 .globl _clock
65 kwlp: jsr r0,call; jmp _clock


jsr r0,call; jmp _clock
jsrはサブルーチンを実行する命令です。
実行すると以下のようになります。
PC=call サブルーチン:callへ分岐します。
r0=サブルーチンからの戻り先、つまりjsrの次の命令を示します。
この場合、;で命令を続けて記述しています。
よって、r0=jmp _clockとなります。



1、callの実行
callは、m40.sに定義されています。
http://tamacom.com/tour/kernel/unix/S/1.html


114 call:
115 mov PS,-(sp)
116 1:
117 mov r1,-(sp)
118 mfpi sp
119 mov 4(sp),-(sp)
120 bic $!37,(sp)
121 bit $30000,PS
122 beq 1f
123 jsr pc,(r0)+ ⇒前モードがユーザモード
124 tstb _runrun
125 beq 2f
126 mov $12.,(sp) / trap 12 is give up cpu
127 jsr pc,_trap
128 2:
129 tst (sp)+
130 mtpi sp
131 br 2f
132 1:
133 bis $30000,PS
134 jsr pc,(r0)+ ⇒前モードがカーネルモード
135 cmp (sp)+,(sp)+
136 2:
137 mov (sp)+,r1
138 tst (sp)+
139 mov (sp)+,r0
140 rtt


115から順番に
・現在のPSをスタックに積む
・現在のr1をスタックに積む
・前モードのspをスタックに積む
・さっき積んだ現在のPSW(スタック1回で2なので2個前)をもう一度スタック積む







スタック
現在のPS(マスクされた)
前モードのsp
現在のr1
現在のPS


120行目
スタックに乗せた現在のPSに対してマスクを掛けています。
bic命令はソースの非ゼロビットに対応するディスティネーションの各ビットを0クリアします。
37(8進数)は、011111。!でひっくり返しているので、1111111111100000
非ゼロを0にするので、PSWの下位5ビット以外を0クリアすることになります。

121行目
bit命令はソースとディスティネーションの論理積(AND)なので、$30000とPSの論理積
$30000(8進数)は2進数で、11000000000000です。
つまりPSWの12,13桁(前モードの状態を示すビット)をチェックします。
カーネルモードの場合は1fへ分岐します。



●前モードがカーネルモードの場合
132行以降へ分岐

133行目
PSに$30000を設定しています。(ユーザモード)

134行目
jsrでサブルーチンジャンプします。ジャンプ先はr0です。
先ほど書いたようにr0は(jmp _clock)です。

これで_clockつまりC言語のclockがコールされることになります。

●clockの実行
http://tamacom.com/tour/kernel/unix/S/84.html#L28

クロック割り込み(clock)は以下のブログが凄く詳しいので、こちらをどうぞ
http://d.hatena.ne.jp/takahirox/20110211/1297403488

以下に概要を示します。


28 clock(dev, sp, r1, nps, r0, pc, ps)

このパラメータは、こういう形で呼んでくれるようです。Linons本のP344参照
dev, sp, r1, npsは、callでスタックに積んでいました。

「タイマ割り込みのまず最初にやることは次の割り込みの設定。」

41 lks->r[0] = 0115;


callout、p2->c_timeが0より大きい最初のエントリのc_timeをデクリメントします。


54 if(callout[0].c_func == NULL)
55 goto out;
56 p2 = &callout[0];
57 while(p2->c_time<=0 && p2->c_func!=NULL)
58 p2++;
59 p2->c_time--;



callout構造体の定義は以下の通り
時間とルーチン、ルーチン用の引数を格納しています。
http://tamacom.com/tour/kernel/unix/S/53.html#L17


11 struct callo
12 {
13 int c_time; /* incremental time */
14 caddr_t c_arg; /* argument to routine */
15 int (*c_func)(); /* routine */
16 };
17 struct callo callout[NCALL];


優先度を5に設定します。
73~77でc_timeが0以下のもののc_funcを実行します。
78~84で既に実行したものは不要なので後ろから前へつめています。


71 spl5();
72 if(callout[0].c_time <= 0) {
73 p1 = &callout[0];
74 while(p1->c_func != 0 && p1->c_time <= 0) {
75 (*p1->c_func)(p1->c_arg);
76 p1++;
77 }
78 p2 = &callout[0];
79 while(p2->c_func = p1->c_func) {
80 p2->c_time = p1->c_time;
81 p2->c_arg = p1->c_arg;
82 p1++;
83 p2++;
84 }
85 }
86


ユーザモードで動作しているなら、ユーザの時間をインクリメントする
カーネルモードで動作しているなら、システム時間をインクリメントする


93 if (USERMODE(ps)) {
94 u.u_utime++;
95 if(u.u_prof.pr_scale)
96 addupc(pc, &u.u_prof, 1);
97 if(u.u_procp->p_nice > NZERO)
98 a += 8;
99 } else {
100 a += 16;
101 if (pc == waitloc)
102 a += 8;
103 u.u_stime++;
104 }


clockからcallへ戻り、135行以降を実施します。
・まず2回ポップしてspから2ワード取り除きます。
なぜcmpしてるかは、その方が高速だからだろうとV6の読書会で話題になりました。
・r1をspから戻す
・PSを取り除いて
・r0をspから戻す
・割り込みからリターンする


133 bis $30000,PS
134 jsr pc,(r0)+
135 cmp (sp)+,(sp)+
136 2:
137 mov (sp)+,r1
138 tst (sp)+
139 mov (sp)+,r0
140 rtt




●前モードがユーザモードの場合
基本はカーネルモードと同じですが、clock実行後に優先度の高いプロセスがあれば、そちらに切り替えます。
つまり、割り込まれたルーチンが再開するとは限りません。

callルーチン
123行以降を実行
123行目で_clockへジャンプします。これは前モードがカーネルモードと同じなので省略

_clockからcallへ戻り、124行以降を実施します。
runrunで優先度の高いプロセスが実行可能かどうかのチェック

なければ、スタックの内容を戻して、140行目で割り込みからリターンする

優先度の高いプロセスが存在する場合
V6だと_swtchを呼ぶだけですが、V7はちょっと複雑です。


124 tstb _runrun
125 beq 2f
126 mov $12.,(sp) / trap 12 is give up cpu
127 jsr pc,_trap


126,127行目
10進数の12をスタックに積んで、_trap(C言語のtrap)へ分岐します。

●trap
trapのswitch文の206でいきなりgoto out;です。


203 /*
204 * Allow process switch
205 */
206 case USER+12:
207 goto out;


230、231でrunrunをチェックして、qswtch()を呼びます。


225 out:
226 if(issig()) {
227 psig();
228 }
229 curpri = setpri(u.u_procp);
230 if (runrun)
231 qswtch();
232 if(u.u_prof.pr_scale)
233 addupc((caddr_t)pc, &u.u_prof, (int)(u.u_stime-syst));
234 if (u.u_fpsaved)
235 restfp(&u.u_fps);


●qswtch
http://tamacom.com/tour/kernel/unix/S/96.html#L329
qswtchは、swtchを呼んでいるだけです。
setrqは実行するプロセスの選択?


329 qswtch()
/* */
330 {
331
332 setrq(u.u_procp);
333 swtch();
334 }


●swtch
setrqのすぐ下にswtchがあります。
プロセスの切り替えのために、現在のプロセスの状態をsaveで保存し、切り替えるプロセスの状態をresumeで復帰させます。


347 swtch()
/* */
348 {
349 register n;
350 register struct proc *p, *q, *pp, *pq;
351
352 /*
353 * If not the idle process, resume the idle process.
354 */
355 if (u.u_procp != &proc[0]) {
356 if (save(u.u_rsav)) {
357 sureg();
358 return;
359 }
360 if (u.u_fpsaved==0) {
361 savfp(&u.u_fps);
362 u.u_fpsaved = 1;
363 }
364 resume(proc[0].p_addr, u.u_qsav);
365 }


●プロセス状態の保存先
プロセスの保存先はu.u_rsavのようです。
http://tamacom.com/tour/kernel/unix/S/80.html#L19


19 label_t u_rsav; /* save info when exchanging stacks */


コメントにあるとおり、(カーネル)スタックを交換する場合に情報を保存する場所だと思います。

●saveとresume
http://tamacom.com/tour/kernel/unix/S/1.html
saveとresumeは、以下のようにレジスタの値を保存したり、戻したりしています。


713 .globl _save
714 _save:
715 mov (sp)+,r1
716 mov (sp),r0
717 mov r2,(r0)+
718 mov r3,(r0)+
719 mov r4,(r0)+
720 mov r5,(r0)+
721 mov sp,(r0)+
722 mov r1,(r0)+
723 clr r0
724 jmp (r1)
725
726 .globl _resume
727 _resume:
728 mov 2(sp),r0 / new process
729 mov 4(sp),r1 / new stack
730 bis $HIPRI,PS
731 mov r0,KISA6 / In new process
732 mov (r1)+,r2
733 mov (r1)+,r3
734 mov (r1)+,r4
735 mov (r1)+,r5
736 mov (r1)+,sp
737 mov $1,r0
738 bic $HIPRI,PS
739 jmp *(r1)+

2011年2月13日日曜日

第9回 V7から始めるUNIX講座 復習とまとめ(割り込みPart1)

第9回 割り込み

●前提知識

・メモリ上に命令がリニアに並んでいる。
・PC(プログラムカウンタ)に次に実行する命令のアドレスが書いてある。
PCについては、以下を参照して下さい。
http://ja.wikipedia.org/wiki/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%A0%E3%82%AB%E3%82%A6%E3%83%B3%E3%82%BF#.E3.83.97.E3.83.AD.E3.82.B0.E3.83.A9.E3.83.A0.E3.82.AB.E3.82.A6.E3.83.B3.E3.82.BF

命令の実行は以下の3サイクルで行われる。
①フェッチ(読み込み)
②解析(デコード)
③実行

詳細は以下の「動作」を参照して下さい。
http://ja.wikipedia.org/wiki/CPU
上記より引用
プログラムは数値列として何らかのメモリに格納されている。CPUでは、フェッチ、デコード、実行という3つのステップがほぼ必ず存在する。
最初の段階であるフェッチとは、実行すべき命令(ある数値または数値の並び)をプログラムの置かれたメモリから取り出すことである。メモリ上の実行すべき命令の位置はプログラムカウンタで指定される。プログラムカウンタはCPUが現在見ているプログラム上の位置を示しているとも言える。命令フェッチに使用されると、プログラムカウンタはフェッチしたぶんだけ増加させられる。

①フェッチが終わると
PCの内容は
PC=PC+長さ(命令)となって次の命令のアドレスを指します。

(備考)
RISC:命令の長さが固定
CISC:命令の長さが可変長

分岐処理
上記だと順番に命令を実行するだけです。条件分岐したい時は、PCを書き換えます。
絶対アドレス(例:PC=xxxxxxx)
相対ブランチ(例:PC=PC+3)

●割り込み

・割り込みの例
0除算
0除算したら続行できないので、0除算したら実行するサブルーチンを用意して、そのアドレスを持っておきます。
0除算が発生したら、そのアドレスにPCを書き換えます。
⇒0除算用のサブルーチンへ分岐する。

・タイマー
あるプロセスが無限ループしているような場合でも、他のプロセスは実行できます。
ある時間経ったら、強制的にカーネルモードに戻すからです。これも割り込みです。
スケジューラが他に優先度が高いプロセスがそちらに切り替えます。
変な無限ループプロセスは優先度を最低にしたりして、調整します。
⇒スケジューラの役割

・システムコール
システムコールも割り込みを使っています。
例えばgetpid

カーネルの機能を使ってPIDを取得するわけですが、カーネルの機能を勝手にユーザから呼ばれると困ります。
なので、ユーザ側から使えるカーネルの機能を制限をしています。

●UNIX関数のライブラリとシステムコールの違い
システムコールはカーネルの機能を使います。
ライブラリは
各種処理を行うが、システムコールを呼び出さないもの
各種処理を行い、最終的にはシステムコールを呼び出し、カーネルの機能を使うもの
に分かれます。

詳細は以下を参照
http://x68000.q-e-d.net/~68user/unix/func.html

システムコールについては以下が詳しいです。
http://ja.wikipedia.org/wiki/%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E3%82%B3%E3%83%BC%E3%83%AB

●システムコールの呼び出し方
以下、getpidを例に流れを説明します。

まず、システムコールに対して番号を振っておきます。
例:getpidなら20とか

ライブラリのgetpidを見てみます。
いつもUSTで見てるいるサイトはカーネルのみです。
それ以外のソースは以下のサイトで公開されています。
http://minnie.tuhs.org/cgi-bin/utree.pl

getpidのソースファイルは以下
http://minnie.tuhs.org/cgi-bin/utree.pl?file=V7/usr/src/libc/sys/getpid.s


/ getpid -- get process ID

.globl _getpid
.getpid = 20.

_getpid:
mov r5,-(sp)
mov sp,r5
sys .getpid
mov (sp)+,r5
rts pc


sys .getpidを実行すると割り込みが起きます。

カーネルモードに移ってカーネルのgetpid()を実行します。

●ベクターテーブル
割り込みが発生した場合の対処窓口がベクターテーブルになります。

l.s(本来はlow.s)を見てください。
http://tamacom.com/tour/kernel/unix/S/7.html

これがベクターテーブルです。


15 / trap vectors
16 trap; br7+0. / bus error
17 trap; br7+1. / illegal instruction
18 trap; br7+2. / bpt-trace trap
19 trap; br7+3. / iot trap
20 trap; br7+4. / power fail
21 trap; br7+5. / emulator trap
22 start;br7+6. / system (overlaid by 'trap')


CPUは、PC(プログラムカウンタ)とStatus(特権状態かどうか、比較命令の結果ビット)を持っています。
CPUによっては呼び方が異なります。
PDP-11ではStatusは、PSW(Processor Status Word)と呼びます。
PSWは16ビットでそれぞれのビットには以下の意味があります。Lions本、P259より転載
14,15 現モード(00:カーネル、11:ユーザ)
12,13 前モード(00:カーネル、11:ユーザ)
5,6,7 プロセッサの優先度
4 トラップビット
3 N 直前の結果が負であった場合にセットされる
2 Z 直前の結果がゼロであった場合にセットされる
1 V 直前の操作がオーバーフローを示した場合にセットされる
0 C 直前の操作がキャリーを示した場合にセットされる。

ベクターテーブルは、それぞれの例外において、PCとPSWを並べたものです。
つまり、それぞれの例外において、PC(どこへ分岐して処理するか)とPSW(どういう状態で実行するか)を並べたものです。

もう一度ベクターテーブルを見てみます。
PSWがbr7+6になっています。
br7 = 340(8進数)なので、346で二進数にすると
11100110
を設定しています。

PSWの下位3ビットをどんな割り込みが発生したかを判定するために使っています。(後述)
システムコールの場合は、6を設定しています。
11100110

●疑問
PSWは16ビットですが、上記だと下位の8ビットしか設定していないように見えますが?
上位8ビットはCPU側で設定するのでしょうか?

●システムコールが発生したことの判定
PCにあたる部分が、trapになっています。
trapを実行するということです。
m40.sを見てみましょう。アセンブラのルーチンです。
http://tamacom.com/tour/kernel/unix/S/1.html


93 trap:
94 mov PS,saveps
95 tst nofault
96 bne 1f
97 mov SSR0,ssr
98 mov SSR2,ssr+4
99 mov $1,SSR0
100 jsr r0,call1; jmp _trap
101 / no return


100行目 jsr r0,call1; jmp _trap
設定を行ってから、_trap、つまりC言語のtrapへ飛びます。

C言語のtrapは、
http://tamacom.com/tour/kernel/unix/S/104.html#L34

switch文で分岐します。
49 switch(minor(dev)) {
minor(dev)
で割り込みが発生した原因を特定できます。

システムコールの場合
100 case 6+USER: /* sys call */

Case文の6は、ベクターテーブルで指定した6です。
PSWの下位を使ってないところを、割り込み原因のコードとして使ってます。
22 start;br7+6. / system (overlaid by 'trap')
 
+USERはユーザモードで発生した。
何もなければカーネルモードで発生した。
ということを示します。
システムコールはユーザ側で呼び出しますから、+USERになります。

●どのシステムコールを実行するかの判定
ここまでで、割り込みが発生し、それがシステムコールであることが分かりました。
どのシステムコールを実行するのかの判定です。
最初の方にシステムコールに番号を振っていると書きました。
sysを発行した場合にその番号を指定しています。
sys .getpid
.getpidは20

fuiwordでシステムコールを呼び出したときに指定した番号を取り出します。(sys .getpid)
104 callp = &sysent[fuiword((caddr_t)(a-1))&077];
fuiwordは以下にあります。
http://tamacom.com/tour/kernel/unix/S/1.html


615 _fuiword:
616 _fuword:
617 mov 2(sp),r1
618 fuword:
619 jsr pc,gword
620 rts pc
621
622 gword:
623 mov PS,-(sp)
624 bis $HIPRI,PS
625 mov nofault,-(sp)
626 mov $err,nofault
627 mfpi (r1)
628 mov (sp)+,r0
629 br 1f


mfpiで番号を取得しています。
LIONS本のP262に
mfpi 前のアドレス空間中の指定ワードの値をカレントスタックにプッシュする。
とあります。
前のアドレス空間、つまりユーザモードから、指定した番号(この場合は20)を取り出してカレントスタックに置きます。

その番号を元にして、sysentから実際に実行するシステムコールを決定します。
http://tamacom.com/tour/kernel/unix/S/102.html#L65

87 0, 0, getpid, /* 20 = getpid */

これでようやく、番号からカーネルモードのgetpid まで辿り着きました。

getpid を実行して値を返します。

●疑問点

大まかな流れは分かったのですが、いくつか疑問が

1、結果のリターン
プロセスIDを返す場合は、どうやってユーザモードのプロセスまで戻るのでしょう?

2、システムコールのパラメータ
getpid はパラメータがありませんでしたが、パラメータがある、例:open()の場合は、システムコールを識別する番号とパラメータをカーネルに渡す必要があると思いますが、どう渡すのでしょう?

パラメータの取り出しは以下でやっているような気がします。
120 for(; isy_narg; i++)
121 u.u_arg[i] = (*fetch)((caddr_t)a++);

また、取り出したパラメータを使っての実行がソースを見ても不明でした。

3、C言語の呼び出し
アセンブラのtrapからC言語のtrap(_trap)を呼んでいるようです。
C言語のtrapはパラメータが7つありますが、これはどう設定しているのでしょう?

34 trap(dev, sp, r1, nps, r0, pc, ps)

4、システムコールを発生させるために
22 start;br7+6. / system (overlaid by 'trap')
ベクターテーブルでシステムコールは上記のように記載しています。
sysで意図的に割り込みを発生させているわけですが、sysを発行した際に上記のベクターテーブルが使われるように何らかの設定(レジスタあるいはPSW?)をしているのでしょうか?

2011年2月6日日曜日

第8回 V7から始めるUNIX講座 復習とまとめ(キャッシュバッファ)

●第8回:キャッシュバッファ

先読み、遅延書き込みを行うための機能
「UNIXカーネルの設計」の3章 バッファキャッシュに構造やロジックが記載されています。
本日の内容を理解するのに役立ちます。お持ちの方は参照ください。
http://www.amazon.co.jp/UNIX%E3%82%AB%E3%83%BC%E3%83%8D%E3%83%AB%E3%81%AE%E8%A8%AD%E8%A8%88-Maurice-J-Bach/dp/4320025512

・遅延書き込み
ディスクIOを減らすために、書き込みの命令があってもすぐには書き込まずにメモリに保持しておいてまとめて書く。
⇒DBなどでも良くあります。
当時のUNIXのハードウエアは現在のマイコン並みの性能しかなかったので色々工夫したのだと思います。
⇒メインフレームの世界ではありえない。書き込み命令が来たらすぐ実行して終了させなければならない。
・先読み
ディスクから読み込んだ時に、次使いそうなデータも先に読んでおく。

参照するのは、buff.c、bio.c
です。
まずはバッファキャッシュの構造から
http://www.tamacom.com/tour/kernel/unix/S/52.html


21 struct buf
22 {
23 int b_flags; /* see defines below */
24 struct buf *b_forw; /* headed by d_tab of conf.c */
25 struct buf *b_back; /* " */
26 struct buf *av_forw; /* position on free list, */
27 struct buf *av_back; /* if not BUSY*/
28 dev_t b_dev; /* major+minor device name */
29 unsigned b_bcount; /* transfer count */
30 union {
31 caddr_t b_addr; /* low order core address */
32 int *b_words; /* words for clearing */
33 struct filsys *b_filsys; /* superblocks */
34 struct dinode *b_dino; /* ilist */
35 daddr_t *b_daddr; /* indirect block */
36 } b_un;
37 daddr_t b_blkno; /* block # on device */
38 char b_xmem; /* high order core address */
39 char b_error; /* returned after I/O */
40 unsigned int b_resid; /* words not transferred after error */
41 };


b_flagsは、そのバッファの状態を示すフラグです。後の関数で色々出てきます。
定義は


46 /*
47 * These flags are kept in b_flags.
48 */
49 #define B_WRITE 0 /* non-read pseudo-flag */
50 #define B_READ 01 /* read when I/O occurs */
51 #define B_DONE 02 /* transaction finished */
52 #define B_ERROR 04 /* transaction aborted */
53 #define B_BUSY 010 /* not on av_forw/back list */
54 #define B_PHYS 020 /* Physical IO potentially using UNIBUS map */
55 #define B_MAP 040 /* This block has the UNIBUS map allocated */
56 #define B_WANTED 0100 /* issue wakeup when BUSY goes off */
57 #define B_AGE 0200 /* delayed write for correct aging */
58 #define B_ASYNC 0400 /* don't wait for I/O completion */
59 #define B_DELWRI 01000 /* don't write till block leaves available list */
60 #define B_TAPE 02000 /* this is a magtape (no bdwrite) */
61 #define B_PBUSY 04000
62 #define B_PACK 010000


キャッシュバッファは双方向リストを2つ持っています。
b_forw:キャッシュの前方(forword)
b_back:キャッシュの後方(back)
av_forw:フリーリストの前方(forword)
av_back:フリーリストの後方(back)


28 dev_t b_dev; /* major+minor device name */
メジャー番号(8ビット)とマイナー番号(8ビット)を並べて16ビットで格納しています。
メジャー番号がデバイスドライバの識別に使われ、マイナー番号がそのドライバが制御する個々の機器の識別に使われます。
そのため、256個の装置を識別できます。

37 daddr_t b_blkno; /* block # on device */
ブロックが識別した装置のどの位置にあるかを示しています。

つまり「b_devとb_blknoでどのデバイスのどのブロック番号か特定できる。」 ことになります。

ファイルの読み込み

1、ファイルのOpen
システムコール:openを呼び出します。
File Descriptor(FD)と呼ばれる小さな整数を返します。
http://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E8%A8%98%E8%BF%B0%E5%AD%90

2、ファイルのRead
システムコールのreadを呼び出します
FDをFP(FilePointer)に変換します。
FPの定義は以下
http://www.tamacom.com/tour/kernel/unix/S/58.html#L8

FPからinodeへのポインタを取得します。

inodeへのポインタをパラメータとして、readiを呼び出します。

3、readi
http://www.tamacom.com/tour/kernel/unix/S/94.html#L19

bmap関数でbn(b_blkno)を取得
52 bn = bmap(ip, bn, B_READ);

inodeへのポインタからi_dev(b_dev)を取得
55 dev = ip->i_dev;

breadを呼び出します。
64 bp = bread(dev, bn);

●キャッシュのアルゴリズム
bread
http://www.tamacom.com/tour/kernel/unix/S/18.html#L56

1、キャッシュヒット(一番実行時間が短い)
パラメータとして受け取った、dev, blknoを引数として、バッファキャッシュの中を探します。
getblk
http://www.tamacom.com/tour/kernel/unix/S/18.html#L238
バッファの中に一致するものがあればそのバッファを、一致するものばなければ、一番古くて空いているバッファを返します。
受け取ったバッファの状態が「B_DONE」であれば、それを返します。


62 bp = getblk(dev, blkno);
63 if (bp->b_flags&B_DONE) {
64 #ifdef DISKMON
65 io_info.ncache++;
66 #endif
67 return(bp);
68 }


2、キャッシュヒットしたが誰かが使っている。
getblkで、そのバッファが欲しいという印を付けて、スリープして、待つ。


260 if (bp->b_flags&B_BUSY) {
261 bp->b_flags |= B_WANTED;
262 sleep((caddr_t)bp, PRIBIO+1);
263 goto loop;
264 }


他の誰かが処理が終わって、wakeupを呼ぶとsleepが解除される。
念のため、もう1回やってみてから、処理を続行する。
⇒若干原始的

3、キャッシュ上にない(ディスクを読む)
B_READをセットして、d_strategyに読み込みの依頼をして、iowaitで待ちます。
iowaitはスリープしているだけです。
d_strategyがデバイスドライバとの境目になります。


69 bp->b_flags |= B_READ;
70 bp->b_bcount = BSIZE;
71 (*bdevsw[major(dev)].d_strategy)(bp);
72 #ifdef DISKMON
73 io_info.nread++;
74 #endif
75 iowait(bp);
76 return(bp);


●先読み
breada aはaheadの略
http://www.tamacom.com/tour/kernel/unix/S/18.html#L84

基本はbreadと同じです。
パラメータが1個増えています。(先読み用のブロックを指定)

今回読みたいブロック、breadと同じです


94 bp->b_flags |= B_READ;
95 bp->b_bcount = BSIZE;
96 (*bdevsw[major(dev)].d_strategy)(bp);



先読みしたいブロック、B_ASYNCを設定します。これは非同期を設定しています。


107 rabp->b_flags |= B_READ|B_ASYNC;
108 rabp->b_bcount = BSIZE;
109 (*bdevsw[major(dev)].d_strategy)(rabp);


今回読みたいブロックだけ、待ちます。読み終わったらリターンします。
先読みしたブロックの読み込みは待ちません(非同期)
117 iowait(bp);
118 return(bp);

先読みしたいブロックは、今回読みたいブロック+1を指定しているので、次のreadの時には、先読みしたいブロックが既に読み込み中かもしくは、読み込みが終わってバッファ上にあるという動作を期待しています。

●遅延書き込み
書き込みは後でゆっくり、印だけつけといてリターン


137 if ((flag&B_ASYNC) == 0) {
138 iowait(bp);
139 brelse(bp);
140 } else if (flag & B_DELWRI)
141 bp->b_flags |= B_AGE;


●バッファのLRU
よく使われるバッファはリンクの先へ付け替えます。
使われないバッファはリンクの後ろの方へ残ります。

●バッファサイズ
V7ではどれくらいbufの領域を用意するかということをカーネルのビルド時に固定的に決めていた。
不適切な値にするとパフォーマンスが出ないということがあった。
⇒以降のUNIXの発展でこれが動的になるように変更された。