Skip to content

select(2)

Seonghun Lim edited this page Oct 13, 2019 · 4 revisions

NAME

select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - 동기적 I/O 다중화

SYNOPSIS

/* 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

DESCRIPTION

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()를 쓰는 경우에는 일단 시그널들을 막아 두고, 들어온 시그널들을 처리한 다음에 원하는 sigmaskpselect()를 호출하여 경쟁을 피할 수 있다.)

타임아웃

관련된 시간 구조체들이 <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의 값은 규정돼 있지 않다고 보면 된다.

RETURN VALUE

성공 시 select()pselect()는 반환되는 세 디스크립터 집합에 담긴 파일 디스크립터들의 수를 (즉 readfds, writefds, exceptfds에 설정된 비트들의 총개수를) 반환한다. 뭔가 이벤트가 생기기 전에 타임아웃이 만료된다면 0일 수도 있다. 오류 시 -1을 반환하며 오류를 나타내도록 errno를 설정한다. 그 경우 파일 디스크립터 집합들은 변경되지 않으며 timeout은 규정되지 않은 상태가 된다.

ERRORS

EBADF
한 집합에 유효하지 않은 파일 디스크립터가 있다. (아마 이미 닫혔거나 오류가 발생했던 파일 디스크립터일 것이다.) 하지만 BUGS 참고.
EINTR
시그널을 잡았다. signal(7) 참고.
EINVAL
nfds가 음수이거나 RLIMIT_NOFILE 자원 제한값(getrlimit(2) 참고)을 초과한다.
EINVAL
timeout에 담긴 값이 유효하지 않다.
ENOMEM
내부 테이블을 위한 메모리를 할당할 수 없다.

VERSIONS

리눅스 커널 2.6.16에서 pselect()가 추가되었다. 그 전에는 glibc에서 pselect()를 에뮬레이션 했다. (하지만 BUGS 참고.)

CONFORMING TO

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에 규정돼 있다.

NOTES

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_tsuseconds_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() 알림의 대응 관계

리눅스 커널 소스에서 찾을 수 있는 다음 정의들이 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()에 영향을 주지 않는다. 요컨데 이 상황에서 특정 동작 방식에 의존하는 응용은 버그가 있는 것으로 봐야 한다.

C 라이브러리/커널 차이

리눅스 커널에서는 임의 크기의 파일 디스크립터 집합이 가능하고 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)를 보라.

BUGS

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을 지역 변수로 복사하고 그 변수를 시스템 호출에 전달함으로써 그 동작 방식을 감춘다.

EXAMPLE

#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);
}

SEE ALSO

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

Clone this wiki locally