Process 生成

平成17年4月10日(日)作成

トップページ

MONA起動直後のシェルやBAYGUIのシェルでアプリケーションの名前を入力して エンターキーを押せばそのアプリケーションが処理を開始します。要するに、プロセスが ひとつ生成されて実行されることになります。

ところで一重にプロセスの生成・実行といってもその内部ではさまざまな段階に 分けられます。生成時の時系列を追って簡単に見ていくと次のようになります。

  1. 実行ファイルの読み込み
  2. 実行ファイルフォーマットを解析し、CPUが実行できる実行コードを取得
  3. メモリ空間の生成
  4. 実行コードを開始アドレスにロード
  5. スレッドを生成し、スケジューラに登録

5つの項目が出てきましたが、このうちの初めの2つの作業はカーネルが行うのではなく サーバーと呼ばれるユーザープロセスが行っています。サーバーはいくつかあって、 プロセス全体を管理するサーバー、ファイルの読み込みを管理するサーバー、そして、 実行ファイルを解釈して実行コードを得るサーバーの3つがあります。これらのサーバーが 互いに連絡しながら目的の作業を行っています。そして、残りの3つの作業を カーネルが行っているわけです。

このようにプロセスの生成では多数のプロセスが関与してきます。 しかし、それぞれのプロセスは設計上互いに関与することが出来ません。 ここで重要になって来るのがプロセス間通信です。これはカーネルが 用意しているものですが、この機能により各プロセスが連携し合い同調した 処理を可能にしています。

プロセス生成に関わるプロセス間通信には次の2つがあります。

Messageというのは文字通りプロセス同士でメッセージを送受信するものです。これは おもにプロセス同士で動作のタイミングを取るために用いられます。つぎに、SharedMemory というものですが、これはプロセス同士でデータのやり取りを行うために実装されています。 SharedMemoryでデータを用意してMessageでその旨をプロセスに伝えるといった流れが プロセス間通信の基本です。

プロセス生成の処理の経過

ユーザープロセスによるプロセス生成は次の2つの関数を用います。

これらの関数はコマンド行で指定されたプロセスを起動し、実行します。前者の関数は 後者の関数の簡易版で、内部で後者の関数を呼び出しています。 ここで余談ですが、monapi_call_で始まる関数は全てその内部でプロセス間通信を 行っています。この場合ではPROCESS.BINというサーバーと通信しています。

早速monapi_call_process_execute_file_get_tid関数の中身を見てみましょう。

プロセス生成の始まり

//Mona/src/lib/monapi/messages.cppより。
int monapi_call_process_execute_file_get_tid(const char* command_line, MONAPI_BOOL prompt,
                                             dword* tid, dword stdout_id /* = NULL */)
{
    dword svr = monapi_get_server_thread_id(ID_PROCESS_SERVER);

    MessageInfo msg;
    if (Message::sendReceive(&msg, svr, MSG_PROCESS_EXECUTE_FILE, prompt, stdout_id, 0, command_line) != 0)
    {
        if (tid != NULL) *tid = THREAD_UNKNOWN;
        return -1;
    }

    if (tid != NULL) *tid = msg.arg3;
    return msg.arg2;
}

Message::sendReceiveという関数は目的のスレッドにメッセージを送って、返答があるまで 待つ関数です。ここではPROCESSサーバーにメッセージを送って、指定された プロセスを生成させることを伝えて待っています。

プロセスが生成されると処理が戻ってきてプロセスのメインスレッドのIDが返ってきます。 ソースコードを見る限りでは、メッセージの第3引数にこのIDが格納されているようです。

次にPROCESSサーバーでの処理を見てみましょう。PROCESSサーバーのソースコードは MONA/src/servers/processで見ることが出来ます。

プロセスサーバー

PROCESSサーバーの処理はMessageLoop()関数でMSG_PROCESS_EXECUTE_FILEメッセージを 受け取ることから始まります。このメッセージはmonapi_call_execute_file_get_tid()関数を 発したシェルから送られたもので、サーバーがMessage::reply()関数を呼び出すまでは シェルの動作は停止しています。

ファイルの読み込みと展開は内部で呼び出しているExecuteFile()関数が行っています。

static void MessageLoop()
{
    for (MessageInfo msg;;)
    {
        if (Message::receive(&msg) != 0) continue;

        switch (msg.header)
        {
            case MSG_PROCESS_EXECUTE_FILE:
            {
                dword tid;

                // 中略

                int result = ExecuteFile(msg.from, msg.str, msg.arg1 != 0, msg.arg2, &tid);
                Message::reply(&msg, result, tid);
                break;
            }
            
            //中略
        }
    }
}

次にExecuteFile関数を見ていきます。この関数は長いので3つに分けて説明していきます。 まず初めはコマンドラインを解析して実行ファイルのパス名と引数に分割しているところです。 ここはさして重要でないので紹介する程度にとどめておきます。

static int ExecuteFile(dword parent, const CString& commandLine, bool prompt, dword stdout_id, dword* tid)
{
    /* list initilize */
    CommandOption list;
    list.next = NULL;

    CommandOption* option = NULL;
    CString path;
    _A<CString> args = commandLine.split(' ');

    FOREACH (CString, arg, args)
    {
        if (arg == NULL) continue;

        if (path == NULL)
        {
            path = arg.toUpper();
            continue;
        }

        option = new CommandOption;
        strncpy(option->str, arg, sizeof(option->str));
        option->next = list.next;
        list.next = option;
    }
    END_FOREACH

実行ファイルのパスと引数が分割できたら実行ファイルを読み込みます。 このとき、実行ファイルは通常 ELF や PE といった再配置可能なフォーマットになっているので、 それを正しく解析して実行できるコードを得る必要があります。そのための処理を ELFサーバーや PEサーバーが行うことになります。

どちらのサーバーを呼び出してファイルを読み込むのかはファイルのフォーマットによって 決まってきます。例えば実行ファイル名が .ELF や .EL2 のように EL という文字が入って いれば ELFサーバーを用いますし、EX の文字が入っていれば PEサーバーを用います。

もし、実行ファイル名が上で述べたような拡張子で終わっていなければFILEサーバーを 用いて直接読み込みます。この場合は実行ファイルの内容がそのまま実行可能な コードになっていることを表しています。

ELFサーバーやPEサーバ、あるいはFILEサーバーとのデータのやり取りは共有メモリを 用いて行っています。共有メモリはmonapi_cmemoryinfo型のオブジェクト mi で 管理されており、mi->Dataで共有領域にアクセスできます。共有メモリに関することは 後日別の解析で明らかにするつもりです。

    monapi_cmemoryinfo* mi = NULL;
    dword entryPoint = 0xa0000000;
    int result = 1, svr_id = -1;

    if (path.endsWith(".ELF") || path.endsWith(".EL2") || path.endsWith(".EL5"))
    {
        svr_id = ID_ELF_SERVER;
    }
    else if (path.endsWith(".EXE") || path.endsWith(".EX2") || path.endsWith(".EX5"))
    {
        svr_id = ID_PE_SERVER;
    }
    if (svr_id != -1)
    {
        MessageInfo msg;
        dword tid = monapi_get_server_thread_id(svr_id);

        if (tid != THREAD_UNKNOWN)
        {
            Message::sendReceive(&msg, tid, MSG_PROCESS_CREATE_IMAGE, prompt ? MONAPI_TRUE : MONAPI_FALSE, 0, 0, path);
            if (msg.arg2 != 0)
            {
                result = 0;
                entryPoint = msg.arg3;
                mi = monapi_cmemoryinfo_new();
                mi->Handle = msg.arg2;
                mi->Owner  = tid;
                mi->Size   = atoi(msg.str);
                monapi_cmemoryinfo_map(mi);
            }
            else
            {
                result = msg.arg3;
            }
        }
    }
    else if (path.endsWith(".BN2"))
    {
        mi = monapi_call_file_decompress_bz2_file(path, prompt ? MONAPI_TRUE : MONAPI_FALSE);
    }
    else if (path.endsWith(".BN5"))
    {
        mi = monapi_call_file_decompress_st5_file(path, prompt ? MONAPI_TRUE : MONAPI_FALSE);
    }
    else
    {
        mi = monapi_call_file_read_data(path, prompt ? MONAPI_TRUE : MONAPI_FALSE);
    }

ここまでで処理がうまく言っていれば共有メモリ領域 mi->Data にアクセスすることで 実行コードが得られるはずです。もし、mi 自体がNULLであれば処理がうまくいかなかった ということなのでその旨を表示して処理を終えます。そうでなければ、ExecuteProcess関数を 呼び出してプロセスの実行に移ります。その後で共有メモリ領域を破棄して後片付けを します。

    if (mi == NULL)
    {
        if (prompt) printf("%s: can not execute!\n", SVR);
    }
    else
    {
        result = ExecuteProcess(parent, mi, entryPoint, path, GetFileName(path), &list, prompt, stdout_id, tid);
        monapi_cmemoryinfo_dispose(mi);
        monapi_cmemoryinfo_delete(mi);
    }

    for (option = list.next; option; option = option->next)
    {
        delete option;
    }
    return result;
}

ここがプロセスを起動させるExecuteProcess関数です。が、この関数がプロセスを起動 させるわけではありません。プロセスを起動させているのはシステムコールの syscall_load_process_imageというものです。ここでやっとシステムコールが出てきました。 長かったですね。このコールが正常に返ってくると晴れてプロセスの起動と相成ります。

static int ExecuteProcess(dword parent, monapi_cmemoryinfo* mi, dword entryPoint, const CString& path, const CString& name, CommandOption* option, bool prompt, dword stdout_id, dword* tid)
{
    LoadProcessInfo info;
    info.image = mi->Data;
    info.size  = mi->Size;
    info.entrypoint = entryPoint;
    info.path = path;
    info.name = name;
    info.list = option;

    addProcessInfo(name);
    int ret = syscall_load_process_image(&info);
    *tid = addProcessInfo(parent, name, path);

    if (prompt)
    {
        switch(ret)
        {
            case 4:
                  printf("%s: Shared Memory error1", SVR);
                  break;
            case 5:
                  printf("%s: Shared Memory error2", SVR);
                  break;
        }
    }

    return ret;
}

システムコール syscall_load_process_image

さて、ここが最後です。システムコールの呼び出しでは詳しい経緯は省きますが 次のコードが実行されます。ここでいつか見たLoader::Load関数が実行されています。 このLoader::Load関数でプロセス、メモリ空間およびスレッドを生成しプロセスを 実行させています。この処理については別の資料を見てください。

    case SYSTEM_CALL_LOAD_PROCESS_IMAGE:
    {
        LoadProcessInfo* p = (LoadProcessInfo*)(info->esi);
        info->eax = Loader::Load(p->image, p->size, p->entrypoint, p->name, true, p->list);
        break;
    }

トップページ
inserted by FC2 system