FPUを使えるようにしよう!
- (by K, 2006.09.30)
- 発展課題のページ(全部読む前にここを読むのは混乱するのでおすすめじゃないです)
- 30日目までの「はりぼてOS」のままでは、1.23や3.14などの小数を含む計算、つまりfloatやdoubleの計算ができません。タスク切り替え時のFPUレジスタの保存・読み込みに対応できていないためです。FPUというのは、floatやdoubleに関する計算を一手に引き受けるCPU内部の回路のことです(昔はCPUとは独立した半導体になっていましたが、486DX以降はCPU内に組み込まれました)。
- なお、このページのとおりにしても、387を拡張していない386機や486SX機ではFPUがないので、floatやdoubleは使えません。
仕組み
- タスク切り替え部分にFPUレジスタの保存と読み込みを付ければいいだけだろうと思うかもしれませんが、実はそうではありません。その方法だとタスク切り替え時間が長くなってしまい、ダメなOSになってしまいます。
- FPUのレジスタは80ビットのものが8本もあって、それに制御用のレジスタがいくつかついて、結果として合計108バイトを保存したり読み込んだりすることになるのですが、こんなたくさんのデータをタスク切り替えごとに読み書きしていたら、タスク切り替え時間が倍増してしまうのです。
- そもそも今までdoubleやfloatを使わなくても何とかやってこれたわけで、これは逆にいえば、FPUなんかめったに使わないのです(だから386までは別売りオプションだったわけですしね)。そのとき動いているすべてのタスクを見回してみても、FPU命令を使うタスクなんてきっと数個でしょう。もしかしたら1個かもしれません。もし1つしかないとしたら、そもそもタスク切り替えのたびにFPUレジスタを切り替える必要なんて全くありません(たとえそのときの総タスク数が100個だとしてもです)。・・・というか0個の場合が最も多いかもしれません。0個ならもちろんFPUレジスタの切り替えも不要です。
- ということでムダなFPUレジスタの保存や読み込みをしないようにするためにいろいろ工夫するのですが、CPUのほうもOSが当然そういう工夫をするだろうということでそれを前提に命令が用意されています。
- struct TASK の中に int fpu[108 / 4]; を追加。これはタスクがFPUレジスタを使った場合の保存先・読み込み元。
- struct TASKCTL の中に int tss_fpu; を追加。これは現在のFPUレジスタがどのタスクに所属しているものなのかを記憶させておくためのもの。
- CPUはタスクスイッチをするたびに、自動でCR0レジスタ内のTSビットを1にする(これは今までもやっていた)。
- CPUはFPU命令を実行するにあたり、TSビットをチェックする。もしTSビットが1だったら、FPU命令は実行せずに、INT(0x07);の例外を起こす(これも特に設定しなくてもやってくれる)。
- OSはこのINT07の発生を確認したら、tss_fpuの値と現在のTRレジスタの値を比較する。もしこれが一致したら、FPUレジスタの切り替えの必要はないので、TSビットを0にするだけで他には何もせずにIRETDする。一致しない場合は、tss_fpuの値で示されるタスクのfpu[]へレジスタデータを保存し、現在のTRのfpu[]を読み込み、その後にTSビットを0にしてIRETDする。
改造点
- naskfunc.nas
(中略)
GLOBAL _clts, _fnsave, _frstor, _asm_inthandler07
EXTERN _inthandler07
_clts: ; void clts(void);
CLTS
RET
_fnsave: ; void fnsave(int *addr);
MOV EAX,[ESP+4] ; addr
FNSAVE [EAX]
RET
_frstor: ; void frstor(int *addr);
MOV EAX,[ESP+4] ; addr
FRSTOR [EAX]
RET
_asm_inthandler07:
STI
PUSH ES
PUSH DS
PUSHAD
MOV EAX,ESP
PUSH EAX
MOV AX,SS
MOV DS,AX
MOV ES,AX
CALL _inthandler07
CMP EAX,0
JNE _asm_end_app
POP EAX
POPAD
POP DS
POP ES
IRETD ; INT07では ESP += 4; はいらない
- mtask.c
(中略)
struct TASK *task_init(struct MEMMAN *memman)
{
(中略)
task_run(idle, MAX_TASKLEVELS - 1, 1);
taskctl->task_fpu = 0; /* ココ! */
return task;
}
struct TASK *task_alloc(void)
{
(中略)
for (i = 0; i < MAX_TASKS; i++) {
if (taskctl->tasks0[i].flags == 0) {
(中略)
task->tss.ss0 = 0;
task->fpu[0] = 0x037f; /* CW(control word) */ /* ココから */
task->fpu[1] = 0x0000; /* SW(status word) */
task->fpu[2] = 0xffff; /* TW(tag word) */
for (i = 3; i < 108 / 4; i++) {
task->fpu[i] = 0;
} /* ココまで */
return task;
}
}
return 0; /* もう全部使用中 */
}
int *inthandler07(int *esp)
{
struct TASK *now = task_now();
io_cli();
clts();
if (taskctl->task_fpu != now) {
if (taskctl->task_fpu != 0) {
fnsave(taskctl->task_fpu->fpu);
}
frstor(now->fpu);
taskctl->task_fpu = now;
}
io_sti();
return 0;
}
- bootpack.c
(中略)
void close_constask(struct TASK *task)
{
(中略)
memman_free_4k(memman, (int) task->fifo.buf, 128 * 4);
io_cli(); /* ココから */
task->flags = 0; /* task_free(task); の代わり */
if (taskctl->task_fpu == task) {
taskctl->task_fpu = 0;
}
io_sti(); /* ココまで */
return;
}
- dsctbl.c
(中略)
void init_gdtidt(void)
{
(中略)
/* IDTの設定 */
set_gatedesc(idt + 0x07, (int) asm_inthandler07, 2 * 8, AR_INTGATE32); /* ココ! */
set_gatedesc(idt + 0x0c, (int) asm_inthandler0c, 2 * 8, AR_INTGATE32);
(中略)
}
サンプルアプリ(1)
- sincurve.c (stack=16K, malloc=0)
#include "apilib.h"
#include <math.h>
void HariMain(void)
{
char buf[160 * 100];
int win, i;
win = api_openwin(buf, 160, 100, -1, "sincurve");
for (i = 0; i < 144; i++) {
api_point(win, i + 8, sin(i * 0.05) * 30 + 60, 0);
}
api_getkey(1); /* 何かキーを押せば終了 */
api_end();
}
サンプルアプリ(2)
- pi.c (stack=1K)
#include "apilib.h"
void HariMain(void)
{
/* 超頭悪い円周率計算 pi = 4 arctan(1) = 4(1-1/3+1/5-1/7+1/9-...) より */
double s = 0.0;
int i, d;
for (i = 1; i < 500000000; i += 4) { /* 5億くらいまでやらないと値がまともにならない */
s += 1.0 / i - 1.0 / (i + 2);
}
s *= 4.0;
for (i = 0; i < 15; i++) { /* 10未満の正の数を表示 */
d = (int) s;
api_putchar('0' + d);
s = (s - d) * 10.0;
if (i == 0) {
api_putchar('.');
}
}
api_putchar('\n');
api_end();
}
- これでも3.1415926までしか一致しません(正しくは3.14159265358979323...)。
- エミュレータなどで試す場合は5億の部分を減らしたほうがいいかもしれません。
- これはdoubleやfloatをprintf等を使わずに表示する例として出しました。
注意事項
- この改造によりMMX命令も使えるようになります(MMX命令はFPU命令と同じレジスタを使うため)。もちろん対応していないCPUではダメですが。
- CD-ROMに付属のmath.hには、sin()、cos()、sqrt()くらいしか入っていません。もちろん自分で追加すれば他の関数だって使えますよ。
- CD-ROMに付属のsprintf()には、%fや%eを入れていないので、float/doubleの値の表示には使えません。
こめんと欄
|