-
Notifications
You must be signed in to change notification settings - Fork 7
select(2)
select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - 동기적 I/O 다중화
/* POSIX.1-2001, POSIX.1-2008에 따르면 */
#include <sys/select.h>
/* 이전 표준들에 따르면 */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *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);
#include <sys/select.h>
int pselect(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, const struct timespec *timeout,
const sigset_t *sigmask);
glibc 기능 확인 매크로 요건 (feature_test_macros(7) 참고):
-
pselect()
: _POSIX_C_SOURCE >= 200112L
select()
와 pselect()
를 이용해 프로그램에서 여러 파일 디스크립터들을 감시하고 그 중 하나 이상이 어떤 유형의 I/O 동작에 "준비" 상태(가령, 입력 가능)가 될 때까지 대기할 수 있다. 해당 I/O 동작(가령 read(2)
나 충분히 작은 write(2)
)을 블록킹 없이 수행하는 게 가능하면 파일 디스크립터가 준비 상태라고 본다.
select()
에서는 FD_SETSIZE
보다 작은 파일 디스크립터 번호만 감시할 수 있다. poll(2)에는 그런 제한이 없다. BUGS 참고.
select()
와 pselect()
의 동작은 다음 세 가지 차이를 빼면 동일하다.
(i) select()
의 타임아웃은 struct timeval
(초와 마이크로초)이지만 pselect()
에서는 struct timespec
(초와 나노초)을 쓴다.
(ii) select()
에서는 남은 시간이 얼마인지 나타내기 위해 timeout
인자를 갱신할 수도 있다. pselect()
에서는 그 인자를 바꾸지 않는다.
(iii) select()
에는 sigmask
인자가 없고, 그래서 sigmask
를 NULL로 해서 호출한 pselect()
처럼 동작한다.
세 가지 독립적인 파일 디스크립터 집합을 감시한다. readfds
에 나열된 파일 디스크립터들은 읽기 가능한 문자가 있는지 살펴본다. (더 정확히는 읽기가 블록 하지 않을지 확인한다. 특히 파일 끝에서도 파일 디스크립터가 준비된 상태이다.) writefds
의 파일 디스크립터들은 쓰기를 위한 공간이 있는지 살펴본다. (큰 데이터를 쓰면 여전히 블록할 수도 있다.) exceptfds
의 파일 디스크립터들은 예외 상황들을 확인한다. (예외 상황들의 몇 가지 예를 poll(2)의 POLLPRI
설명에서 볼 수 있다.)
빠져 나올 때 파일 디스크립터 집합 각각은 실제 상태가 바뀐 파일 디스크립터들을 나타내도록 변경되어 있다. (따라서 루프 안에서 select()
를 쓴다면 호출 전에 매번 집합을 다시 설정해야 한다.)
해당 이벤트 유형에 대해 감시할 파일 디스크립터가 없다면 세 파일 디스크립터 집합 각각을 NULL로 지정할 수도 있다.
집합 조작을 위한 매크로 네 가지가 있다. FD_ZERO()
는 집합을 비운다. FD_SET()
과 FD_CLR()
는 집합에 파일 디스크립터를 더하고 뺀다. FD_ISSET()
은 파일 디스크립터가 집합에 포함돼 있는지 확인하는데, select()
반환 후에 쓰게 된다.
nfds
는 세 집합에서 번호가 가장 높은 파일 디스크립터에 1을 더한 값으로 설정하면 된다. 각 집합에서 파일 디스크립터가 표시돼 있는지를 그 제한치까지 확인한다. (하지만 BUGS 참고.)
timeout
인자는 파일 디스크립터가 준비 상태가 되기를 기다리며 select()
에서 블록 할 시간을 나타낸다. 다음 어느 경우든 해당할 때까지 호출이 블록 하게 된다.
-
파일 디스크립터가 준비 상태가 된다.
-
호출이 시그널 핸들러에 의해 중단된다.
-
타임아웃이 만료된다.
참고로 timeout
시간을 시스템 클럭 해상도에 따라 올림 하게 되며 커널 스케줄링 지연도 있기 때문에 그 블록 시간을 약간 넘길 수도 있다. timeval
구조체의 두 필드가 모두 0이면 select()
가 즉시 반환한다. (폴링에 유용하다.) timeout
이 NULL이면 (타임아웃이 없으면) select()
에서 무한정 블록 할 수 있다.
sigmask
는 시그널 마스크(sigprocmask(2) 참고)에 대한 포인터다. NULL이 아닌 경우 pselect()
에서는 먼저 현재 시그널 마스크를 sigmask
가 가리키는 마스크로 교체하고, "select" 동작을 하고서, 원래 시그널 마스크를 복원한다.
timeout
인자의 정밀도 차이를 제외하면 다음 pselect()
호출은
ready = pselect(nfds, &readfds, &writefds, &exceptfds,
timeout, &sigmask);
다음 호출들을 원자적으로 실행하는 것과 동등하다.
sigset_t origmask;
pthread_sigmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
pthread_sigmask(SIG_SETMASK, &origmask, NULL);
pselect()
가 필요한 이유는 시그널과 파일 디스크립터 상태 변화를 함께 기다리려 할 때 경쟁 조건을 막으려면 원자적 검사가 필요하기 때문이다. (시그널 핸들러에서 전역 플래그를 설정하고 반환한다고 하자. 그 전역 플래그를 검사한 다음에 select()
호출을 한다고 할 때, 검사 후이자 호출 전에 시그널이 도착한다면 호출이 무한정 멈춰 있을 수도 있다. 반면 pselect()
를 쓰는 경우에는 일단 시그널들을 막아 두고, 들어온 시그널들을 처리한 다음에 원하는 sigmask
로 pselect()
를 호출하여 경쟁을 피할 수 있다.)
관련된 시간 구조체들이 <sys/time.h>
에 정의돼 있으며 다음과 같은 형태이다.
struct timeval {
long tv_sec; /* 초 */
long tv_usec; /* 마이크로초 */
};
struct timespec {
long tv_sec; /* 초 */
long tv_nsec; /* 나노초 */
};
(하지만 아래의 POSIX.1 버전들 참고.)
어떤 코드에서는 세 집합을 모두 비우고 nfds
를 0으로 하고서 NULL 아닌 timeout
을 써서 select()
를 호출하는데, 이는 초 단위 이하 정밀도로 잠잘 수 있는 꽤 이식성 좋은 방법이다.
리눅스에서는 잠자지 않은 시간의 양을 반영하도록 timeout
을 변경한다. 하지만 대부분의 다른 구현들에서는 그러지 않는다. (POSIX.1에서는 어느 쪽 동작이든 허용한다.) 이 때문에 timeout
을 읽는 리눅스 코드를 다른 운영 체제로 이식할 때, 그리고 루프에서 struct timeval
을 재설정하지 않고 select()
여러 번에 재사용하는 코드를 리눅스로 이식할 때 문제가 생긴다. select()
반환 후에 timeout
의 값은 규정돼 있지 않다고 보면 된다.
성공 시 select()
와 pselect()
는 반환되는 세 디스크립터 집합에 담긴 파일 디스크립터들의 수를 (즉 readfds
, writefds
, exceptfds
에 설정된 비트들의 총개수를) 반환한다. 뭔가 이벤트가 생기기 전에 타임아웃이 만료된다면 0일 수도 있다. 오류 시 -1을 반환하며 오류를 나타내도록 errno
를 설정한다. 그 경우 파일 디스크립터 집합들은 변경되지 않으며 timeout
은 규정되지 않은 상태가 된다.
EBADF
- 한 집합에 유효하지 않은 파일 디스크립터가 있다. (아마 이미 닫혔거나 오류가 발생했던 파일 디스크립터일 것이다.) 하지만 BUGS 참고.
EINTR
- 시그널을 잡았다. signal(7) 참고.
EINVAL
-
nfds
가 음수이거나RLIMIT_NOFILE
자원 제한값(getrlimit(2) 참고)을 초과한다. EINVAL
-
timeout
에 담긴 값이 유효하지 않다. ENOMEM
- 내부 테이블을 위한 메모리를 할당할 수 없다.
리눅스 커널 2.6.16에서 pselect()
가 추가되었다. 그 전에는 glibc에서 pselect()
를 에뮬레이션 했다. (하지만 BUGS 참고.)
select()
는 POSIX.1-2001, POSIX.1-2008, 4.4BSD를 (4.2BSD에서 select()
가 처음 등장) 준수한다. BSD 소켓 계층 복제 형태를 지원하는 BSD 외 시스템들(시스템 V 계열 포함)과의 사이에서 일반적으로 서로 이식 가능하다. 하지만 시스템 V 계열에서는 보통 나오기 전에 타임아웃 변수를 설정하는 반면 BSD 계열에서는 그러지 않는다.
pselect()
는 POSIX.1g에, 그리고 POSIX.1-2001 및 POSIX.1-2008에 규정돼 있다.
fd_set
은 고정 크기 버퍼이다. 음수이거나 FD_SETSIZE
와 같거나 더 큰 fd
값으로 FD_CLR()
이나 FD_SET()
을 실행할 때의 동작 방식은 규정돼 있지 않다. 또한 POSIX에서는 fd
가 유효한 파일 디스크립터여야 한다고 요구한다.
select()
및 pselect()
의 동작은 O_NONBLOCK
플래그에 영향을 받지 않는다.
일부 다른 유닉스 시스템에서는 커널 내부 자원을 할당하지 못했을 때 리눅스의 ENOMEM
이 아니라 EAGAIN
오류로 실패할 수 있다. POSIX에서 poll(2)에 이 오류를 명세하고 있지만 select()
에 대해선 아니다. 이식 가능한 프로그램에서는 EAGAIN
을 확인해서 EINTR
경우처럼 루프를 계속 도는 게 좋을 수 있다.
pselect()
가 없는 시스템에서는 자가 파이프 요령을 써서 신뢰성 있게 (그리고 더 이식성 있게) 시그널 잡기를 할 수 있다. 이 기법은 시그널 핸들러에서 파이프로 한 바이트를 써넣고 그 반대쪽을 주 프로그램의 select()
로 감시하는 것이다. (가득 찬 파이프에 써넣거나 빈 파이프에서 읽을 때 블록 될 가능성을 피하기 위해 파이프에 읽고 쓸 때 논블로킹 I/O를 쓴다.)
사용 타입과 관련해서 전통적 환경에서는 timeval
구조체의 두 필드가 (위에 보인 것처럼) long
타입으로 되어 있으며 그 구조체가 <sys/time.h>
에 정의돼 있다. 하지만 POSIX.1 환경에서는 다음과 같다.
struct timeval {
time_t tv_sec; /* 초 */
suseconds_t tv_usec; /* 마이크로초 */
};
<sys/select.h>
에 구조체가 정의돼 있으며 데이터 타입 time_t
와 suseconds_t
는 <sys/types.h>
에 정의돼 있다.
원형과 관련해서 전통적 환경에서는 select()
를 위해선 <time.h>
를 포함시켜야 한다. POSIX.1 환경에서는 select()
와 pselect()
를 위해 <sys/select.h>
를 포함시켜야 한다.
glibc 2.0 하에서 <sys/select.h>
이 제공하는 pselect()
원형이 잘못돼 있다. glibc 2.1에서 2.2.1까지 하에선 _GNU_SOURCE
가 정의돼 있을 때 pselect()
를 제공한다. glibc 2.2.2부터는 요건이 SYNOPSIS에 나와 있는 대로이다.
리눅스 커널 소스에서 찾을 수 있는 다음 정의들이 select()
의 읽기 가능, 쓰기 가능, 예외 상황 알림과 poll(2)(과 epoll(7))에서 제공하는 이벤트 알림 사이의 대응 관계를 보여 준다.
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP |
POLLERR)
/* 읽기 준비됨 */
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
/* 쓰기 준비됨 */
#define POLLEX_SET (POLLPRI)
/* 예외 상황 */
select()
로 감시 중인 파일 디스크립터를 다른 스레드에서 닫는 경우의 결과는 명세돼 있지 않다. 어떤 유닉스 시스템에서는 select()
가 블록을 멈추고 반환하며 그 파일 디스크립터가 준비 상태라고 표시한다. (그러면 이어지는 I/O 동작이 오류로 실패하게 된다. 단 select()
반환 시점과 I/O 동작 수행 시점 사이에 다른 프로세스에서 그 파일 디스크립터를 다시 열지 않아야 한다.) 리눅스에서는 (그리고 어떤 다른 시스템에서는) 다른 스레드에서 파일 디스크립터를 닫는 게 select()
에 영향을 주지 않는다. 요컨데 이 상황에서 특정 동작 방식에 의존하는 응용은 버그가 있는 것으로 봐야 한다.
리눅스 커널에서는 임의 크기의 파일 디스크립터 집합이 가능하고 nfds
의 값으로 검사할 집합의 길이를 알아낸다. 하지만 glibc 구현에서는 fd_set
타입의 길이가 고정돼 있다. BUGS도 참고.
이 페이지에서 기술하는 pselect()
인터페이스는 glibc에서 구현하고 있다. 기반 리눅스 시스템 호출의 이름은 pselect6()
이다. 이 시스템 호출은 glibc 래퍼 함수와 동작 방식이 좀 다르다.
리눅스의 pselect6()
시스템 호출에서는 timeout
인자를 변경한다. 하지만 glibc 래퍼 함수에서 타임아웃 인자에 대한 지역 변수를 쓰고 그 변수를 시스템 호출로 전달하여 그 동작 방식을 감춘다. 그리하여 glibc의 pselect()
함수는 timeout
인자를 변경하지 않는다. 이는 POSIX.1-2001에서 요구하는 동작 방식이다.
pselect6()
시스템 호출의 마지막 인자는 sigset_t *
포인터가 아니라 다음 형태의 구조체이다.
struct {
const kernel_sigset_t *ss; /* 시그널 집합에 대한 포인터 */
size_t ss_len; /* 'ss'가 가리키는 객체의 크기
(바이트 단위) */
};
그래서 여러 아키텍처에서 시스템 호출에 최대 6개 인자만 지원한다는 점을 감안하면서 시스템 호출에서 시그널 집합 포인터와 그 크기를 모두 받을 수 있다. 커널과 libc에서의 시그널 집합 개념 차이에 대한 설명은 sigprocmask(2)를 보라.
POSIX에서는 파일 디스크립터 집합에 지정할 수 있는 파일 디스크립터들의 범위에 대해 구현에서 상한을 정해서 상수 FD_SETSIZE
를 통해 알리는 것을 허용한다. 리눅스 커널에서는 어떤 고정 제한도 두지 않지만 glibc 구현에서는 FD_SETSIZE
를 1024로 해서 fd_set
을 고정 크기 타입으로 만들고 FD_*()
매크로가 그 제한에 따라 동작하게 한다. 1023보다 큰 파일 디스크립터를 감시하려면 poll(2)을 써야 한다.
fd_set
인자가 값-결과 인자로 구현돼 있으므로 select()
를 호출할 때마다 다시 설정해 줘야 한다. poll(2)에서는 호출의 입력과 출력에 별도의 구조체 필드를 사용해서 이 설계 오류를 피한다.
POSIX에 따르면 select()
에서는 세 파일 디스크립터 집합에 지정된 모든 파일 디스크립터들을 상한 nfds-1
까지 확인해야 한다. 하지만 현재 구현에서는 프로세스가 현재 열어 둔 파일 디스크립터 번호의 최댓값보다 큰 파일 디스크립터를 무시한다. POSIX에 따르면 한 집합에 그런 파일 디스크립터가 지정돼 있으면 EBADF
오류가 발생해야 한다.
glibc 2.0에서는 sigmask
인자를 받지 않는 pselect()
버전을 제공했다.
glibc 버전 2.1부터 sigprocmask(2)와 select()
를 이용해 구현한 pselect()
에뮬레이션을 제공했다. 그 구현은 pselect()
가 방지해야 하는 바로 그 경쟁 조건에 여전히 취약했다. 최근의 glibc 버전들에서는 커널에서 (경쟁 없는) pselect()
시스템 호출을 제공하면 그걸 쓴다.
리눅스에서는 select()
에서 어떤 소켓 파일 디스크립터를 "읽기 준비됨"으로 보고했는데 이어지는 읽기가 블록 될 수도 있다. 예를 들어 데이터가 도착했지만 조사해 보니 체크섬이 틀려서 폐기할 때 그럴 수 있다. 그 외에도 파일 디스크립터가 준비 상태라고 잘못 보고하는 다른 경우들이 있을 수 있다. 따라서 블록 해서는 안 되는 소켓에서는 O_NONBLOCK
을 쓰는 게 안전할 수 있다.
리눅스에서는 시그널 핸들러에 의해 호출이 중단된 경우(즉 EINTR
오류 반환)에도 select()
에서 timeout
을 변경한다. 이는 POSIX.1에서 허용하지 않는 동작이다. 리눅스의 pselect()
시스템 호출도 동일하게 동작하지만 glibc 래퍼에서 내부적으로 timeout
을 지역 변수로 복사하고 그 변수를 시스템 호출에 전달함으로써 그 동작 방식을 감춘다.
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int
main(void)
{
fd_set rfds;
struct timeval tv;
int retval;
/* stdin(fd 0)에 입력이 있는지 감시. */
FD_ZERO(&rfds);
FD_SET(0, &rfds);
/* 5초까지 기다림. */
tv.tv_sec = 5;
tv.tv_usec = 0;
retval = select(1, &rfds, NULL, NULL, &tv);
/* tv의 값에 의존하지 말 것! */
if (retval == -1)
perror("select()");
else if (retval)
printf("Data is available now.\n");
/* FD_ISSET(0, &rfds)가 참임. */
else
printf("No data within five seconds.\n");
exit(EXIT_SUCCESS);
}
accept(2)
, connect(2)
, poll(2), read(2)
, recv(2), restart_syscall(2), send(2), sigprocmask(2), write(2)
, epoll(7), time(7)
설명과 예시가 있는 자습서인 select_tut(2) 참고.
2019-03-06