멀티플렉싱이란 하나의 채널을 통해 여러개(명)의 device가 통신 가능하게 하는 기술이다.
이번 학기 데이터통신이나 시스템 프로그래밍과 같은 과목들에서 자주 언급이 되었던 용어이나 이론적으로만
배웠기때문에 크게 와닿지는 않았다. 다만 select와 epoll기반의 멀티 플렉싱 서버 예제를 공부하며 다시 접하게 되었다.
select와 epoll모두 멀티 플렉싱 서버를 구현하는데 필요한 API라는데에 공통점이 있으며, 둘 사이에는 몇 가지 차이점이 존재한다. 정리하면 아래와 같다.
select | epoll | |
운영체제 | 다양한 운영체제에서 동작 | Linux 한정 |
FD(file descriptor)관리 | 사용자(개발자)가 관리 | 운영체제가 관리 |
trigger | level trigger | edge/level trigger |
epoll은 리눅스환경에서 멀티플렉싱 서버를 구현하는데에 사용되며, 다른 운영체제도 마찬가지로 select상위 호환의 API가 존재하여 그것을 통해 멀티플렉싱 서버를 구현할 수 있다.
select기반 멀티플렉싱 서버 모델은 level trigger 방식으로 동작하지만 epoll은 설정에 따라 edge/level trigger로 동작할 수 있으며, edge trigger사용시 non-blocking 소켓을 사용하여야 한다. trigger에 대한 내용은 다른 글에서 따로 정리할 생각이다.
FD_ZERO(fd_set * fdset) | 인자로 전달된 fd_set형 변수의 모든 비트를 0으로 초기화 |
FD_SET(int fd, fd_set * fdset) | 매개변수 fd를 fdset에 관리 대상으로 등록한다. |
FD_CLR(int fd, fd_set* fdset) | 매개변수 fd를 fdset에 관리 대상에서 삭제한다. |
FD_ISSET(int fd, fd_set * fdset) | 매개변수 fd에 변화가 일어났는지 확인 후, 참이면 양수를 반환 |
select는 fd_set이라는 변수형을 이용하여 fd를 관리하는데 단순히 비트단위의 배열로 볼 수 있다.
fd_set형 변수는 위의 함수를 통해 다룰 수 있다.
#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set *readset, fd_set *writeset, fd_set *exceptset, const struct timeval *timeout)
select함수의 원형으로 maxfd는 검사(관리)할 fd의 수, timeval은 변화가 일어나기까지 최대한으로 기다릴 시간이다. 이말은 select함수를 실행하고 fd에 변화가 없다면 blocking상태가 된다는 의미이다. readset, writeset, exceptset은 각각 수신된 데이터 여부, 전송가능여부, 예외상황 발생여부를 나타내는 fd_set형 변수로 예제에서는 수신된 메시지의 유무에만 관심이 있으므로, readset을 제외한 나머지 값들은 NULL값을 넘겨주었다.
아래는 select함수 기반의 멀티 플렉싱 서버 예제이다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
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;
char buf[BUF_SIZE];
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); // serv_sock을 관리 대상으로 설정
fd_max=serv_sock; // 현재 관리중인 fd의 수, serv_sock을 가장 최근에 생성하였으므로 현재 최대
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) // 변화가 일어난 대상이 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 // 변화가 일어난 대상이 clnt_sock인 경우 -> 클라이언트로부터 메시지 수신
{
str_len=read(i, buf, BUF_SIZE);
if(str_len==0) // close request!
{
FD_CLR(i, &reads);
close(i);
printf("closed client: %d \n", i);
}
else
{
write(i, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
// 출처 : TCP/IP 윤성우 열혈 소켓 프로그래밍 p.283 echo_selectserv.c
코드분석
- 멀티 플렉싱 서버의 전체적인 흐름을 보면 변화가 일어나는지 검사할 소켓을 등록하고 select 함수를 호출하여 변화가 일어날 때까지 기다리는 순서로 이루어지는 것을 확인 할 수 있다.
위의 select 기반의 멀티 플렉싱 서버 예제에서는 크게 2가지의 단점을 찾을 수 있다. 첫 번째는 모든 fd를 개발자가 관리하며 매번 for문을 돌면서 모든 fd에 대해 검사를 해야 한다는것, 두 번째로는 버퍼의 크기가 부족하여 read 함수를 호출하였으나 메시지가 소켓의 입력버퍼에 남는 경우 다시 for문을 돌때 입력 받는다는 것이다. 두 번째는 trigger에 관련한 내용으로 select가 level trigger 기반으로 동작하기 때문에 일어 날 수 있는 일이다.
이러한 단점을 없앨 수 있는 epoll은 다음과 같은 함수들을 사용하여 제어할 수 있다.
int epoll_create(int size) | epoll 파일 디스크립터 저장소 생성 |
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event) | epoll 저장소에 파일 디스크립터 등록 및 삭제 |
int epoll_wait(int epfd, struct epoll_event *event, int maxevents, int, timeout) | select함수와 동일하게 fd의 변화를 대기 |
먼저 epoll_create함수를 통해 fd를 관리할 저장소를 운영체제에게 요청하는 역할을 한다. 앞서 epoll은 fd의 관리를 개발자가 하지 않는다고 언급하였는데, select에서는 fd_set이라는 변수를 직접 선언하여 관리하는 것과 확실히 비교되는 모습을 보인다.
epoll_ctl 함수의 두 번째 인자를 통해 fd를 등록하거나 삭제 할 수 있다.
EPOLL_CTL_ADD : 파일 디스크립터를 epoll 인스턴스에 등록
EPOLL_CTL_DEL : 파일 디스크립터를 epoll 인스턴스에서 삭제
epoll_ctl의 네번째 인자를 보면 epoll_event라는 구조체를 사용하는 것을 알 수 있는데, 이 구조체를 통해 관찰하고자 하는 fd의 상황을 설정 할 수 있다. 이는 select함수에서 변화의 종류를 나누어서 관찰할 수 있던것과 동일하다. 이는 다음과 같은 flag를 통해 설정할 수 있다.
EPOLLIN | 수신할 데이터가 존재하는 상황 |
EPOLLOUT | 출력 버퍼가 비어서 데이터를 송신할 수 있는 상황 |
EPOLLPRI | OOB 데이터가 수신된 상황 |
EPOLLRDHUP | 연결이 종료되거나 Half-close가 진행된 상황 |
EPOLLERR | 에러가 발생한 상황 |
EPOLLE | 이벤트의 감지를 엣지 트리거 방식으로 동작하게끔 설정 |
EPOLLONESHOT | 이벤트가 감지되면 해당 fd에서 더 이상 이벤트가 발생하지 않게끔 설정. |
해당 flag들은 OR연산자를 통해 원하는 값들을 합쳐서 사용할 수 있다. 아래는 epoll 기반의 멀티 플렉싱 서버 예제이다.
#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;
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");
// epoll 인스턴스 생성
epfd=epoll_create(EPOLL_SIZE);
ep_events=malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
// serv_sock을 epoll 인스턴스에 등록 후 이벤트 발생 여부 관찰
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;
}
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) // close request!
{
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); // echo!
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
}
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
// 출처 : TCP/IP 윤성우 열혈 소켓 프로그래밍 p.373 echo_epollserv.c
코드분석
- select기반의 멀티 플렉싱 서버와 거의 유사한 흐름을 보인다. 다만 epoll_wait함수를 호출하는 부분에서 ep_events라는 구조체 배열에 묶이는 것을 알 수 있는데, 이는 select 기반의 멀티플렉싱 서버에서 매번 모든 fd를 검사해야 했던 것과 비교된다고 할 수 있다. 즉, epoll에서는 변화가 일어난 fd만 반복문을 돌릴 수 있다는 얘기이다.
사실 반복문을 도는 횟수는 성능면에서 그렇게 중요한 요소는 아니다. 오히려 중요한 것은 select함수는 매 호출시 관찰 대상에 대한 정보를 운영체제에 넘겨야 했으나, epoll 방식에서는 관찰 대상의 정보를 최초 한번만 알린 후 변화가 있을 때에 변경 사항만 알리도록 하게끔 한 것이다.
epoll 방식을 사용하여 select방식의 단점을 어느정도 해소 하였으나 아직 해결되지 못한 점들이 있다. 이는 edge trigger방식을 통해 해결이 가능하며, epoll 방식에서 설정을 변경하여 구현이 가능하다. trigger에 대한 내용은 다음 글에서 정리할 것이다.
'네트워크 > 예제' 카테고리의 다른 글
[Network] - Java로 구현한 소켓 통신 서버(2) (0) | 2023.02.27 |
---|---|
[Network] - Java로 구현한 소켓 통신 서버(1) (0) | 2023.01.22 |