シグナルの使い方と実装について

今回は、Linuxのシグナルについて、自分なりにちゃんと調べてみましたので、
記事にまとめてみます。

ほとんどUNIXシグナルと変わらない話だとは思いますが、ソースコード
Linuxのものを参照しているので、Linuxに限った話も混じっているかと思いま
す。


そもそも「シグナル」とは?

辞書を引いてみると、"signal"は

signal
【他動】
    〜に信号を送る、合図する、〜を信号で伝える
    〜の前兆となる、〜を示す、示唆する、知らしめる文例

【名】
    〔メッセージを伝えるための〕合図、信号、信号機
    〔合図で伝える〕メッセージ、意図、警告
    〔行動を起こす〕きっかけ、引き金
    《電気》信号

【形】
    信号の(働きをする)
    際立った、目立つ、顕著な、目覚ましい、注目に値する

とのことです。


動詞の意味としては、「信号を送る/伝える」という意味があり、
名刺の意味としては、「送る/伝える信号のメッセージ」という意味があります。


ちなみに、signalの語源は、ラテン語の"signum"だそうです。これは「しるし、記号、合図」という意味を持ちます。


Linuxのシグナルとしては、受け取る側はプロセスです。
送る側はカーネルの場合もあります。
そして、やり取りするシグナルには種類があり、各シグナルにメッセージ(意図)があります。
なので、シグナルを受信したプロセスはその意図を汲み取って、その意図に即した処理を行います。

例えば、プロセスAからBへシグナルXを送信する場合、以下の関係となります。

[プロセスA] -(シグナルX)-> [プロセスB]


この関係は、CPUの割り込みを知っていれば、それに似た関係だと考えることができます。

「割り込み」の場合、外部割り込みであれば、デバイスがCPUへ割り込みを通知し、
CPUはそれまで実行していた処理を中断して、受信した割り込みに即した処理を行います。

例えば、デバイスAがCPUへ割り込みXを通知する関係は、以下のとおりです。

[デバイスA] -(割り込みX)-> [CPU]

シグナルを利用するには?

プロセスへシグナルを送るには?

Ctrl+Cなど、特定のキーにバインドされているものは、
その組み合わせでキーを入力すれば、対応するシグナルが
現在フォアグラウンドで実行中のプロセスへ送信されます。



任意のシグナルを任意のプロセスへ送信する簡単な方法は
killコマンドを使うことです。

killコマンドは、プロセスIDで指定されたプロセスに対して、
シグナル名、あるいはシグナル番号で指定されたシグナルを送信するコマンドです。

書式は以下のとおりです。
(man 1 killの抜粋です)

kill pid ... リストされた全てのプロセスに SIGTERM を送る
kill -signal pid ... リストされた全てのプロセスにシグナルを送る
kill -s signal pid ... リストされた全てのプロセスにシグナルを送る
kill -l 全てのシグナルの名前をリストする
kill -L 全てのシグナルの名前を見栄えの良い表形式でリストする
kill -l signal シグナルの番号と名前を変換する
kill -V,--version プログラムのパージョンを表示する
  • 1つ目の書式にある通り、シグナルを指定しなかった場合、SIGTERMがデフォルトで選択されます

シグナルには様々なものがあります。
POSIX.1-1990に定義されているものは以下のとおりです。(man 7 signalの抜粋)

シグナル      値      動作   コメント
------------------------------------------------------------------------------
SIGHUP         1      Term   制御端末(controlling terminal)のハングアップ検出、
                             または制御しているプロセスの死
SIGINT         2      Term   キーボードからの割り込み (Interrupt)
SIGQUIT        3      Core   キーボードによる中止 (Quit)
SIGILL         4      Core   不正な命令
SIGABRT        6      Core   abort(3) からの中断 (Abort) シグナル
SIGFPE         8      Core   浮動小数点例外
SIGKILL        9      Term   Kill シグナル
SIGSEGV       11      Core   不正なメモリ参照
SIGPIPE       13      Term   パイプ破壊:
                             読み手の無いパイプへの書き出し
SIGALRM       14      Term   alarm(2) からのタイマーシグナル
SIGTERM       15      Term   終了 (termination) シグナル
SIGUSR1    30,10,16   Term   ユーザ定義シグナル 1
SIGUSR2    31,12,17   Term   ユーザ定義シグナル 2
SIGCHLD    20,17,18   Ign    子プロセスの一時停止 (stop) または終了
SIGCONT    19,18,25   Cont   一時停止 (stop) からの再開
SIGSTOP    17,19,23   Stop   プロセスの一時停止 (stop)
SIGTSTP    18,20,24   Stop   端末 (tty) より入力された一時停止 (stop)
SIGTTIN    21,21,26   Stop   バックグランドプロセスの tty 入力
SIGTTOU    22,22,27   Stop   バックグランドプロセスの tty 出力
  • killコマンドで指定する場合、シグナル名の頭の"SIG"は無くても大丈夫です
    • ex. KILLの場合、"SIGKILL"でも"KILL"でも可

「動作」には、デフォルト動作が書かれています。
それぞれの意味は以下のとおりです。

Term   デフォルトの動作はプロセス終了。
Ign    デフォルトの動作はこのシグナルの無視。
Core   デフォルトの動作はプロセス終了とコアダンプ出力 (core(5)  参照)。
Stop   デフォルトの動作はプロセスの一時停止。
Cont   デフォルトの動作は、プロセスが停止中の場合にその実行の再開。

いくつか、シグナルについて説明を追加すると、
SIGINTは、Ctrl+Cで送信されるシグナルで、SIGTSTPは、Ctrl+Zで送信されるシグナルです。



また、SIGHUPは子プロセスを生成したシェルが死んだ時に、子プロセスに対して送信されるシグナルです。
デフォルト動作がTermで、シェルが死ぬと子プロセスも死んでしまうのですが、
それを回避するためのコマンドとしてnohupコマンドがあります。
よく、「ログアウトしても実行され続けるように」とnohupコマンドを使いますが、その理由はこのためです。



シグナルの種別について、加えて、シグナルは「標準シグナル」と「リアルタイムシグナル」に分けられます。
シグナル番号の1〜32が標準シグナル、33〜64がリアルタイムシグナルです。
両者の違いは、同じシグナルが繰り返し送信された時の挙動です。
標準シグナルの場合、2つ目のシグナルは、1つ目がまだ処理されていなければ無視されますが、
リアルタイムシグナルの場合、2つ目は待ち行列に追加されます。



C言語からは、システムコール kill を使います。

man 2 killからの抜粋ですが、書式は以下のとおりです。

#include <sys/types.h>
#include <signal.h>

int kill(pid_t pid, int sig);

第2引数のsigの指定方法は、
/usr/include/ 以下を調べると、
/usr/include/asm-generic/signal.h
で、以下のように定義されています(一部抜粋)。

#define SIGHUP		 1
#define SIGINT		 2
#define SIGQUIT		 3
#define SIGILL		 4
#define SIGTRAP		 5
#define SIGABRT		 6
#define SIGIOT		 6
#define SIGBUS		 7
#define SIGFPE		 8
#define SIGKILL		 9
#define SIGUSR1		10

なので、第2引数は定数名を使うことで、
シグナル番号だけでなくシグナル名による指定も可能です。




プロセスのシグナル受信時の動作を定義するには?

デフォルトで処理が定義されているものは、何もしなくてもシグナル受信時にプロセスはデフォルトの動作を行います。

  • ex. Ctrl+Cで発生するSIGINTは、デフォルトではプロセスが終了する

簡単な方法は、Bashの組み込みコマンドであるtrapコマンドを使うことです。

trapコマンドは、シグナル受信時の動作を特定のコマンド、あるいはデフォルトに設定するコマンドです。

書式は以下のとおりです。

trap [-lp] [[arg] sigspec ...]
  • arg: シグナル受信時に実行するコマンド。"-"が設定されると、デフォルト動作が設定される
  • sigspec: で定義されているシグナル番号、あるいはシグナル名
  • pオプション: arg無し・sigspec有りの場合、sigspecに関連付けられたtrapコマンドを表示する。argもsigspecも無い場合、各シグナルに関連付けられたコマンドのリストを表示する
  • lオプション: シグナル名と対応する番号のリストを表示する

C言語から利用する場合は、システムコールsigactionを使います。
書式は以下のとおりです。(man 2 sigactionの抜粋です。)

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  • signum: システムコール番号
  • act: NULL以外であれば、シグナルの新しい動作としてactが設定される
  • oldact: NULL以外であれば、これまでの動作がoldactに設定される

第2、第3引数は、struct sigactionのポインタです。struct sigactionの定義はアーキテクチャ依存です。
ここではman 2 sigactionで「一般的な定義」として紹介されているものを紹介します。

struct sigaction {
    void     (*sa_handler)(int);
    void     (*sa_sigaction)(int, siginfo_t *, void *);
    sigset_t   sa_mask;
    int        sa_flags;
    void     (*sa_restorer)(void);
};
  • sa_handler: signumに対する動作を指定する。設定できる値は、デフォルトの動作を指定する SIG_DFL、signumのシグナルを無視する SIG_IGN、シグナルハンドラへのポインタのいずれか。なお、シグナルハンドラの関数の引数は一つのみで、引数にはシグナル番号が渡される。
  • sa_sigaction: 後述するsa_flagsにSA_SIGINFOが指定された場合は、sa_handlerではなくこちらが使われる。関数の第1引数はsa_handlerと同じくシグナル番号だが、第2、第3引数からシグナルに関する情報をより多く得られる。引数について詳しくはman 2 sigactionを参照。
  • sa_mask: シグナルハンドラ実行中に禁止(ブロック)すべきシグナルのマスクを指定する。SA_NODEFERフラグが指定されると、ハンドラを起動するきっかけのシグナルにもsa_maskが適用される
  • sa_flags: シグナルハンドラの動作を変更するためのフラグ。詳しくはman 2 sigactionを参照
  • sa_restorer: 廃止予定。POSIXでも規定されていない

なお、シグナルはプロセスが実行する機械語命令の単位で、プロセスの実行に割り込みます。
メモリ空間に作業用のデータがある恐れもあり、シグナルハンドラから安全に呼び出せる関数は限られています。

シグナルハンドラから安全に呼び出せる関数は、man 7 signalに記載されています。
記載されていない関数は、シグナルハンドラから呼び出すべきではありません。



また、同じ理由でerrnoにも注意すべきです。
シグナルハンドラ内でerrnoを書き換える関数を呼び出す場合、
プロセス内に「ある関数の結果をerrnoで確認する」処理があると、
errnoを確認する直前にシグナルを受信した時、シグナルハンドラによりerrnoが上書きされてしまいます。




シグナルの実装

Linuxカーネルソースコードから、シグナルの仕組みがどのように実装されているのか見てみます。

これまでは、シグナルはプロセスからプロセスへ送信するというように、あたかも通信しているかのような観点で説明していました。
しかし、実装としてはシグナルを送信する何らかの事象が発生した時、カーネル内のシグナルを管理する枠組みに登録され、
何らかのタイミングで、そのシグナルが処理されるということになります。

なので、ここでは以下の3つの順に説明したいと思います。

1. シグナルが使用するデータ構造
2. シグナルがカーネル内の枠組みに登録されるまでの流れ
3. シグナルが処理されるまでの流れ

なお、参照したソースコードは、Linux 3.4.74 で、アーキテクチャに依存する箇所はARMについて見ていきます。
また、まずは簡単な例ということで、今回は、標準シグナルがデフォルト動作で登録、処理される流れを見てみます。


シグナルが使用するデータ構造

シグナルに関して重要なデータ構造を抜粋しました。
抜粋なので、各構造体のメンバはここに書かれているものが全てではありません。

- struct task_struct
  - struct sigpending pending
	- struct list_head list
	  - struct sigqueue のリストの先頭を指す
	  - 個別保留中シグナルキュー
	- sigset_t signal
  - struct signal_struct *signal
	- int notify_count
	- struct sigpending shared_pending
	  - struct list_head list
		- struct sigqueue のリストの先頭を指す
		- 共有保留中シグナルキュー
	  - sigset_t signal
  - struct sighand_struct *sighand
	- atomic_t count
	- struct k_sigaction action[_NSIG]
	  - struct k_sigaction の定義はアーキテクチャ依存
	  - 以下はarmの場合
	  - struct sigaction sa
		- __sighandler_t sa_handler
		- unsigned long sa_flags
		- sigset_t sa_mask
	- spinlock_t siglock

このリストの通り、シグナルのデータ構造は
struct task_struct (プロセスディスクリプタ)から参照できるようになっています。




シグナルがカーネル内の枠組みに登録されるまでの流れ

シグナルをカーネル内の枠組みに登録する本質的な処理を行なっているのは、
send_signal() です。

send_signal() が呼ばれるまでの流れはコールグラフを紹介するに留め、
send_signal() の中を見ていきます。

send_signal() が呼ばれるまでのコールグラフ(killシステムコールから呼ばれる場合)

- SYSCALL_DEFINE2(sys_kill)
  - kill_something_info
	- __kill_pgrp_info
	  - group_send_sig_info
		- send_signal



send_signal() の中を見てみます。
send_signal() は、ほとんど __send_signal() を呼び出すだけのようなものです。

static int send_signal(int sig, struct siginfo *info, struct task_struct *t,
			int group)
{
	int from_ancestor_ns = 0;

#ifdef CONFIG_PID_NS
	from_ancestor_ns = si_fromuser(info) &&
			   !task_pid_nr_ns(current, task_active_pid_ns(t));
#endif

	return __send_signal(sig, info, t, group, from_ancestor_ns);
}
  • send_signal() 第4引数 group は、group_send_sig_info() から呼ばれるとき 1 が渡され
    • なお、specific_send_sig_info() から呼ばれるときに 1 が渡されます
  • 参考: specific_send_sig_info() と group_send_sig_info() について
    • specific_send_sig_info() は指定されたプロセスに対してシグナルを送信します
      • send_sig() などから呼ばれます
    • group_send_sig_info() は指定されたプロセスグループに対してシグナルを送信します
      • send_group_sig_info() などから呼ばれます

カーネルコンフィギュレーションの CONFIG_PID_NS は、
make menuconfig の

- General setup
  - Namespaces support
	- PID Namespaces

で有効/無効を切り替えられるコンフィグで、デフォルト有効です。

これはPID名前空間というもので、名前空間を分けることで、同じPIDを持つプ
ロセスが複数存在することを許すものです。

ネームスペースは、Dockerで注目されているコンテナー型仮想化を実現する上
で重要な機能です。
(まだあまり詳しくは知りません)



si_fromuser(info) では、 struct siginfoのsi_codeメンバの値から、ユーザ
空間のコードを実行していたのか否かを確認しています。

task_pid_nr_ns(current, task_active_pid_ns(t)) では、
task_active_pid_ns(t) で、シグナル送信先プロセスのネームスペースを取り出して、
task_pid_nr_ns(current, task_active_pid_ns(t)) で、
カレントプロセスのPIDを、シグナル送信先プロセスのネームスペースから取り出そうとします。
なお、Not(!)が指定されているので、取り出せなかった時に真となります。

したがって、from_ancestor_ns(先祖のネームスペースから)には、
送信元プロセスがユーザ空間で実行中で、かつ送信先プロセスからPIDが取り出せなかった時に真が代入されます。




__send_signal() の中を見てみます。

static int __send_signal(int sig, struct siginfo *info, struct task_struct *t,
			int group, int from_ancestor_ns)
{
	struct sigpending *pending;
	struct sigqueue *q;
	int override_rlimit;
	int ret = 0, result;

	assert_spin_locked(&t->sighand->siglock);

	result = TRACE_SIGNAL_IGNORED;
	if (!prepare_signal(sig, t,
			from_ancestor_ns || (info == SEND_SIG_FORCED)))
		goto ret;
  • prepare_signal() の戻り値によって、ラベル ret へジャンプするようになっています


prepare_signal() の中の return 文を見てみます。

	return !sig_ignored(p, sig, force);

sig_ignored() は、タスクtのシグナル番号sigへのシグナルが無視されるかどうかを調べます

  • 無視される場合は1、無視されない場合は0を返します



__send_signal() に戻ります

  • prepare_signal() では if (!prepare_signal(...)) としていますので、無

視されるシグナルならば条件が成立してラベルretへ飛ぶことになります


	pending = group ? &t->signal->shared_pending : &t->pending;
  • specific_send_sig_info() から呼ばれるパスなので、group = 0 です
  • なので、pending = &t->pending となります
  • struct task_struct の pending メンバは「個別保留中シグナルキュー」です
    • 特定のプロセスに対する保留中シグナルを記録します
  • struct signal_struct の shared_pending メンバは「共有保留中シグナルキュー」です
    • スレッドグループ全体に対する保留中シグナルを記録します
    • 今回はこれ以上は追いません
	/*
	 * Short-circuit ignored signals and support queuing
	 * exactly one non-rt signal, so that we can get more
	 * detailed information about the cause of the signal.
	 */
	result = TRACE_SIGNAL_ALREADY_PENDING;
	if (legacy_queue(pending, sig))
		goto ret;
  • legacy_queue関数では、シグナルが通常シグナルかリアルタイムシグナルかチェックします。通常シグナルでしかもそれが既に保留しているなら、send_signal関数はその処理をしないでreturnしています。
static inline int legacy_queue(struct sigpending *signals, int sig)
{
	return (sig < SIGRTMIN) && sigismember(&signals->signal, sig);
}
  • シグナルが標準シグナルであり(sig < SIGRTMIN)、かつペンディング中である(sigismember(&signals->signal, sig))場合に真を返しています
  • 標準シグナルは同種のシグナルは一つしかキューしないので、retまで飛びます
	result = TRACE_SIGNAL_DELIVERED;
	/*
	 * fast-pathed signals for kernel-internal things like SIGSTOP
	 * or SIGKILL.
	 */
	if (info == SEND_SIG_FORCED)
		goto out_set;
  • infoに siginfo のアドレスではなく、SEND_SIG_FORCED が設定されている場合、SIGKILL か SIGSTOP が送られたことを示します。
  • この場合、ラベル out_set へジャンプし、カーネルにより即座にこのシグナルに対応する動作が実行されます
  • 保留中シグナルキューにシグナルを登録する処理は行われません
	/*
	 * Real-time signals must be queued if sent by sigqueue, or
	 * some other real-time mechanism.  It is implementation
	 * defined whether kill() does so.  We attempt to do so, on
	 * the principle of least surprise, but since kill is not
	 * allowed to fail with EAGAIN when low on memory we just
	 * make sure at least one signal gets delivered and don't
	 * pass on the info struct.
	 */
	if (sig < SIGRTMIN)
		override_rlimit = (is_si_special(info) || info->si_code >= 0);
	else
		override_rlimit = 0;

コメントの意訳

  • sigqueue あるいは他のリアルタイムメカニズムにより送信された時、リアルタイムシグナルは必ずキューされなければならない。
  • kill() がそのように定義されているかどうかは実装によります。
  • 驚き最小の原則に則り、そのように実装しようとしたが、killはメモリ不足時、EAGAINで失敗することを許さないので、我々は今、少なくともひとつのシグナルが配送され、info構造体を逃さない(?)ことを確認する。
  • 今回は、送信されるシグナルが標準シグナルである場合を見てみるので、通るパスは override_rlimit = (is_si_special(info) || info->si_code >= 0); のパスです
  • is_si_special() は以下のように定義されています
static inline int is_si_special(const struct siginfo *info)
{
	return info <= SEND_SIG_FORCED;
}
  • __send_signal() の引数 info がアドレス以外の値を取るパターンは以下の3つあります
/* These can be the second arg to send_sig_info/send_group_sig_info.  */
#define SEND_SIG_NOINFO ((struct siginfo *) 0)
#define SEND_SIG_PRIV	((struct siginfo *) 1)
#define SEND_SIG_FORCED	((struct siginfo *) 2)
  • include/linux/sched.h
  • is_si_special() は、info がアドレスを持たない場合に真を返し、そうでないときに偽を返すことがわかります
  • 各定数の意味は以下のとおりです
SEND_SIG_NOINFO ユーザが(sys_kill などで)シグナルを発行した
SEND_SIG_PRIV カーネル内部でのイベントによりシグナルが生成された
SEND_SIG_FORCED カーネル内部でのイベントによりシグナルが生成され、プロセスに強制的に受信させる
SIGKILL、SIGSTOP の場合に強制的に動作を終了/停止させる場合に使用
  • そして、info がアドレスを持つときに、info->si_code >= 0 をチェックしています
  • si_code は以下の値をとります
#define __SI_MASK	0xffff0000u
#define __SI_KILL	(0 << 16)
#define __SI_TIMER	(1 << 16)
#define __SI_POLL	(2 << 16)
#define __SI_FAULT	(3 << 16)
#define __SI_CHLD	(4 << 16)
#define __SI_RT		(5 << 16)
#define __SI_MESGQ	(6 << 16)
#define __SI_CODE(T,N)	((T) | ((N) & 0xffff))
・・・省略・・・
/*
 * si_code values
 * Digital reserves positive values for kernel-generated signals.
 */
#define SI_USER		0		/* sent by kill, sigsend, raise */
#define SI_KERNEL	0x80		/* sent by the kernel from somewhere */
#define SI_QUEUE	-1		/* sent by sigqueue */
#define SI_TIMER __SI_CODE(__SI_TIMER,-2) /* sent by timer expiration */
#define SI_MESGQ __SI_CODE(__SI_MESGQ,-3) /* sent by real time mesq state change */
#define SI_ASYNCIO	-4		/* sent by AIO completion */
#define SI_SIGIO	-5		/* sent by queued SIGIO */
#define SI_TKILL	-6		/* sent by tkill system call */
#define SI_DETHREAD	-7		/* sent by execve() killing subsidiary threads */

#define SI_FROMUSER(siptr)	((siptr)->si_code <= 0)
#define SI_FROMKERNEL(siptr)	((siptr)->si_code > 0)
  • si_code >= 0 となるのは、SI_USER あるいは SI_KERNEL です
  • SI_USER はシステムコール kill によるシグナル送信、SI_KERNEL はカーネルの様々な部分によって生成されたシグナルを示します
    • man 3 siginfo より
  • 以上から、kill() システムコールでプロセスにシグナルを送信する場合は、override_rlimit は 真 が代入されることがわかります
	q = __sigqueue_alloc(sig, t, GFP_ATOMIC | __GFP_NOTRACK_FALSE_POSITIVE,
		override_rlimit);
	if (q) {
		list_add_tail(&q->list, &pending->list);
		switch ((unsigned long) info) {
		case (unsigned long) SEND_SIG_NOINFO:
			q->info.si_signo = sig;
			q->info.si_errno = 0;
			q->info.si_code = SI_USER;
			q->info.si_pid = task_tgid_nr_ns(current,
							task_active_pid_ns(t));
			q->info.si_uid = current_uid();
			break;
		case (unsigned long) SEND_SIG_PRIV:
			q->info.si_signo = sig;
			q->info.si_errno = 0;
			q->info.si_code = SI_KERNEL;
			q->info.si_pid = 0;
			q->info.si_uid = 0;
			break;
		default:
			copy_siginfo(&q->info, info);
			if (from_ancestor_ns)
				q->info.si_pid = 0;
			break;
		}

		userns_fixup_signal_uid(&q->info, t);
  • ここでは、struct sigqueue を新たに一つアロケートして、成功した場合に、info の値に応じて初期化しています
  • userns_fixup_signal_uid() はデフォルトコンフィギュレーションでは無効なので、空の関数となりますが、名前空間に関するものです
    • CONFIG_USER_NS の有効/無効に応じて定義されますが、CONFIG_USER_NS は CONFIG_EXPERIMENTAL に依存しており、CONFIG_EXPERIMENTAL が無効なため、CONFIG_USER_NS も無効になります

なお、__send_signal() では、from_ancestor_ns の真偽値を使って、
以下のように、送信元と先のPID名前空間が異なる場合は、si_pidに0を代入しています。

			if (from_ancestor_ns)
				q->info.si_pid = 0;
	} else if (!is_si_special(info)) {
		if (sig >= SIGRTMIN && info->si_code != SI_USER) {
			/*
			 * Queue overflow, abort.  We may abort if the
			 * signal was rt and sent by user using something
			 * other than kill().
			 */
			result = TRACE_SIGNAL_OVERFLOW_FAIL;
			ret = -EAGAIN;
			goto ret;
		} else {
			/*
			 * This is a silent loss of information.  We still
			 * send the signal, but the *info bits are lost.
			 */
			result = TRACE_SIGNAL_LOSE_INFO;
		}
	}
  • struct sigqueue のアロケートに失敗した場合、info にアドレスが格納されていれば、標準シグナル(sig < SIGRTMIN)の場合、result に TRACE_SIGNAL_LOSE_INFO を設定し、info の情報は失われたことを記録します
  • ただし、コメントにある通り、info が失われてもシグナルは送信します
out_set:
	signalfd_notify(t, sig);
	sigaddset(&pending->signal, sig);
	complete_signal(sig, t, group);
ret:
	trace_signal_generate(sig, info, t, group, result);
	return ret;
}
  • signalfd_notify() は、シグナル受け付け用のファイルディスクリプタを生成する signalfd というシステムコールを利用する際のものです
  • sigaddset() は、保留中シグナルキューのビットフィールドにシグナルを設定する関数です
/* We don't use <linux/bitops.h> for these because there is no need to
   be atomic.  */
static inline void sigaddset(sigset_t *set, int _sig)
{
	unsigned long sig = _sig - 1;
	if (_NSIG_WORDS == 1)
		set->sig[0] |= 1UL << sig;
	else
		set->sig[sig / _NSIG_BPW] |= 1UL << (sig % _NSIG_BPW);
}
  • ここのパスは必ず通るので、struct sigqueue を確保出来なくて、キューに要素を追加出来なくても、ビットフィールドは必ず更新されることがわかります
  • complete_signal() は、該当のシグナルを保留しているプロセスを起床させる関数です



complete_signal() の中を見ていきます

static void complete_signal(int sig, struct task_struct *p, int group)
{
	struct signal_struct *signal = p->signal;
	struct task_struct *t;

	/*
	 * Now find a thread we can wake up to take the signal off the queue.
	 *
	 * If the main thread wants the signal, it gets first crack.
	 * Probably the least surprising to the average bear.
	 */
	if (wants_signal(sig, p))
		t = p;
	else if (!group || thread_group_empty(p))
		/*
		 * There is just one thread and it does not need to be woken.
		 * It will dequeue unblocked signals before it runs again.
		 */
		return;
	else {
		/*
		 * Otherwise try to find a suitable thread.
		 */
		t = signal->curr_target;
		while (!wants_signal(sig, t)) {
			t = next_thread(t);
			if (t == signal->curr_target)
				/*
				 * No thread needs to be woken.
				 * Any eligible threads will see
				 * the signal in the queue soon.
				 */
				return;
		}
		signal->curr_target = t;
	}
  • まず、この冒頭の処理では、今回スレッドグループ/プロセスグループでない単一のプロセスへシグナル送信を検討しているので、wants_signal() は真を返し、t = p が実行されます
	/*
	 * Found a killable thread.  If the signal will be fatal,
	 * then start taking the whole group down immediately.
	 */
	if (sig_fatal(p, sig) &&
	    !(signal->flags & (SIGNAL_UNKILLABLE | SIGNAL_GROUP_EXIT)) &&
	    !sigismember(&t->real_blocked, sig) &&
	    (sig == SIGKILL || !t->ptrace)) {
		/*
		 * This signal will be fatal to the whole group.
		 */
		if (!sig_kernel_coredump(sig)) {
			/*
			 * Start a group exit and wake everybody up.
			 * This way we don't have other threads
			 * running and doing things after a slower
			 * thread has the fatal signal pending.
			 */
			signal->flags = SIGNAL_GROUP_EXIT;
			signal->group_exit_code = sig;
			signal->group_stop_count = 0;
			t = p;
			do {
				task_clear_jobctl_pending(t, JOBCTL_PENDING_MASK);
				sigaddset(&t->pending.signal, SIGKILL);
				signal_wake_up(t, 1);
			} while_each_thread(p, t);
			return;
		}
	}
  • sig_fatal() は以下のように定義されています
#define sig_fatal(t, signr) \
	(!siginmask(signr, SIG_KERNEL_IGNORE_MASK|SIG_KERNEL_STOP_MASK) && \
	 (t)->sighand->action[(signr)-1].sa.sa_handler == SIG_DFL)
  • カーネルによってマスクされておらず、かつハンドラがデフォルトである場合に真を返します
  • 今回は、ハンドラを独自のもので設定している場合を想定しているので、sig_fatal() は偽を返します
	/*
	 * The signal is already in the shared-pending queue.
	 * Tell the chosen thread to wake up and dequeue it.
	 */
	signal_wake_up(t, sig == SIGKILL);
	return;
}
  • 最後に、signal_wake_up() を呼び出して complete_signal() は終了です。
static inline void signal_wake_up(struct task_struct *t, bool resume)
{
	signal_wake_up_state(t, resume ? TASK_WAKEKILL : 0);
}
/*
 * Tell a process that it has a new active signal..
 *
 * NOTE! we rely on the previous spin_lock to
 * lock interrupts for us! We can only be called with
 * "siglock" held, and the local interrupt must
 * have been disabled when that got acquired!
 *
 * No need to set need_resched since signal event passing
 * goes through ->blocked
 */
void signal_wake_up_state(struct task_struct *t, unsigned int state)
{
	set_tsk_thread_flag(t, TIF_SIGPENDING);
	/*
	 * TASK_WAKEKILL also means wake it up in the stopped/traced/killable
	 * case. We don't check t->state here because there is a race with it
	 * executing another processor and just now entering stopped state.
	 * By using wake_up_state, we ensure the process will wake up and
	 * handle its death signal.
	 */
	if (!wake_up_state(t, state | TASK_INTERRUPTIBLE))
		kick_process(t);
}
  • kernel/signal.c
  • このように、signal_wakeup() は signal_wake_up_state() を呼び出します
  • signal_wake_up_state() は、プロセスに新たなシグナルが来たことを伝える関数です
  • signal_wake_up_state() は、まず、set_tsk_thread_flag() で struct thread_info の flags メンバに TIF_SIGPENDING をセットします
    • これは重要で、システムコールカーネルカーネルモードからユーザモードに戻る際、このフラグをチェックし、シグナルを配信処理をする必要があるかどうかチェックをするようにしています
  • 次に、wake_up_state() でプロセスを起床させます
int wake_up_state(struct task_struct *p, unsigned int state)
{
	return try_to_wake_up(p, state, 0);
}
  • wake_up_state() は、try_to_wake_up() を呼び出します
  • try_to_wake_up() は、休止または中断しているプロセスを起床させます
    • プロセスの状態を TASK_RUNNING に設定し、ローカルCPUの実行キューにプロセスを挿入します
    • この関数を呼び出すことにより、待ちキュー上のプロセスを起床したり、シグナルを待っているプロセスを再開させたりできます
    • プロセスの起床に成功した場合は1、プロセスの起床ができなかった場合は0を返します
  • try_to_wake_up() に失敗し、wake_up_state() が偽を返すと、kick_process() を実行します
  • kick_process() は、
    • 引数で与えられたタスクが現在実行中の他のCPUのカレントタスクである場合、その他のCPUへ再スケジュール要求を送信する



__send_signal() に戻ります

out_set:
	signalfd_notify(t, sig);
	sigaddset(&pending->signal, sig);
	complete_signal(sig, t, group);
ret:
	trace_signal_generate(sig, info, t, group, result);
	return ret;
}
  • trace_signal_generate() は、
    • シグナルが生成された時に呼ばれる関数です
    • トレーサーのためのものと思われるので、今回は注目しません
    • それと、ソースコード内を検索しても、なぜか定義している場所が見つかりませんでした。。。
    • man 9 trace_signal_generate
    • http://dev.man-online.org/man9/trace_signal_generate/
  • これで、__send_signal() と、send_signal() は終わりです
シグナルが処理されるまでの流れ

前節でプロセスにシグナルが送信される(struct task_struct から参照できる構造体に値を設定する)ところまでを解説しました。
ここでは、その後、シグナルが処理されるまでの流れを見てみます。

送信されたシグナルが、送信先のプロセスで処理されるタイミングは、割り込みやシステムコールなどでカーネルモードからユーザモードへ遷移するタイミングです。
カーネルは、割り込みや例外でプロセスがユーザモードに復帰する際に、struct thread_info の flags メンバで TIF_SIGPENDING フラグが立っているかどうかを調べます

  • このフラグから、保留中シグナルの有無を確認します

その処理を行なっているのが、do_notify_resume() です。(arch/arm/kernel/signal.c)

asmlinkage void
do_notify_resume(struct pt_regs *regs, unsigned int thread_flags, int syscall)
{
	if (thread_flags & _TIF_SIGPENDING)
		do_signal(regs, syscall);

	if (thread_flags & _TIF_NOTIFY_RESUME) {
		clear_thread_flag(TIF_NOTIFY_RESUME);
		tracehook_notify_resume(regs);
		if (current->replacement_session_keyring)
			key_replace_session_keyring();
	}
}
  • if (thread_flags & _TIF_SIGPENDING) の箇所で、thread_info の flags を見て、TIF_SIGPENDING フラグが立っていれば、do_signal() を呼び出しています。

do_notify_resume() が呼ばれるまでの流れは、コールグラフを紹介するに留めます。

arch/arm/kernel/entry-common.S:
-> arch/arm/kernel/signal.c:do_notify_resume()
   -> arch/arm/kernel/signal.c:do_signal()



そして、do_signal() は、ブロックしていない保留中シグナルを扱うための関数です。
それでは、do_signal() の中を見てみます。

/*
 * Note that 'init' is a special process: it doesn't get signals it doesn't
 * want to handle. Thus you cannot kill init even with a SIGKILL even by
 * mistake.
 *
 * Note that we go through the signals twice: once to check the signals that
 * the kernel can handle, and then we build all the user-level signal handling
 * stack-frames in one go after that.
 */
static void do_signal(struct pt_regs *regs, int syscall)
{
	unsigned int retval = 0, continue_addr = 0, restart_addr = 0;
	struct k_sigaction ka;
	siginfo_t info;
	int signr;

	/*
	 * We want the common case to go fast, which
	 * is why we may in certain cases get here from
	 * kernel mode. Just return without doing anything
	 * if so.
	 */
	if (!user_mode(regs))
		return;

	/*
	 * If we were from a system call, check for system call restarting...
	 */
	if (syscall) {
		・・・省略・・・
	}

	if (try_to_freeze())
		goto no_signal;

	/*
	 * Get the signal to deliver.  When running under ptrace, at this
	 * point the debugger may change all our registers ...
	 */
	signr = get_signal_to_deliver(&info, &ka, regs, NULL);
  • if (!user_mode(regs)) の箇所では、スタックに積まれているプロセスのレジスタ情報から、プロセスがユーザモードで動作していなかった場合は、何もせず return しています
    • 「シグナルはカーネルモードからユーザモードへの遷移のタイミングで処理される」と説明した通り、システムコールや割り込みから戻る先の情報(スタックに積まれているプロセスの情報)を確認して、戻る先がカーネルモードであれば何かがおかしいのでreturnしています
  • if (syscall) の箇所では、システムコールのサービスルーチンから復帰する際にこのコードパスへ来た場合に、システムコール再実行が必要であれば再実行しています
    • システムコールによっては、サービスルーチン実行中にシグナルを受信するとエラーを返し終了するものがあるため
  • try_to_freeze() は、自身のカーネルスレッドを停止させて、システムをスリープさせるための関数だと思うのですが、なぜこのタイミングで呼ばれているのかわかりません
  • get_signal_to_deliver() は、保留中シグナルを取得する関数です
    • ですが、デフォルト動作のままの場合、この関数の中で処理されるため、デフォルト動作について追っている今回は、この関数の中を見てみます



get_signal_to_deliver() は、/kernel/signal.c に定義されています。
この関数では、保留中シグナルキューからひとつずつデキューし、
シグナルハンドラが設定されていれば、そのシグナルを返して get_signal_to_deliver() を抜け、
シグナルハンドラが設定されていなければ、デフォルト動作を実施し、デキューを続けます。

int get_signal_to_deliver(siginfo_t *info, struct k_sigaction *return_ka,
			  struct pt_regs *regs, void *cookie)
{
	struct sighand_struct *sighand = current->sighand;
	struct signal_struct *signal = current->signal;
	int signr;

relock:
	・・・省略・・・

	for (;;) {
		struct k_sigaction *ka;

		・・・省略・・・

		signr = dequeue_signal(current, &current->blocked, info);

		・・・省略・・・

		/*
		 * Now we are doing the default action for this signal.
		 */
		if (sig_kernel_ignore(signr)) /* Default is nothing. */
			continue;

		/*
		 * Global init gets no signals it doesn't want.
		 * Container-init gets no signals it doesn't want from same
		 * container.
		 *
		 * Note that if global/container-init sees a sig_kernel_only()
		 * signal here, the signal must have been generated internally
		 * or must have come from an ancestor namespace. In either
		 * case, the signal cannot be dropped.
		 */
		if (unlikely(signal->flags & SIGNAL_UNKILLABLE) &&
				!sig_kernel_only(signr))
			continue;

		if (sig_kernel_stop(signr)) {
			/*
			 * The default action is to stop all threads in
			 * the thread group.  The job control signals
			 * do nothing in an orphaned pgrp, but SIGSTOP
			 * always works.  Note that siglock needs to be
			 * dropped during the call to is_orphaned_pgrp()
			 * because of lock ordering with tasklist_lock.
			 * This allows an intervening SIGCONT to be posted.
			 * We need to check for that and bail out if necessary.
			 */
			if (signr != SIGSTOP) {
				spin_unlock_irq(&sighand->siglock);

				/* signals can be posted during this window */

				if (is_current_pgrp_orphaned())
					goto relock;

				spin_lock_irq(&sighand->siglock);
			}

			if (likely(do_signal_stop(info->si_signo))) {    /* デフォルト動作プロセス停止のパス */
				/* It released the siglock.  */
				goto relock;
			}

			/*
			 * We didn't actually stop, due to a race
			 * with SIGCONT or something like that.
			 */
			continue;
		}

		spin_unlock_irq(&sighand->siglock);

		/*
		 * Anything else is fatal, maybe with a core dump.
		 */
		current->flags |= PF_SIGNALED;

		if (sig_kernel_coredump(signr)) {
			if (print_fatal_signals)
				print_fatal_signal(regs, info->si_signo);
			/*
			 * If it was able to dump core, this kills all
			 * other threads in the group and synchronizes with
			 * their demise.  If we lost the race with another
			 * thread getting here, it set group_exit_code
			 * first and our do_group_exit call below will use
			 * that value and ignore the one we pass it.
			 */
			do_coredump(info->si_signo, info->si_signo, regs);    /* デフォルト動作コアダンプのパス */
		}

		/*
		 * Death signals, no core dump.
		 */
		do_group_exit(info->si_signo);    /* デフォルト動作プロセス終了のパス */
		/* NOTREACHED */
	}
	spin_unlock_irq(&sighand->siglock);
	return signr;
}
  • for (;;) がシグナルをひとつずつデキューするループで、ループ冒頭で dequeue_signal() を呼び出しています
  • デフォルト動作の場合の処理は "Now we are doing the default action for this signal." のコメント以降です
  • 各デフォルト動作時のコードパスはコメントのとおりです
  • シグナルの無視は、continue で次のループへ進むことで実現しています
  • 継続(Cont)の場合のパスがよくわかっていないです

参考文献

本記事は、以下の文献から多くを学ばせていただいた結果です。