2012年12月15日土曜日

Boost.Context on Cygwin

1.51.0 において Boost.Context がリリース入りしました。Boost.Context はコンテキスト切り替えのためのライブラリです。コンテキストの最も一般的な訳語は「文脈」ですが、この場合は実行中のアドレス、CPU のレジスタなど、実行時の状態情報とでもいうべきものです。(プリエンプティブ)マルチタスクは OS がこのコンテキスト情報を切り替えることで成立していますが、これを自前で切り替えられるようにするのが Boost.Context です。あるいは、setjmp/longjmp だと setjmp した時点に戻ることしかできませんが、longjmp した時に同時に setjmp が実行されその場所にまた戻ることが出来ると言っても良いかもしれません(スタックの取り扱いが違いますが)。 これが出来て何が嬉しいかというと、C# の yield みたいなことが実現できるわけですが、それはともかく。CPU のレジスタとか書いていることでお分かりかもしれませんが、Boost.Context は C/C++ の範囲では実現できません。ということでアセンブリ言語で実装されています。Windows だと MASM が要求されるのが面倒だったので gas に移植(というほどのこともないですが)し、Cygwin でビルドできるところまで到達したのですが、example の jump.cpp をコンパイル、動作させてみると、コンテキストを切り替えた先の文字列出力で詰まってしまいました。

//          Copyright Oliver Kowalke 2009.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

#include <cstdlib>
#include <cstring>
#include <iostream>
#include <vector>

#include <boost/assert.hpp>
#include <boost/context/all.hpp>

namespace ctx = boost::context;

ctx::fcontext_t fcm;
ctx::fcontext_t * fc1 = 0;
ctx::fcontext_t * fc2 = 0;

void f1( intptr_t)
{
        std::cout << "f1: entered" << std::endl;
        std::cout << "f1: call jump_fcontext( fc1, fc2, 0)" << std::endl;
        ctx::jump_fcontext( fc1, fc2, 0);
        std::cout << "f1: return" << std::endl;
        ctx::jump_fcontext( fc1, & fcm, 0);
}

void f2( intptr_t)
{
        std::cout << "f2: entered" << std::endl;
        std::cout << "f2: call jump_fcontext( fc2, fc1, 0)" << std::endl;
        ctx::jump_fcontext( fc2, fc1, 0);
        BOOST_ASSERT( false && ! "f2: never returns");
}

int main( int argc, char * argv[])
{
        ctx::guarded_stack_allocator alloc;

        void * base1 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base1);
        fc1 = ctx::make_fcontext( base1, ctx::guarded_stack_allocator::default_stacksize(), f1);
        BOOST_ASSERT( fc1);
        BOOST_ASSERT( base1 == fc1->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc1->fc_stack.size);

        void * base2 = alloc.allocate(ctx::guarded_stack_allocator::default_stacksize());
        BOOST_ASSERT( base2);
        fc2 = ctx::make_fcontext( base2, ctx::guarded_stack_allocator::default_stacksize(), f2);
        BOOST_ASSERT( fc2);
        BOOST_ASSERT( base2 == fc2->fc_stack.sp);
        BOOST_ASSERT( ctx::guarded_stack_allocator::default_stacksize() == fc2->fc_stack.size);

        std::cout << "main: call start_fcontext( & fcm, fc1, 0)" << std::endl;
        ctx::jump_fcontext( & fcm, fc1, 0);

        std::cout << "main: done" << std::endl;

        return EXIT_SUCCESS;
}

このコードは本来、

main: call start_fcontext( & fcm, fc1, 0)
f1: entered
f1: call jump_fcontext( fc1, fc2, 0)
f2: entered
f2: call jump_fcontext( fc2, fc1, 0)
f1: return
main: done

と出力されて終了するのですが、main: call start ... の行だけ出力されて詰まってしまう状態です。実際にハイライトされている 22 行目で詰まっていたわけですがこれは Cygwin 内部の仕組みのせいでした。

規格に定義されているわけではありませんが、一般的に、ローカル変数(自動変数)、関数の引数などはスタックと呼ばれる領域に格納されています。関数の呼び出しがネストするごとにスタックは伸びていきます。setjmp/longjmp ならば戻るだけ、なので伸びた先のことは忘れてしまえばいいわけですが、コンテキスト切り替えによってまた戻ってくるためにはスタックの状態が保存されていなければなりません。ということで Boost.Context ではスタック領域自体を別に用意した領域に切り替えます。この時、OS 側で管理している情報である NT TIB(Thread Information Block) のスタック情報(top と bottom)も切り替えています。一方、Cygwin ではスタックの底に cygtls というスレッド固有の情報を格納しており、NT TIB を経由して参照しています。このため、Boost.Context によるコンテキスト切り替えの結果、NT TIB が指すスタックの底に cygtls が存在しないことになり(恐らく排他制御に失敗して)詰まってしまっていたわけです。

実際、f1(), f2() 内の出力をコメントアウトすると、正しく切り替え出来 main: done も出力されます(NT TIB が元のスタックの底を指すため)。コンテキスト切り替え先で Cygwin のシステムコールがまともに使えないのはペナルティが大きすぎるので、NT TIB のスタック情報を切り替えをしない版を作ってみたところ正しく動作しているようです(Boost.Context gas on Windows パッチ)。

スタック領域のチェックをするだとかいったデバッグ系ツールと組み合わせられないでしょうが他は大丈夫だと思っているのですがどうでしょうか。

0 件のコメント:

コメントを投稿