[C/C++][socket] select 함수

2024. 5. 8. 15:08네트워크 프로그래밍

1. 개요

C/C++ 언어에서 소켓통신에서 사용되는 함수인 select 함수에 대해서 알아보자.

 

2. 정의

int select(int nfds, fd_set *_Nullable restrict readfds,
                  fd_set *_Nullable restrict writefds,
                  fd_set *_Nullable restrict exceptfds,
                  struct timeval *_Nullable restrict timeout);

select 함수는 multiple I/O operator 즉 다중 입출력 시스템을 모니터링(검사) 해주는 함수이다.

즉, 여러개의 fd(파일디스크립터)를 사용가능한 상태인지 검사해주는 함수인 것이다.

예를들어 서버 fd(3), 클라이언트 fd(4)를 미리 등록해놓고 select함수를 실행하면 select함수에서 등록된 파일디스크립터중

하나이상이 준비된 상태가 되면 해당 fds값을 바꿔주고 반환값을 반환해주는 것이다.

 

이를 활용해 조건문을 통해 서버에 입장한 클라이언트를 체크해 준다던지, 클라이언트에서 보낸 메세지를 받는다던지 할수있는것이다.

 

3. 활용

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>

int main(void)
{
    fd_set readfds;
    fd_set writefds;
    fd_set exceptfds;

    size_t i = 0;

    int maxfd;
    while (1)
    {
        printf("Loop %ld\n", i++);
        FD_ZERO(&readfds);
        FD_ZERO(&writefds);
        FD_ZERO(&exceptfds);

        FD_SET(STDIN_FILENO, &readfds);   // 모니터링할 읽기 파일 디스크립터 추가
        FD_SET(STDOUT_FILENO, &writefds); // 모니터링할 쓰기 파일 디스크립터 추가
        // 예외 상태를 모니터링할 파일 디스크립터 추가 (일반적으로 사용되지 않음)

        maxfd = STDOUT_FILENO;
        int nfds = maxfd + 1; // nfds는 가장 높은 파일 디스크립터 값에 1을 더한 값

        struct timeval timeout;
        timeout.tv_sec = 2; // 5초 후에 타임아웃
        timeout.tv_usec = 0;

        printf("Waiting on select()...\n");
        int rv = select(nfds, &readfds, &writefds, &exceptfds, &timeout);
        printf("rv: %d\n", rv);
        if (rv == -1)
        {
            perror("select"); // select() 호출에 오류 발생
        }
        else if (rv == 0)
        {
            printf("Timeout occurred! No data after 5 seconds.\n");
        }
        else
        {
            // 데이터를 읽거나 쓸 수 있음
            if (FD_ISSET(STDIN_FILENO, &readfds))
            {
                char buff[1024];
                int len = read(STDIN_FILENO, buff, sizeof(buff));
                buff[len] = '\0';
                    printf("Read: %s\n", buff);
            }
            if (FD_ISSET(STDOUT_FILENO, &writefds))
            {
                printf("Can write to stdout\n");
            }
        }
    }
    return 0;
}

위 예시 코드를 통해 설명해보면,

1) 일단 기본적으로 반복문을 통해서 select 함수를 실행하면서 계속 모니터링 해준다.

2) FD_ZERO()를 통해 set을 초기화 해준다.

void FD_ZERO(fd_set *set);

3) FD_SET()을 통해 내가 모니터링할 fd를 set에 등록해준다.

void FD_SET(int fd, fd_set *set);

4) select 함수를 실행할때는 등록해둔 fd값중 가장 값이 큰값 + 1 을 nfds값으로 넣어준다.

int select(int nfds, fd_set *_Nullable restrict readfds, // 모니터링할 읽기 파일 디스크립터
                  fd_set *_Nullable restrict writefds, // 모니터링할 쓰기 파일 디스크립터
                  fd_set *_Nullable restrict exceptfds, // 예외 상태를 모니터링할 파일 디스크립터 디스으로 사용되지 않음)
                  struct timeval *_Nullable restrict timeout); // 시간제한 (NULL값 삽입시 시간제한 없이 블로킹)

: 이렇게 해주면 select함수는 0 ~ nfds -1 에 해당하면서 FD_SET으로 등록된 fd값을 검사해준다.

그리고 같이 넣어주는 인자를 통해 검사 및 시간 제한을 걸어준다.

이때 timeout을 넣어주면 해당하는 시간만큼 모니터링 후에 준비된 fd가 없다면 return 0 을 해준다.

 

4. timeout을 굳이 하는 이유?

처음에는 굳이 해줄 필요가 없어서 보통 아래와 같이 사용했다.

select(nfds, &readfds, NULL, NULL, NULL);

하지만 스레드 환경에서 while문과 select를 사용을 한다면?

메인 스레드가 시그널(Ctrl+C) 등으로 인해 종료가 되는 상황에

해당 스레드는 select함수에 의해 블로킹이 걸려있으므로 종료가 되지 않는 문제가 발생했다.

 

따라서 이럴때는 timeout을 위 예시와같이 걸어주어서 일정시간 동안 이벤트가 발생하지 않으면 다시 루프문을 돌게 해주면서

메인스레드가 종료됬는지 알려주는 변수 등으로 인해 해당 스레드도 종료를 해주었다.

 

 

#참고

https://man7.org/linux/man-pages/man2/select.2.html

 

select(2) - Linux manual page

select(2) — Linux manual page select(2) System Calls Manual select(2) NAME         top select, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO, fd_set - synchronous I/O multiplexing LIBRARY         top Standard C library (libc, -lc) SYNOPSIS      

man7.org