본문 바로가기

C/C++

Nonblocking I/O in C

@markdown


# Nonblocking I/O in C


나는 보통은 C로 I/O를 수행하는 프로그램을 작성하지 않는다.왜냐하면 다른 언어를 사용해도 충분히 퍼포먼스가 좋고, I/O에 대한 추상화가 잘 되어있기 때문이다. 그러나 C를 사용했을 때 얻을 수 있는 가벼움과 빠름, 그리고 결과물이 바이너리라는 점에서 강력한 이식성을 얻을 수 있기에 꼭 필요할 때가 있다. 특히 임베디드 기기를 대상으로 개발할 때는 더욱 그렇다.

_

일단 C로 I/O를 수행하는 방식은 블록킹, 논블록킹으로 나뉜다.


**블록킹 I/O**를 사용하면 이해하기 쉬운 코드를 간단하게 작성할 수 있다. 대신, 상황에 따라 I/O를 처리하는 도중 프로그램 흐름이 블록된다. 보통 `read()`를 호출했으나 읽을 것이 없을 때, `write()`를 호출했으나 쓰기 큐에 빈 자리가 없어서 기다려야 할 때, `connect()`를 호출했으나 연결이 완료되기까지 시간이 걸릴 때 스레드가 블록된다.


**논블록킹 I/O**를 사용하면 블록킹 I/O의 장점을 그대로 가져가고, 추가로 **어떠한 상황에서도 프로그램 흐름이 블락되지 않는**다. 만약 I/O를 곧바로 완료할 수 없다면 `-1`이 반환되고  `errno` 변수가 왜 당장 요청한 작업을 완료할 수 없었는지를 알려준다.

_

일견 논블록킹 I/O가 정답인 것처럼 보인다. 그러나, 논블록킹을 사용하면 사용자 입장에서는 언제 I/O가 가능할지 알 방법이 없다.


따라서 계속해서 I/O함수를 호출하는 폴링을 하거나, `select` 또는 `epoll`의 도움을 받아 I/O가 가능할 때를 알아내야 한다. 전자를 선택하면 무한 루프 기반으로 막대한 CPU자원을 낭비하게 되고, 후자를 선택하면 사용하면 코드 작성에 있어 진입 장벽과 난이도가 올라가고 I/O흐름을 한눈에 보고 따라가기가 어려워진다.


코루틴까지 동원하면 상황이 나아질 것 같으나 아직 내가 그렇게 일을 해본 적이 없고, 한창 연구중이기 때문에 정말 해결사일지는 잘 모르겠다.


_


각설하고, 논블록킹을 적용하는 방법과 실제로 사용하는 방법을 정리한다.


## 논블록킹 모드로 변환


주어진 파일 디스크립터 `fd`를 논블록킹 모드로 변환하는 코드는 다음과 같다.


```

int flags = fcntl(fd, F_GETFL, 0);

fcntl(fd, F_SETFL, flags | O_NONBLOCK);

```

이 코드는 기존 fd의 속성값을 가져와 `flags`에 저장한 뒤, 논블록킹 플래그를 덮어씌운다.

_

참고로 `open()`을 할 때 `O_NONBLOCK` 플래그를 사용해 논블록킹 모드를 지정할 수 있으나, 파일 디스크립터는 `open()`외에도 다양한 방법으로 생성되기 때문에 fd의 속성을 변경하는 위의 방법을 사용하는 것을 추천한다.


## 논블록킹 모드에서의 I/O 함수들


기존 I/O함수들을 사용하던 대로 사용하면 된다.


대신 주어진 I/O 요청에 대해 all or nothing으로 행동한다는 점에 주의하고, 반환값과 errno를 반드시 확인해야 하는 점을 잊지 말도록 하자.


### read()

읽기 요청이 성공하면 읽은 바이트 수를 반환

- EOF를 만나면 0을 반환

- 당장 읽을 것이 없으면 -1을 반환하고 errno가 `EAGAIN`으로 설정됨

- 에러가 발생한 경우 -1을 반환


### write()

- 쓰기 요청이 성공하면 쓴 바이트 수를 반환

- EOF를 만나거나 에러가 발생하면 `SIGPIPE`를 발생시키고 -1을 반환


### connect()

connect()를 하기 전에 해당 클라이언트 소켓을 논블록킹 모드로 설정해 두는 것을 잊지 말자(한번만 해두면 된다). 그리고 connect()를 너무 자주 호출하면 비정상 동작하는 기기가 가끔 있다. PC를 포함한 일반적인 기기는 문제가 없으나, 임베디드 기기인 경우 너무 자주 호출하지 않도록 주의하자.

- 연결이 생성되면 0을 반환

- 연결이 진행중인 경우 -1을 반환하고 errno에 `EALREADY` 또는 `EINPROGRESS`를 설정

- 연결 도중 에러가 발생한 경우 -1을 반환하고 errno에 에러를 나타내는 값을 설정


### accept()

accept()를 하기 전에 해당 서버 소켓을 논블록킹 모드로 설정해 두는 것을 잊지 말자(한번만 해두면 된다).

- 새 클라이언트와 연결되면 클라이언트의 fd를 반환

- 새 클라이언트와 연결되지 않았다면 -1을 반환