[Subject Prev][Subject Next][Thread Prev][Thread Next][Subject Index][Thread Index]

[OSASK 3649] メモリモデル.



  こんにちは、川合です。非常に長文です。すみません。

  OSASKアプリのメモリモデルの考え方をここにまとめておくのは有益
だろうと思ったので、書くことにしました。

  まずOSASKのメモリモデルにどういう特徴があるのか分かりやすくす
るために、win32などのフラットモデルについて僕の書き方で説明して
おきます。

・フラットモデル

  IA-32では、任意の個数の仮想空間を作れます。この仮想空間のサイ
ズは最大で4GBになっています。フラットモデルでは、この仮想空間を
タスクごとに用意しています(この4GBをシステム用とユーザ用に分け
ているため、アプリが4GBの全てを自由に使えるというわけではありま
せん)。

・ミディアムモデル

  OSASKでみなさんが普通に扱っているのはこれです。IA-32にはセグメ
ンテーションというものがあり、これは4GBの仮想空間の好きなように
分割してサブ仮想空間を作る機能で、しかも複数のサブ仮想空間を切り
替えて使えるようになっています。IA-32におけるセグメンテーション
については参考までに後ほどもうちょっと詳しく書いておきます。

  OSASKでは仮想空間の数はタスク数とは等しくありません。狭い仮想
空間で十分にやっていけるのなら、そういうものをまとめて一つの仮想
空間でやりくりします。アプリは自分のサブ仮想空間にしかアクセスし
ないので(はみ出すと即座に保護例外が出ます)、問題は起きません。
サブ空間の位置が4GBの中のどこなのかを意識する必要もありません。
そういうことは全部CPUが面倒を見てくれていますので。

  ミディアムモデルは、複数のコードセグメントと単一のデータセグメ
ントで構成されるメモリモデルで、OSASKにおいてはDLLを使うとコード
セグメントが複数になるので、ミディアムです。APIそのものがDLLなの
でDLLを全く使わないということはありません。皆さんは意識していな
いでしょうが。

(IA-32でのセグメンテーション)

  IA-32にはセグメントレジスタというものがあり、これにサブ仮想空
間番号を入れます。たとえばOSASKの場合、0x0007がコードバイナリだ
けが置かれているサブ空間番号で、0x00c7にはAPIを解釈するルーチン
が置かれています。また、スタックやヒープは0x000fにあります。こ
れらの番号はタスクごとにローカルに設定できるので、同じ0x0007であ
ってもその内容はタスクごとに異なります。

  IA-32では全てのメモリアクセスの際に、必ずセグメントレジスタを
指定しなければいけません。このレジスタにはサブ空間番号が入ってい
るわけです。たとえばASKAで、

    EAX = [DS:EBX + 8];           省略時 EAX = [EBX + 8];

などとやるわけです。この「DS」がセグメントレジスタです。他にCS、
ES、SS、FS、GSがあります。なお、このセグメントレジスタの指定を省
略することはできます。しかしその場合は、DSが指定されたものとみな
します(場合によってはSSやESとみなす場合もあります)。またコード
バイナリがどのサブ仮想空間にあるのか分からないとCPUは命令のある
場所が分からないので、それはCSで指定に入れておくことになっていま
す。

  フラットモデルではCS〜SSまでのセグメントが同じサブ仮想空間を指
していて、そしてそのサブ仮想空間は4GBの仮想空間そのものになって
いるというだけです。IA-32ではセグメンテーションそのものをOFFには
できないので、こうやって事実上無効にしているわけです。

  OSASKではアプリが起動したときに、

    CS = 0x0007;
    ES = SS = DS = FS = 0x000f;

となっています。これをそのまま使えばセグメンテーションを意識しな
いでプログラミングができます。必要があれば、自由に値を変えて、た
くさんのサブ仮想空間を使い分けることもできます。

---

  ここから先はもっと技術的な内容です。ここから先はモジュールとい
う語を多用しますが、それはファイルと読み替えてください。OSASKで
はファイルのことをモジュールと呼ぶのがより正確な表現なのです。

  bim2binで指定するmmarea(旧file)はリザーブしておくアドレス空間
のサイズであり、mallocやstackはアプリ起動時に必要とするL3_stack
モジュールのサイズの決定に必要なパラメータです。

  OSASKアプリが起動するときに行われることは、以下のことです(他
のこともやっていますが、関係ないので省略しています)。

1.L3_stackモジュールをタスクディレクトリ内に生成。
2.コードモジュールを適当にマッピングしてそこを0x0007でアクセス
    できるように設定。
3.L3_stackモジュールのサイズとmmareの値を加えた領域を仮想空間か
    ら切り出して、そこを0x000fでアクセスできるようにする。
4.そして0x000fのアドレス0のところからL3_stackモジュールをマッピ
    ング。
5.0x0007:0x00000000へ分岐。

(註)マッピングとローディングは違います。誤解しないでください。ロ
    ーディングは基本的にオンデマンドで行われます。マッピングとい
    うのはページフォールト時にどこからロードするのかを設定するこ
    とです。この際も共有できるページは積極的に共有されますので、
    同じ物が重複してメモリに存在してメモリを浪費するということは
    ありません。

  もしDLLを使いたくなったらどうすればいいのかという話がよくあり
ますのでそれを書いておきます。OSASKにとってDLLの扱いかたはいくつ
かあります。フラットモデルのようなやり方もできます。しかしそれは
説明するまでもないでしょうから保留にして、セグメンテーションを使
う場合に限定しましょう。

(方法1:ver.2.4でサポート済み)
  まずアプリは、DLLをmmareaのどこかにマッピングします。どこにマ
ッピングしてもかまいません。mmareaのどこに何をマップするかは、ア
プリが管理することです(もちろんこれを支援してほしいということで
したらしかるべきライブラリを用意しますから、それを使っていただい
てもかまいません)。これは事前にDLLの大きさが分かっていなければ
いけないということになります。そうでないとmmareaが不足するかもし
れませんから。したがってDLLのサイズについてはあらかじめDLL作者と
の合意に達していなければいけません。まあ、僕なら1つのDLLにつき1M
Bも取っておけば十分だとは思います。

  DLLが使うローカルなデータはどうすればいいのか?・・・これはこ
ういう風に考えてください。DLLには確かに各種のデータが必要でしょ
う。それはオブジェクトのようなものにまとめられるはずです。そして
呼び出しの際にそのthisポインタを渡せばいいでしょう。このオブジェ
クトのサイズはDLLの規定として固定でもいいですし、DLL内の特定のフ
ァンクションで、アプリ側がサイズを獲得しなければいけないというこ
とにしてもいいでしょう。つまり、アプリがやることはこういうことで
す。

1.DLLをマッピング。
2.マッピングした部分をセグメントとして設定。適当なセレクタを割
    り振る。ここでは0x0203ということにする。
3.DLLの特定のアドレス(たとえば0x0203:0x00000000)をコールする
    とEAXにオブジェクトのサイズが返される。
4.このサイズをmallocしてポインタをEDI等に入れて、DLLのオブジェ
    クト初期化ファンクションをコールする。
5.以後、どのファンクションを呼び出すときもEDIにはオブジェクト
    のポインタが入っているようにする。

  もしかしたらDLLが可変データを扱うなどの理由で以上の扱いだけでは
不充分かもしれません。その場合は、アプリ側にfmalloc()という関数を
作っておきます。

void * _far_ fmalloc(int size)
{
    return malloc(size);
}

そしてオブジェクトの初期化時にDLLへこのfmallocのポインタを教えて
やってください。おそらくDLL側は自分のオブジェクト内にこのfmalloc
()へのポインタを格納しておくでしょう。そして必要に応じてコールす
るはずです。同様にffree()も作って、やはり初期化時のパラメータに
これも入れておいてください。

  DLL側としてはもし非常に頻繁にfmalloc()やffree()を呼ぶのなら、
内部で自分専用のmalloc()やfree()を作って、時々fmallocやffreeを呼
ぶようにコーディングするでしょう。これでfar-callが多くなりすぎる
ことを簡単に回避できます。

  DLLがモジュールにアクセスしたい場合はどうすればいいでしょうか?
・・・もちろん、アプリにmmareaを分けてもらいます。どこに空きがある
のか知っているのはアプリなんですから。あらかじめ必要な量が分かって
いるのなら、これはオブジェクト初期化時などに教えてやればいいわけで
すが、動的に必要になったり不要になったりするのなら、やはりそういう
関数を作ればいいのです、先のfmalloc()のように。

  DLLはこれでmmareaをもらえるので、任意のモジュールへアクセスする
ことができます。モジュールアクセスの際にはスロットが必要ですが、こ
れもアプリが管理しているリソースなのでアプリにもらってください。ま
たDLLがDLLをさらに必要とすることもあるでしょう。その場合は空きセレ
クタ番号が必要です。自分に割り振られたセレクタはCSをリードすればす
ぐに分かるので、この直後の番号を使うなどと事前に仕様に書いておけば
アプリはそのつもりで書かれるでしょうし、セレクタ番号を得るための関
数も用意するということもかまいません。

  [OSASK 3441]などではシグナルとDLLについての発言がありました。こ
れについても書いておくことにします。DLLは基本的にシグナルボックス
を持ちません(持つようにもできますが)。DLLはアプリから自由に使っ
ていいシグナル番号をもらいます。たとえば1000〜1299をもらったとしま
しょう。DLL側ではそのシグナル番号が返されるようにプログラムするわ
けです。そしてアプリ側は、

const int getsignal()
/* 0が返されたら、シグナルなし */
{
    int signal;
retry:
    if (*sig_ptr == REWIND_CODE) {
        lib_waitsignal(0x0000, *(sig_ptr + 1), 0);
        sig_ptr = signalbox0;
    }
    signal = *sig_ptr;
    if (1000 <= signal && signal <= 1299) {
        int i = DLLsingalhandler(sig_ptr);
        sig_ptr += i;
        lib_waitsignal(0x0000, i, 0);
        goto retry;
    }
    if (signal != 0) {
        sig_ptr++;
        lib_waitsignal(0x0000, 1, 0);
    }
    return signal;
}

などとやるわけです。DLLsingalhandler()はDLLのシグナルハンドラを呼
び出す関数です。もちろんDLL用のシグナルボックスを作ってやって、そ
こにバッファリングしてやることはできますし、多くのプログラマが望
のならそういうのを支援するライブラリも作られるでしょう。

  もちろん発想を逆にして、DLL側にシグナルボックスの管理を任せて、
アプリ側が必要なシグナルだけを受け取るという方式もできます。この
方法なら[OSASK 3441]でいうところのタスク内フィルタリングもできま
す。

  もう一つ書いておくと、必要なヒープ領域が後で足りなくなったらど
うしようという意見に対しては「だからできるだけ多めに取っておいて
ください」という毎度の解答のほかに、こんな方法もあります。

  たとえばあと1MBほど追加したくなったとしましょう。そうしたら、
タスクディレクトリ内に"moreheap"という名前の1MBのモジュールを作
り、これをmmareaの空きにマッピングします。そして、

void addheap(int size, void *moreheap)
{
    union MallocHeader {
        struct {
            union MallocHeader *ptr;
            unsigned int size;
        } s;
    } *p;
    p = (union MallocHeader *) moreheap;
    p->s.size = size;
    free((void *) (p + 1));
}

を使ってこの領域をaddheap()してやれば、malloc()可能な量を後から
でも増やせます。これがおっくうでないのなら、mmareaを多めに取って
おきさえすればリンク時のmalloc:の値が小さくても問題ありません。

  こんなのは面倒だということでも、これをもっと簡単にやってくれる
mallocを作るという方法で利用する手もあります。

(方法2:ver.2.4では未サポート)
  ・・・先にDLLはmmareaにマッピングしてそしてそこをセグメントに
していましたが、これは違う方法をとることもできます。皆さんが何気
なく使っているAPIはDLLですが、mmareaの外にありまして、サイズを気
にすることはありません。・・・これはセグメントマッピングとでも呼
べるような機能で、アプリにとってはセグメントさえ提供してくれるの
なら仮想空間内のどこにマッピングされても構わないというやり方です
。このファンクションはまだ整備されていませんが、いずれ使えるよう
になります。これは非常にDLL向きの機能といえます。DLLがmmareaの外
にあることによるデメリットは何もなく、むしろmmareaのことで悩まな
くていいので楽です。もし複数のタスクが同じDLLをセグメントマッピ
ングすれば、実メモリだけでなくアドレス空間も共有されるでしょう。

・ラージモデル

  OSASKはミディアムだけではなく、ラージモデルもサポートしていま
す。つまりデータセグメントは何も一つでなければいけないということ
はありません。もしコードモジュールだけでなくデータモジュールもセ
グメントマッピングするなら、まさにラージモデル状態です。これの利
点は必要に応じてアドレス空間も共有してもらえるということとmmarea
はもう一切気にしないでいいということです。ややこしい管理は全部シ
ステム任せにしたことになります。

  DLLがアプリにメモリをもらったりmmareaをもらうということを嫌う
なら、この方法でmmareaの外で勝手にやることもできます。

  OSASKではこのようにプログラマの好みで好きなメモリモデルを選べ
ます。フラットモデルもそのうちサポートされて、それらを自由に組み
合わせられます。

---

  世間ではやたらとセグメンテーションは遅いなどといいますが、遅い
のは主にヒュージモデルのデータアクセスであって、それはIA-32では
不要となったメモリモデルです。ループの内側でセグメントオーバーラ
イドやセグメントレジスタへの代入などをしないようにしさえすれば、
セグメンテーションをたくさん使っても遅くなりません。むしろ仮想空
間が効率的に使われるようになり、メモリが節約され、L2キャッシュの
ヒット率向上などに貢献するでしょう。このプラスの寄与の方がずっと
大きのではないかと僕は思っています。

  それでは。

--
    川合 秀実(KAWAI Hidemi)
OSASK計画代表 / システム設計開発担当
E-mail:kawai !Atmark! imasy.org
Homepage http://www.imasy.org/~kawai/