https://pupuduck.tistory.com/78에서 select 함수를 통한 IO 멀티플렉싱을 다뤘습니다. select 함수는 대부분의 운영체제에서 작동한다는 큰 강점이 있습니다.
다만 select 함수는 두 가지 큰 문제점이 있는데, 첫 번째로 모든 파일 디스크립터를 대상으로 반복문을 돌려야 한다는 것이고, select 함수를 호출할 때마다 인자로 관찰대상에 대한 정보들을 매번 전달해야 합니다.
프로그램으로서, 운영체제에게 데이터를 매번 전달하는 것은 많은 부담이 따릅니다. 따라서 운영체제에게 관찰대상에 대한 정보를 한 번만 알려주고 관찰대상의 범위나 내용에 변경이 있을 때 변경 사항만 알려줄 필요가 있습니다. 이는 운영체제가 지원을 해줘야 작동이 가능한데, 이 지원하는 방식을 리눅스에서는 epoll, 윈도우에서는 IOCP라 합니다.
epoll의 장점을 정리하면 다음과 같습니다.
상태변화의 확인을 위해 모든 파일 디스크립터를 확인할 필요가 없다.
select 함수 역할을 하는 epoll_wait 함수를 호출하면 관찰대상의 정보를 매번 전달할 필요 없다.
typedef union epoll_data{
void * ptr;
int fd;
__unit32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event{
__uint32_t events;
epoll_data_t data;
}
select 함수에서는 fd_set형 변수의 변화를 통해 관찰대상의 상태변화를 확인하지만 epoll 방식 에서는 위 구조체 epoll_event를 기반으로 상태변화가 발생한 파일 디스크립터가 묶입니다. 구조체 epoll_event의 배열에 변화가 발생한 파일 디스크립터가 저장되게 됩니다. events에는 이벤트의 유형이 들어갑니다.
events
EPOLLIN : 수신할 데이터가 존재
EPOLLOUT : 출력버퍼가 비워져 당장 데이터를 전송할 수 있는 상황
EPOLLPRI : OOB 데이터가 수신된 상황
EPOLLRDHUP : 연결이 종료되거나 Half-close가 진행된 상황
EPOLLERR : 에러가 발생한 상황
EPOLLET : 이벤트의 감지를 엣지 트리거 방식으로 동작
EPOLLONESHOT : 이벤트가 한 번 감지되면 해당 파일 디스크립터에서는 더 이상 이벤트 발생하지 않음
위 유형을 OR연산자를 통해 둘 이상을 함께 등록할 수 있습니다.
#include<sys/epoll.h>
int epoll_create(int size);
//success: epoll 파일 디스크립터 fail: -1
epoll 방식에서는 관찰대상 파일 디스크립터의 저장을 운영체제가 담당합니다. 파일 디스크립터의 저장을 위한 저장소의 생성을 운영체제에 요청하는 함수가 epoll_create입니다. size는 epoll 인스턴스의 크기를 결정하는 정보로 사용되는데, 이 값은 운영체제에 전달되어 힌트가 될 뿐 그대로 결정되진 않을 수 있고, 리눅스 커널 2.6.8 이후로는 아예 무시됩니다.
또, 위 함수호출을 통해 생성되는 리소스는 소켓과 마찬가지로 파일 디스크립터를 반환하고 epoll 인스턴스를 구분하는 목적으로 사용됩니다. 소멸 시에는 다른 파일 디스크립터와 마찬가지로 close 함수 호출이 필요합니다.
#include<sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//success:0 fail:-1
epfd: 관찰대상을 등록할 epoll 인스턴스의 파일 디스크립터
op: 관찰대상의 추가, 삭제 또는 변경여부 지정
EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 추가
EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 제거
EPOLL_CTL_MOD : 등록된 파일 디스크립터의 이벤트 발생상황을 변경
fd: 등록할 관찰대상의 파일 디스크립터
event: 관찰대상의 관찰 이벤트 유형
epoll 인스턴스 생성 후 관찰대상이 되는 파일 디스크립터를 등록하기 위해 사용되는 함수입니다.
만약 epoll_ctl(A, EPOLL_CTL_ADD,B,C)를 호출하면 epoll 인스턴스 A에 파일 디스크립터 B를 등록하고 C를 통해 전달된 이벤트의 관찰을 목적으로 등록을 진행합니다. 파일 디스크립터를 제거하기 위해 EPOLL_CTL_DEL을 사용할 땐 event 포인터 인자 자리에 NULL을 입력하면 됩니다.(리눅스 커널 2.6.9 이전에서는 오류 발생)
#include<sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
//success: 이벤트가 발생한 파일 디스크립터의 수 fail: -1
epfd: 이벤트 발생의 관찰영역인 epoll 인스턴스의 파일 디스크립터
events: 이벤트가 발생한 파일 디스크립터가 채워질 버퍼의 주소 값
maxevents: 두 번째 인자로 전달된 주소 값의 버퍼에 등록 가능한 최대 이벤트 수
timeout: 1ms 단위의 대기 시간, -1 전달 시 이벤트가 발생할 때까지 무한 대기
epoll_wait 함수는 이벤트가 발생한 파일 디스크립터를 두 번째 인자로 전송된 events 배열에 묶어줍니다. 이때 events 주소 값의 버퍼는 동적으로 할당해줘야 합니다.
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// EPOLL_SIZE는 직접 지정한 매크로 상수 값
아래 예제코드는 select 함수로 만든 echo 서버를 epoll 방식으로 바꿔준 것입니다. select 함수로 구현된 부분은 비교할 수 있도록 주석처리했습니다.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf);
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
//struct timeval timeout;
//fd_set reads, cpy_reads;
socklen_t adr_sz;
//int fd_max, str_len, fd_num, i;
int str_len, i;
char buf[BUF_SIZE];
// epoll 방식에서 사용할 변수들
struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt;
if(argc!=2){
printf("Usage : %s <port>\n",argv[0]);
exit(1);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0);
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
serv_adr.sin_port=htons(atoi(argv[1]));
if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("bind() error");
if(listen(serv_sock,5)==-1)
error_handling("listen() error");
// FD_ZERO(&reads);
// FD_SET(serv_sock, &reads);
// fd_max=serv_sock;
// epoll 인스턴스, 파일 디스크립터 생성
epfd=epoll_create(EPOLL_SIZE);
// epoll_wait 에 들어갈 epoll_event 배열 동적할당
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// 서버 소켓을 이벤트가 발생할 때(연결 요청이 들어올 때) 관찰하도록 등록
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1){
// cpy_reads=reads;
// timeout.tv_sec=5;
// timeout.tv_usec=5000;
// if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
// break;
// if(fd_num==0)
// continue;
// for(i=0; i<fd_max+1; i++){
// if(FD_ISSET(i, &cpy_reads)){
// if(i==serv_sock){
// adr_sz=sizeof(clnt_adr);
// clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr, &adr_sz);
// FD_SET(clnt_sock, &reads);
// if(fd_max<clnt_sock)
// fd_max=clnt_sock;
// printf("connected client: %d\n",clnt_sock);
// }
// else{
// str_len=read(i,buf,BUF_SIZE);
// if(str_len==0){
// FD_CLR(i, &reads);
// close(i);
// printf("closed client: %d\n", i);
// }
// else{
// write(i,buf,str_len);
// }
// }
// }
// }
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE,-1);
if(event_cnt==-1){
puts("epoll_wait() error");
break;
}
//변화가 있는 파일 디스크립터의 수만큼 반복
for(i=0; i<event_cnt; i++){
if(ep_events[i].data.fd==serv_sock){ //새로운 연결요청이 있었을 때
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
event.events=EPOLLIN;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n",clnt_sock);
}
else{ // 관찰중인 파일 디스크립터에 변화가 있을 때
str_len=read(ep_events[i].data.fd,buf,BUF_SIZE);
if(str_len==0){
// 관찰대상에서 제거할 파일 디스크립터 지정
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
close(ep_events[i].data.fd);
printf("closed client: %d\n",ep_events[i].data.fd);
}
else{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char *buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
'현생 > TCP 소켓 프로그래밍' 카테고리의 다른 글
18-1. 쓰레드란 무엇일까? (0) | 2022.02.16 |
---|---|
17-2. 레벨 트리거와 엣지 트리거 [TCP/IP][C][LINUX] (0) | 2022.02.16 |
16. 파일 포인터 기반 입출력 스트림 분리에서 Half-close [TCP/IP][C][LINUX] (0) | 2022.02.16 |
15. 표준 입출력 함수와 파일 디스크립터의 비교 및 변환 [TCP/IP][C][LINUX] (0) | 2022.02.11 |
14. 멀티캐스트와 브로드캐스트 [TCP/IP][C][LINUX] (0) | 2022.02.11 |