トップページ

Loader

この文書ではKernel.cpp内で呼び出されているLoaderクラスについて述べています。 Loaderはシステムの処理を行うサーバープログラムをファイルから読み取って実行させますが、 プログラムを格納させる領域がユーザープロセスの独立したメモリ領域であるため そのままではプログラムを格納することができません。そこで、ページングによる共有メモリ 機構を使うことになります。この文書ではLoaderが何をしているのかから始めて ページングについて簡単に説明した後、メモリを共有するためにどのようなことを しているのかを説明し、最後にMONAでの実装を見ていきます。

Loaderとは

まず、Loaderはいったい何をしているのかを説明します。そもそもMONAは マイクロカーネルを採用しており、ハードウエアに関する処理のみをカーネルモードで 行っています。そして、それ以外の処理、たとえばキーボード入力の処理や ファイルシステムの処理などはすべてサーバーと呼ばれるユーザーモードのプロセスで 処理させています。 これらのプロセスはカーネルを切り離されることになるため、カーネルが起動した後、 そういったプロセスをファイルから読み取って起動しなければいけません。Loaderはまさに この処理を行っており、大まかには次のような段階を踏んでいます。

  1. サーバープログラムをファイルからカーネル領域に読み込む。
  2. サーバープログラム用のプロセスをユーザーモードで生成する。
  3. 2.で作成したプロセスのメモリ領域に1.で読み取ったプログラムをロードする。
  4. サーバープログラム用のスレッドを生成する。
  5. 4.で作成したスレッドをスケジューリングに参加させて プロセスが実行されるようにする。

これらの処理の中で一番難しい部分は3.のプロセスのメモリ領域にプログラムをロードする 部分です。理由は、おのおののプロセスが独立したメモリ空間を持っておりカーネルといえども 容易にはアクセスできないためです。この辺の詳しい事情はページングのところで説明する つもりです(といって逃げるw)。

カーネルがサーバープロセスのメモリ領域にアクセスできないままでは話が進みません。 この問題を解決するためにここではページングによる共有メモリ機構を用いています。 カーネルとサーバープロセスのそれぞれのメモリ領域の一部を共有することで、 共有された部分はカーネルでもサーバープロセスでもアクセスできるようになります。 これでカーネルはプログラムをロードすることもでき、サーバーはプログラムを 実行できるようになります。


ページングとは

共有メモリ機構の説明に入る前にページングについて説明しておきます。すでに ページングについての知識があればこの節は読み飛ばしてもかまいません。 また、 Linux JF Project の方に 有用な資料がおいてあるのでそれも参考にしてください。おそらくこのページの 説明よりずっとよいはずです。

よく、ページングを仮想記憶システムの一部と説明しているところがあります。 この説明は間違ってはいませんが共有メモリ機構を説明するに当たって重大なことが 抜けています。共有メモリ機構を実現するにあたって重要なのはメモリ容量を増やすこと ではなく、むしろメモリ全体の細かな管理とアドレス変換機能です。この2つがあって 初めて仮想記憶システムが成り立ち、各プロセスごとに独立したメモリ空間を割り当てる ことができるのです。

ページングでは何が起こっているのか

各プロセスが独立したメモリ空間を持っているということは、メモリアドレスが同じでも それぞれ違う物理メモリを指しているということです。たとえば下の図を見てください。

ページングの様子

この図では2つのプロセスAとBのメモリ空間とと物理メモリのメモリ空間を描いています。 そして、プロセスAのAddressA-1とプロセスBのAddressA-2はそれぞれ同じアドレスの値を あらわしています。たとえば両方とも0xA0000000という値になっているとしましょう。 しかし、プロセスAでは実メモリのAddressR-2を、プロセスBでは実メモリのAddressR-1を それぞれ指していて互いに別々のところを参照しています。一般にプロセスのメモリ空間内の アドレスが実メモリのどの部分を指すのかはほとんど自由に指定できます。したがって、 あるプロセスのメモリアドレスが実メモリ上のどこを指しているのかということは容易には 決定できないのです。

一方それぞれのプロセスのAddressA-2とAddressB-2を見てください。これらのアドレスは ともに実メモリのAddressR-3を指しています。これはプロセスAがAddressA-2にアクセスしても プロセスBがAddressB-2にアクセスしても同じ実メモリを参照することを表しています。 つまり、この部分ではメモリの共有が起きているわけです。

このようにAddressA-1を指していると思ったら実メモリ上ではAddressR-2を指していた、 ということが起こるのはCPUの内部でアドレス変換がなされているためです。

ページングによるアドレス変換機構

ページングを使用している間はCPUがアドレス変換を行うということでした。では実際に アドレス変換とはどのようにして行われるのでしょうか。一番手っ取り早いのは配列を 使うことでしょう。つまり、配列の0番目にプロセスでのメモリアドレス0x00000000に対応する 実メモリのアドレスを、配列の1番目にプロセスでのメモリアドレス0x00000001に対応する 実メモリのアドレスを、・・・としていくことです。しかし、プロセスのメモリアドレスは 0x00000000から0xFFFFFFFFまであるので1バイトずつの精度で変換するのはあまりいい考え ではありません。実際にはメモリ空間を4キロバイトごとに分割した「ページ」ごとに アドレスの変換を行っています。つまり、メモリアドレスの0x00000000から初めの4キロバイト すなわち0x00000FFFまでが初めのページに属し、0x00001000から0x00001FFFまでが次の ページに属します。そして、ページにはそれぞれ番号がつけられており0番から番号が 始まっています。丁度メモリアドレスの上位20ビットがそのアドレスが属している ページの番号を表していることになります。ちなみに下位12ビットはオフセットと呼ばれ、 ページの初めから何バイト目のアドレスであるのかを指定します。 下の図はこの状況を示しています。

アドレス変換

アドレスの変換はページテーブルと呼ばれる変換表にしたがって行われます。繰り返しますが、 ページテーブルの0番目の要素にはプロセスのメモリ空間の0番目のページに対応する 実メモリのページ番号が、1番目の要素にはプロセスのメモリ空間の1番目のページに対応する 実メモリのページ番号が格納されています。ページテーブルの1つの要素は32ビットで、 そのうちの20ビットを実メモリのページ番号に使用しており、残りの12ビットでページに対する 属性、たとえば書き込みの可および不可、スーパバイザ用かユーザー用か、などの情報を 保持しています。また、プロセスのページが実メモリのページに対応していないこともあります。 対応する実メモリのページを持たないプロセスのページにアクセスしたとき、CPUは ページフォルト例外を発生させます。この例外が発生したときオペレーティングシステムは 何らかの処理を行って問題を解決しなければなりません。


共有メモリ機構の実現方法

ページングについての知識があれば共有メモリ機構がどのように実現されているのかを 理解するのは容易なことです。要は、別々のプロセスのあるページが同じ実メモリの ページを参照していればいいだけのことです。難しいことは実際の物理メモリは そのアドレスにアクセスしようとしたときになって初めて確保されるということです。 これは極力物理メモリの使用量を抑えようという考えから来ており、デマンドページングと 呼ばれています。デマンドページングは共有メモリに限らず一般のメモリの使用においても 用いられている手法です。

さて、デマンドページングではページにアクセスされてから必要な物理メモリが確保 されるということでした。逆に言えば、アクセスするまでは必要な物理メモリは確保されて おらず、ページテーブルには対応する物理メモリのページはない、と書かれているわけです。 つまりデマンドページングを実現させるためには、プログラムが確保されていないページに アクセスしたのを何らかの方法で検知して、プログラムに気づかれないように物理メモリを 確保するということをしなければなりません。これを行うのがページフォルトです。

一般にプログラムが物理メモリが確保されていないページにアクセスした時、ページフォルトが 発生し処理がOSに移ります。このときOSは何らかの処理を行って、プログラムがその後 メモリにアクセスできるようにしなければなりません。デマンドページングではこの ページフォルトをわざと起こさせることで、プログラムがページにアクセスしたことがわかる ようになっています。つまり、ページフォルトが起きたときOSはページフォルトを起こした プロセスのページ表を書き換えてプロセスがページにアクセスできるようにし、それから プロセスに処理を移すわけです。

以上がデマンドページングに関する一般的な話ですが、メモリを共有するときには 少し注意が必要です。察しのいい人なら気づくかもしれませんが、ページフォルトは 2回起きます。共有メモリを2つのプロセスに割り当てたとき、どちらのプロセスの ページ表にはまだ物理メモリは割り当てられません。そして、2つのプロセスのうちの 1つが共有メモリを使用するとページフォルトが起き、OSが物理メモリを割り当てて ページ表を更新しますが、もう一方のプロセスのページ表は更新してくれません。 つまり、ページに対して物理メモリを割り当てるとき既に割り当てたかどうかを 判断する必要があります。このことに気をつければ共有メモリを扱えると思います。


MONAによる実装

最後にMONAがどのようにしてサーバープログラムをロードし、どのような共有メモリの インターフェイスを用いているのかを説明します。 まず、MONAの共有メモリでは次の3つのクラスがかかわってきます。

この中でProcessクラスはカーネルとサーバーの両方のプロセスにそれぞれ存在します。 また、このクラスはSharedMemorySegmentクラスを含んでいて自分のメモリ空間の どの部分が共有されているのかを管理し、管理しているメモリの領域内のページフォルトの 処理もこのクラスで行っています。そして、それぞれのSharedMemorySegmentクラスは 共有されているメモリに対して1つのSharedMemoryObjectクラスを共有します。 これはそれぞれのSharedMemorySegmentクラスがSharedMemoryObjectクラスのポインタを持つことで 成り立っています。つまり、それぞれのプロセスが共有されたSharedMemoryObjectクラスを 間接的に保持しているわけです。

Loaderクラスはカーネルとサーバー間でこのような所持関係を成立させる処理を 行っています。

カーネルによるローダーの呼び出し

サーバーのロードと起動はカーネル プロセスであるmainProcessからexecSysConf、loadServerを経てLoader::Load関数を 呼び出して実行されます。プログラムのロードはこのLoaderクラスが担当しますが、 その内部ではSharedMemoryObjectクラス、SharedSegmentクラスが動いています。 これらのクラスについても説明する予定です。

void loadServer(const char* server, const char* name)
{
    g_console->printf("loading %s....", server);
    if (strstr(server, ".BIN"))
    {
        g_console->printf("%s\n", Loader::Load(server, name, true, NULL) ? "NG" : "OK");
    }
    else
    {
        g_console->printf("unknown server type!\n");
    }
    //以下、略

Loadrクラスの動作

Loader::Load関数はカーネルから真っ先に呼び出されており、次のように定義されています。 ここで、第3引数は起動させるサーバーが ユーザープロセスとして実行するかどうかを指定するためのものであり、第4引数が サーバーに与える引数のリストであることがわかります。ここでLoad関数はReadFile関数を 呼び、そこでメモリを確保してサーバープログラムを読み込んでいます。そして、 別のオーバーロードしたLoad関数を呼び出してサーバープログラムのロードと実行を 行っているわけです。

int Loader::Load(const char* path, const char* name, bool isUser, CommandOption* list)
{
    int result;
    byte* image;
    dword size;

    image = Loader::ReadFile(path, &size);
    if (image == NULL || size == 0)
    {
        return 1;
    }

    result = Loader::Load(image, size, Loader::ORG, name, isUser, list);

    if (result != 0)
    {
        free(image);
        return result;
    }

    free(image);
    return 0;
}

Loader::ReadFileの動作はプログラムのロードにあまりかかわらないので説明は 省略します。そして、オーバーロードされた別のLoader::Loader関数ですが、これは 少し長いので2つに分けて説明します。まず、前半部分は次のようになっており、 プロセスを生成し、メモリを共有して、プログラムをロードしています。

プロセスの生成はProcessOperation::createで行っています。そして、 共有メモリに関係するところはSharedMemorObject::open, SharedMemoryObject::attachという 部分です。SharedMemoryObject::openでSharedMemoryObjectのインスタンスを作成し、 引数sharedIDで指定されたIDを割り当て、メモリを確保する準備を行っています。 ここでもし指定されたIDを持つインスタンスが見つかれば、何もせずに帰ることになります。 SharedMemoryObjectのインスタンスは物理メモリの どのページを共有して使用するかといった情報を持っています。しかし、物理メモリが 確保されるのは初めて共有メモリにアクセスされたときなので、この段階ではまだ何も 確保されていません。そして、SharedMemoryObject::attachで先ほど作成した SharedMemoryObjectのインスタンスを指定されたプロセスに関連付けます。それと同時に どのメモリアドレスから共有するのかを指定します。 これで、Loader関数が行う仕事は終わりです。後は読み込んだプログラムを 指定の場所にコピーすればプログラムのロードは終わります。さらっと書いているようですが、 ここの部分は多少複雑です。プログラムの中でもmemcpy関数でさらっと書いていますが、 この関数が実行されるときにページフォルトが起きて、指定されたハンドラが呼び出されて 云々・・・となるわけです。この部分はまた後ほど触れる予定です。

上の説明では散々「SharedMemoryObjectのインスタンス」という言葉が出てきました。しかし、 以下のコードには件のインスタンスは見当たらず、その処理はstaticな関数に任せています。 そしてLoader関数の中ではsharedIdというID番号で管理するだけになっています。このように、 わずらわしい処理はすべてstaticな関数の中に押し込んでしまうことがMONA流のプログラミングで あるといえます。

int Loader::Load(byte* image, dword size, dword entrypoint, const char* name, bool isUser, CommandOption* list)
{
    /* shared ID */
    static dword sharedId = 0x2000;
    sharedId++;

    bool   isOpen;
    bool   isAttaced;

    /* attach Shared to this process */
    systemcall_mutex_lock(g_mutexShared);

    isOpen    = SharedMemoryObject::open(sharedId, Loader::MAX_IMAGE_SIZE);
    isAttaced = SharedMemoryObject::attach(sharedId, g_currentThread->process, 0x80000000);

    systemcall_mutex_unlock(g_mutexShared);

    if (!isOpen || !isAttaced) return 4;

    /* create process */
    enter_kernel_lock_mode();
    Process* process = ProcessOperation::create(isUser ? ProcessOperation::USER_PROCESS 
                                                       : ProcessOperation::KERNEL_PROCESS, name);

    /* attach binary image to process */
    systemcall_mutex_lock(g_mutexShared);

    isOpen    = SharedMemoryObject::open(sharedId, Loader::MAX_IMAGE_SIZE);
    isAttaced = SharedMemoryObject::attach(sharedId, process, Loader::ORG);

    systemcall_mutex_unlock(g_mutexShared);

    if (!isOpen || !isAttaced) return 5;

    memcpy((byte*)0x80000000, image, size);

カーネルがプログラムをロードした後は最早そのメモリ領域にアクセスする必要が なくなるので、カーネルがそれ以降メモリを参照しなくなるようにしなければなりません。 この処理がSharedMemoryObject::detachです。この関数はプロセスと共有メモリとの 関連付けを取り消します。そして、どのプロセスからも参照されなくなったとき 物理メモリを開放します。この場合ではカーネルからの参照が途絶えたとしても、まだ サーバープログラムから参照されているので物理メモリの開放はされません。 後は、プログラムの引数を設定してスレッドを作成して、スケジューリングに合流させる だけです。

    /* detach from this process */
    systemcall_mutex_lock(g_mutexShared);

    SharedMemoryObject::detach(sharedId, g_currentThread->process);
    systemcall_mutex_unlock(g_mutexShared);

    /* set arguments */
    if (list != NULL)
    {
        char* p;
        CommandOption* option;
        List<char*>* target = process->getArguments();

        for (option = list->next; option; option = option->next)
        {
            p = new char[32];
            strncpy(p, option->str, 32);
            target->add(p);
        }
    }

    /* now process is loaded */
    Thread*  thread = ThreadOperation::create(process, entrypoint);
    g_scheduler->Join(thread);
    exit_kernel_lock_mode();

    return 0;
}

SharedMemoryObjectクラスの動作

SharedMemoryObject::openが呼び出されるとこのクラスのインスタンスが作成されます。 しかし、指定されたIDを持つインスタンスが既に作成されていた場合はインスタンスは 作成されません。インスタンスが作成されたときはg_sharedMemoryObjectListに登録します。 これは後にインスタンスが作成されているかどうかを確かめるために用いられます。

bool SharedMemoryObject::open(dword id, dword size)
{
    SharedMemoryObject* target = find(id);

    /* new SharedMemory */
    if (target == NULL)
    {
        target = new SharedMemoryObject(id, size);
        checkMemoryAllocate(target, "SharedMemoryObject memory allcate target");
        g_sharedMemoryObjectList->add(target);

    } else
    {
        if (target->getSize() != size) return false;
    }

    return true;
}

SharedMemoryObject::attachはID番号idを持つSharedMemoryObjectのインスタンスと プロセスprocessとを関連付け、プロセスから参照できるようにします。複数のプロセスで メモリを共有するときは同じIDを持つSharedMemoryObjectのインスタンスを共有する仕組みに なっています。setAttachedCount関数で現在いくつのプロセスから参照されているのかを 設定します。この数が0になればどのプロセスからも参照されなくなったということを あらわし、インスタンスともども削除されることになります。

bool SharedMemoryObject::attach(dword id, Process* process, LinearAddress address)
{
    SharedMemorySegment* segment;
    SharedMemoryObject* target = find(id);
    if (target == NULL)
    {
        return false;
    }

    segment = new SharedMemorySegment(address, target->getSize(), target);
    if (segment == NULL) return false;

    process->getSharedList()->add(segment);
    target->setAttachedCount(target->getAttachedCount() + 1);
    return true;
}

SharedMemoryObject::detach関数はプロセスprocessが共有メモリを参照しなくなったときに 呼び出されます。このときページディレクトリ(ページテーブル)のエントリを削除し、 プロセスから物理メモリに参照できなくなるようにします。そして、プロセスが持っている 共有メモリのリストからも削除します。

bool SharedMemoryObject::detach(dword id, Process* process)
{
    SharedMemoryObject* target = find(id);
    if (target == NULL) return false;

    SharedMemorySegment* segment = SharedMemorySegment::find(process, id);
    if (segment == NULL) return false;

    /* destroy */
    g_page_manager->setAbsent(process->getPageDirectory(), segment->getStart(), segment->getSize());
    process->getSharedList()->remove(segment);
    delete(segment);

    target->setAttachedCount(target->getAttachedCount() - 1);

    /* should be removed */
    if (target->getAttachedCount() == 0)
    {
        g_sharedMemoryObjectList->remove(target);
        delete(target);
    }
    return true;
}

SharedSegmentクラスの動作

SharedSegmentクラスの主な処理はページフォルトの処理です。ページフォルトは プロセスのメモリ空間に物理メモリが割り当てられていないときに発生し、MONAに対して 物理メモリの確保を要求します。MONAのページフォルトハンドラはページフォルトが 発生したアドレスを解析し、共有メモリ内でページフォルトが発生したことを 検知すると、このSharedMemorySegmentクラスのfaultHandler関数を呼び出します。 この関数が呼び出されるときは次のような状況になっています。

どのような状況でページフォルトハンドラが呼び出されたのかは、一番最後の if文で判断しています。 共有メモリに初めてアクセスした場合は、共有メモリのためのメモリ領域は 確保されていないので、mappedAddressがSharedMemoryObject::UN_MAPPEDと なっています。この場合物理メモリの新たな確保が行われ、プロセスのメモリ空間にも その物理メモリが割り当てられます。一方、既に共有メモリのためのメモリ領域が 確保されている場合、mappedAddressに有効な物理アドレスが入っています。 このときはプロセスのメモリ空間にmappedAddressで示されている物理アドレスを 割り当てれば、プロセスから共有メモリに参照できるようになります。

bool SharedMemorySegment::faultHandler(LinearAddress address, dword error)
{
    int mapResult;
    if (error != PageManager::FAULT_NOT_EXIST)
    {
        errorNumber_ = FAULT_UNKNOWN;
        return false;
    }

    if (address < start_ || address > start_ + size_)
    {
        errorNumber_ = FAULT_OUT_OF_RANGE;
        return false;
    }

    /* page fault point */
    dword tableIndex1     = PageManager::getTableIndex(address);
    dword directoryIndex1 = PageManager::getDirectoryIndex(address);

    /* segment start point */
    dword tableIndex2     = PageManager::getTableIndex(start_);
    dword directoryIndex2 = PageManager::getDirectoryIndex(start_);

    /* check already allocated physical page? */
    dword physicalIndex = tableIndex1 + directoryIndex1 * 1024 - tableIndex2 - directoryIndex2 * 1024;

    int mappedAddress   = sharedMemoryObject_->isMapped(physicalIndex);
    Process* current = g_currentThread->process;

    if (mappedAddress == SharedMemoryObject::UN_MAPPED)
    {
        mapResult = g_page_manager->allocatePhysicalPage(current->getPageDirectory(), address, true, true, true);
        sharedMemoryObject_->map(physicalIndex, mapResult == -1 ? SharedMemoryObject::UN_MAPPED : mapResult); 
    } else
    {
        mapResult = g_page_manager->allocatePhysicalPage(current->getPageDirectory(), address, 
                                                         mappedAddress, true, true, true);
    }
    return (mapResult != -1);
}
トップページ
inserted by FC2 system