タイマーファイル記述子 HOWTO
****************************

リリース:
   1.13

この HOWTO では、Linux のタイマーファイル記述子を Python で使う方法を
説明します。


使用例
======

次の例は、タイマーファイル記述子を使って 1 秒に 2 回関数を実行する方法
を示します。

   # 実用的なスクリプトではノンブロッキングのタイマーを使うべきだが、
   # ここでは簡単にするためブロッキングのタイマーを使う。
   import os, time

   # タイマーファイル記述子を作成する
   fd = os.timerfd_create(time.CLOCK_REALTIME)

   # 1 秒後に初回の満了を迎え、その後は 0.5 秒間隔とする
   os.timerfd_settime(fd, initial=1, interval=0.5)

   try:
       # タイマーのイベントを 4 回処理する。
       for _ in range(4):
           # タイマーが満了するまで read() はブロックする
           _ = os.read(fd, 8)
           print("Timer expired")
   finally:
       # 忘れずにタイマーファイル記述子を閉じる。
       os.close(fd)

"float" 型による精度の低下を避けるため、タイマーファイル記述子の関数に
は "_ns" 版が用意されています。 これらを使うと、最初にタイマーが満了す
るまでの時間と間隔を、整数のナノ秒単位で指定できます。

この例は、タイマーファイル記述子と "epoll()" を組み合わせ、ファイル記
述子が読み取り可能になるまで待つ方法を示します。

   import os, time, select, socket, sys

   # epoll オブジェクトを作成する
   ep = select.epoll()

   # この例ではループバックアドレスでサーバーに "stop" コマンドを送る。
   #
   # $ telnet 127.0.0.1 1234
   # Trying 127.0.0.1...
   # Connected to 127.0.0.1.
   # Escape character is '^]'.
   # stop
   # Connection closed by foreign host.
   #
   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   sock.bind(("127.0.0.1", 1234))
   sock.setblocking(False)
   sock.listen(1)
   ep.register(sock, select.EPOLLIN)

   # ノンブロッキングモードでタイマーファイル記述子を作成する。
   num = 3
   fds = []
   for _ in range(num):
       fd = os.timerfd_create(time.CLOCK_REALTIME, flags=os.TFD_NONBLOCK)
       fds.append(fd)
       # タイマーファイル記述子を読み取りイベント用に登録する
       ep.register(fd, select.EPOLLIN)

   # os.timerfd_settime_ns() でナノ秒単位の時間を指定してタイマーを開始する。
   # タイマー 1 は 0.25 秒間隔、タイマー 2 は 0.5 秒間隔、以下も同様。
   for i, fd in enumerate(fds, start=1):
       one_sec_in_nsec = 10**9
       i = i * one_sec_in_nsec
       os.timerfd_settime_ns(fd, initial=i//4, interval=i//4)

   timeout = 3
   try:
       conn = None
       is_active = True
       while is_active:
           # タイマーの満了を最大 3 秒待つ。
           # epoll.poll() は (fd, event) のペアのリストを返す。
           # fd はファイル記述子。
           # sock と conn [socket.accept() の戻り値] はファイル記述子ではなくソケットオブジェクト。
           # そのため、ファイル記述子は sock.fileno() と conn.fileno() で取得する。
           events = ep.poll(timeout)

           # 複数のタイマーファイル記述子が同時に読み取り可能になった場合、
           # epoll.poll() は (fd, event) のペアのリストを返す。
           #
           # この例の設定では、
           #    第 1 タイマーは 0.25 秒後から 0.25 秒間隔で満了する。(0.25, 0.5, 0.75, 1.0, ...)
           #    第 2 タイマーは 0.5 秒後から 0.5 秒間隔。(0.5, 1.0, 1.5, 2.0, ...)
           #    第 3 タイマーは 0.75 秒後から 0.75 秒間隔。(0.75, 1.5, 2.25, 3.0, ...)
           #
           #    0.25 秒後には第 1 タイマーだけが満了する。
           #    0.5 秒後には第 1 タイマーと第 2 タイマーが同時に満了する。
           #    0.75 秒後には第 1 タイマーと第 3 タイマーが同時に満了する。
           #    1.5 秒後には第 1、第 2、第 3 タイマーが同時に満了する。
           #
           # タイマーファイル記述子が前回の os.read() の呼び出し以降に
           # 複数回通知された場合、os.read() は通知回数を
           # ホストのバイト順の bytes オブジェクトとして返す。
           print(f"Signaled events={events}")
           for fd, event in events:
               if event & select.EPOLLIN:
                   if fd == sock.fileno():
                       # 接続要求があるか確認する。
                       print(f"Accepting connection {fd}")
                       conn, addr = sock.accept()
                       conn.setblocking(False)
                       print(f"Accepted connection {conn} from {addr}")
                       ep.register(conn, select.EPOLLIN)
                   elif conn and fd == conn.fileno():
                       # 読み取るデータがあるか確認する。
                       print(f"Reading data {fd}")
                       data = conn.recv(1024)
                       if data:
                           # 安全のため、UnicodeDecodeError 例外を捕捉するべき。
                           cmd = data.decode()
                           if cmd.startswith("stop"):
                               print(f"Stopping server")
                               is_active = False
                           else:
                               print(f"Unknown command: {cmd}")
                       else:
                           # これ以上データがないので接続を閉じる
                           print(f"Closing connection {fd}")
                           ep.unregister(conn)
                           conn.close()
                           conn = None
                   elif fd in fds:
                       print(f"Reading timer {fd}")
                       count = int.from_bytes(os.read(fd, 8), byteorder=sys.byteorder)
                       print(f"Timer {fds.index(fd) + 1} expired {count} times")
                   else:
                       print(f"Unknown file descriptor {fd}")
   finally:
       for fd in fds:
           ep.unregister(fd)
           os.close(fd)
       ep.close()

この例は、タイマーファイル記述子と "select()" を組み合わせ、ファイル記
述子が読み取り可能になるまで待つ方法を示します。

   import os, time, select, socket, sys

   # In this example, use loopback address to send "stop" command to the server.
   #
   # $ telnet 127.0.0.1 1234
   # Trying 127.0.0.1...
   # Connected to 127.0.0.1.
   # Escape character is '^]'.
   # stop
   # Connection closed by foreign host.
   #
   sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
   sock.bind(("127.0.0.1", 1234))
   sock.setblocking(False)
   sock.listen(1)

   # Create timer file descriptors in non-blocking mode.
   num = 3
   fds = [os.timerfd_create(time.CLOCK_REALTIME, flags=os.TFD_NONBLOCK)
          for _ in range(num)]
   select_fds = fds + [sock]

   # Start the timers with os.timerfd_settime() in seconds.
   # Timer 1 fires every 0.25 seconds; timer 2 every 0.5 seconds; etc
   for i, fd in enumerate(fds, start=1):
      os.timerfd_settime(fd, initial=i/4, interval=i/4)

   timeout = 3
   try:
       conn = None
       is_active = True
       while is_active:
          # Wait for the timer to expire for 3 seconds.
          # select.select() returns a list of file descriptors or objects.
          rfd, wfd, xfd = select.select(select_fds, select_fds, select_fds, timeout)
          for fd in rfd:
              if fd == sock:
                  # Check if there is a connection request.
                  print(f"Accepting connection {fd}")
                  conn, addr = sock.accept()
                  conn.setblocking(False)
                  print(f"Accepted connection {conn} from {addr}")
                  select_fds.append(conn)
              elif conn and fd == conn:
                  # Check if there is data to read.
                  print(f"Reading data {fd}")
                  data = conn.recv(1024)
                  if data:
                      # You should catch UnicodeDecodeError exception for safety.
                      cmd = data.decode()
                      if cmd.startswith("stop"):
                          print(f"Stopping server")
                          is_active = False
                      else:
                          print(f"Unknown command: {cmd}")
                  else:
                      # No more data, close connection
                      print(f"Closing connection {fd}")
                      select_fds.remove(conn)
                      conn.close()
                      conn = None
              elif fd in fds:
                  print(f"Reading timer {fd}")
                  count = int.from_bytes(os.read(fd, 8), byteorder=sys.byteorder)
                  print(f"Timer {fds.index(fd) + 1} expired {count} times")
              else:
                  print(f"Unknown file descriptor {fd}")
   finally:
       for fd in fds:
          os.close(fd)
       sock.close()
       sock = None
