17-2. 레벨 트리거와 엣지 트리거 [TCP/IP][C][LINUX]
레벨 트리거 방식은 입력버퍼에 데이터가 남아있는 동안 계속 이벤트가 등록됩니다.
엣지 트리거 방식은 입력버퍼로 데이터가 수신된 상황에서 딱 한 번만 이벤트가 등록됩니다.
https://pupuduck.tistory.com/88에 따르면 epoll_event 구조체의 event 변수에 EPOLLET를 지정함으로써 엣지트리거로 변경시킬 수 있습니다. ex) event.events=EPOLLIN|EPOLLET;
엣지 트리거 기반의 서버 구현을 위해선 변수 errno를 이용한 오류 원인 파악과 Non-blocking IO를 위해 소켓의 특성을 변경하는 방법을 알아야합니다. 일반적으로 소켓관련 함수는 오류가 발생했을 때 -1을 반환하는데, 이로써 오류의 원인을 알 수는 없습니다. 헤더파일 errno.h를 include하면 errno 변수에 접근할 수 있습니다.
read함수로 예를 들면 입력버퍼가 비어서 더 이상 읽어 들일 데이터가 없을 때 -1을 반환하는데 이 때 errno에는 상수 EAGAIN가 저장됩니다.
#include<fcntl.h>
int fcntl(int filedes, int cmd, ...);
//success: 매개변수 cmd에 따른 값 fail: -1
fcntl함수는 파일의 특성을 변경 및 참조하는 함수인데 , filedes에는 파일 디스크립터, cmd에는 함수호출의 목적에 해당하는 정보를 전달합니다. cmd에 F_GETFL을 전달하면 파일 디스크립터의 특성정보를 int형으로 얻을 수 있고, F_SETFL 인자를 전달하면 특성정보를 변경할 수 있습니다.
소켓을 Non-blocking 모드로 변경하기 위해서는 2번째 인자에 F_SETFL, 3번째 가변인자에 flag|O_NONBLOCK을 전달하면 됩니다.
위 두 방법을 알아야하는 이유는, 엣지 트리거 방식에서는 데이터가 수신되면 딱 한 번 이벤트가 등록되기 떄문에 입력과 관련해서 이벤트가 발생하면 입력버퍼의 모든 데이터를 읽어들여야 하고, 따라서 입력버퍼가 비어있는지 확인하는 과정을 거쳐야 합니다.
그리고 엣지 트리거 방식의 특성상 블로킹 방식으로 동작하는 read&write 함수는 서버를 오랜 시간 멈추게 할 수 있기에 반드시 Non_blocking 소켓을 기반으로 호출해야합니다.
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<arpa/inet.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<fcntl.h>
#include<errno.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf);
int main(int argc, char *argv[]){
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
socklen_t adr_sz;
int str_len, i;
char buf[BUF_SIZE];
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");
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
//서버 소켓을 non-blocking 모드로
setnonblockingmode(serv_sock);
event.events=EPOLLIN;
event.data.fd=serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);
while(1){
event_cnt=epoll_wait(epfd, ep_events, EPOLL_SIZE,-1);
if(event_cnt==-1){
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
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);
// 클라이언트 소켓을 non-blocking모드, 이벤트 방식을 엣지트리거로
setnonblockingmode(clnt_sock);
event.events=EPOLLIN|EPOLLET;
event.data.fd=clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d\n",clnt_sock);
}
else{
//읽어올 데이터가 없을 때까지
while(1){
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);
break;
}
// 버퍼가 비어있을 때
else if(str_len<0){
if(errno==EAGAIN){
break;
}
}
else{
write(ep_events[i].data.fd, buf, str_len);
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd){
int flag=fcntl(fd,F_GETFL,0);
fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}
void error_handling(char *buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
https://pupuduck.tistory.com/88에서 다룬 epoll을 이용한 echo 서버를 엣지트리거를 사용하도록 다듬은 것입니다. epoll 서버와 엣지트리거를 이해했다면 쉽게 이해할 수 있을 것입니다.
엣지트리거는 레벨트리거와 달리 데이터의 수신과 데이터가 처리되는 시점을 분리할 수 있습니다. 구현모델의 특징상 엣지트리거가 레벨트리거보다 성능이 좋을 확률이 높습니다.