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する。

改造点

  • bootpack.h
    (中略)
    
    /* naskfunc.nas */
    (中略)
    void clts(void);
    void fnsave(int *addr);
    void frstor(int *addr);
    void asm_inthandler07(void);
    
    (中略)
    struct TASK {
        (中略)
        struct TSS32 tss;
        int fpu[108 / 4]; /* ココ! */
        struct SEGMENT_DESCRIPTOR ldt[2];
        (中略)
    };
    (中略)
    struct TASKCTL {
        int now_lv; /* 現在動作中のレベル */
        struct TASK *task_fpu; /* ココ! */
        char lv_change; /* 次回タスクスイッチのときに、レベルも変えたほうがいいかどうか */
        (中略)
    };
    (中略)
    void task_sleep(struct TASK *task);
    int *inthandler07(int *esp); /* ココ! */
    
    (中略)
    

  • 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の値の表示には使えません。

こめんと欄


コメントお名前NameLink

リロード   新規 編集 差分 添付   トップ 一覧 検索 最終更新 バックアップ   ヘルプ   最終更新のRSS
Last-modified: 2006-10-07 (土) 19:21:14 (5138d)