poll, select, epoll관 관련한 다중 입출력의 내용과 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

poll

select와 비슷한 함수입니다. 여러 file descriptor에 대해서 I/O를 수행하기 위한 준비가 될 때까지 기다리는 함수입니다. 여러 file descriptor를 감시하여 event가 발생할 때까지 기다린다고 보시면 됩니다.

예를 들어서 어떠한 서버가 여러 클라이언트에 대해서 연결을 갖게 되고, 입력을 처리하여야한다면 어떻게 해야할까요? 한가지 방법으로는 클라이언트 요청이 들어오면 쓰레드를 만들어서 따로 처리해줄 수 있죠. 그런데 이때 한계가 있습니다. 클라이언트의 요청이 많아질 경우에는 쓰레드를 그만큼 생성해야하는 문제가 있죠. 만약 client가 100개가 현재 연결되어 있는 서버에서는 100개의 쓰레드를 생성할 수는 없겠죠. 이보다는 100개의 socket에 대해서 지켜보다가 하나의 socket에 읽을 데이터가 생겼다면 그 socket에서 read를 하고 처리를 하면 되는 것이죠.

이 목적을 달성할 수 있는 함수가 poll입니다. poll은 여러 file descriptor에 대해서 이벤트가 발생하기를 기다렸다가, event가 발생하면 그에 대한 정보를 제공해줍니다. select와는 비슷한데, 더 섬세하게 다룰 수 있습니다. 먼저 함수의 원형을 봅시다.

 

함수 설명

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

 - fds : pollfd의 구조체 포인터입니다. pollfd는 아래와 같이 선언되어 있습니다. 

struct pollfd {
   int   fd;         /* file descriptor */
   short events;     /* requested events */
   short revents;    /* returned events */
};

        fd : 점검할 file descriptor입니다. 

         events : fd에 대해서 주시할 event입니다. 이 event들은 OR(|)로 여러 가지를 줄 수 있습니다. event에 대해서는 아래 표에 설명해놓았습니다. 사용자가 설정합니다.

         revents : fd에 발생한 event를 의미합니다. 이는 사용자가 설정하는게 아니라 kernel이 설정해줍니다. 이 revents를 보고 어떤 event가 발생했는지 확인할 수 있습니다.

아래의 표를 보면 revents에는 모든 event가 설정이 되어질 수 있습니다. 하지만 events에는 아래의 3개는 설정할 수 없죠.

 

event 이름 event에 설정 가능 kernel이 revents에 설정 설명
POLLIN O O high priority 외의 자료를 바로 읽을 수 있음
(POLLRDNORM | POLLRDBAND와 동일)
POLLRDNORM O O 보통 자료를 바로 읽을 수 있음
POLLRDBAND O O 우선 순위 자료를 바로 읽을 수 있음
POLLPRI O O high priority 자료를 바로 읽을 수 있음
POLLOUT O O 보통의 자료를 쓸 수 있음
POLLWRNORM O O 보통의 자료를 쓸 수 있음
POLLWRBAND O O 우선 순위 자료를 바로 기록 할 수 있음
POLLERR   O 오류 발생
POLLHUP   O 연결 끊어짐
POLLNVAL   O fd가 열린 파일이 아님

 

- nfds : fds 배열의 크기를 의미합니다.

- timeout : poll이 대기할 시간을 설정할 수 있습니다. 여기서 단위는 ms이므로 1초만 대기하려면 timeout은 1000값이 되어야합니다. 이외에 0이거나 -1인 경우는 아래와 같습니다.

timeout == -1 : 무한정 기다립니다.

timeout == 0 : 전혀 기다리지 않습니다.

 

이를 이용해서 지난 select와 같은 역할을 하는 서버를 만들어보도록 합시다. 역시 네트워크에 대한 내용은 뺐습니다. 순수하게 poll이 어떻게 동작하는지만 확인할 것입니다. 그런데 한가지 알아두셔야할 것은 select와 마찬가지로 정규 파일에 대해서 사용하는 것은 적절하지 않다는 점은 알아두셔야합니다.  

//multiIO_poll.c 

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <poll.h>

#define FD_SIZE 3
#define BUF_SIZE 128

int main(int argc, char *argv[]){
        char buf[BUF_SIZE];
        struct pollfd fdarr[FD_SIZE];
        int n, i, ret, end_count = 0;

        //3개의 파일의 이름을 받는다.
        if(argc != 4){
                printf("usage : %s file1 file2 file3\n", argv[0]);
                return 0;
        }


        for(i = 0; i < FD_SIZE; i++){
                int fd = open(argv[i+1],O_RDONLY);
                if(fd >= 0){
                        fdarr[i].fd = fd;
                        //차단없이 읽는 것을 주시할 event로 넣는다.
                        fdarr[i].events = POLLIN;
                }
        }

        while(1){

                memset(buf, 0x00, BUF_SIZE);

                // timeout == -1은 지정된 파일 서술자중
                // 하나라도 준비될때까지 무한정 기다린다.
                ret = poll(fdarr, FD_SIZE, -1);

                if(ret == -1){
                        perror("poll error ");
                        exit(0);
                }

                for(i = 0; i < FD_SIZE; i++){
                        //파일 디스크립터가 -1이면 볼 필요 없음.
                        //받은 이벤트인 revents에 POLLIN이 있다면
                        if(fdarr[i].fd != -1 && fdarr[i].revents & POLLIN){
                                while((n = read(fdarr[i].fd, buf, BUF_SIZE)) > 0){

                                        //quit가 들어오면 fdarr[i] read 종료
                                        if(!strcmp(buf,"quit\n")){

                                                //파일디스크립터 close
                                                close(fdarr[i].fd);
                                                //안쓰는 fd로 업데이트하기 위해 -1 지정
                                                fdarr[i].fd = -1;
                                                end_count++;
                                                if(end_count == FD_SIZE) 
                                                        exit(0);
                                                continue;
                                        }

                                        printf("fd[%d] - %s",fdarr[i].fd, buf);
                                }
                        }
                }

        }
        return 0;
}

 

역시 테스트하기 위해서는 클라이언트 프로그램이 필요합니다. 여기서는 select에서 설명했던 클라이언트 역할을 할  소스 코드 fwrite.c를 그대로 사용합니다.

https://reakwon.tistory.com/117

 

[리눅스] 다중입출력 - select개념과 설명과 예제

다중입출력 아래와 같은 상황을 생각해볼까요? 어떤 프로세스가 다음과 같이 파일 3개를 처리하는데 그 중 입력이 있는 파일을 프로세스에 처리하는 상황을 어떻게 구현할 수 있을까요? 이때 우

reakwon.tistory.com

역시 테스트 하기 위해서는 a, b, c라는 파일 3개를 새로 만들고 시작합시다. 그리고 fwrite.c와 multiIO_poll.c는 아래와 같이 컴파일 하시구요. 

# gcc multiIO_poll.c 
# gcc fwrite.c -o fwrite

 

자, 테스트할 준비는 이제 완료되었고, 아래와 같은 결과를 보입니다. 

./a.out a b c
# ./a.out a b c
fd[3] - hello, ifd[3] - 'm writifd[3] - ng file fd[3] - a
fd[4] - hello, ifd[4] - 'm writifd[4] - ng file fd[4] - b~~
fd[5] - hello, ifd[5] - 'm writifd[5] - ng file fd[5] - c..
fd[3] - bye pollfd[3] - 
fd[4] - bye! 
fd[5] - i have tfd[5] - o go nowfd[5] -
./fwrite a ./fwrite b ./fwrite c
# ./fwrite a
hello, i'm writing file a
bye poll
quit
# ./fwrite b
hello, i'm writing file b~~
bye! 
quit 
# ./fwrite c
hello, i'm writing file c..
i have to go now 
quit 

 

역시나 poll은 정규 파일에 대해서 항상 준비되어있다고 판단하기 때문에 루프에다가 로그 printf를 찍어보면 엄청나게 바쁜 루프를 도는 것을 확인할 수 있습니다. 

반응형
블로그 이미지

REAKWON

와나진짜

,

 

 

select, poll, epoll과 같이 더 많은 정보와 예제를 담은 리눅스 교재를 배포했습니다. 아래의 페이지에서 리눅스 교재를 받아가세요.

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

다중입출력 

아래와 같은 상황을 생각해볼까요? 어떤 프로세스가 다음과 같이 파일 3개를 처리하는데 그 중 입력이 있는 파일을 프로세스에 처리하는 상황을 어떻게 구현할 수 있을까요?

이때 우리는 쓰레드를 생각해볼 수 있겠지요. 나쁘지 않은 아이디어입니다. 그렇다면 파일 3개의 대해서 3개의 쓰레드를 돌려야하겠네요.

여러분도 알다시피 쓰레드는 많이듭니다. context switching이 대표적이죠. 그리고 파일 처리에 대해서 동기화 기법을 적용해야할 수도 있습니다. 상당히 복잡하고 어려운 구현이 될 수도 있습니다.

이제 우리는 스레드를 이용하는 방법말고 보다 경량적인 방법을 선택할 것인데 이러한 구현을 도와주는 system call이 바로 select, poll, epoll 입니다. 이 포스팅에서는 select에 대해서만 설명합니다.

1. select

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct tiemval *timeout)

- ndfs : 감시할 파일 디스크립터의 갯수인데 마지막 파일 디스크립터 번호에 +1을 지정해줍니다. fd_set은 배열인데 배열의 index는 0부터 시작하므로 +1을 해줍니다. 아래에서 설명하도록 하죠.

- readfds : 읽을 데이터가 있는지 감시하는 파일의 집합입니다.

- writefds : 파일에 데이터를 쓸 수 있는지를 검사하기 위한 파일 집합입니다.

- exceptfds : 파일의 예외 사항이 있는지 검사하기 위한 파일 집합입니다.

- timeout : 데이터의 변화를 감지하기 위해서 사용하는 time out 값입니다. 예를 들어 readfds에 지정된 시간동안 읽을 데이터가 없다면 select는 0을 반환합니다. 만약 timeout을 NULL로 지정하게 되면 무기한으로 대기하게 됩니다.

 

성공시 fd의 갯수를 반환합니다.

fd_set

fd_set은 아래와 같은 비트 배열입니다. 총 1024개의 파일을 감시할 수 있죠. 아래는 readfds를 표현했습니다. fd_set은 총 1024개의 fd를 감시할 수 있습니다. 

우리는 readfds에 0, 1, 2, 3을 read할 데이터가 있는지 감시한다고 해봅시다. 이때 fd 3에 read할 데이터가 있다면 위의 배열은 아래와 같이 fd 3에 해당하는 비트 배열의 flag가 1이 세팅됩니다. 이렇게 하여 fd 3에 읽을 데이터가 있는지 알 수 있습니다.

이때 fd 3은 마지막에 열었던 파일 디스크립터입니다. 그렇다면 지금까지 감시해야할 fd의 갯수는 몇개인지 나오지요? 마지막 fd의 번호 + 1이 됩니다. 그 때문에 nfds가 마지막 fd의 +1이 되는 것이죠.

 

그래요, 이제 fd를 감시하는 것은 알겠는데, 이것이 스레드처럼 동시에 fd를 관리할까요? 결론은 아닙니다. 만약 파일을 1024개를 관리하고 1023번 파일디스크립터(fd)에 read 플래그가 1이 되었다면 select는 모든 fd_set을 모두 검사하여 변화가 있는 fd를 알아내게 됩니다.

 

 

 

 

 

 

 

 

FD 매크로

보다 파일 디스크립터 집합(fd_set)을 관리하기 편하게 하기 위해서 리눅스에서는 다음과 같이 4개의 매크로를 지원합니다.

 

매크로 설명
void FD_CLR(int fd, fd_set *set) set에서 fd를 제거합니다. 더 이상 검사하지 않습니다.
int  FD_ISSET(int fd, fd_set *set) set에 fd의 플래그가 setting되었는지 확인합니다. 
void FD_SET(int fd, fd_set *set) set에 fd를 추가합니다. 이것은 flag를 setting하는 것이 아닌 검사를 하겠다는 뜻의 setting입니다.
void FD_ZERO(fd_set *set); set에 있는 fd를 전부 제거합니다.

 

이 select를 활용하여 파일 3개로부터 입력이 들어오면 출력해주는 일종의 서버 프로그램을 작성해보도록 합시다. 여기서 나오는 select와 poll은 정규파일에서 다루는 것은 적절하지 않습니다. 이러한 함수들은 정규 파일을 감시하게 되면 바로 준비되었다고 판단하여 return 되어 버리기 때문입니다. 정규 파일은 언제나 디스크에서 읽을 준비가 되어있습니다. 그래서 디스크 파일보다는 네트워크 입출력에서 다루는 것이 적절합니다.

여기서는 네트워크라는 개념은 배제하고 오직 상큼하게 select의 개념을 살펴보고 어떻게 다뤄보는지 느낌만 가져가 봅시다. 

예제

//multiIO_select.c 

#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>

#define FD_SIZE 3
#define BUF_SIZE 8

int main(int argc, char *argv[]){
        int i, n, ret, fd_count, end_count = 0;
        int fds[FD_SIZE];
        char buf[BUF_SIZE] = {0,};
        struct timeval tv;

        fd_set retfds, readfds;

        //3개의 파일의 이름을 받는다.
        if(argc != 4){
                printf("usage : %s file1 file2 file3\n", argv[0]);
                return 0;
        }

        //readfds를 공집합으로 만든다. 
        FD_ZERO(&readfds);

        for(i = 0; i < FD_SIZE; i++){
                fds[i] = open(argv[i+1],O_RDONLY);
                if(fds[i] >= 0){
                        //readfds 집합에 파일디스크립터 fds[i]를 추가한다. 
                        FD_SET(fds[i], &readfds);
                        //맨 마지막 파일열린 파일 디스크립터가 가장 큰 값을 갖는다.
                        fd_count = fds[i];
                }
        }

        while(1){

                //readfds를 retfds로 옮기는 이유는 select를 통해서 readfds가 바뀔 수 있다.
                retfds = readfds;

                //열린 파일 디스크립터의 최대값 +1
                ret = select(fd_count + 1, &retfds, NULL, NULL, NULL);

                if(ret == -1){
                        perror("select error ");
                        exit(0);
                }

                for(i = 0; i < FD_SIZE; i++){
                        //만약 fds[i]에 대해서 읽을 준비가 된 파일 디스크립터이면 
                        if(FD_ISSET(fds[i], &retfds)){
                                //읽고 출력해준다. 
                                while((n = read(fds[i], buf, BUF_SIZE)) > 0){

                                        //quit가 들어오면 fds[i] read 종료
                                        if(!strcmp(buf,"quit\n")){
                                                //readfds에서 지우고 
                                                FD_CLR(fds[i], &readfds);
                                                //파일디스크립터 close
                                                close(fds[i]);

                                                end_count++;
                                                if(end_count == FD_SIZE) 
                                                        exit(0);
                                                continue;
                                        }

                                        printf("fd[%d] - %s",fds[i], buf);
                                        memset(buf, 0x00, BUF_SIZE);
                                }
                        }
                }

        }
}

위의 코드는 아주 간단합니다.  3개의 파일에 대해서 읽을 준비가 된 파일 디스크립터가 있으면 해당 파일을 읽어서 출력해주는 프로그램입니다.

FD_ZERO를 이용해 readfds를 초기화 시킨 후 readfds에 감시할 파일 디스크립터를 추가합니다. 그것이 FD_SET입니다. readfds의 복사본인 retfds select 시스템콜을 통해서 감시를 시작합니다. 만약 복사본을 넣지 않고 쌩으로 readfds를 넣는다면 select함수는 readfds를 단순히 조회만 하지 않고 변경합니다. 이러한 변경은 불필요합니다. FD_ISSET을 통해서 현재 준비된 파일디스크립터를 처리한 후에 다시 변경되지 않은 readfds로 돌아가야합니다.

만약 retfds flag on이 되면 FD_ISSET true를 반환하게 됩니다. 그러면 파일에 업데이트 된 내용을 출력해주게 됩니다결론적으로 위 소스 코드는 파일을 읽었다고 끝내지 않고 계속 파일에 쓰인 내용을 출력해주는 프로그램입니다.

이를 시험해보기 위해서는 다른 프로그램이 필요합니다.

//fwrite.c

#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>

int main(int argc, char *argv[]){
        int fd, ret;
        char buffer[32];

        if(argc != 2){
                printf("usage : %s file \n", argv[0]);
                exit(0);
        }
        //파일을 쓰기 전용으로 연다.
        fd = open(argv[1], O_WRONLY|O_APPEND);

        if(fd < 0){
                perror("file erorr ");
                exit(0);
        }

        while(1){

                //한줄 처리를 위해서 fgets를 사용
                fgets(buffer, sizeof(buffer), stdin);

                //파일에 기록 
                ret = write(fd, buffer, strlen(buffer));

                if(ret < 0){
                        perror("write error ");
                        exit(0);
                }
                //quit문자열은 종료
                if(!strcmp(buffer, "quit\n")) break;
        }

        close(fd);
        return 0;
}

 

이 실행 파일은 단순히 argv[1]로 들어오는 파일명에 기록하는 프로그램입니다. 이제 두 소스 코드를 컴파일하고 쓰일 실행 파일을 생성합니다.

# gcc multiIO_select.c 
# gcc fwrite.c -o fwrite
# touch a b c

 

이제 터미널 총 4개를 띄어서 테스트해볼건데, 하나는 select함수를 사용하는 프로그램, 다른 3개 터미널에는 fwrite를 수행하는 프로그램으로 select를 실험합니다. 단, 같은 디렉토리에 있어야합니다.

./a.out a b c
# ./a.out a b c
fd[3] - writing fd[3] - a...
fd[4] - writing fd[4] - b...
fd[5] - writing fd[5] - c...
fd[3] - hello
fd[4] - world
fd[5] – bye
#
./fwrite a ./fwrite b ./fwrite c
# ./fwrite a
writing a...
hello
quit
#
# ./fwrite b
writing b...
world
quit
# 
# ./fwrite c
writing c...
bye
quit
#

위와 같이 출력이 더럽게 나오는 것은 BUF_SIZE가 작아서 그렇습니다. BUF_SIZE를 늘리게 되면 위와 같은 지저분한 출력을 예방할 수는 있습니다. 

MultiIO_select.c에서는 select를 이용해서 여러 파일 디스크립터의 내용들을 입력받아서 출력해주고 있습니다. 설명드렸듯이 select는 정규파일에 대해서 항상 준비되어있기 때문에 곧 장  return되어 버립니다. 티는 나지 않지만 multiIO_select.c의 select함수 호출 이후 printf로 아무 로그나 찍으면 엄청나게 루프를 도는 것을 확인할 수 있습니다. 여기서는 이해를 돕기 위해서 만든 프로그램일 뿐입니다.

반응형
블로그 이미지

REAKWON

와나진짜

,