Shoeisha Technology Media

CodeZine(コードジン)

特集ページ一覧

signalについて(後篇)

シグナル使用上の注意点と改良ポイント

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加
2007/10/15 14:00

シグナルを利用するにあたっては注意すべきポイントがあります。また、シグナルハンドラを使用せずに最適化するやり方や、別の非同期処理であるスレッドとの共有方法等、実装をさらに改良していく方法を紹介します。

目次

はじめに

 シグナルについての解説最終回です。今回はシグナルを扱う上での注意点や、よりよい実装方法の提案、シグナルの未来等について説明します。

過去の記事

5. 実装上の注意点

 前回まで説明してきたとおり、シグナルは気軽に使うことができる便利なプロセス間通信です。しかしシグナルには、やってはいけないことや気をつけるべき点が多く存在します。以下、代表的な注意点を挙げます。

5.1 最適化問題

 volatileはそれほど使われない宣言修飾子ですが、これは最適化を制御する修飾子です。なぜこの修飾子がシグナルと関係あるのかについてですが、先にソースコードを示します。

signal_test_volatile.c
/*
1. gcc -g -W -Wall signal_test_volatile.c -o signal_test_volatile
2. gcc -g -W -Wall signal_test_volatile.c -o signal_test_volatile -O2
3. gcc -g -DVOL -W -Wall signal_test_volatile.c -o signal_test_volatile -O2 */
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <signal.h> #include <errno.h> #include <string.h> void signal_handler( int no ); #ifdef VOL volatile int flg = 1; #else int flg = 1; #endif int main( ) { struct sigaction sa; sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; sigemptyset( &sa.sa_mask ); /* Ctrl+C で発生するSIGINTをキャッチ */ sigaction( SIGINT, &sa, 0 ); while( 1 ) { if( flg == 1 ) { sleep( 1 ); if( flg != 1 ) { printf( "flg != 1 !! \n" ); } } } return 0; } void signal_handler( int no ) { ( void )no; flg = ( flg ) ? 0 : 1; char *mes = "signal get\n"; write( 1, mes, strlen( mes )); }

 上記のプログラムはシグナルが発生(Ctrl+C押下)しない限りprint文に到達する事は決してありません。スリープ中にシグナルが発生するとprint文を出力し、再度シグナルが発生してもprint文は出力しません。つまり2回シグナルが発生するとprint文が出力するわけです。

 ソースコードの最初のコメントを見てください。このプログラムを1.とコンパイルすると、まさに上記の通り動作します。しかし最適化を行った場合、つまり2.とコンパイルすると上記の通りには動作しません。ところが、3.でコンパイルし実行すると正しく動作することが確認できます。

 これはコンパイラが最適化の中で、初回アクセス時のflgの内容をレジスタに保存し、それ以降のflgのアクセスにもレジスタ内の値を参照しているからです。つまりコンパイラは、flgがシグナルからも変更される可能性があるという事を知らないため、最適化対象にしてしまい、その結果、flgの値を固定値にしたと思われます。

 コンパイラに対してflgがシグナルからも変更される可能性がある事を伝え、flgに対し、レジスタではなく実メモリに対して参照を強制するようにしなくてはいけません。その働きをするのがvolatile修飾子です。つまりコンパイラに対して変数への最適化を抑制します。

 最適化を行うのは、仕様を満たすためのテストを終えて性能測定を行う頃が多いかと思います。すると、それまで問題なく動いていたのにいきなり動作が不安定になることがあります。また、最適化を行ったプログラムはgdb等で追うのが難しく、問題点を明確にするのに時間がかかります。

 シグナルに関係無く、最適化を行う場合は上記の点に注意して仕様確認試験を行うべきです。

5.2 ライブラリのリエントラント問題

 ライブラリ関数のリエントラント性も結構多く見逃される問題の1つです。もともとC言語の関数ははるか昔にインターフェースが定められ、そのまま定着しています。そのため、まったくリエントラント性を考慮せずに定められたライブラリ関数も多くあります。結果的にどの関数がリエントラントを考慮しているかはプログラマに委ねられています。

 下記のプログラムを動かしてみます。

signal_test_deadlock.c
/*
gcc -g -W -Wall signal_test_deadlock.c -o signal_test_deadlock
*/
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <string.h> #include <pthread.h> void signal_handler( int no ); void test_function( const char * msg ); void sig_sleep( unsigned int cnt ); int main( ) { struct sigaction sa; sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; sigemptyset( &sa.sa_mask ); sigaction( SIGINT, &sa, 0 ); sig_sleep( 10 ); while( 1 ) { test_function( "main" ); } return 0; } void signal_handler( int no ) { ( void )no; char *mes = "sigint signal get. go test_function.\n"; write( 1, mes, strlen( mes )); test_function( "signal_handler" ); } void test_function( const char * msg ) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; static int count = 0; printf( "inner test_function from %s.\n", msg ); pthread_mutex_lock( &mutex ); sig_sleep( 1 ); count ++; pthread_mutex_unlock( &mutex ); printf( "out test_function from %s. count:[%d]\n", msg, count ); } void sig_sleep( unsigned int cnt ) { while( cnt ) { cnt = sleep( cnt ); } }

 関数 test_function はmain関数からも呼ばれますがsignal_handlerからも呼ばれます。main関数がtest_functionに到達する前にSIGINT(Ctrl+C)が発生するとtest_functionが呼ばれます。また、main関数からもtest_functionが定期的に呼ばれているのが判ります。しかしmain関数からtest_functionが呼ばれている時にCtrl+Cを押下したとたん、プログラムが止まります。これは関数 test_function がリエントラント性を考慮されていないため、デッドロックが発生したからです。具体的には下記のような動きをしています。

  1. main関数
  2. test_function関数を使用
  3. 内部でpthread_mutex_lock(staticなmutex)を使用
  4. 時間のかかる処理(ここではsleepで実現)を実行
  5. <=== 時間のかかる処理中にSIGINTを受信 ====
    === staticなmutexはロックされたまま中断 ====>
  6. signal_handlerが呼ばれる
  7. シグナルハンドラ中でtest_function関数を使用
  8. 内部でpthread_mutex_lock(staticなmutex)を使用
  9. <=== 同じmutexを再度ロックしようとして、アンロック待ち ===
    === デッドロック発生! ===>

 test_functionのような関数なんて作らないし、存在してないとお思いでしょうが、まさにprintfがtest_functionのような実装を行っています。今までのサンプルプログラムでは、シグナルハンドラ内部で文字列を表示する関数として「write(2)」を使ってきました(一部のプログラムではサボってprintfを使用しています)。これは要するにprintf(3)がリエントラントではないからです。デッドロックが発生するのはタイミングに依存するので、再現が非常に困難です。

 既存の関数でこのようなバグを発生させないためには、シグナルハンドラからは「非同期シグナルセーフ」関数だけを呼ぶように変更するしかありません。

 また、test_functionのように内部で静的変数を抱えてる関数を非同期シグナルセーフに対応させるには、処理中はシグナル受信をマスクする事で可能です。

 下記プログラムは signal_test_deadlock.c の改良版です。

signal_test_deadlock_safe.c
/*
gcc -g -W -Wall signal_test_deadlock_safe.c -o signal_test_deadlock_safe
*/
#include <stdio.h> #include <unistd.h> #include <signal.h> #include <string.h> #include <pthread.h> void signal_handler( int no ); void test_function( const char * msg ); void sig_sleep( unsigned int cnt ); int main( ) { struct sigaction sa; sa.sa_handler = signal_handler; sa.sa_flags = SA_RESTART; sigemptyset( &sa.sa_mask ); sigaction( SIGINT, &sa, 0 ); sig_sleep( 10 ); while( 1 ) { test_function( "main" ); } return 0; } void signal_handler( int no ) { ( void )no; char *mes = "sigint signal get. go test_function.\n"; write( 1, mes, strlen( mes )); test_function( "signal_handler" ); } void test_function( const char * msg ) { static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; sigset_t mask, savemask; sigemptyset( &mask ); sigaddset( &mask, SIGINT ); sigprocmask( SIG_BLOCK, &mask, &savemask ); printf( "inner test_function from %s.\n", msg ); pthread_mutex_lock( &mutex ); sig_sleep( 1 ); pthread_mutex_unlock( &mutex ); printf( "out test_function from %s.\n", msg ); sigprocmask( SIG_SETMASK, &savemask, 0 ); } void sig_sleep( unsigned int cnt ) { while( cnt ) { cnt = sleep( cnt ); } }

  • ブックマーク
  • LINEで送る
  • このエントリーをはてなブックマークに追加

著者プロフィール

  • 赤松 エイト(エイト)

    (株)DTSに勤てます。 WebアプリやJavaやLL等の上位アプリ環境を密かに憧れつつも、ず~っとLinuxとかHP-UXばかり、ここ数年はカーネル以上アプリ未満のあたりを行ったり来たりしています。 mixiもやってまして、こちらは子育てとか日々の日記メインです。

バックナンバー

連載:signalについて
All contents copyright © 2005-2019 Shoeisha Co., Ltd. All rights reserved. ver.1.5