C言語

シグナルハンドラーの正しい書き方

2022年6月19日

はじめに

UNIX系のプログラムでシグナルを扱う場合、まず考えるのがシグナルハンドラーの実装でしょう。
どんなものかとネットで検索すると、全くルールを守っていないサンプルコードに必ず出会います。
日本語なんかで検索していると、ヒット率はさらに高くなります(笑)

この記事では、どんなルールがあり、そのルールを守らないとどういった問題が起こるのか、
また、ルールを守るにはどのように記述すれば良いかを紹介します。

 

シグナルとは?

そもそもシグナルとは、初期の UNIX から採用されており、
カーネルからユーザ空間で実行されるプロセスへ
ある事象が発生したことを伝えるために利用されています。

シグナルは「ソフトウェア割り込み」と呼ばれており、「割り込み」です。
もう一度言います、「割り込み(Interrupt)」です。
ハンドルするには、普通のプログラムとは異なるルールに従う必要があります。

 

例えば、システムコールのエラーとして EINTR があります。
これはシグナルによって割り込まれたためにエラーとなったことを示します。
カーネルが提供するシステムコールでさえ、シグナルに割り込まれると
一旦エラーとしてユーザ空間に処理を戻すのです。
それほど、シグナルの配送は特殊な動きをするということです。

 

一方、シグナルを扱うハンドラは、普通のプログラムと同様に記述できてしまいます。
ルール違反をしていても、コンパイラや静的解析ツールは指摘してくれません。
そのため、プログラマが「ルール」を意識してハンドラを記述する必要があります

 

シグナルハンドラを記述する際のルール

manページの signal(7) では、以下のように記載されています。

非同期シグナルで安全な関数 (async-signal-safe functions)
シグナルハンドラ関数には非常に注意しなければならない。 他の場所の処理はプログラム実行の任意の箇所で中断される可能性があるためである。 POSIX には「安全な関数 (safe function)」という概念がある。 シグナルが安全でない関数の実行を中断し、かつ handler が安全でない関数を呼び出した場合、プログラムの挙動は未定義である。

原文を読まないと意味不明な文にも見えますが、要約すると、シグナルハンドラでは、

非同期シグナル安全な関数以外は使用するな

です。これが、シグナルハンドラを記述する際のルールです。

 

同じmanページに、async-signal-safeな関数一覧が記載されています。
これらの関数のみ、シグナルハンドラ内での動作が保証されており、使用が許されています。

結構な数の関数が羅列されていますが、あまり大したことを行えません。
というか、使い慣れない関数の方が多いのではないのでしょうか。

 

例えば、シリアルに文字を出力する printf(3) 関数は一覧にありません。
プロセスを終了させる exit(3) もありません。(システムコールの _exit(2) はあります)

SANACHAN
昔、SIGTERM を受けて exit(3) を呼び出したのにプロセスが終了しない、という問題に遭遇したことがあります。exit(3) は、標準出力のバッファをフラッシュしてプログラムを終了する関数で、その中でデッドロックが起きているのが原因でした。

 

記述したシグナルハンドラに、これらの関数を使おうとしていませんか?
そのプログラムは間違いです。後述する問題が発生しますので、即刻修正をご検討ください。

 

発生しうる問題

デッドロック

シグナルハンドラは、ユーザプログラムから見ると割り込みのように動きます。
ある処理を行っている最中にシグナルハンドラが呼び出され、ハンドルし終わると戻ってきます。

例えば、mutex を握った後にシグナルハンドラが呼び出され、
シグナルハンドラの内部でも同じ mutex を取得する処理が記述されていると、
簡単にデッドロックします。

意図的に mutex を使用していなくても、malloc(3) などのライブラリ関数が使っていたりします。
malloc(3) は様々な関数(例:realloc, calloc, printf, sprintf, fwrite)からも呼び出されるため、
非同期シグナル安全な関数以外を使うと、痛い目を見ることになります。

参考

malloc(3) の詳しい仕組みについては、「mallocの動的メモリ管理構造」をご覧ください。

 

不正メモリアクセス(SIGSEGV, SIGBUSなど)

マルチスレッドや SMP を意識した大抵の関数は、リエントラント(再入可能)処理となっています。
ただし、これはスレッドセーフであって、非同期シグナルセーフと同義ではありません。

リエントラントに対応するため、クリティカルな処理(例:リスト処理)などに保護を設けますが、
シグナルはリスト操作中にも割り込みます。

その結果、リストの不正な参照が発生し、不正メモリアクセスを起こします。
最悪の場合は、無限ループに陥ります。

 

ここに記載した事例は私が経験したことのあるごく一部の例で、
メモリの不正アクセスを起こす以上、何が起きてもおかしくありません。

厄介なのは、ルールを破ったとしても問題なく動いてしまうことです。
C言語としては正しい記述なため、静的解析ツールやコンパイラは指摘してくれません
問題が起きると解析が困難を極めるため、初めからきちんとコーディングするようにしましょう。

 

シグナルハンドラの正しい書き方

Self-Pipe Trick方式

シグナルを安全にハンドルする王道として、Self-Pipe Trick 方式が有名です。

シグナルハンドラ内では pipe に情報を書くということだけを行い、
main スレッドで pipe から情報を読み出して届いたシグナルに応じた処理を行うという方法です。
複数のシグナルを同じ仕組みで扱いたい場合は、届いた signal 番号も pipe に書きます

システムコールである write(2)_exit(2) で記述できるため、
非同期シグナル安全な関数のみを使って実装することができます。

参考

main スレッドがイベントの多重待ちを行う典型的なループ構造を知らない方は、
select関数の使い方」をご覧ください。

 

signalfd を使う

こんな厄介なルールは嫌だ! ということで、Linux では signalfd(2) という仕組みが用意されています。

SANACHAN
これをシグナルハンドラというのか? という突っ込みはご遠慮ください(笑)

 

man ページの最下部にサンプルコードが記載されているが、内容を要約すると、
シグナルをハンドルせずにブロックし、signalfd(2) で生成したディスクリプタから直接 read(2) します。
この場合、ブロックしているシグナルが要因の EINTR は発生しなくなるという利点もあります。

参考

read(2) や write(2) の正しい使い方は、「read関数の使い方」「write関数の使い方」をどうぞ。

 

シグナルなんてハンドルしない、という選択

おそらく、この記事にたどり着いた方は、「非同期シグナル安全」の概念を初めて耳にしたかもしれません。
そんな方へは今一度、シグナルハンドラに書きたい処理を見直してみてください。
制約の多いシグナルハンドラで行うべき処理でしょうか?

以下を例に、再考されることをお勧めいたします。

 

プロセス終了時の資源解放

SIGTERM を受けてプロセスを終了する際、fd を閉じたり、確保したメモリを開放したり…
と、考えているのかもしれません。

その処理は、プログラム終了時にカーネルが行ってくれるので不要です。

 

一時ファイルを削除したい

一時的なファイルを作成していたため、プロセス終了時に削除することを考えているのかもしれません。
それは、作成直後に削除しておくか、起動直後に削除すればよいです。

SANACHAN
作成直後に削除しても、すでに open(2) しているファイルは継続利用できます。

 

プログラムの異常を検知したい

SIGBUSSIGSEGVSIGABRT などの異常を知らせるシグナル類をキャッチし、
異常を起こしたプログラムの番地や、バックトレースをログに出力する、
といったプログラムが実装されているのをよく見かけます。OSSでも見かけます。

デバッグシンボル付きのライブラリ群を搭載していないとバックトレースは取れないし、
SIGABRTabort() のライブラリ関数がプログラム番地になるので意味がない。

素直に、コアダンプを出力して解析した方がいいです。

 

おわりに

UNIX 系の「シグナル」は、割り込みです。
普通のプログラムと異なり、ハンドラはルールに従って記述しないと奇怪な現象に悩むことになります。

man や POSIX から仕様を熟読してからシグナルハンドラを実装することをお勧めします。

 

こちらの記事もよく読まれています

  • この記事を書いた人
  • 最新記事
SANACHAN

SANACHAN

「生涯一エンジニア」を掲げ、大手グローバル企業でSE/PGとして8年勤め、キャリアアップ転職した現役のエンジニアです。世にあるメジャーな全プログラム言語(コボル除く)を自由に扱えます。一児の父。自分のため、家族のため、日々勉強してます。システムエンジニア、プログラミングに関する情報を蓄積している雑記帳です。

-C言語
-, ,