エラーハンドリングはヌケモレなく、どうもかわしんです。ちゃんとエラーハンドリングしてますか?
以前のブログでも書いた通り、プログラミングする以上発生しうるエラーのうち回復可能なエラーは必ずハンドリングするべきです。
で、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 をハンドリングしましょう。