I/O Multiplexing - select, poll, epoll
File Descriptor
파일은 리눅스에서 가장 기본적이고 핵심이 되는 추상화 개념이다. 리눅스는 모든 것이 파일이라는 철학을 가지고 있다. 따라서 모든 Interaction은 실제로 파일이 아닌 것 처럼 보일 지라도 파일을 읽고 쓰는 것으로 이루어진다.
각각의 프로세스는 “File descriptor table"을 하나씩 가지고 있다.
이 테이블은 “Open file table"을 참조하는 포인터들을 담고있는 배열이고 이 배열의 인덱스가 파일 디스크립터이다. (C에서는 int
)
“Open file table"은 이 테이블은 OS가 구동 중일 때 열리는 모든 파일1들을 저장하는 테이블이다.
I/O Multiplexing
I/O 멀티플렉싱은 여러 파일 디스크립터에서 I/O 작업을 수행할 수 있는 프로세스를 의미한다.
read
, accept
및 메시지를 수신하는 호출과 같은 입력 작업은 들어오는 데이터가 없을 경우 블록(block)된다.
따라서 입력 호출이 수행되고 블록되면 다른 파일 디스크립터에서의 데이터를 놓칠 수 있다.
이를 해결하기 위해 select
, poll
및 epoll
API 같은 I/O 멀티플렉싱 호출이 제공된다.
어플리케이션이 여러 개의 파일 디스크립터를 동시에 블락(Block)하고 그 중 하나라도 블락되지 않고 읽고 쓸 준비가 되면 알려주는 기능을 제공한다.
select
#include <sys/select.h>
typedef /* ... */ fd_set;
int select(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
struct timeval *_Nullable restrict timeout);
void FD_CLR(int fd, fd_set *set);
int FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);
int pselect(int nfds, fd_set *_Nullable restrict readfds,
fd_set *_Nullable restrict writefds,
fd_set *_Nullable restrict exceptfds,
const struct timespec *_Nullable restrict timeout,
const sigset_t *_Nullable restrict sigmask);
select()
호출은 파일 디스크립터가 입출력을 수행할 준비가 되거나 옵션으로 정해진 시간이 경과할 때까지 블락된다.
nfds
: 이 인자는 세 집합에서 번호가 가장 높은 파일 디스크립터에 1을 더한 값으로 설정한다.readfds
: 이 fd 집합에서 읽기 준비 상태인 파일 디스크립터들을 찾는다.writefds
: 이 fd 집합에서 쓰기 준비 상태인 파일 디스크립터들을 찾는다.exceptfds
: 이 fd 집합에서 예외 상태인 파일 디스크립터들을 찾는다. (예를 들어, TCP 소켓에 대역 외의 데이터가 있는 경우)timeout
:select()
가 블록 상태로 기다려야 하는 시간. null이면 계속 대기한다.
poll
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE
#include <signal.h>
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *tmo_p, const sigset_t *sigmask);
select
는 nfds-1
까지 모든 파일 디스크립터를 순회하면서 FD_ISSET
으로 확인해야한다. 이는 비효율적이다.
poll
은 관심있는 파일 디스크립터만 인자로 넘겨준다.
하지만 감시하고 있는 모든 fd에 대해서 순회하며 확인해야하는 단점은 여전히 존재한다.
파일 디스크립터 모두에서 어떤 요청 이벤트나 오류가 발생하지 않았으면 하나가 일어날 때까지 poll()이 블록한다.
epoll
poll과 select가 가지고 있던 근본적인 성능 문제를 해결하고 효율적인 I/O 멀티플렉싱을 제공한다. poll과 select는 관심이 있는 파일 디스크립터의 집합을 사용자 공간에서 커널로 전달하고, 커널은 검사해야할 모든 파일 디스크립터를 순회한다. 이런 동작 구조는 병목 현상으로 작용할 수 있다. 반면 epoll은 실제로 검사하는 부분과 검사할 파일 디스크립터를 등록하는 부분을 분리하여 병목을 회피한다.
epoll_create1()
에서 epoll 인스턴스를 생성한다.epoll_ctl()
에서 epoll 인스턴스에 파일 디스크립터를 추가하거나 제거한다.epoll_wait()
에서 epoll 인스턴스에 등록된 파일 디스크립터의 이벤트를 기다린다.
epoll_create
#include <sys/epoll.h>
int epoll_create1(int flags);
새로운 epoll 인스턴스를 생성하고 그 인스턴스를 가리키는 파일 디스크립터를 반환한다. 폴링이 끝난 뒤에 반드시 시스템콜로 닫아줘야 한다.
epoll_ctl
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/* epoll_event 구조체 */
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
};
typedef union epoll_data epoll_data_t;
epoll 컨텍스트에 파일 디스크립터를 추가하거나 제거할 수 있다.
epfd
: epoll 인스턴스를 가리키는 파일 디스크립터op
: 이벤트를 추가할지 제거할지를 나타내는 값.EPOLL_CTL_ADD
: epoll 인스턴스가 파일 디스크립터fd
와 연관된 파일을 감시하도록 추가하며, 각 이벤트에 대한 설정은event
구조체에 저장된다.EPOLL_CTL_DEL
: epoll 인스턴스에 파일 디스크립터fd
를 감시하지 않도록 제거한다.EPOLL_CTL_MOD
: epoll 인스턴스가 기존에 감시하고 있던 파일 디스크립터fd의 이벤트를
event` 구조체에 저장된 값으로 수정한다.
fd
: 이벤트를 추가하거나 제거할 파일 디스크립터
epoll_wait
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll 인스턴스와 연관된 파일 디스크립터에 대한 이벤트를 timeout
시간 동안 기다린다.
성공 시, events
에는 해당 이벤트(읽기나 쓰기가 가능한 상태인지 나타내는 epoll_event
구조체)에 대한 포인터가 최대 maxevents
만큼 기록된다.
Edge trigger vs Level trigger
epoll_ctl()
의 변수인 event
인자의 events
필드를 EPOLLET
으로 설정하면 이벤트 모니터가 레벨 트리거(Level trigger)가 아닌 엣지 트리거(Edge trigger)로 동작한다.
유닉스 파이프로 통신하는 입출력에 대한 예시를 들어보자.
- 출력하는 측에서 파이프에 1KB 만큼의 데이터를 쓴다.
- 입력을 받는 쪽에서는 파이프에 대하여
epoll_wait()
를 수행하고 파이프에 데이터가 들어와서 읽을 수 있는 상태가 되기를 기다린다.
레벨트리거로 이벤트를 모니터링하면 2의 epoll_wait()
호출은 즉시 반환하면 파이프가 읽을 준비가 되었음을 알려준다.
하지만 엣지트리거로 이벤트를 모니터링 1단계가 완료할 때까지 호출이 반환되지 않는다. 즉, 호출하는 시점에 파이프를 읽을 수 있는 상황이더라도 파이프에 데이터가 들어오기 전까지는 결과를 반환하지 않는다.
기본 동작은 레벨 트리거 방식이며, poll과 select도 마찬가지이다.
일반적으로 논블로킹 입출력을 활용하도록 하는 다른 프로그래밍 접근 방식을 요구하며 EAGAIN
을 주의깊게 확인해야 한다.
파일은 리눅스 시스템에서 관리되는 모든 것을 의미한다. 예를 들어, 디바이스, 소켓, 파이프, FIFO 등이 파일로 취급된다. ↩︎