Javaでのシグナル制御の注意事項
Javaアプリケーションに他言語で作成されたOSSを組み合わせて利用すると、hs_err_pidファイルが作成されて異常終了するなど意図せぬ動作をする場合があります。
極めて単純なバグの場合が大半でしょうが、JavaVMとOSSのシグナルハンドラの競合により発生する場合がありましたのでメモを残しておきます。
Javaとシグナルの関係
- JavaVMは内部処理においてシグナルを利用しており、JavaVM起動時にシグナルハンドラを登録します
- JavaアプリからC言語など他言語で作成されたOSSを(JNI等で)ロードすると、Javaが登録したシグナルハンドラをOSS内が上書きすることがあり、プログラム全体が不正な動きとなる場合があります
JavaVMはSIGSEGV、SIGBUS、SIGFPE、SIGPIPE、SIGILLのシグナルが発生すると、実行中の命令を判断してNullPointerExceptionなどにマップしているようです。しかし、OSSや自作ライブラリでこれらのシグナルハンドラを上書きしてしまうと、本来Javaで発生したシグナルをOSS側で勝手に解釈してしまうため意図せぬ動作を招くことになります。
対処方法
自作ライブラリならともかく、OSSのシグナル制御などコアな部分を修正するのは困難です。
Javaの機能としてシグナルハンドラを書き換えない仕組み(シグナルチェーン)が提供されています。
export LD_PRELOAD=/usr/lib/jvm/java-1.8.0-openjdk-1.8.0.201.b09-2.el7_6.x86_64/jre/lib/amd64/libjsig.so
この設定後にJavaアプリケーションを起動するだけです。
解説
シグナルハンドラはsignal, sigactionなどのシステムコールで登録します。
LD_PRELOAD環境変数にlibjsig.soを設定すると、これらのシステムコールはlibjsig.soに実装された同名関数経由で登録され、JavaVMが登録するシグナルハンドラを上書きしないようになります。
これにより、シグナルが発生したときJavaVMのシグナルハンドラが優先的に動作し、Javaで発生したシグナルでない場合は、OSSなどで登録したシグナルハンドラに処理がチェーンされるようになります。
事例
JavaからrJavaを使用してR言語を利用した場合
rJavaからR言語のシグナルハンドラを登録するシーケンス。
Rengine() # rJava: jri/Rengine.java
-> Java_org_rosuda_JRI_Rengine_rniSetupR() # rJava: jri/src/Rengine.c
-> initR() # rJava: jri/src/Rinit.c
-> setup_Rmainloop() # R: src/main/main.c
-> init_signal_handlers() # R: src/main/main.c(シグナルハンドラ登録部)
static void init_signal_handlers(void)
{
/* <FIXME> may need to reinstall this if we do recover. */
struct sigaction sa;
signal_stack = malloc(SIGSTKSZ + R_USAGE);
if (signal_stack != NULL) {
sigstk.ss_sp = signal_stack;
sigstk.ss_size = SIGSTKSZ + R_USAGE;
sigstk.ss_flags = 0;
if(sigaltstack(&sigstk, NULL) < 0)
warning("failed to set alternate signal stack");
} else
warning("failed to allocate alternate signal stack");
sa.sa_sigaction = sigactionSegv;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_ONSTACK | SA_SIGINFO;
sigaction(SIGSEGV, &sa, NULL);
sigaction(SIGILL, &sa, NULL);
#ifdef SIGBUS
sigaction(SIGBUS, &sa, NULL);
#endif
signal(SIGINT, handleInterrupt);
signal(SIGUSR1, onsigusr1);
signal(SIGUSR2, onsigusr2);
signal(SIGPIPE, handlePipe);
}
- SIGSEGV, SIGBUS, SIGBUSの各シグナルに対するハンドラをsigactionで 登録しているが、第3パラメタにNULLを指定しているため、先行登録されているハンドラを上書する。
- SIGINT, SIGUSR1, SIGUSR2, SIGPIPEの各シグナルに対するハンドラは、signalで 登録しているが、復帰値を無視しているため先行登録されているハンドラが上書きされる。
これによりJavaVMの制御上で必要なシグナルハンドラが上書きされ、JavaVMが正しく動作できない状態に陥る。
シグナルチェーンなし
- JavaVMがシグナルハンドラを登録
- rJavaからR言語をロード
- R言語がシグナルハンドラを登録
- JavaVMのシグナルハンドラを上書き!!
シグナルチェーンあり
- LD_PRELOADでシグナルチェーンライブラリを登録する
- JavaVMがシグナルハンドラを登録
- シグナルチェーンライブラリ経由で登録
- rJavaからR言語をロード
- R言語がシグナルハンドラを登録
- シグナルチェーンによりJavaVMのハンドラの背後にチェーンして登録される。
- JavaVMで処理すべきシグナルはJavaVMのハンドラで処理され、その他のシグナルはチェーンされたハンドラで処理される。
hs_err_pidログファイル
Javaアプリケーションのクラッシュ時に作成されるhs_err_pid.logの「Signal Handlers」セクションでlibjvm.so以外のシグナルハンドラが登録されていれば要注意である。(以下例ではlibNative.soがユーザ定義のシグナルハンドラ)
Signal Handlers:
SIGSEGV: [libjvm.so+0xade3c0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGBUS: [libjvm.so+0xade3c0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGFPE: [libNative.so+0x7d5], sa_mask[0]=00000000000000000000000000000000, sa_flags=SA_RESTART|SA_SIGINFO
SIGPIPE: [libNative.so+0x7d5], sa_mask[0]=00000000000000000000000000000000, sa_flags=SA_RESTART|SA_SIGINFO
SIGXFSZ: [libjvm.so+0x8ce930], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGILL: [libNative.so+0x7d5], sa_mask[0]=00000000000000000000000000000000, sa_flags=SA_RESTART|SA_SIGINFO
SIGUSR1: SIG_DFL, sa_mask[0]=00000000000000000000000000000000, sa_flags=none
SIGUSR2: [libjvm.so+0x8ce7e0], sa_mask[0]=00000000000000000000000000000000, sa_flags=SA_RESTART|SA_SIGINFO
SIGHUP: [libjvm.so+0x8ce9f0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGINT: [libjvm.so+0x8ce9f0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGTERM: [libjvm.so+0x8ce9f0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO
SIGQUIT: [libjvm.so+0x8ce9f0], sa_mask[0]=11111111011111111101111111111110, sa_flags=SA_RESTART|SA_SIGINFO