논 블로킹 모드로 프로그래밍을 한다는 것은 먼저 블로킹의 의미 부터 명확히 해야한다.
일반적으로 블로킹은 "운영체제(커널)에게 어떤 요청을 하고 결과를 운영체제가 알려줄 때까지 기다린다" 를 의미한다. 예를 들어 데이터베이스 서버에 요청하고 그 결과를 데이터베이스 서버가 알려줄 때까지 기다린다도 같은 얘기이다.
- 블로킹: 프로그램이 운영체제(커널) 또는 외부 서비스(DB, 네트워크 등) 에 요청을 보내고, 해당 요청의 결과가 반환될 때까지 대기(멈춤) 하는 상태.
connect() 를 호출하면 보통은 운영체제가 원격 서버와 연결하고 그 결과를 알려준다. 연결하는데 지연가능한 시간은 1초 이내, 수초 또는 수분이 될 수 있다. 하나의 연결을 위해 이를 수행하는 프로세스는 장시간을 대기(블로킹)할 수 있으며 그 수행시간을 예측할 수 없고 특히 한 프로세스 나 thread로 다수 개의 연결을 관리하고자 할 때는 부적절하다. 한 프로세스/쓰레드가 블록 상태에 빠지면 다른 작업을 처리하지 못하게 되기 때문이다.
이는 connect() 함수 뿐아니라 read(), write(), accept() 등도 블로킹 모드로 실행된다. (Unix 의 시스템 콜은 blocking/non-blocking 으로도 분류할 수 있다.) 그 중 connect(), accept()는 비교적 사용 빈도가 낮은 호출이고 read(), write()는 사용빈도가 높다. 따라서 이러한 시스템 호출에서 논블로킹 모드가 필요함을 알 수 있다.
- connect(): 원격 서버에 연결 요청 → 서버가 응답할 때까지 대기.
- read(), write(), accept(): 데이터 입출력 작업 → 데이터가 준비될 때까지 대기.
또한 서버가 클라이언트와의 통신뿐 아니라 다른 서버(전형적으로 데이터베이스 등) 와 연계되어있는 경우 여기서도 블록되지 않도록 해야한다. 즉, 통신 프로그램에서 사용한 함수 (시스템 콜, 라이브러리 호출 등) 하나 하나가 절대로 블록되어서는 안된다는 의미이다.
심지어 사용자가 만든 유틸리티 함수들도 루프를 가지는 경우 절대적으로 그 수행이 예측되는 응답시간내에 이뤄지도록 만들어야함을 의미한다.
read()에서 블로킹을 피한다는 것은 궁극적으로 버퍼 관리를 사용자 프로그램에서 수행한다는 것을 의미한다. 그 동안 커널의 tcpip 프로토콜 스택에서 받아주고 보내주고 했던 부분을 사용자 수준에서 상당 부분 판단 및 처리해야함을 의미한다.
그나만 단일 연결에서 이를 처리하는 것은 비교적 단순한 편이지만 다중 연결을 가지는 통신 프로그램에서 버퍼관리를 사용자 수준에서 하는 것은 까다로운 작업에 해당한다.
이러한 부분(read/write 및 외부 서버 인터페이스)을 논블로킹으로 만든 것들은 미들웨어 프로그램에서 찾아볼 수 있다. 즉, tcp layer와 application layer 사이에 존재하는 미들웨어 수준의 프로그램 또는 라이브러리에서 찾아볼 수 있다. 예시는 다음과 같다.
- libevent: 논블로킹 이벤트 드리븐 기반 네트워크 라이브러리(Memcached, tor등에서 사용)
- 그외 이벤트 드리븐 프로그래밍 라이브러리: libevent, epoll, select/poll, asyncio
- libuv: 비동기 i/o 라이브러리 (Node.js 등에서 사용)
- Boost.asio: c++ non-blocking network 프로그래밍 지원
- Twisted(Python): 비동기 네트워크 프로그래밍 어플리케이션
- Netty(Java): 메세지큐
- Nginx: http 및 리버스 프록시 서버(로드밸런서, API gateway)
- OpenSSL: HTTPS 암호화
ssl 도 한 예가 될 수 있다. TCP Layer 와 Application Layer 사이에 자신만의 SSL Layer 를 만들어 Application과 TCP 단의 통신을 제어하고 있다.
기본적인 논-블록킹 구현이 어려운 경우, 송신 또는 수신 직후에만 블록킹 해제하는 방식으로 타협 가능하다.
// fd: 소켓 파일 디스크립터
nonblock(fd, 1); // 논블록 모드로 설정
int ret = recv(fd, buffer, buffer_size, 0); // 논블록 모드로 데이터 수신
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 데이터가 아직 준비되지 않음
printf("No data available yet.\n");
} else {
// 실제 에러 발생
perror("recv error");
}
} else if (ret == 0) {
// 연결이 닫힘
printf("Connection closed by peer.\n");
} else {
// 데이터 수신 성공
printf("Received %d bytes of data.\n", ret);
}
nonblock(fd, 0); // 블록킹 모드로 복원
결론
논-블록킹 방식은 효율적인 연결 및 데이터 처리를 가능하게 하지만, 구현 난이도가 높음
미들웨어나 프레임워크를 활용하거나, 논-블록킹과 블록킹 방식을 혼합하여 사용할 수도 있음. 비즈니스 로직에 따라 선택하는 것이 중요
기존 라이브러리 활용할 경우 libevent, asyncio와 같은 도구를 활용하여 논-블록킹 프로그래밍의 복잡성을 줄일 수 있음
References
'System Programming > Operating System' 카테고리의 다른 글
[C/interrupt] IRQs: Hardware, Software (0) | 2024.11.18 |
---|---|
[C/CPU-affinity] 프로세스 CPU Affinity 설정 (0) | 2024.11.13 |
BPF 를 통한 Linux Performance 분석 #1 BPF Program 구성요소 (3) | 2024.10.09 |
GCC Compiler Manual (1) | 2024.10.05 |
linux에서 process 간 통신 #1 program and process (1) | 2024.09.08 |