ちょっとakrさんと議論する機会があったのでメモ

現状のRubyだと子プロセス実行中に Ctrl-Cが効かないという問題がある

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/31007

実際に、困っているひともいて。Rubyのtarに含まれているmake testが子プロセスをつくって子供がテストして結果をかえすというスタイルなので、Ctrl-Cで中断できなくて開発者は日々イライラしている。


で、このスレッドで、そうなっている理由が明らかに。

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-dev/31117


ところで、別件であるがperl の真似をしたといいつつ、perlはSIGHUPをマスクしてはいない。
ちょっと気になったので実装をいくつか調べてみた。

Perl のコード

if (childpid > 0) {
Sigsave_t ihand,qhand; /* place to save signals during system() */
int status;

if (did_pipes)
PerlLIO_close(pp[1]);
#ifndef PERL_MICRO
rsignal_save(SIGINT, (Sighandler_t) SIG_IGN, &ihand);
rsignal_save(SIGQUIT, (Sighandler_t) SIG_IGN, &qhand);
#endif
do {
result = wait4pid(childpid, &status, 0);
} while (result == -1 && errno == EINTR);
#ifndef PERL_MICRO
(void)rsignal_restore(SIGINT, &ihand);
(void)rsignal_restore(SIGQUIT, &qhand);
#endif


SIGINTとSIGQUITは無視してるけど、SIGHUPの手当はなし。また、forkしたあとでSIG_IGNしてるからレースがある



次、glibc

if (__sigaction (SIGINT, &sa, &intr) < 0)
{
SUB_REF ();
goto out;
}
if (__sigaction (SIGQUIT, &sa, &quit) < 0)
{
save = errno;
SUB_REF ();
goto out_restore_sigint;
}
}

pid = __fork ();
if (pid == (pid_t) 0)
{
/* Child side. */
const char *new_argv[4];
new_argv[0] = SHELL_NAME;
new_argv[1] = "-c";
new_argv[2] = line;
new_argv[3] = NULL;

/* Restore the signals. */
(void) __sigaction (SIGINT, &intr, (struct sigaction *) NULL);
(void) __sigaction (SIGQUIT, &quit, (struct sigaction *) NULL);
(void) __sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL);
INIT_LOCK ();

/* Exec the shell. */
(void) __execve (SHELL_PATH, (char *const *) new_argv, __environ);
_exit (127);
}
else if (pid < (pid_t) 0)
/* The fork failed. */
status = -1;
else
/* Parent side. */
{
/* Note the system() is a cancellation point. But since we call
waitpid() which itself is a cancellation point we do not
have to do anything here. */
if (TEMP_FAILURE_RETRY (__waitpid (pid, &status, 0)) != pid)
status = -1;
}

if ((SUB_REF () == 0
&& (__sigaction (SIGINT, &intr, (struct sigaction *) NULL)
| __sigaction (SIGQUIT, &quit, (struct sigaction *) NULL)) != 0)
|| __sigprocmask (SIG_SETMASK, &omask, (sigset_t *) NULL) != 0)
{
status = -1;
}

return status;
}


適当に削ったけど、ようするに
・SIGINTとSIGQUITはSIG_IGN。SIGHUPのケアはなし
・forkする前にシグナルハンドラを変えているのでレースなし。えらい

次、bash。
うーん、正直ソースが汚くてよくわからないけど、たぶんこれ。


void
setup_async_signals ()
{
#if defined (__BEOS__)
set_signal_handler (SIGHUP, SIG_IGN); /* they want csh-like behavior */
#endif

#if defined (JOB_CONTROL)
if (job_control == 0)
#endif
{
set_signal_handler (SIGINT, SIG_IGN);
set_signal_ignored (SIGINT);
set_signal_handler (SIGQUIT, SIG_IGN);
set_signal_ignored (SIGQUIT);
}
}


たぶん、これだろう。asyncってのは command & のような奴、で job_control == 0は
!インタラクティブシェル みたいな意味。

なので、

o インタラクティブシェルはCtrl-Cでバンバン死ぬ
o バッチでも & 使わないケースでは普通に死ぬ(なんでだ??)
o SIGINT と SIGQUITは無視。SIGHUPはノーケア。BeOS?しらんしらん。

$(()) のケースとかはめんどくさいことやってるが、読むのめんどいから無視無視。
終了ステータスみて、SIGINTだったら自分にたいしてSIGINT送り直すとかやってた。
なんでコマンド置換のときだけ、そういう気の利いたことするのかはよくわからん。


次、tcsh。コードは結構綺麗なのに読めないのは私の修行が足りないから
うーん、forkの時とかに一時的にSIGINTをBLOCKしているほかはノーガード戦法に見える