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

와나진짜

,