Python

타이머 파일 기술자(timer file descriptor) HOWTO

릴리스:

1.13

이 HOWTO는 리눅스 타이머 파일 기술자에 대한 파이썬의 지원에 대해 다룹니다.

예시

다음 예제는 타이머 파일 기술자를 사용하여 초당 두 번 함수를 실행하는 방법을 보여줍니다.

# 실제 스크립트에서는 비차단(non-blocking) 타이머를 사용해야 하지만,
# 여기서는 단순함을 위해 차단(blocking) 타이머를 사용합니다.
import os, time

# 타이머 파일 기술자 생성
fd = os.timerfd_create(time.CLOCK_REALTIME)

# 1초 후부터 시작하며 0.5초 간격으로 작동하는 타이머 설정
os.timerfd_settime(fd, initial=1, interval=0.5)

try:
    # 타이머 이벤트를 네 번 처리합니다.
    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
# 127.0.0.1에 시도 중...
# 127.0.0.1에 연결됨.
# 이스케이프 문자는 '^]'입니다.
# stop
# 외부 호스트에 의해 연결이 종료되었습니다.
#
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) 쌍의 리스트를 반환합니다.
        #
        # 이 예제 설정에서,
        #    첫 번째 타이머는 0.25초부터 0.25초마다 실행됩니다. (0.25, 0.5, 0.75, 1.0, ...)
        #    두 번째 타이머는 0.5초부터 0.5초마다 실행됩니다. (0.5, 1.0, 1.5, 2.0, ...)
        #    세 번째 타이머는 0.75초부터 0.75초마다 실행됩니다. (0.75, 1.5, 2.25, 3.0, ...)
        #
        #    0.25초에는 첫 번째 타이머만 실행됩니다.
        #    0.5초에는 첫 번째 타이머와 두 번째 타이머가 동시에 실행됩니다.
        #    0.75초에는 첫 번째 타이머와 세 번째 타이머가 동시에 실행됩니다.
        #    1.5초에는 첫 번째, 두 번째, 세 번째 타이머가 동시에 실행됩니다.
        #
        # 마지막 os.read() 호출 이후 타이머 파일 디스크립터가 한 번 이상 신호되면,
        # os.read()는 신호된 횟수를 클래스 바이트의 호스트 순서로 반환합니다.
        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

# 이 예제에서는 루프백 주소를 사용하여 서버에 "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)

# 비차단 모드로 타이머 파일 기술자 생성.
num = 3
fds = [os.timerfd_create(time.CLOCK_REALTIME, flags=os.TFD_NONBLOCK)
       for _ in range(num)]
select_fds = fds + [sock]

# os.timerfd_settime()을 사용하여 초 단위로 타이머 시작.
# 타이머 1은 0.25초마다, 타이머 2는 0.5초마다 등 작동.
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:
       # 3초 동안 타이머 만료를 기다립니다.
       # select.select()는 파일 기술자 또는 객체의 목록을 반환합니다.
       rfd, wfd, xfd = select.select(select_fds, select_fds, select_fds, timeout)
       for fd in rfd:
           if fd == sock:
               # 연결 요청이 있는지 확인합니다.
               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:
               # 읽을 데이터가 있는지 확인합니다.
               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}")
                   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

분실물 보관소