はじめに
signal()の実装にはまったので、調査結果を覚書として残す。
調査環境は、
- kernel : linux ver.5.5.5
- glibc : Debian GLIBC 2.28-10
- arch :x86_64
- C言語(C++は未調査)
背景
自分が生まれた年の前後に制作され、動作プラットフォームの変化に合わせて修正・拡張が行われたレガシーなコードのリファクタリングをしていた。
その中でsignal()システムコールを使用しているものがあった。
signal()のmanを見ると、signal()は、プラットフォームにより挙動が変わるため使用を避け、sigaction()を使用することとあったためsigaction()を使うように修正したが、ここで盛大に嵌った。
SIGNAL(2) Linux Programmer's Manual
名前
signal - ANSI C シグナル操作
説明
signal() の動作は UNIX のバージョンにより異なる。
また、歴史的に見て Linux のバージョンによっても異なっている。
このシステムコールの使用は避け、 代わりに sigaction(2) を使用すること。
………
実装
結論から言うとLinuxのsignal()は、コンパイルオプションにより動作が異なる。
signal()を呼び出してもglibcでラップされ、内部的にはsigaction()システムコールを呼び出している。
このsigaction()システムコール呼び出し時、コンパイルオプションにより引数であるsa_flagsの値が異なってくる。
Linuxカーネル内部の実装
まず、Linuxカーネルの内部実装を見てみる。
/*
* For backwards compatibility. Functionality superseded by sigaction.
*/
SYSCALL_DEFINE2(signal, int, sig, __sighandler_t, handler)
{
struct k_sigaction new_sa, old_sa;
int ret;
new_sa.sa.sa_handler = handler;
new_sa.sa.sa_flags = SA_ONESHOT | SA_NOMASK;
sigemptyset(&new_sa.sa.sa_mask);
ret = do_sigaction(sig, &new_sa, &old_sa);
return ret ? ret : (unsigned long)old_sa.sa.sa_handler;
}
注目したいのは、sa_flagsに指定するフラグが(SA_ONESHOT | SA_NOMASK)であること。
SA_ONESHOTは、プロセスがシグナルを受けるとsa_handlerがSIG_DFLに戻るというもの。
SA_NOMASKは、プロセスがシグナルを受けてハンドラを実行しているときに再度同じシグナルを受ける(つまりマスクしない)というもの。
これは、System Vのsignal()の挙動である。(Unixには、System VとBSDという大きな2つの系統があり、所々で実装が異なるらしい。筆者は2012年に初めてLinux(Windows以外のOS)を触ったような人間なので、詳しく知らないです。)
従って、Linux kernel ver.5.5.5を使ってるいればsignal()の仕様はSystem Vの実装となっている、と思ってしまうが前述したとおりglibcを介した場合、カーネル内のsignal()は呼ばれない。
glibcの実装
C言語からシステムコールを呼び出す場合、基本的には、glibcのラッパー関数を呼び出す形となる。
signal()を呼び出す場合にインクルードするsignal.h(/usr/include/signal.h)を見てみる。
/* Set the handler for the signal SIG to HANDLER, returning the old
handler, or SIG_ERR on error.
By default `signal' has the BSD semantic. */
#ifdef __USE_MISC
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
__THROW;
#else
/* Make sure the used `signal' implementation is the SVID version. */
# ifdef __REDIRECT_NTH
extern __sighandler_t __REDIRECT_NTH (signal,
(int __sig, __sighandler_t __handler),
__sysv_signal);
# else
# define signal __sysv_signal
# endif
#endif
以下の3通りで異なってくる。
- __USE_MISCが定義されている場合
- __USE_MISCが定義されておらず、__REDIRECT_NTHが定義されている場合
- __USE_MISCも__REDIRECT_NTHも定義されていない場合
それぞれ解説する。
__USE_MISCが定義されている場合
デフォルトでは、__USE_MISCが定義される。
これは、/usr/include/features.hを見ると分かる。ここの解説が分かりやすかった。
extern __sighandler_t signal (int __sig, __sighandler_t __handler)
__THROW;
signal()がextern宣言され、実体はglibc内。
実装は以下。
sigset_t _sigintr attribute_hidden; /* Set by siginterrupt. */
__sighandler_t
__bsd_signal (int sig, __sighandler_t handler)
{
struct sigaction act, oact;
<略>
act.sa_handler = handler;
__sigemptyset (&act.sa_mask);
__sigaddset (&act.sa_mask, sig);
act.sa_flags = __sigismember (&_sigintr, sig) ? 0 : SA_RESTART;
if (__sigaction (sig, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler;
}
weak_alias (__bsd_signal, signal)
weak_alias (__bsd_signal, signal)となっており、signal()を呼ぶと__bsd_signal()が呼ばれる。
__bsd_signal()という名前からも分かるようにこれはBSDの挙動。
これは、/usr/include/signal.hの先頭行のコメントに"By default `signal' has the BSD semantic."とある説明と一致する。
今回も注目するのは、act.sa_flagsへの指定値。
__sigisempty()は0が返ってくる(これ0以外返ってくるのだろうか)と思うので、act.sa_flagsは常にSA_RESTART。
SA_RESTARTは、プロセスがシグナルを受けてハンドラ処理修了後、自動でハンドラが再登録される。
システムコール実行中にシグナル受けた場合は、処理継続となり、(wait状態であった場合には再度待ち状態となり、)EINTRによるエラー返却とならない。
例外として、socketファミリ、msgファミリのシステムコールでは、シグナルを受けた場合、EINTRによるエラー返却となる。
__USE_MISCが定義されておらず、__REDIRECT_NTHが定義されている場合
__USE_MISCの定義の有無
signal()の実装を解説する前に、__USE_MISCが定義されない(る)場合を紹介する。
例えば、-ansiオプションを加えると__USE_MISCは定義されなくなる。
以下のコマンドで確認できる。
$ echo "#include <signal.h>" | gcc -ansi -dM -E - | grep __USE_
#define __USE_FORTIFY_LEVEL 0
#define __bos(ptr) __builtin_object_size (ptr, __USE_FORTIFY_LEVEL > 1)
ちなみに-D_XOPEN_SOURCEをオプションで指定した場合も__USE_MISCは定義されない。
$ echo "#include <signal.h>" | gcc -D_XOPEN_SOURCE -dM -E - | grep __USE_
#define __USE_FORTIFY_LEVEL 0
#define __USE_ISOC11 1
#define __USE_ISOC95 1
#define __USE_ISOC99 1
#define __USE_XOPEN 1
#define __USE_POSIX2 1
#define __USE_POSIX 1
#define __bos(ptr) __builtin_object_size (ptr, __USE_FORTIFY_LEVEL > 1)
#define __USE_POSIX_IMPLICITLY 1
一方、以下のように-ansiや-D_XOPEN_SOURCEをオプションとして指定しなかった場合、つまりデフォルトでは、__USE_MISCが定義されていることが分かる。
$ echo "#include <features.h>" | gcc -dM -E - | grep __USE_
#define __USE_FORTIFY_LEVEL 0
#define __USE_ISOC11 1
#define __USE_ISOC95 1
#define __USE_ISOC99 1
#define __USE_XOPEN2K 1
#define __USE_POSIX199506 1
#define __USE_POSIX2 1
#define __USE_XOPEN2K8 1
#define __USE_MISC 1
#define __USE_POSIX 1
#define __bos(ptr) __builtin_object_size (ptr, __USE_FORTIFY_LEVEL > 1)
#define __USE_POSIX199309 1
#define __USE_POSIX_IMPLICITLY 1
#define __USE_ATFILE 1
なお、__REDIRECT_NTHは、-ansiや-D_XOPEN_SOURCEの有無に関わらず、定義される。
signal()の実装
__USE_MISCが定義されておらず、__REDIRECT_NTHが定義されている場合のsignal()の宣言は、下記。
extern __sighandler_t __REDIRECT_NTH (signal,
(int __sig, __sighandler_t __handler),
__sysv_signal);
__REDIRECT_NTHは、signalというシンボルの代わりに__sysv_signalというシンボルを使用するようにコンパイラに指示するマクロらしいので、signal()を呼び出すとglibcの__sysv_signal()が呼び出される。
__sysv_signal()の実装は以下。
/* Set the handler for the signal SIG to HANDLER,
returning the old handler, or SIG_ERR on error. */
__sighandler_t
__sysv_signal (int sig, __sighandler_t handler)
{
struct sigaction act, oact;
<略>
act.sa_handler = handler;
__sigemptyset (&act.sa_mask);
act.sa_flags = SA_ONESHOT | SA_NOMASK | SA_INTERRUPT;
act.sa_flags &= ~SA_RESTART;
if (__sigaction (sig, &act, &oact) < 0)
return SIG_ERR;
return oact.sa_handler;
}
weak_alias (__sysv_signal, sysv_signal)
毎度のことながらsa_flagsの値を見てみる。
SA_ONESHOTとSA_NOMASKは、カーネルの内部のsignal()と同じだが、SA_INTERRUPTというフラグも立てられている。
このSA_INTERRUPTは、sigactionのmanにも説明がなく、glibcを見てみると /* Historical no-op. */ と書いてあったりするので、無視でしてよさげ?
ご丁寧にSA_RESTARTのビットを倒しているが、よく分からない。
SA_INTERRUPTと同じビットとなる環境が存在するのだろうか?
そもそも、SA_RESTARTはBSD拡張独自のフラグなので、system vの実装には存在しないということで念入りに倒しているのだろうか。
__USE_MISCも__REDIRECT_NTHも定義されていない場合
/usr/include/signal.hで以下のように宣言されるため、__REDIRECT_NTHの有無にかかわらずsignal()を呼ぶと__sysv_signal()が呼ばれることが分かる。
__REDIRECT_NTHの使用の有無が何かに影響を与えるのだろうか?よく分からなかった。
# define signal __sysv_signal
余談
glibcのソースコードを調査していると同じ名前の関数が複数箇所で実装されており、どれが呼び出されるのかよく分からなかった。
とりあえず、自信の環境を鑑みて以下のソースコードを見るようにしていた。
- sysdeps/unix/sysv/linux
- sysdeps/posix
- sysdeps/x86_64
まとめ
コンパイルオプションによりsignal()の挙動が変わってしまうため、signal()->sigaction()への修正を脳死で全て同じように修正すると挙動が変わってしまった。
- manにある通り移植性が下がるため、signal()は使わない。
- signal()をsigaction()に変更する場合は、アプリケーションの既存の動作がどうなっているかを意識して変更する。
この記事がレガシーコードのメンテナンスやリファクタリングをする方(もちろんそれ以外の方も)の助けになると幸いです。