timer 파일 디스크립터 HOWTO
timer 파일 디스크립터 HOWTO (Timer File Descriptor)
Linux에는 **타이머 파일 디스크립터(timer file descriptor)**라는 게 있는데, 타이머가 만료(expire)되면 읽기 가능하게 되는 파일 디스크립터예요. 이 HOWTO는 Python이 이 Linux 타이머 파일 디스크립터를 어떻게 지원하는지 다룹니다. Release 1.13 문서를 기준으로 해요.
출처: Python 공식 문서
예제 (Examples)
타이머 파일 디스크립터로 1초에 두 번씩 함수를 실행하는 예제를 먼저 볼게요:
# Practical scripts should use really use a non-blocking timer,
# we use a blocking timer here for simplicity.
import os, time
# Create the timer file descriptor
fd = os.timerfd_create(time.CLOCK_REALTIME)
# Start the timer in 1 second, with an interval of half a second
os.timerfd_settime(fd, initial=1, interval=0.5)
try:
# Process timer events four times.
for _ in range(4):
# read() will block until the timer expires
_ = os.read(fd, 8)
print("Timer expired")
finally:
# Remember to close the timer file descriptor!
os.close(fd)
os.timerfd_create()로 타이머 파일 디스크립터를 만들고, os.timerfd_settime()으로 initial=1(1초 뒤 첫 만료)과 interval=0.5(그 후 0.5초 간격)를 설정했어요. read()는 타이머가 만료될 때까지 블록하다가, 8바이트(만료 횟수)를 반환합니다. 코드 마지막에 finally 블록에서 os.close(fd)로 디스크립터를 닫는 것도 잊지 말아야 해요.
float 타입이 일으키는 정밀도 손실을 피하기 위해, 타이머 파일 디스크립터 함수들은 _ns 접미사가 붙은 변형으로 정수 나노초 단위로 초기 만료 시간과 간격을 지정할 수 있게 해 줍니다.
다음 예제는 epoll()을 타이머 파일 디스크립터와 함께 써서, 파일 디스크립터가 읽기 가능해질 때까지 기다리는 모습을 보여줍니다:
import os, time, select, socket, sys
# Create an epoll object
ep = select.epoll()
# 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)
ep.register(sock, select.EPOLLIN)
# Create timer file descriptors in non-blocking mode.
num = 3
fds = []
for _ in range(num):
fd = os.timerfd_create(time.CLOCK_REALTIME, flags=os.TFD_NONBLOCK)
fds.append(fd)
# Register the timer file descriptor for read events
ep.register(fd, select.EPOLLIN)
# Start the timer with os.timerfd_settime_ns() in nanoseconds.
# Timer 1 fires every 0.25 seconds; timer 2 every 0.5 seconds; etc
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:
# Wait for the timer to expire for 3 seconds.
# epoll.poll() returns a list of (fd, event) pairs.
# fd is a file descriptor.
# sock and conn[=returned value of socket.accept()] are socket objects, not file descriptors.
# So use sock.fileno() and conn.fileno() to get the file descriptors.
events = ep.poll(timeout)
# If more than one timer file descriptors are ready for reading at once,
# epoll.poll() returns a list of (fd, event) pairs.
#
# In this example settings,
# 1st timer fires every 0.25 seconds in 0.25 seconds. (0.25, 0.5, 0.75, 1.0, ...)
# 2nd timer every 0.5 seconds in 0.5 seconds. (0.5, 1.0, 1.5, 2.0, ...)
# 3rd timer every 0.75 seconds in 0.75 seconds. (0.75, 1.5, 2.25, 3.0, ...)
#
# In 0.25 seconds, only 1st timer fires.
# In 0.5 seconds, 1st timer and 2nd timer fires at once.
# In 0.75 seconds, 1st timer and 3rd timer fires at once.
# In 1.5 seconds, 1st timer, 2nd timer and 3rd timer fires at once.
#
# If a timer file descriptor is signaled more than once since
# the last os.read() call, os.read() returns the number of signaled
# as host order of class bytes.
print(f"Signaled events={events}")
for fd, event in events:
if event & select.EPOLLIN:
if fd == sock.fileno():
# 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}")
ep.register(conn, select.EPOLLIN)
elif conn and fd == conn.fileno():
# 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}")
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()
socket 객체는 파일 디스크립터가 아니라는 점을 주의 깊게 보세요. epoll.poll()이 돌려주는 fd 값과 비교하려면 sock.fileno()와 conn.fileno()로 실제 파일 디스크립터를 얻어야 해요. 하나의 타이머 디스크립터가 마지막 os.read() 이후 두 번 이상 신호를 받았다면, os.read()는 신호된 횟수를 호스트 바이트 순서의 unsigned long 바이트로 돌려줍니다(int.from_bytes로 읽으면 됩니다).
이번에는 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
select()를 쓸 때 주의할 점은, epoll()과 달리 타이머 디스크립터 숫자를 그대로 넣는 게 아니라 파일 디스크립터나 객체 목록(select_fds)을 넘긴다는 거예요. 타이머 1은 0.25초마다, 타이머 2는 0.5초마다, 타이머 3은 0.75초마다 만료되도록 설정돼 있으니, 0.75초 시점에는 타이머 1과 3이 동시에, 1.5초 시점에는 세 개가 한 번에 신호됩니다. telnet 127.0.0.1 1234로 연결해 stop을 입력하면 서버가 종료되는 구조예요.
더 알아보기 (Learn more)
os모듈 — 타이머 파일 디스크립터 함수 —os.timerfd_create,os.timerfd_settime,os.timerfd_settime_ns,os.timerfd_gettime등- Linux
timerfd_create(2)매뉴얼 — 시스템 호출 동작 상세 - Python 공식 문서: timer file descriptor HOWTO