2014年9月10日水曜日

言語Cで synchronizedを実現する(その1)

Javaの synchronized構文は便利だ。
オブジェクトをロックし、クリチカルセクションを実行したあと、確実にロックを解除するすることが言語でサポートされている。

そのような機構がない言語の場合、つぎのように記述するだろう。
lock();
// critical section
unlock();


クリチカルセクションが 1~2行程度ならよいが、長くなると何が起こるか分からない。
うっかり、この中で
if (hasError)
 return errorCode;


などとやってしまうとロックが解除されず、処理がとまってしまうか、再びロックしようとして多重ロックしてしまうかも知れない。

今回のチャレンジは、次のように関数を記述することだ。

void synchronized function(Something this) {
 // critical section
}


Javaでは synchronizedなメソッドのほかに、synchronizedブロックが記述できるが、ここでは関数だけを対象とする。

synchronizedな関数に入るときオブジェクトをロックし、出るときにアンロックする。
これを実現するための課題は、
  1. 関数に入るときと出る時に処理を行なえること。
  2. 関数に synchronized属性を持たせること。
  3. 関数が synchronizedを持って意いるか判断可能なこと。
  4. ロックするオブジェクトを認識できること。

1.関数に入るときと出る時に処理を行なう

これは以前の記事で取り上げたことのある、gcc拡張の __cyg_profile_func_enter, __cyg_profile_func_exitを使えそうである。
実験ではオブジェクトをロックせず、下記のようにトレース出力をする。
void NO_INSTRUMENT __cyg_profile_func_enter(void *function, void *caller) {
        if (isSynchronized(function))
                printf("enter synchronized function %p\n", function);
        else
                printf("enter %p\n", function);
}

void NO_INSTRUMENT __cyg_profile_func_exit(void *function, void *caller) {
        if (isSynchronized(function))
                printf("exit synchronized function %p\n", function);
        else
                printf("exit %p\n", function);
}


なお、NO_INSTRUMENTはコードを見やすくするための下記のようなマクロである。
#define NO_INSTRUMENT __attribute__((no_instrument_function))

2.関数に synchronized属性を持たせる

これはちょっと難しい。

上記の例にある、 isSynchronized(function)をどうやって実現するか。

関数に任意の属性を持たせたい。
関数とは別管理のルックアップテーブルを持たせれば簡単そうだが、別管理が面倒くさい。
関数が synchronizedかどうか人間が知りたいとき、いちいち管理テーブルを見ないとならない。
void synchronized function...のような記述できるほうが管理しやすい。
そのためには Javaのアノテーションのような機能が必要になる。
しかし、言語Cにはアノテーションは無い。 gcc拡張を探したがそれに近いものは無い。

諦め掛けていたが、トンでもないアイデアを思いついた。
synchronized用のメモリセクションを作り、そこに配置した関数を synchronizedとみなすことにしよう。

synchronizedセクションを作って関数を配置するには、gcc拡張の __attribute__構文が使える。
次のようなマクロを定義すれば、あとは関数に synchronizedを付けるだけ。
#define synchronized __attribute__((section("synchronized")))
gcc以外にもソースコード上にセクションを明記できるものがあるが、#pragma構文を使うものは残念ながら #defineすることはできない。

早速書いてみよう。 使い分けできるのが分かるよう synchronizedでない普通の関数も作っておく。

Sub.h(主要部分だけ)
extern void sub1();
extern synchronized void sub2();


Sub.c(主要部分だけ)
void sub1() {
 printf("sub1 called.\n");
}

void synchronized sub2() {
 printf("sub2 called.\n");
}


これをビルドしてできた Sub.oを逆アセンブルしてみると、ちゃんと synchronizedセクションに対応づけられている。
$ objdump -d Sub.o

Sub.o:     file format elf32-i386


Disassembly of section .text:

00000000 :
   0: 55                    push   %ebp
   1: 89 e5                 mov    %esp,%ebp
   3: 83 ec 18              sub    $0x18,%esp
   6: 8b 45 04              mov    0x4(%ebp),%eax
   9: 89 44 24 04           mov    %eax,0x4(%esp)
   d: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
  14: e8 fc ff ff ff        call   15 
  19: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
  20: e8 fc ff ff ff        call   21 
  25: 8b 45 04              mov    0x4(%ebp),%eax
  28: 89 44 24 04           mov    %eax,0x4(%esp)
  2c: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
  33: e8 fc ff ff ff        call   34 
  38: c9                    leave  
  39: c3                    ret    

Disassembly of section synchronized:

00000000 :
   0: 55                    push   %ebp
   1: 89 e5                 mov    %esp,%ebp
   3: 83 ec 18              sub    $0x18,%esp
   6: 8b 45 04              mov    0x4(%ebp),%eax
   9: 89 44 24 04           mov    %eax,0x4(%esp)
   d: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
  14: e8 fc ff ff ff        call   15 
  19: c7 04 24 0d 00 00 00  movl   $0xd,(%esp)
  20: e8 fc ff ff ff        call   21 
  25: 8b 45 04              mov    0x4(%ebp),%eax
  28: 89 44 24 04           mov    %eax,0x4(%esp)
  2c: c7 04 24 00 00 00 00  movl   $0x0,(%esp)
  33: e8 fc ff ff ff        call   34 
  38: c9                    leave  
  39: c3                    ret 

因みに synchronizedでない普通の関数は .textセクション。

3.関数が synchronizedを持って意いるか判断

そして、isSynchronized関数はつぎの通り。
int NO_INSTRUMENT isSynchronized(void* function) {
 extern char __start_synchronized;
 extern char __stop_synchronized;

 return (function >= (void*)&__start_synchronized)
  && (function < (void*)&__stop_synchronized);
}

gccでは、__start_, __stop_の後にセクション名をつけたものが、それぞれセクションの先頭と後端のアドレスを示している。 これを実行時に関数アドレスと比較することで、そのセクションの中の関数かどうか判断できるというわけである。

あとは次のようなプログラムで sub1()と sub2()を実際に呼び出してみる。 動作が確認しやすいよう、synchronizedセクションの先頭、後端アドレスと、sub1(), sub2()のアドレスも出力しておき、つづいてsub1()と sub2()を実際に呼び出す。

int NO_INSTRUMENT main(int argc, char** argv) {
 extern char __start_synchronized;
 extern char __stop_synchronized;

 printf("[synchronized section]\n");
 printf("start: %p\n", &__start_synchronized);
 printf("end: %p\n", &__stop_synchronized);
 printf("\n");

 printf("[function address]\n");
 printf("sub1: %p\n", sub1);
 printf("sub2: %p\n", sub2);
 printf("\n");

 printf("[call functions]\n");
 sub1();
 printf("\n");
 sub2();
 return 0;
}
結果は次のように、synchronizedとそうでないものがちゃんと区別できることを確認できた。
$ ./Main
[synchronized section]
start: 0x80486ec
end: 0x8048726

[function address]
sub1: 0x8048600
sub2: 0x80486ec

[call functions]
enter 0x8048600
sub1 called.
exit 0x8048600

enter synchronized function 0x80486ec
sub2 called.
exit synchronized function 0x80486ec

なお、Sub.h, Sub.cの両方で sub2()に synchronizedを記述したが、Sub.h, Sub.cのいづれか一方だけに記述しても結果は同じになる。

この手法は、synchronized以外にも使えるかも知れない。 なんとも馬鹿げたやり方だが。
※本プロジェクトの名称 softiesの先頭の sは stupidの略ですから、何でもアリです。 お許しください。

0 件のコメント:

コメントを投稿