タイマーファイル記述子 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