エラーハンドリングはヌケモレなく、どうもかわしんです。ちゃんとエラーハンドリングしてますか?
以前のブログでも書いた通り、プログラミングする以上発生しうるエラーのうち回復可能なエラーは必ずハンドリングするべきです。
で、system call のエラーハンドリングで忘れられがちなのが EINTR
(Error INTerRupt) エラーです。
ブロッキングする ("slow" な) システムコール中にシグナルや ptrace が発生して処理が中断された時に返されるエラーで、大抵の場合エラーハンドリングとして同じシステムコールを再度呼び直すことになります。
いちいち EINTR
の時だけ再度システムコールを呼びなおすのは面倒くさいので、SA_RESTART
というフラグをシグナルハンドラに設定するとシグナルが送られた場合でも EINTR
を返すことなく処理を継続してくれます。(ただし、再開不能な場合は EINTR
が発生するらしいです。)
さて、シグナルの検証をしていたところ、SIGTERM
などのシグナルを送ると確かに EINTR
が発生するが、SIGSTOP
を送って SIGCONT
で再開した時には予想に反して EINTR
が発生しないことがわかりました。今回はその謎に迫ります。
SIGSTOP とは
SIGSTOP
とはプロセスを一時停止させるためのシグナルです。SIGCONT
を送るとプロセスは再開します。
SIGSTOP
は特別なシグナルで SIGKILL
同様、シグナルハンドラを設定することもシグナルを無視することもできません。そのため、SA_RESTART
をつけるかつけないかを選ぶことはできないです。
カーネルのコードを読む
実際に SIGSTOP
が他のシグナルと比べてどのように実装されているのかを Linux のコードを読んで調べます。Linux version は v5.19.17
, アーキテクチャは x86
を対象としてみます。
とりあえず、SA_RESTART
がどのように実装されているかを確かめます。実は Linux のコードでは SA_RESTART
は handle_signal()
関数の1ヶ所でしか使われていません。
SA_RESTART
がついていない場合は EINTR
を返すようにしてますが、そうでない場合は続行するようにしてるっぽいです。
static void handle_signal(struct ksignal *ksig, struct pt_regs *regs) { bool stepping, failed; struct fpu *fpu = ¤t->thread.fpu; if (v8086_mode(regs)) save_v86_state((struct kernel_vm86_regs *) regs, VM86_SIGNAL); /* Are we from a system call? */ if (syscall_get_nr(current, regs) != -1) { /* If so, check system call restarting.. */ switch (syscall_get_error(current, regs)) { case -ERESTART_RESTARTBLOCK: case -ERESTARTNOHAND: regs->ax = -EINTR; break; case -ERESTARTSYS: if (!(ksig->ka.sa.sa_flags & SA_RESTART)) { regs->ax = -EINTR; break; } fallthrough; case -ERESTARTNOINTR: regs->ax = regs->orig_ax; regs->ip -= 2; break; } }
https://elixir.bootlin.com/linux/v5.19.17/source/arch/x86/kernel/signal.c#L805
handle_signal()
は arch_do_signal_or_restart()
から呼び出されています。get_signal()
から non-zero な値が返ってきた時に handle_signal()
を呼び出し、それ以外の場合は handle_signal()
の -ERESTARTNOINTR
のような処理を行いシステムコールを続行させるみたいです。get_signal()
は シグナルハンドラが設定されている時に non-zero な値を返す ことになっています。そのため、SIGSTOP
は SA_RESTART
を設定されている時と同じような挙動をする みたいです。
void arch_do_signal_or_restart(struct pt_regs *regs) { struct ksignal ksig; if (get_signal(&ksig)) { /* Whee! Actually deliver the signal. */ handle_signal(&ksig, regs); return; } /* Did we come from a system call? */ if (syscall_get_nr(current, regs) != -1) { /* Restart the system call - no handlers present */ switch (syscall_get_error(current, regs)) { case -ERESTARTNOHAND: case -ERESTARTSYS: case -ERESTARTNOINTR: regs->ax = regs->orig_ax; regs->ip -= 2; break; case -ERESTART_RESTARTBLOCK: regs->ax = get_nr_restart_syscall(regs); regs->ip -= 2; break; } } /* * If there's no signal to deliver, we just put the saved sigmask * back. */ restore_saved_sigmask(); }
https://elixir.bootlin.com/linux/v5.19.17/source/arch/x86/kernel/signal.c#L865
本を読む
コードを読んで、SIGSTOP
は SA_RESTART
を設定されている時と同じような挙動をすることがわかりました。ただ、これは Linux v5.17.19 の x86 だけの挙動で他の場合はどうかはわかりません。そこで仕様としてどうなっているのかを調べます。
ただ、Linux の仕様は文書としてはないような気がするので本を読みました。
詳解 UNIX プログラミング (第3版)
"Advanced Programming in the UNIX Environment" (Third Edition) の日本語版です。
"10.5 割り込まれたシステムコール" にはおまけみたいな文体で、signal()
関数でシグナルハンドラを確立したときにシステムコールを再開するかどうかは UNIX 実装によって違い、System V ではデフォルトではシステムコールを再開せず、Linux では再開すると書いてありました。つまり、シグナルに対するデフォルトの挙動は Linux では SA_RESTART
ということみたいです。
LINUX プログラミング インタフェース
"The Linux Programming Interface" の日本語版です。
"21.5 システムコールへの割り込みと再開" では 2 つの重要なことが書いてありました。
- いくつかのシステムコールでは
SA_RESTART
をつけていても再開できずEINTR
を返す場合がある SIGSTOP
で停止した後にSIGCONT
で再開した時にいくつかのシステムコールではEINTR
を発生させる。epoll_pwait()
epoll_wait()
- inotify file descriptor に対する
read()
,semop()
,semtimedop()
,sigtimedwait()
,sigwaitinfo()
1つ目については、確かに handle_signal()
内の ERESTART_RESTARTBLOCK
と ERESTARTNOHAND
の場合に EINTR
を設定していたので確認できました。
2つ目については、Linux のコードをざっと最初に眺めただけでは知らなかったので重要な情報でした。SIGSTOP
は RA_RESTART
を有効にした場合と同じような挙動をするものの、例外もあるということでした。
まとめ
SIGSTOP
は RA_RESTART
を有効にした場合と同じような挙動をするものの、例外もあるということでした。頑張って正しく EINTR
をハンドリングしましょう。