논-블로킹(non-blocking) I/O, 비동기(Asynchronous) I/O 차이 이해하기
블로킹(Blocking) I/O 및 동기(Synchronous) I/O
블로킹 I/O(혹은 동기 I/O)는 I/O 작업 시 대상 파일의 디스크립터(Descriptor)가 준비되어 있지 않은 프로세스는 시스템 호출 응답 대기상태(차단 상태)가 된다. 즉, 그 동안 프로그램 처리를 진행할 수 없다. 아래는 블로킹 I/O 사용자 모드와 커널 모드의 상호 전환을 나타내는 흐름이다. 커널 모드의 컨텍스트를 전환하는 시점에서 시스템 호출이 완료되며, 원래 사용자 모드 컨텍스트로 돌아감으로서 대기상태가 해제된다.

논-블로킹 (non-blocking) I/O
호출 직후 프로그램으로 부터 제어가 돌아옴으로서 시스템 호출 종료를 기다리지 않고 다음 처리로 넘어갈 수 있다. 일반적으로 O_NONBLOCK 플래그를 이용해 논-블로킹 모드를 선언하지만, 이 때 프로세스가 블로킹 상태가 아니기 때문에 CPU 를 다른 프로세스에서 사용함으로서 I/O 대기시간을 줄이거나 활용할 수 있다. 이 때 발생하는 오류는 응용프로그램에서 처리하고 재시도 하는 타이밍을 따로 정의할 필요가 있으며, 논-블로킹 I/O 소켓을 포함한 네트워크에 사용되는 I/O 모델에서는 디스크 I/O가 개입하지 않는다. 참고로, C10K 문제의 대책으로 논-블로킹 I/O에 이벤트 루프 모델을 차용함으로서 단일 스레드에서 여러 요청을 처리할 수 있게 되었다.

비동기(Asynchronous) I/O
I/O 처리가 완료된 타이밍으로 결과를 회신하는 I/O모델을 비동기 I/O라고 한다. 비동기 I/O의 회신은 시그널 또는 콜백의 형태로 이뤄지며, 회신이 있을 때 까지 어플리케이션은 다른 작업을 진행할 수 있다. 프로세스가 블록 상태가 되지 않는 점에서 논-블로킹 I/O와 같지만 비공기 I/O 는 I/O처리를 완료 했을 때 통지를 하는 반면 비-블로킹 I/O는 I/O가 처리 가능한 상태를 오류로 판단하는 차이가 있다.

논-블로킹 I/O 와 비동기 I/O 차이
논-블로킹 I/O는 처리가 완료되지 않으면 에러를 회신하고, 블록 상태로 만들지 않는 반면 비동기 I/O는 처리를 바로할 수 없을 때. 처리가 완료 되는 시점가지 백그라운드에서 대기하고, 종료한 타이밍을 회신 하는 차이가 있다.
I/O 다중화 (Multiplexing)
I/O관련된 내용을 정리하다 보면 반드시 언급되는 것이 I/O 다중화다. I/O 다중화는 poll(), select(), epoll 시스템 호출을 이용해 여러 파일 디스크립터를 하나의 프로세스로 관리한다. 이러한 시스템 호출은 파일 디스크립터 상태 변화를 모니터링 할 수 있다. poll, epoll, select의 차이는 다음과 같다.
select
select는 지정한 소켓의 변화를 확인하고자 사용되는 함수로, 소켓에 변화가 생길 때 까지 기다리다 어떤 소켓이 어떤 동작을 하면 동작한 소켓을 제외한 나머지 소켓을 제거하고 해당 소켓에 대한 확인을 진행하낟. 디스크립터 수에 제한되어 있어 적극적으로 사용되지는 않지만, 사용이 쉽고 지원 OS 가 많아 이식성이 좋은 편이다. 다음은 select를 사용한 socket(), bind(), listen()을 사용한 소켓 감시 프로그램의 예제다.
void accept_loop(int soc)
{
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
int child[MAX_CHILD];
struct timeval timeout;
struct sockaddr_storage from;
int acc, child_no, width, i, count, pos, ret;
socklen_t len;
fd_set mask;
/* child배열의 초기화 */
for (i = 0; i < MAX_CHILD; i++) {
child[i] = -1;
}
child_no = 0;
for (;;) {
/* select()용 마스크 작성 */
FD_ZERO(&mask);
FD_SET(soc, &mask);
width = soc + 1;
count = 0;
for (i = 0; i < child_no; i++) {
if (child[i] != -1) {
FD_SET(child[i], &mask);
if (child[i] + 1 > width) {
width = child[i] + 1;
count++;
}
}
}
(void) fprintf(stderr, "<<child count:%d>>\n", count);
/* select()용 시간 초과 값 설정 */
timeout.tv_sec = 10;
timeout.tv_usec = 0;
switch (select(width, (fd_set *) &mask, NULL, NULL, &timeout)) {
case -1:
/* 에러 */
perror("select");
break;
case 0:
/* 타임 아웃 */
break;
default:
/* ready */
if (FD_ISSET(soc, &mask)) {
/* 서버 소켓 ready */
len = (socklen_t) sizeof(from);
/* 연결 수락 */
if ((acc = accept(soc, (struct sockaddr *)&from, &len)) == -1) {
if(errno!=EINTR){
perror("accept");
}
} else {
(void) getnameinfo((struct sockaddr *) &from, len,
hbuf, sizeof(hbuf),
sbuf, sizeof(sbuf),
NI_NUMERICHOST | NI_NUMERICSERV);
(void) fprintf(stderr, "accept:%s:%s\n", hbuf, sbuf);
/* child 여유 확인 */
pos = -1;
for (i = 0; i < child_no; i++) {
if (child[i] == -1) {
pos = i;
break;
}
}
if (pos == -1) {
/* 여유 없음 */
if (child_no + 1 >= MAX_CHILD) {
/* child에 더이상 저장 불가 */
(void) fprintf(stderr,
"child is full : cannot accept\n");
/* 닫는다 */
(void) close(acc);
} else {
child_no++;
pos = child_no - 1;
}
}
if (pos != -1) {
/* child에 저장 */
child[pos] = acc;
}
}
}
/* 준비 완료된 파일 디스크립터 확인 (계산량 O(n)) */
for (i = 0; i < child_no; i++) {
if (child[i] != -1) {
if (FD_ISSET(child[i], &mask)) {
/* 전송 */
if ((ret = send_recv(child[i], i)) == -1) {
/* 오류 */
/* close */
(void) close(child[i]);
/* child 비움 */
child[i] = -1;
}
}
}
}
break;
}
}
}
유심히 봐야할 부분은 이 부분이다. 파일 디스크립터 수 만큼 루프를 돌리는 부분.
for (i = 0; i < child_no; i++) {
if (child[i] != -1) {
if (FD_ISSET(child[i], &mask)) {
/* 전송 */
if ((ret = send_recv(child[i], i)) == -1) {
/* 오류 */
/* close */
(void) close(child[i]);
/* child 비움 */
child[i] = -1;
}
}
}
}
poll
poll은 select의 문제였던 디스크립터 수 제한이 없다. 처리 방식은 select와 비슷하며, 어려개의 파일 디스크립터를 동시에 모니터링 하다 한개라도 읽을 수 있는 상태면 블로킹을 해제한다. 단, 디스크립터 수가 늘어날 수록 퍼포먼스가 떨어지는 단점이 있지만, 디스크립터 수 제한이 없어 select 보다 많이 사용된다. 단, 이식성이 나쁜편. 다음은 poll을 이용한 socket(), bind(), listen() 후 소켓 감시하는 프로그램의 예제다.
void accept_loop(int soc)
{
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
int child[MAX_CHILD];
struct sockaddr_storage from;
int acc, child_no, i, j, count, pos, ret;
socklen_t len;
struct pollfd targets[MAX_CHILD + 1];
/* child 배열 초기화 */
for (i = 0; i < MAX_CHILD; i++) {
child[i] = -1;
}
child_no = 0;
for (;;) {
/* poll()용 데이터 작성 */
count = 0;
targets[count].fd = soc;
targets[count].events = POLLIN;
count++;
for (i = 0; i < child_no; i++) {
if (child[i] != -1) {
targets[count].fd = child[i];
targets[count].events = POLLIN;
count++;
}
}
(void) fprintf(stderr,"<<child count:%d>>\n", count - 1);
switch (poll(targets, count, 10 * 1000)) {
case -1:
/* 에러 */
perror("poll");
break;
case 0:
/* 타임아웃 */
break;
default:
if (targets[0].revents & POLLIN) {
/* 서버 소켓 ready */
len = (socklen_t) sizeof(from);
/* 연결 수락 */
if ((acc = accept(soc, (struct sockaddr *)&from, &len)) == -1) {
if(errno!=EINTR){
perror("accept");
}
} else {
(void) getnameinfo((struct sockaddr *) &from, len,
hbuf, sizeof(hbuf),
sbuf, sizeof(sbuf),
NI_NUMERICHOST | NI_NUMERICSERV);
(void) fprintf(stderr, "accept:%s:%s\n", hbuf, sbuf);
/* child 여유 검색 */
pos = -1;
for (i = 0; i < child_no; i++) {
if (child[i] == -1) {
pos = i;
break;
}
}
if (pos == -1) {
/* 여유 없음 */
if (child_no + 1 >= MAX_CHILD) {
/* child에 더이상 저장 불가 */
(void) fprintf(stderr,
"child is full : cannot accept\n");
/* close */
(void) close(acc);
} else {
child_no++;
pos = child_no - 1;
}
}
if (pos != -1) {
/* child에 저장 */
child[pos] = acc;
}
}
}
/* 준비 완료된 파일 디스크립터 확인 (계산량 O(n)) */
for (i = 1; i < count; i++) {
if (targets[i].revents & (POLLIN | POLLERR)) {
/* 전송 */
if ((ret = send_recv(targets[i].fd, i - 1)) == -1) {
/* 오류 */
/* close */
(void) close(targets[i].fd);
/* child 비우기 */
for (j = 0; j < child_no; j++) {
if (child[j] == targets[i].fd) {
child[j] = -1;
break;
}
}
}
}
}
break;
}
}
}
select를 사용한 프로그램과 마찬가지로 루프에서 감시하는 파일 디스크립터의 수만큼 루프를 돌 필요가 있기 때문에 성능이 저하된다. 이 부분을 해결할려면 epoll를 사용해야 한다.
for (i = 1; i < count; i++) {
if (targets[i].revents & (POLLIN | POLLERR)) {
/* 전송 */
if ((ret = send_recv(targets[i].fd, i - 1)) == -1) {
/* 오류 */
/* close */
(void) close(targets[i].fd);
/* child 비우기 */
for (j = 0; j < child_no; j++) {
if (child[j] == targets[i].fd) {
child[j] = -1;
break;
}
}
}
}
}
epoll
epoll API 는 Linux 커널 2.5.44에서 도입되었다. 파일 디스크립터 수에 제한이 없으며 상태 변화 모니터링도 크게 개선되었다. 구체적으로 파일 디스크립터 상태를 커널에서 감시하고 변화된 내용을 직접 확인할 수 있기 떄문에 select, poll 처럼 루프를 사용한 모니터링이 불필요하다. 때문에 효율적인 I/O 를 구현할 수 있다.
void accept_loop(int soc)
{
char hbuf[NI_MAXHOST], sbuf[NI_MAXSERV];
struct sockaddr_storage from;
int acc, count, i, epollfd, nfds, ret;
socklen_t len;
struct epoll_event ev, events[MAX_CHILD];
if ((epollfd = epoll_create(MAX_CHILD + 1)) == -1) {
perror("epoll_create");
return;
}
/* EPOLL용 데이터 작성 */
ev.data.fd = soc;
ev.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, soc, &ev) == -1) {
perror("epoll_ctl");
(void) close(epollfd);
return;
}
count = 0;
for (;;) {
(void) fprintf(stderr,"<<child count:%d>>\n", count);
switch ((nfds = epoll_wait(epollfd, events, MAX_CHILD+1, 10 * 1000))) {
case -1:
/* 에러 */
perror("epoll_wait");
break;
case 0:
/* 타임아웃 */
break;
default:
/* 소켓 ready */
for (i = 0; i < nfds; i++) {
/* events[i].data.fd는 모두 읽을 수 있다 */
if (events[i].data.fd == soc) {
/* 서버 대기 */
len = (socklen_t) sizeof(from);
/* 접속 수락 */
if ((acc = accept(soc, (struct sockaddr *)&from, &len))==-1) {
if (errno != EINTR){
perror("accept");
}
} else {
(void) getnameinfo((struct sockaddr *) &from, len,
hbuf, sizeof(hbuf),
sbuf, sizeof(sbuf),
NI_NUMERICHOST | NI_NUMERICSERV);
(void) fprintf(stderr, "accept:%s:%s\n", hbuf, sbuf);
/* 여유 없음 */
if (count + 1 >= MAX_CHILD) {
/* 더이상 저장 불가 */
(void) fprintf(stderr,
"connection is full : cannot accept\n");
/* close */
(void) close(acc);
} else {
ev.data.fd = acc;
ev.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, acc, &ev) == -1) {
perror("epoll_ctl");
(void) close(acc);
(void) close(epollfd);
return;
}
count++;
}
}
} else {
/* 보내는중 */
if ((ret = send_recv(events[i].data.fd, events[i].data.fd)) == -1) {
/* 오류 */
if (epoll_ctl(epollfd, EPOLL_CTL_DEL, events[i].data.fd, &ev) == -1) {
perror("epoll_ctl");
(void) close(acc);
(void) close(epollfd);
return;
}
/* close */
(void) close(events[i].data.fd);
count--;
}
}
}
break;
}
}
(void) close(epollfd);
}
epoll은 select 나 poll과 달리 events에 들어있는 데이터는 모두 읽을 수있는 파일 디스크립터 덕분에 O (1)의 계산량으로 끝난다.
if (events[i].data.fd == soc) {

