fork on Windows

うーん、CsrpConnectToServerの代わりにCsrClientConnectToServerを使えば良いのじゃという情報の元にテストしてみたんだけど、うまくいかず。(forkしたプロセスからのprintfが表示されない。)
引数が悪いのかなぁ…?といっても、どうすればいいのかは分からないんだけど。
もー、とりあえずあきらめるか?共有メモリがちゃんと実現できるかもテストしてないし。

fork on Windows

Win32サブシステムへの接続をのぞけば、動いた、ような、気がする。多分。
Device Drive Kitのヘッダファイルにも無い呼び出しとかがあったので(バージョンによるのか?)、検索して適当にヘッダファイルをでっち上げ、先日の日記のようにGetProcAddressでアドレスを取得。構造体等の定義もヘッダに入れないといけないので面倒でした。
あとは、WINDOWS NT/2000 NATIVE API REFERENCEの本の例を若干変更。コンパイルでエラーになる部分に明治的なキャストを入れるとか。これでコンパイルは通ったけど、実行時エラー。さんざん悩んだあげく、#pragma optimize("y", off)の場所を移動すると、動いた(ような気がする)。で、これが何なのかだが…

forkの定義がどうなっているかだけど(ものはこの辺にある)、

  • ハンドルの継承を設定
  • ZwCreateProcessでプロセス生成
  • Thread生成のための情報を今のプロセスから取得:ここで、生成したスレッドが動き出す場所を指定
  • ZwCreateThreadで先に生成したプロセス上にスレッドを生成
  • 今のスレッドのexception handlerを新しく生成したスレッドにコピー(よくわからん)
  • Win32サブシステムに接続を通知
  • 生成したスレッドを起動

という感じ。本によると。
で、「生成したスレッドが動き出す場所」というのが、こう指定されている:

   __declspec(naked) int child()
{
    typedef BOOL (WINAPI *CsrpConnectToServer)(PWSTR);

    CsrpConnectToServer(0x77F8F65D)(L"\\Windows");
    __asm mov   eax, 0
    __asm mov   esp, ebp
    __asm pop   ebp
    __asm ret
}


#pragma optimize("y", off)  // disable frame pointer omission

int fork()
{
...
    context.Eip = ULONG(child);
...
    NT::ZwCreateThread(&hThread, THREAD_ALL_ACCESS, &oa,
                       hProcess, &cid, &context, &stack, TRUE);
....
}

fork()の中のcontext.Eip = ULONG(child)で、生成したスレッドがchildのアドレスから実行を開始することを指定している(インストラクションポインタの設定)。
で、childの方を見てみる。CsrpConnectToServerというのが、強制的にWin32サブシステムに接続する関数。これはntdll.dllから公開されていないので、アドレスを直書きしている。で、このアドレスがWindowsのバージョンによって異なると :-( (あ、先日の日記で変数のアドレスって書いたけど、それは間違いでした。)
で、このままだとエラーになったので、ここはとりあえずコメントアウト
その下で、謎のアセンブリコードがあるけど、これは、関数からのリターンを指定しているみたい。fork()関数の中からgoto的に飛ぶので?(親プロセスのメモリ・スタックの状態のままで、インストラクションポインタがchildを指すようになるので)、fork()関数からのリターンになる様子。
eaxに0を指定することで、fork()の返り値を0にして、ebpをpopすることで、スタックポインタを戻している。で、これが動作するためには、「フレームポインタを使わない」という最適化をoffにする必要がある。それが、#pragma optimize("y", off)の正体。

とそこまではいいんだけど、僕の環境(Visual Studio 2005 / VISTA)では、#pragmaをchildの前に持っていかないと動かなかった。__declspec(naked)というのが謎なんだけど、以前の環境では、これがあると#pragmaは不要だったのかも知れない。

まー、何となく動いているという状態ではあるのだが、これからどうしようかなぁ。Win32サブシステムと接続しなくても、共有メモリが実現できれば並列計算はできるけど… I/Oが使えないのは寂しいが…

Windowsのnative APIの呼び方

を調査。普通はDevice Driver Kitとか使うみたいなんだけど。
Visual Studioとかでは、そもそもプロトタイプ(関数の引数とか返り値の型とか)がヘッダに無いので、それはどこかで調べてくる必要がある。(件のWINDOWS NT/2000 NATIVE API REFERENCEの本とかで)
で、リンクするのにどうするかなんだけど、昔はntdll.libを指定すればよかったらしい。んだけど、Visual Studio 2005ではこの方法は使えなかったので、いろいろ調べてみると、GetProcAddress(GetModuleHandle(TEXT("ntdll.dll")), "NtCreateProcess"))とかして、呼び出したい関数のアドレスを得るものみたい(たとえば、この辺に例がある)。
ntdll.dllは必ずロードされるので、このモジュールからNtCreateProcessのアドレスをGetProcAddressで取り出すそうな。
この場合、関数へのポインタが得られるだけなので、その関数の型をtypedefするとかして、その関数へのポインタの正しい型で変数を定義しないといけないのが面倒。
ちょっと試したところ、まぁ動くみたいだけど。

それはそうと、WINDOWS NT/2000 NATIVE API REFERENCEにのってるforkの実装は、かなりWindowsのバージョンに依存するらしいことが発覚。Win32サブシステムに接続するところで、Win32サブシステム側でプロセスの初期化をしてもらわないといけないんだけど、初期済みかどうかはある変数に格納されていると。で、fork直後は値はすべて親プロセスと同じなので、初期化済みになっていますねと。仕方が無いので、その変数の値を「未初期化」に変えないといけないんだけど、この部分のアドレスがハードコードされている。orz このアドレス、本当にバージョンによって違うらしい。

あーもー、どうしよ。Windows上のforkはあきらめるか?

SUAでのbzip2の速度

Mac OS Xより若干高速であることが発覚(並列化効率は同じくらいだった)。gccのバージョンのせいか?(Mac OS Xは4.01, SUAは3.3)。
いまさらだけど、2CPUできれいに2倍にならないのはなぜなんだろう?同期やプロセスフォークのコストはあんまり無いと思うんだけど。

SUAでの実行速度

前の日記にも追記しましたが、単純にプロセッサ数を取得する関数がSUAではうまく動いてなくて、デフォルト値の1が返ってきていただけでした。orz というわけで、ちゃんと速くなっているように見えます。すばらしい。(ちょっと速度向上率が低いようにも見えるけど)つーか、SUAだとどうやってプロセッサ数取得するんだ?

Cygwinでテスト

しようとインストールしたが、System V IPCが動作しない :-(
…そういえば、なんかデーモンを立ち上げるんだったなと思い出し、調べると、cygserverというのを設定する必要があるそう:Cygwin/cygserver - discypus
ていうか、以前はipc-daemonだったり、ipc-daemon2だったり、cygipcだったりいろいろ違ったらしいので、検索ではまる。
で、cygserver-configとかで、Windowsのサービスを設定するんだけど、権限がどうとかでうまくいかない…
いろんなファイルのセキュリティを「管理者としてこのプログラムを実行する」とか「互換モードでこのプログラムを実行する」にしたんだけど…
これだからVISTAは…(苦笑)
疲れたから、ここまで。

[追記] リンク先に書いてあるとおり、サービスとして立ち上げるのをあきらめて、手動で「/usr/sbin/cygserver を起動し、 環境変数 CYGWIN に文字列 'server' を追加する」ことで、IPCが使えることを確認。で、この状態でPARDSのサンプルプログラムを試すと、一応動く。で、並列化したbzip2を試すと、エラーが出るので、共有メモリのサイズやセマフォの数を変更する(/etc/cygserver.conf)。でも、大きなファイルだと動かない…子プロセスのwaitあたりとかでおかしくなっているようだが、ちょっと追いかけてられんなぁ。

Intel Threading Building Block

GPLになったそうなので、見てみる。(昔見たような気がしたけど、すっかり内容を忘れていた…)
…んー、個人的には微妙。

チュートリアルから引用すると、例えば、

void SerialApplyFoo( float a[], size_t n ) { 
    for( size_t i=0; i<n; ++i ) 
        Foo(a[i]); 
} 

というループを並列化するためには、

#include "tbb/blocked_range.h" 
 
class ApplyFoo { 
    float *const my_a; 
public: 
    void operator()( const blocked_range<size_t>& r ) const { 
        float *a = my_a; 
        for( size_t i=r.begin(); i!=r.end(); ++i )  
     Foo(a[i]);      
    } 
    ApplyFoo( float a[] ) : 
        my_a(a) 
    {} 
}; 
#include "tbb/parallel_for.h" 
 
void ParallelApplyFoo( float a[], size_t n ) { 
    parallel_for(blocked_range<size_t>(0,n,IdealGrainSize), ApplyFoo(a) ); 
} 

と、こうなる。ナガスギ。
ループのボディを、別Class中でoperator()の中に定義して、呼び出し側は、parallel_for中で、そのClassを使うという形。
C++で、言語を拡張せずに、Threadを利用するためには、こうせざるを得ないのかも知れないけど…

なんというか、ループの並列化がしたいんだったら、OpenMPツカエ、という感じ。今時、gccVC++もサポートしてたと思うし。

タスク並列の方も、あんまり変わらず:

long ParallelFib( long n ) { 
    long sum; 
    FibTask& a = *new(task::allocate_root()) FibTask(n,&sum); 
    task::spawn_root_and_wait(a); 
    return sum; 
} 
class FibTask: public task { 
public: 
    const long n; 
    long* const sum; 
    FibTask( long n_, long* sum_ ) : 
        n(n_), sum(sum_) 
    {} 
    task* execute() {  // Overrides virtual function task::execute 
        if( n<CutOff ) { 
            *sum = SerialFib(n); 
        } else { 
            long x, y; 
            FibTask& a = *new( allocate_child() ) FibTask(n-1,&x); 
            FibTask& b = *new( allocate_child() ) FibTask(n-2,&y); 
            // Set ref_count to "two children plus one for the wait". 
            set_ref_count(3); 
            // Start b running. 
            spawn( b ); 
            // Start a running and wait for all children (a and b). 
            spawn_and_wait_for_all( a ); 
            // Do the sum 
            *sum = x+y; 
        } 
        return NULL; 
    } 
}; 

長いヨ!

PARDSはfork()(とCPPマクロ)を使っているので、シンタックス的にはずっと簡潔になっている。

まぁ、オーバヘッドは頑張って小さくしているように見えるけど、本当にこんなんで書くんかいな。