책/운영체제

운영체제 26. 이벤트 기반의 병행성(event-based concurrency)

에린_1 2024. 3. 24. 19:01
728x90

26. 이벤트 기반의 병행성(event-based concurrency)

  • 이벤트 기반 프로그래밍은 쓰레드 기반의 병령 프로그래밍에 내제한 두 가지 문제점을 해결할 수 있다. 무엇보다 멀티 쓰레드 기반 프로그래밍은 어렵다. 자료구조를 락으로 보호하는 것을 잊을 수 있고, 교착 상태나 혹은 다른 문제들이 발생할 수 있다. 또 다른 문제는 멀티 쓰레드 프로그램에서는 개발자가 쓰레드 스케줄링에 대한 제어권을 갖고 있지 않다는 것이다. 운영체제가 CPU스케줄링에 대한 전권을 갖는다. 개발자는 운영체제가 합리적으로 쓰레드들의 실행 순서를 결정하기만을 기대할 수 밖에 없다.

26.1 개본 개념: 이벤트 루프

  • 특정 사건의 발생을 대기한다. 사건이 발생하면, 사건의 종류를 파악한 후 I/O를 요청하거나 추후 처리를 위하여 다른 이벤트를 발생시키거나 하는 작업을 한다.

이벤트 루프(event loop) 구조

  • 루프 내에서 이벤트 발생을 대기한다. 이벤트가 발생하면 하나씩 처리한다. 이벤트를 처리하는 코드를 이벤트 핸들러(event handler)라고 부른다. 중요한 것은 이벤트의 처리가 시스템의 유일한 작업이기 때문에, 다음에 처리할 이벤트를 결정하는 것이 스케줄링과 동일한 효과를 갖는다. 스케줄링을 제어할 수 있는 기능이 이벤트 기반 방법의 큰 장점 중 하나이다.

26.2 중요 API : select() (또는 poll())

  • 이벤트 발생은 대부분의 시스템의 경우 select() 또는 poll() 시스템 콜로 감지한다.
  • 이들 인터페이스 기능은 간단하다. I/O 사건이 발생했을 때, 처리를 필요로 하는 것이 있는지를 검사한다.
  • select()에 대한 두 가지 알아두어야 할 사항이 있다. 첫 번째, select()를 통해 디스크립터에 대한 읽기 또는 쓰기 가능 여부를 파악할 수 있다. 읽기 가능 상태를 검사하면, 새로운 패킷이 도착했는지를 파악 할 수 있다. 쓰기 상태를 검사하면, 서비스가 답신을 전송할 수 있는 시점인지를 파악토록 해준다.
  • 두 번째는 timeout 인자이다. 일반적으로 NULL로 설정한다. 그러면 select()는 디스크립터에 읽거나 쓸 수 있게 상태가 변경될 때까지 무한정 대기한다. 하지만 이러한 오류에 대비하여 설계된 서버들의 경우 timeout 값을 설정 해 두기도 한다. 널리 사용되는 방법으로는 timeout값을 0으로 설정하여 select()가 즉시 리턴하도록 하는 것이다.
  • 이런 인터페이스를 사용하여 non-blocking event loop를 제작한다. 이를 통하여 패킷의 도착여부를 확인하고 소켓에서 메세지를 읽고, 필요에 따라 답신을 전송한다.

26.3 왜 간단한가? 락이 필요없다.

  • 단일 CPU에서 이벤트 기반 응용 프로그램을 사용하면, 쓰레드 기반 병행 프로그램을 다룰 때 등장했던 문제점들은 더 이상 발생하지 않는다. 한 번에 하나의 이벤트만을 처리하기 때문에, 락을 획득하거나 해제해야 할 필요가 없다. 이벤트 기반의 서버는 단일 쓰레드로 구성된다. 다른 쓰레드에 의해서 인터럽트에 걸릴 가능성이 존재하지 않는다. 쓰레드 프로그램에서 흔히 등장하는 버그들은 이벤트 기반 접근법에서는 나타나지 않는다.

26.4 문제점 : 블로킹 시스템 콜(Blocking System Call)

  • 하나의 쓰레드가 I/O를 대기할 때, 다른 쓰레드를 실행한다. 서버는 동작을 계속한다. I/O 처리와 다른 연산을 중첩(Overlap) 시킴으로써 서버의 동작이 효율적이 된다. 쓰레드 기반 프로그래밍의 장점이다.
  • 이벤트 기반의 접근법에서는 쓰레드는 없고 단순히 이벤트 루프만 존재한다. 이벤트 핸들러가 블록킹 콜을 호출하면 서버 전체가 오직 그 일을 처리하기 위해 정지된다. 명령어가 끝날 때까지 다른 요청들의 처리가 중단되기에 심각한 자원 낭비가 발생한다. 이벤트 기반 시스템의 기본 원칙은 블로킹 호출을 허용하면 안된다.

26.5 해법 : 비동기 I/O

  • 이벤트 기반 서버의 한계를 극복하기 위해 비동기 I/O(asynchronous I/O) 라는 방법을 개발했다. 이 인터페이스는 프로그램이 I/O를 요청하면 I/O요청을 완료하기 전에 리턴한다. 이 API는 struct aiocb 또는 AIO 제어 블록(AIO control block)이라고 불리는 구조를 사용한다.
  • 파일을 비동기 읽기를 통해 읽으려면 우선 AIO 제어블록을 적절히 설정해야 한다. 읽고자 하는 파일 디스크립터, 파일 내에서 위치와 더불어 요청의 길이, 그리고 마지막으로 읽기 결과로 얻은 데이터를 저장할 대상 메모리 위치와 같은 내용을 저장한다. 필요한 정보를 모두 설정하면, 응용 프로그램은 비동기 콜을 호출한다. Mac에서는, 이 작을 하는 API를 비동기 읽기(asynchronous read)라 칭한다.
  • 호출에 성공하면, 읽기 작업의 완료를 대기하지 않고 즉시 리턴한다. 응용 프로그램은 하던 일을 계속 진행한다.
  • I/O의 완료를 어떻게 파악할까? 요청한 데이터가 버퍼로 제대로 읽혔다는 것을 어떻게 보장할까?
    • API가 하나 더 필요하다. Max OS X에서는 이 API를 aio_error()라고 한다.
    • 이 시스템 콜은 aiocbp에 의해 참조된 요청이 완료되었는지를 검사한다. 완료되었다면 성공했다고 리턴을 하고(0으로 표시) 실패했다면 EINPROGRESS를 반환한다. 모든 대기 중인 비동기 I/O는 주기적으로 aio_error() 시스템 콜로 시스템에 폴링(polling)하여 해당 I/O가 완료되었는지 확인할 수 있다.
  • I/O 완료 여부를 확인 하는 것은 프로그램 입장에서 매우 성가신 일이다. 이런 문제의 해결을 위해서 어떤 시스템들은 인터럽트 기반의 접근법을 제공한다. 유닉스의 시그널(signal)을 사용하여 비동기 I/O가 완료되었다는 것을 응용 프로그램에게 알려주기 때문에 시스템 시스템에 반복적으로 완료 여부를 확인할 필요가 없다. 폴링을 사용할 것인지 인터럽트를 사용할 것인지에 대한 선택의 문제는 I/O 장치들을 다룰 때에도 나타난다.

26.6 또 다른 문제점 : 상태관리

  • 이벤트 기반 접근법의 또 다른 문제점은 전통적인 쓰레드 기반 코드보다 일반적으로 복잡하다는 것이다. 이벤트 핸들러가 비동기 I/O를 발생시킬 때, I/O 완료시 사용할 프로그램 상태를 정리해 놓아야 한다. 이 작업은 쓰레드 기반 프로그램에서는 불필요하다. 왜냐하면 쓰레드 스택에 그 정보들이 이미 들어있기 때문이다. adya등은 이것을 수동 스택 관리(manual stack management)라고 부르며 이벤트 기반 프로그래밍에서는 기본이다.
  • continuation을 사용해서 문제를 해결한다. 이벤트를 종료하는데에 필요한 자료들을 한곳에 저장해둔다. 이벤트가 발생하면(디스크 I/O가 완료되면), 저장해놓은 정보를 활용하여 이벤트를 처리한다.

26.7 이벤트 사용의 어려움

  • 이벤트 기반 접근법에는 몇 가지의 또 다른 어려움이 있다. 예를 들어 단일 멀티 CPU의 경우에서는 이벤트 기반 접근법의 단순함이 사라진다. 다수의 CPU를 활용하기 위해 이벤트 서버는 다수의 이벤트 핸들러를 병렬적으로 실행한다. 그렇게 되면 임계영역 보호등과 같은 동기화 문제가 발생하며, 락과 같은 기법들을 사용할 수밖에 없다. 때문에 최근에 등장하는 멀티코어 시스템에서는 락이 없는 이벤트 처리 방식을 더 이상 사용할 수 없다.
  • 또 다른 문제는 이벤트 기반의 접근법과 운영체제 동작간의 상관관계이다. 이벤트 기반 서버는 페이징(paging)과 같은 종류의 시스템 작업과 조화롭게 실행되지 않을 수 있다. 예를 들면 이벤트 핸들러에서 페이지 폴트가 발생하면, 이벤트 서버는 블럭된다. 서버는 페이지 폴트가 처리완료 되기 전까지 진행을 할 수 없게 된다. 서버가 비차단(non-blocking)방식으로 설계되었다 할지라도, 이벤트 서버 입장에서 페이지 폴트와 같이 운영체제 내부적에서 발생하는 사건까지 대비하기는 어렵다. 이런 상황이 자주 발생하는 경우에 심각한 성능 저하를 가져올 수 있다.
  • 세 번째 문제는 루틴들의 작동 방식 변화이다. 소프트웨어가 개선되고 갱신되면서 각 루틴들의 특성이 변경될 수 있다. 이벤트 기반 서버에서는 변경되는 특성에 적합하게 코드를 다시 작성해야 하는데, 이것이 쉽지 않다. 개발자는 이벤트 처리 API의 특성이 변경되었는지를 항상 주의깊게 살펴야 한다.
  • 마지막으로 비동기 디스크 I/O의 사용 가능 여부이다. 현재는 대부분의 운영체제가 비동기 I/O를 지원한다. 아직까지도 비동기 네트워크 I/O는 생각하는 것 만큼 간단하고 일관성 있게 적용되어 있지 않다.
728x90