CS/Network

[네트워크] Blocking I/O & Non-Blocking I/O

bu119 2023. 7. 6. 14:20
728x90
반응형

I/O (Input / Output)

I/O 작업은 Kernel level에서만 수행할 수 있다.
따라서, Process, Thread는 커널에게 I/O를 요청해야 한다.

 

어떠한 기기(디바이스)를 통해 입출력이 이루어지는 작업을 모두 I/O라고 한다.
즉, 네트워크를 통한 데이터 전송, 컨솔 출력 등과 같은 것도 포함된다.

 

이러한 I/O는 어플리케이션 성능에 가장 많은 영향을 끼친다.
I/O에서 발생하는 시간은 CPU를 사용한 시간과 대기 시간 중에 대기 시간에 속하기 때문에

I/O가 많아진다느 것은 어플리케이션이 연산을 할 때까지

CPU가 아무것도 못하고 대기하는 시간이 길어진다는 의미이고,

이는 어플리케이션의 처리 속도 저하로 이어진다.


따라서 높은 성능을 보장해야하는 어플리케이션에서는 I/O가 큰 장애물이 될 수 있다.

사실 어떤 함수든 일정시간 동안 CPU를 잡아두고 사용하기 때문에 다음 작업 실행을 block한다.
즉, CPU를 사용하는 작업으로 인한 blocking은 개발자가 할 수 있는 부분이 거의 없는데,

I/O로 인한 blocking은 CPU를 긴 시간동안 대기시키기 때문에

다른 작업을 할 수 있어도 하지 못해 매우 비효율적이다.

 

Blocking I/O & Non-Blocking I/O

Blocking / NonBlocking I/O는 호출되는 함수가 바로 리턴하느냐 마느냐가 관심사
이다.

 

Blocking I/O

  • 호출된 함수가 자신의 작업을 모두 마칠 때까지 호출한 함수에게 제어권을 넘겨주지 않고 대기하게 하는 방식
  • 자신의 작업을 진행하다가 다른 주체의 작업이 시작되면 다른 작업이 끝날 때까지 기다렸다가 자신의 작업을 시작하는 것

 

Non-Blocking I/O

  • 호출된 함수가 바로 리턴해서 호출한 함수에게 제어권을 넘겨주고, 호출한 함수가 다른 일을 할 수 있는 기회를 줄 수 있는 방식
  • 다른 주체의 작업에 관련없이 자신의 작업을 하는 것

 

일반적으로 아무런 언급 없이 Blocking I/O와 Non-Blocking I/O를 말한다면 Synchronous(동기) 방식을 말한다.

 

Blocking I/O

Blocking I/O는 I/O 작업이 진행되는 동안 User Process가 자신의 작업을 중단한채, I/O가 끝날때까지 대기하는 방식을 말한다.

 

 

Blocking I/O 형태의 작업은 다음과 같이 진행된다.

  1. Process(Thread)가 Kernel에게 I/O를 요청하는 함수를 호출한다.
  2. Process(Thread)는 작업 결과를 반환 받을 때까지 대기한다.
  3. Kernel이 작업을 완료하면 작업 결과를 반환 받는다.

이 경우 말 그대로 block이 되고, 어플리케이션에서 다른 작업을 수행하지 못하고 대기하게 되므로 Resource 낭비가 심하다.

  • I/O 작업이 진행되는 동안 user Process(Thread) 는 자신의 작업을 중단한 채 대기
  • I/O 작업이 CPU 자원을 거의 쓰지 않으므로 Resource 낭비가 심하다.

 

ex)

만약 Blocking I/O 방식으로 여러 Client 가 접속하는 서버를 Blocking 방식으로 구현한다면

다음과 같이 진행해야 할 것이다.

  1. I/O 작업을 진행하는 작업을 중지한다.
  2. 접속자 수가 매우 많아진다.
  3. 다른 client가 진행중인 작업을 중지하면 안되므로, client 별로 별도의 Thread를 생성한다.

 

이러한 진행으로 인해 많아진 Threads로 context switching 횟수가 증가하므로써, 비효율적인 동작을 하게 된다.

context switch

하나의 프로세스가 CPU를 사용 중인 상태에서 다른 프로세스가 CPU를 사용하도록 하기 위해, 이전의 프로세스의 상태(context)를 보관하고 새로운 프로세스의 상태를 적재하는 작업을 말한다.
한 프로세스의 문맥은 그 프로세스의 프로세스 제어 블록에 기록되어 있다.

 

이러한 Blocking I/O 방식을 도입할 경우의 문제점을 예를 들자면

카카오톡이 사용자가 메세지를 전송할 때까지 대기하고 있는거라고 생각하면 될 것이다.
카카오톡은 메신저 이외에도 다양한 기능이 많은데 만약 이러한 상황이라면 매우 비효율적일 것이다.

 

Non-Blocking I/O

Non-Blocking I/O는 작업이 진행되는 동안 User Process의 작업을 중단하지 않고 I/O 호출에 대해 즉시 리턴하고, User Process가 이어서 다른 일을 수행할 수 있도록 하는 방식을 의미한다.

 

 

Non-Blocking I/O 형태의 작업은 다음과 같은 형태로 진행된다.

  1. read I/O를 하기 위해 system call 한다.
  2. 커널의 I/O 작업 완료 여부와 관계없이 즉시 응답한다.
    • 이는 커널이 system call을 받자마자 CPU 제어권을 다시 어플리케이션에 넘겨주는 작업이다.
  3. 어플리케이션은 I/O 작업이 완료되기 전에 다른 작업을 수행 가능하다.
  4. 어플리케이션은 다른 작업 수행 중간중간에 system call을 보내 I/O가 완료되었는지 커널에 요청하고, 완료되면 I/O 작업을 완료한다.

 

이러한 과정을 통해 모든 작업 수행이 I/O의 진행시간과는 관계없이 빠르게 동작하기 때문에, User Process는 자신의 작업을 오랜시간 중지하지 않고도 I/O 처리를 수행할 수 있다.

 

그러나 반복적으로 system call이 발생하기 때문에 이것 또한 Resource 낭비가 된다.
이러한 Non-Blocking I/O는 데이터를 입력할 때만 전송하는 게 아니라 주기적으로 계속 반복하기 때문이다.

 

그래서 Non-Blocking I/O 문제인 반복적인 system call 호출을 해결하기 위해 I/O 이벤트 통지 모델이 도입되었다.

 

이벤트란?

수신 버퍼나 출력 버퍼에 데이터를 처리하는 동작을 의미한다.

 

수신 버퍼의 이벤트: 입력 버퍼에 데이터가 수신되었다는 것을 알림
출력 버퍼의 이벤트: 출력 버퍼가 비었으니 데이터 전송이 가능한 상황을 알림

 

카톡을 예시로 들면 non-blocking 방식 일시 "너 보낸 메시지 있어?"를 계속 물어봐야 한다. 하지만 이벤트 통지 방식을 사용하면 먼저 입력 버퍼에서 "사용자가 보낸 메시지가 있습니다"라고 알림(이벤트 통지 모델)을 준다면 계속 물어봐야 했던 그 시간을 카카오톡은 이제 효율적으로 사용할 수 있다. 즉, 의존성이 없어진다.

 


 

I/O 이벤트 통지 방식

I/O 이벤트 통지 방식에는 동기/비동기(synchronus/asynchronus) 모델로 분류 가능하다.
I/O 작업 상황(결과) 반환 방식에 따라 sync, async 방식으로 분류된다.

 

Synchronus & Asynchronus

Synchronous/Asynchronous는 호출되는 함수의 작업 완료 여부를 누가 신경쓰냐가 관심사다.

 

Synchronous(동기)

  • 호출하는 함수가 호출되는 함수의 작업 완료 후 리턴을 기다리거나, 또는 호출되는 함수로부터 바로 리턴 받더라도 작업 완료 여부를 호출하는 함수 스스로 계속 확인하며 신경쓰는 방식이다.
  • 작업을 동시에 수행하거나, 동시에 끝나거나, 끝나는 동시에 시작함을 의미

Asynchronous(비동기)

  • 호출되는 함수에게 callback을 전달해서, 호출되는 함수의 작업이 완료되면 호출되는 함수가 전달받은 callback을 실행하고, 호출하는 함수는 작업 완료 여부를 신경쓰지 않는 방식이다.
  • 시작, 종료가 일치하지 않으며, 끝나는 동시에 시작을 하지 않음을 의미

 

Sync Blocking I/O

함수가 바로 리턴하지 않고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우

 

Sync Blocking I/O는 I/O가 실행되는 동안 어플리케이션이 다른 일을 하지 못하고 있다가, I/O가 끝나고 나서 이어 작업을 처리하는 경우를 말한다.

 

분리하여 이해해보면 다음과 같다.

  • sync: 작업이 완료되면 해당 작업 결과를 가지고 어플리케이션에서 직접 처리한다.
  • blocking I/O: I/O 호출이 발생했을 때 커널의 I/O 작업이 완료될 때까지 제어권을 커널에서 가지고 있어, User Process는 I/O가 완료되기 전에 다른 작업을 할 수 없다.

 

system call마다 Thread를 생성하기 때문에 I/O요청이 많은 서비스에서는 작업 한 번의 context switching이 발생하기 때문에 점점 성능이 떨어진다.
또한 block될 동안 커널 응답만 기다리기 때문에 CPU를 사용하지 못한다는 점에서 Resource 사용 관점에서 비효율적이다.

 

Sync Non-Blocking I/O

함수가 바로 리턴하고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우

Sync Non-Blocking I/O는 I/O을 요청하고 즉시 리턴 받아 CPU 제어권을 받는다. 이로써 지속적으로 작업을 실행하면서 I/O 작업이 완료될 때까지 system call을 보내고 I/O가 완료 되면 해당 작업을 처리하는 경우를 말한다.

 

 

분리하여 이해해보면 다음과 같다.

  • sync: 다른 작업을 수행하다 중간중간 system call을 보내 I/O 작업이 완료됐는지 커널에게 지속적으로 물어본다. 그리고 I/O 작업이 처리되었을 때 결과를 호출한 함수에서 처리한다. 직접 결과를 처리해야 하기 때문에 지속적으로 I/O 종료를 물어보는 것이다.
  • non-blocking: I/O 호출이 발생했을 때 커널의 I/O 작업 완료 여부와 관계없이 즉시 응답한다. 커널이 system call을 받자마자 제어권을 다시 어플리케이션에 넘겨주기 때문에 User Process가 I/O가 완료되기 전에 다른 작업을 할 수 있다.

 

커널로부터 제어권을 받기 때문에 Sync Blocking I/O 보다 효율적인 것처럼 느껴질 수 있지만 커널로 부터 결과를 받기까지 계속 상태를 체크하는 busy-wait 상태가 된다. 즉, 작업 order를 맞추기 위해 I/O 작업의 완료를 기다리기 때문에 어떻게 보면 context switching만 빈번하게 일어나는 구조가 될 수 있다.

또한 system call 주기도 적절하게 설정하지 않으면 커널에게 의미없는 요청이 빈번하게 갈 수 있기 때문에 오히려 I/O 작업의 지연을 가져올 수 있다.

 

Async Blocking I/O

함수가 바로 리턴하지 않고, 호출하는 함수는 작업 완료 여부를 신경쓰지 않는 경우

 

Async Blocking I/O는 I/O 작업을 호출을 할 때 callback을 같이 넘겨주면서, I/O 작업이 종료됐을 때 어플리케이션에 해당하는 callback 함수가 호출되는 방식이지만 실질적으로 I/O 로직이 처리될 때까지 어플리케이션이 block되는 경우를 의미한다.

좀 더 명확히 구분하지면 I/O 작업 자체에 의해 block되는 것이 아니라 system call에 대한 커널의 응답이 block된다고 할 수 있다. 첫 요청에 대해서는 즉각 미완료 상태를 반환하는 non-blocking의 동작을 보여주기 때문이다.

 

 

분리하여 이해해보면 다음과 같다.

  • async: 작업이 완료되면 해당 작업 결과를 가지고 어플리케이션에서 호출한 함수가 직접 처리하는게 아니라 callback을 넘기면서 callback 함수 호출을 통해 작업 결과를 처리한다.
  • blocking I/O: I/O 호출이 발생했을 때 커널의 I/O 작업이 완료될 때까지 제어권을 커널에서 가지고 있기 때문에 User Process는 I/O가 완료 되기 전에 다른 작업을 할 수 없다.

 

사실 의도적으로 이 모델을 쓰는 경우는 거의 없다고 할 수 있고, Async Non-Blocking I/O 방식을 사용하는데 그 과정 중에 하나가 Blocking 방식으로 동작하는 경우 Async Blocking I/O 으로 동작할 수 있다.

 

이러한 Async Blocking I/O의 대표적인 케이스가 Node.js와 MySQL의 조합이라고 한다.

Node.js 쪽에서 callback 통해 Async로 동작해도, 결국 DB 작업 호출 시에는 MySQL에서 제공하는 드라이버를 호출하게 된다.


그런데 이 드라이버가 Blocking 방식이라고 한다.

이러한 경우는 Node.js 뿐아니라 Java의 JDBC도 마찬가지라고 하낟. 다만 Node.js가 싱글 쓰레드 루프 기반이라 멀티 쓰레드 기반인 Java의 Servlet 컨테이너보다 문제가 더 두드러져 보일 뿐이지 Blocking-Async라는 근본 원인은 같다고 한다.

 

Async Non-Blocking I/O

호출되는 함수는 바로 리턴하고, 호출하는 함수는 작업 완료 여부를 신경쓰는 경우

 

Async Non-Blocking I/O는 어플리케이션은 system call 이후 I/O 처리에 신경 쓰고 있지 않다가 작업이 완료되면 커널로부터 signal, thread 기반 callback 등으로 결과를 마치 event처럼 전달받는 경우이다. 이 경우 응답이 오기 전까지 User Process는 I/O와 독립적인 다른 processing이 가능한 구조이다.

 

 

분리하여 이해해보면 다음과 같다.

  • async: I/O 처리는 백그라운드에서 실행되다가 완료되면 커널이 User Process에게 작업 완료 시그널을 보내거나 callback을 보낸다. 즉, I/O가 완료되면 그 때 커널이 User Process에게 알려주는 방식이다.
  • non-blocking I/O: I/O 호출이 발생했을 때 system call이 들어오면 커널의 I/O 작업 완료 여부와 관계없이 즉시 응답한다. 따라서 User Process는 I/O가 완료되기 전에 다른 작업을 할 수 있다.

 

성능과 자원의 효율적 사용 관점에서 가장 유리한 모델이라고 할 수 있다.

 

요약

Blocking / NonBlocking I/O
호출되는 함수가 바로 리턴하느냐 마느냐가 관심사이다.
Synchronous / Asynchronous
호출되는 함수의 작업 완료 여부를 누가 신경쓰냐가 관심사다.

 

 


 

추가 자료

IBM 아티클에 기반한 분류

IBM 아티클에서는 blocking, non-blocking, sync, async 의 차이를 명시하지는 않았지만, 아티클의 내용에 따라서 분류해보자면 다음과 같을 것이다.

 

blocking
작업을 요청하면 일단 요청한 쪽은 일단 block이 되고, 작업이 완료가 된후에 응답을 받을수 있다. 그렇기에 완료가 되기 전에는 요청한 쪽은 block이 되어 다른 작업을 수행하지 못한다.

non-blocking
작업을 요청하면, 즉시 응답이 돌아온다.

동기(sync)
작업을 요청한 측에서 작업의 완료 여부를 체크한다.

비동기(async)
작업을 요청 받은 측에서 작업의 완료 여부를 알려준다

 

운영체제 교재에 기반한 분류

Operating System Concepts 라는 운영체제 교재(흔히 공룡책이라고 부르는 교재)에서도 blocking, non-blocking, async, sync의 차이를 설명하고있다.

blocking
wait queue 에 들어가고, 시스템 콜이 완료된 후에 응답을 보낸다.

non-blocking
wait queue 에 들어가지 않고, 즉시 리턴한다. (응답 또는 에러코드)

동기(sync)
wait queue 에 머무는게 필수가 아니고, 시스템 콜의 완료를 기다린다.

비동기(async)
즉시 리턴하고, 시스템콜의 완료을 기다리지않는다.

참고 자료

https://etloveguitar.tistory.com/140

https://velog.io/@octo__/BlockingNon-Blocking-IO-IO-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%ED%86%B5%EC%A7%80-%EB%AA%A8%EB%8D%B8

728x90
반응형