poll(),select()はsignalに気を付けて使うんだ

早いもので1月も終盤を迎えてます.

そんな中、久しぶりのTECHネタ.

気が付いたら去年の7月からTECHネタ書いてなかったんですね…反省

今回は以前に書いた pidfd やsignalfd を使ってプロセス終了監視を行う件に関連した
poll()やselect()を使う場合には signal に気を付ける、という話です.

ファイル・ディスクリプタのイベントを待つ

Cannon EOS R6 RF24-105 IS STM

以前、プロセスの終了をpidfdというファイルディスクリプタを経由したイベントで待つ話を書きました.

その時に紹介したサンプルコードはこちらです.

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <poll.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open 434   /* System call # on most architectures */
#endif

static int
pidfd_open(pid_t pid, unsigned int flags)
{
  return syscall(__NR_pidfd_open, pid, flags);
}

#define TIMEOUT (5 * 1000)	/* TimeOut = 5sec */
int main (int argc, char ** argv)
{
  pid_t pid = fork ();
  int status;
  sigset_t mask;
  int pfd;

  if (pid == 0)
    {
      /* Child Process */
      execv("/bin/sleep", argv);
    }
  else
    {
      struct pollfd pollfd;
      int pidfd, ready;

      printf ("Created process pid:%d\n", pid);
      
      pfd = pidfd_open(pid, 0);
      if (pfd == -1)
	     {
	      perror("pidfd_open");
	      return -1;
	      }
      pollfd.fd = pfd;
      pollfd.events = POLLIN;
      ready = poll (&pollfd, 1, TIMEOUT);
      if (ready < 0)
	     {
	        perror("poll");
	        return -1;
	      }
      else if (ready == 0)
	      {
	       printf ("pid:%d Timeout\n",pid);
	      }
      else
	     {
	       printf ("process: %d done\n", pid);
	     }
      /* Parent Process */
      pid_t dpid = waitpid (pid, &status, 0);
      printf ("pid:%d done, exit status: %d\n", dpid,  WEXITSTATUS(status));
      close (pfd);
    }
  return 0;
}

ここでは fork() で新たに生成した子プロセスで /bin/sleep を実行し,親プロセスでは pidfd を poll() で監視することによって子プロセスの終了を待っています.

ここでpoll()で終了待ちしている間のシグナル受信について考えます.

イベント待ち中のシグナル受信

SIGURG(23) の入力を許可してpoll()でイベント待ちしているプロセスにシグナルを投げてみます. シグナル SIGURG を受け付けるようにしたコードです.

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <poll.h>
#include <signal.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open 434   /* System call # on most architectures */
#endif

static
void signal_handler(int num)
{
  puts("caught signal");
}

static int
pidfd_open(pid_t pid, unsigned int flags)
{
  return syscall(__NR_pidfd_open, pid, flags);
}

#define TIMEOUT (120 * 1000)    /* TimeOut = 120sec */


int main (int argc, char ** argv)
{
  pid_t pid = fork ();
  int status;
  sigset_t mask;
  int pfd;
  struct sigaction sa;

  sigemptyset(&sa.sa_mask);
  sa.sa_handler = signal_handler;
  sa.sa_flags = SA_SIGINFO;
  if (sigaction(SIGURG, &sa, NULL) < 0)
    {
      perror("sigaction");
      return -1;
    }

  printf ("my process id:%d\n", getpid());

  if (pid == 0)
    {
      /* Child Process */
      execv("/bin/sleep", argv);
    }
  else
    {
      struct pollfd pollfd;
      int pidfd, ready;

      printf ("Created process pid:%d\n", pid);

      pfd = pidfd_open(pid, 0);
      if (pfd == -1)
        {
          perror("pidfd_open");
          return -1;
        }

      pollfd.fd = pfd;
      pollfd.events = POLLIN;

      ready = poll (&pollfd, 1, TIMEOUT);
      if (ready < 0)
        {
          perror("poll");
          return -1;
        }
      else if (ready == 0)
        {
          printf ("pid:%d Timeout\n",pid);
        }
      else
        {
          printf ("process: %d done\n", pid);
        }
      /* Parent Process */
      pid_t dpid = waitpid (pid, &status, 0);
      printf ("pid:%d done, exit status: %d\n", dpid,  WEXITSTATUS(status));
      close (pfd);
    }
  return 0;
}

起動するとプロセスIDを表示します.

$ ./sample00 100
 my process id:4872
 Created process pid:4873

そのプロセスIDに対して SIGURG(23) を投げます.

$kill -23 4872

そうすると, シグナルハンドラを実行後, poll は -1 を返して終了.
perror は以下のようなエラーメッセージを表示します.

caught signal
 poll: Interrupted system call

man poll によると EINTR エラー、イベント待ち中に発生したシグナルで終了した模様.

ERRORS
    EINTR  A signal occurred before any requested event; see signal(7).

シグナルを受信してもイベント待ちを継続したい場合, 何か対策が必要です.

EINTR エラー対策

イベント待ち中にシグナルを受信し, EINTR エラー発生の対応を行ったコードです.

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <poll.h>
#include <signal.h>
#include <errno.h>

#ifndef __NR_pidfd_open
#define __NR_pidfd_open 434   /* System call # on most architectures */
#endif

static
void signal_handler(int num)
{
  puts("caught signal");
}

static int
pidfd_open(pid_t pid, unsigned int flags)
{
  return syscall(__NR_pidfd_open, pid, flags);
}

#define TIMEOUT (120 * 1000)    /* TimeOut = 120sec */

int main (int argc, char ** argv)
{
  pid_t pid = fork ();
  int status;
  sigset_t mask;
  int pfd;
  struct sigaction sa;

  sigemptyset(&sa.sa_mask);
  sa.sa_handler = signal_handler;
  sa.sa_flags = SA_SIGINFO;
  if (sigaction(SIGURG, &sa, NULL) < 0)
    {
      perror("sigaction");
      return -1;
    }

  printf ("my process id:%d\n", getpid());
  if (pid == 0)
    {
      /* Child Process */
      execv("/bin/sleep", argv);
    }
  else
    {
      struct pollfd pollfd;
      int pidfd, ready;

      printf ("Created process pid:%d\n", pid);

      pfd = pidfd_open(pid, 0);
      if (pfd == -1)
        {
          perror("pidfd_open");
          return -1;
        }

    retry:
      pollfd.fd = pfd;
      pollfd.events = POLLIN;

      ready = poll (&pollfd, 1, TIMEOUT);
      if (ready < 0)
        {
          perror("poll");
          if (errno == EINTR)
            goto retry;

          return -1;
        }
      else if (ready == 0)
        {
          printf ("pid:%d Timeout\n",pid);
        }
      else
        {
          printf ("process: %d done\n", pid);
        }
      /* Parent Process */
      pid_t dpid = waitpid (pid, &status, 0);
      printf ("pid:%d done, exit status: %d\n", dpid,  WEXITSTATUS(status));
      close (pfd);
    }
  return 0;
}

上記のプログラムを実行し, 同じくSIGURG(23) 投げると、シグナルハンドラ実行後もイベント待ちを継続します.

caught signal
 poll: Interrupted system call
 process: 5037 done
 pid:5037 done, exit status: 0

sigaction の SA_RESTART では救えない

man 7 signal によると, 何やら気になる記述が.

   Interruption of system calls and library functions by signal handlers
       If a signal handler is invoked while a system call or library function call is blocked, then either:

       * the call is automatically restarted after the signal handler returns; or

       * the call fails with the error EINTR.

      Which of these two behaviors occurs depends on the interface and whether or not the signal handler was established using the SA_RESTART flag (see sigaction(2)).  The details vary across UNIX
       systems; below, the details for Linux.

       If a blocked call to one of the following interfaces is interrupted by a signal handler, then the call is automatically restarted after the signal handler returns if the SA_RESTART flag  was
       used; otherwise the call fails with the error EINTR:

sigaction() の SA_RESTART を使えば, シグナルハンドラ実行後, 自動的にイベント待ちに入るようなことが書いてます.

man 2 sigaction によると

       SA_RESTART
              Provide behavior compatible with BSD signal semantics by making certain system calls restartable across signals.  This flag is meaningful only when establishing a signal handler.  See
              signal(7) for a discussion of system call restarting.

ふむふむ、シグナル受信後のシステムコールの再起動ができそうな感じではありますが, 詳しくは man 7 signal を読め、とあります.

再度 man 7 signal を読むと

      The  following  interfaces  are  never restarted after being interrupted by a signal handler, regardless of the use of SA_RESTART; they always fail with the error EINTR when interrupted by a
       signal handler:

       <snip>
       * File descriptor multiplexing interfaces: epoll_wait(2), epoll_pwait(2), poll(2), ppoll(2), select(2), and pselect(2).

poll(), select() は残念ながら SA_RESTART の恩恵にはあずかれず, EINTR エラーで処理終了してしまう、とのこと.

ですので poll(), select() でイベントを待つ際には EINTR エラー対策が必要、となりそうです.

ただ、大抵の場合はそこまで気にしなくても…

ちょっと今まで書いていたことと矛盾するかもしれませんが, 明示的にsignalを受け付けるようにしていなければ, poll()のEINTRエラーを気にする必要はありません.

…というのも, man 7 signal によるとシグナルのデフォルトの動作は(少々乱暴ですが)

       SIGABRT      P1990      Core    Abort signal from abort(3)
       SIGALRM      P1990      Term    Timer signal from alarm(2)
       SIGBUS       P2001      Core    Bus error (bad memory access)
       SIGCHLD      P1990      Ign     Child stopped or terminated
       SIGCLD         -        Ign     A synonym for SIGCHLD
       SIGCONT      P1990      Cont    Continue if stopped
       SIGEMT         -        Term    Emulator trap
       SIGFPE       P1990      Core    Floating-point exception
       SIGHUP       P1990      Term    Hangup detected on controlling terminal
                                       or death of controlling process
       SIGILL       P1990      Core    Illegal Instruction
       SIGINFO        -                A synonym for SIGPWR
       SIGINT       P1990      Term    Interrupt from keyboard
       SIGIO          -        Term    I/O now possible (4.2BSD)
       SIGIOT         -        Core    IOT trap. A synonym for SIGABRT
       SIGKILL      P1990      Term    Kill signal
       SIGLOST        -        Term    File lock lost (unused)
       SIGPIPE      P1990      Term    Broken pipe: write to pipe with no
                                       readers; see pipe(7)
       SIGPOLL      P2001      Term    Pollable event (Sys V);
                                       synonym for SIGIO
       SIGPROF      P2001      Term    Profiling timer expired
       SIGPWR         -        Term    Power failure (System V)
       SIGQUIT      P1990      Core    Quit from keyboard
       SIGSEGV      P1990      Core    Invalid memory reference
       SIGSTKFLT      -        Term    Stack fault on coprocessor (unused)
       SIGSTOP      P1990      Stop    Stop process
       SIGTSTP      P1990      Stop    Stop typed at terminal
       SIGSYS       P2001      Core    Bad system call (SVr4);
                                       see also seccomp(2)
       SIGTERM      P1990      Term    Termination signal
       SIGTRAP      P2001      Core    Trace/breakpoint trap
       SIGTTIN      P1990      Stop    Terminal input for background process
       SIGTTOU      P1990      Stop    Terminal output for background process
       SIGUNUSED      -        Core    Synonymous with SIGSYS
       SIGURG       P2001      Ign     Urgent condition on socket (4.2BSD)
       SIGUSR1      P1990      Term    User-defined signal 1
       SIGUSR2      P1990      Term    User-defined signal 2
       SIGVTALRM    P2001      Term    Virtual alarm clock (4.2BSD)
       SIGXCPU      P2001      Core    CPU time limit exceeded (4.2BSD);
                                       see setrlimit(2)
       SIGXFSZ      P2001      Core    File size limit exceeded (4.2BSD);
                                       see setrlimit(2)
       SIGWINCH       -        Ign     Window resize signal (4.3BSD, Sun)
  • シグナルが無視される (Ign)
  • シグナル受けるとプロセスが終了する (Core, Term)

のいずれか、だからです.

ただ、全体が見渡せない大規模なプログラムの場合, どこかでシグナル受信が有効になっている可能性がありますので, EINTRエラーの対応はしておいた方がよいでしょう.

今回のサンプルコード

今回紹介したサンプルコードも github のこちらにおいてます.

参考になれば幸いです.

では