시스템 프로그래밍 19장 - 비동기 i/o와 APC

김주현·2021년 10월 23일
0

시스템 프로그래밍

목록 보기
19/21

01 비동기 I/O

-비동기 i/o의 이해

위 그림을 CPU 동작시간 관점에서 보면 플레이 할때는 CPU가 동작하고 데이터를 수신할 때에는 CPU가 동작하지 않는다. I/O는 CPU와 거의 독립적으로 수행되는 연산이기 때문이다.

위그림과 같은 데이터 수신이 빈번하게 발생하면 그 빈도수만큼 CPU가 일을 하지 않는 시간도 늘어나게 되고 이는 성능의 저하로 이어진다.

블로킹 함수: 한번 호출되면, 완료될 때가지 블로킹되는 함수

블로킹함수들을 활용한 입출력 연산을 가리켜 동기I/O라 표현함

데이터 수신은 CPU할당을 크게 요구하지않음 I/O작업은 작업대로 전개하면서 버퍼링된 데이터를 기반으로 플레이를 병행하면 사이사이 발생하는 시간지연 문제는 상딩히 줄어듬

이러한 구조의 I/O를 가리켜 비동기 I/O라 한다.

-중첩 I/O

윈도우즈에서 제공하는 비동기 I/O 방식 중에서 가장 대표적인 것이 중첩 I/O이다.

행동을 동시에 진행하기위해선 함수가 넌블로킹 함수여야함.

넌블로킹 함수라는 것은 블로킹 함수와는 상대ㅚ는 개념이다. 블로킹 함수가 작업이 완료된 후에나 반환한다면, 넌블로킹 함수는 작업의 완료에 상관없이 바로 반환해 버리는 특성을 지닌다.

ANSI 표준 함수에는 넌블로킹이라는 개념이 존재하지않음. Windows 시스템 함수인 ReadFile 함수는 넌 블로킹 방식으로 동작시킬 수 있음

위 그림은 넌블로킹 함수 호출을 통해서 여러 작업이 동시에 진행될 수 있음을 보여줌.
말 그래도 I/O연산이 중첩되어 실행되서 중첩 I/O라 부른다.

-중첩 I/O 예제

  1. 중첩 I/O 기반 파이프 통신 1단계

중첩 I/O 기반 파이프 통신을 위해서는 중첩 I/O 가능한 비동기 특성을 띄는 파이프를 생성해야 하는데, 파이프 생성시 상수 FILE_FLAG_OVERLAPPED를 인자로 전달해서 비동기 특성을 부여하면 된다.

  1. 중첩 I/O 기반 파이프 통신 2단계


복잡해 보이지만, 지금 당장 신경을 쓸 멤버는 hEvent 하나다.
OVERLAPPED가 화살표로 EVENT를 가리키고 있다. 이는 이벤트 오브젝트를 생성해서 그 핸들을 OVERLAPPED 구조체 변수의 멤버 hEvent에 저장한다는 의미이다. 이 이벤트 오브젝트는 입.출력 연산이 완료되었음을 확인하기 위한 것이다. 입출력 연산이 완료되면 멤버 hEvent가 가리키는 이벤트 오브젝트는 Signaled 상태가 된다.

  1. 중첩 I/O 기반 파이프 통신 3단계

파이프 생성 및 OVERLAPPED 구조체 변수 초기화가 끝나면, WriteFile 함수를 호출하게 되는데, 반드시 FILE_FLAG_OVERLAPPED가 설정된 파이프와 OVERLAPPED 구조체 변수의 포인터를 인자로 전달한다. 그러면 중첩 I/O 방식으로 동작하게 된다.

-완료루틴 기반 확정 I/O

중첩 I/O를 보면 입력 및 출력이 완료되었음을 확인하는 번거로운 작업을 항시 고려해야하는 불편한점이있다.

위 그림은 I/O 연산 A가 끝이 났을 때 루틴 D가 실행되어야 함을 보여준다. 여기서 루틴이라는 것은 보통 함수로 구성된다. 여기서 말하는 루틴은 "I/O 연산이 완료되었을 때 실행되는 루틴"을 의미하는데 이를 줄여서 "완료루틴"이라 표현한다.

확장 I/O의 입출력 연산도 기본적으로 중첩방식이다. 따라서 확장 I/O를 기반으로 I/O 연산을 할 때에도 연산이 중첩된다. 그렇다면 다음과 같은 공식을 세워볼 수 있다.

확장 I/O 제공 기능 = 중첩 I/O 제공기능 + 알파

여기서 알파는 앞서 설명한 것처럼 루틴 컨트롤을 자동으로 해준다는 것이다. 확장 I/O의 기본 모델은 다음과 같다.

이벤트 오브젝트를 생성하지 않는다. 어차피 입력 및 출력이 완료되면. 해당 완료루틴(함수)이 자동으로 호출되기 때문에 이벤트 오브젝트가 필요 없어졌다.

그리고 WriteFile/ ReadFile 함수 대신에 WriteFileEx / ReadFileEx 함수를 호출한다. 이 함수는 입력 및 출력 연산이 완료되었을 때, 호출되어야 할 완료루틴 지정을 위한 매개변수가 추가로 선언되어 있다.

BOOL WriteFileEx (
HANDLE hFile // 데이터 전송(쓰기)의 대상 핸들을 지정한다. 여기 인자로 전달되는 핸들은 FILE_FLAG_OVERLAPPED 속성이 지정되어야한다.

LPCVOID lpBuffer // 전송할 데이터를 지니는 버퍼의 주소를 지정한다.

DWORD nNumverOfBytedToWrite // 전송할 데이터의 크기를 지정한다.

LPOVERLAPPED // OVERLAPPED 구조체 변수의 포인터를 지정한다.

LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 출력 연산 완료시 자동으로 호출되는 완료루틴의 지정을 위해 사용된다.

LPOVERLAPPED_COMPLETION_ROUTINE은 완료루틴을 가리킬 수 있는 함수 포인터의 typedef 선언이다 

typdef VOID (WINAPI * LPOVERLAPPED_COMPLETION_ROUTINE) (
	DWORD dwErrorCode,
    DWORD dwNumverOfBytedTransfered,
    LPOVERLAPPED lpOverlapeed
    );
    
    이는 완료루틴을 가리킬 수 있는 함수포인터의 typedef 선언이다. 완료루틴은 다음과 같이 선언되어야 한다.
    
      VOID WINAPI FileIOCompletionRoutine (
	DWORD dwErrorCode,
    DWORD dwNumverOfBytedTransfered,
    LPOVERLAPPED lpOverlapeed
    );
    
    );
    

다음은 ReadFileEx 함수의 선언이다.

BOOL ReadFileEx(
HANDLE hFile // 데이터 수신 대상을 지정한다. 여기 인자로 전달되는 핸들은 FILE_FLAG_OVERLAPPED 속성을 지녀야 한다.
LPVOID lpBuffer // 데이터 수신을 위한 버퍼의 주소를 지정한다.
DWORD nNumberOfBytesTORead // 읽어 들일 데이터의 최대 크기를 바이트 단위로 지정한다. 보통 버퍼의 최대 길이를 바이트 단위로 지정한다.
LPOVERLAPPED lpOverlapped // OVERLAPPED 구조체 변수의 포인터를 전달한다.
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine // 입력 연산 완료시 자동으로 호출되는 완료루틴을 지정한다.
);

두 함수모두 완료된 출력 및 입력 데이터 크기를 얻기 위한 매개변수는 존재할 이유가 없다. 반환하는 시점에서 얻을 수 있는 정보가 아니니 말이다.

-알람 가능한 상태

I/O 연산이 완료되어서 완료루틴을 실행할 차례가 되었다. 그렇다면 바로 완료루틴이 실행되어야할 텐데, 고맙게도 윈도우즈는 완료루틴 실행 타이밍을 우리들이 결정할 수 있도록 하고 있다.

DWORD SleepEx(
DWORD dwMilliseconds // 쓰레드의 실행을 잠시 멈추는데 사용하는 Sleep 함수의 전달인자와 동ㅇㄹ한 역할을 한다 전달되는 값을 1/1000초 단위로 멈추게한다.
BOOL bAlertable //FALSE가 전달되면, Sleep 함수와 완히 동일한 형태가 된다. 그러나 TRUE가 전달되면 SleepEx함수에서 빠져 나오는 조건이 Sleep 함수에 비해 하 나더 추가된다 TRUE가 전돨되면 이 함수를 호출한쓰레드를 알림 가능한 상태로 변경한다.
);

알림가능한 상태에서는 호출되어야 할 완료루틴의 호출이 이뤄진다. 물론 완료루틴 실행 후에는 SleepEx 함수도 반환하게 된다.
호출되어야 할 완료루틴이 둘 이상인 경우에는 모두 다 호출된다.(중복호출할 필요 X)
만약에 완료된 I/O가 없어서 호출할 완료루틴도 없고, 첫 번째 전달인자를 통해서 전달된 시간도 지나지 않았다면, 위 함수는 계속해서 블로킹 상태에 놓이게 됨, 첫번째 인자로 지정한 시간이 다 되면 0을 반환하며 함수를 빠져 나오고, 완료 루틴이 실행되어 함수를 빠져나올 때는 WAIT_IO_COMPLETION을 반환함

알림 가능한 상태가 없다면 10개의 비동기 입출력연산이있을때 다섯번째가 실행되어야하나 1~4번째가 끝나 완료루틴이 실행되어 5번째가 실행되는데 시간이 걸림 하지만 알림 가능한 상태 개념을 도입해 해결. 쓰레드를 "알림 가능한 상태"로 만드는 함수 WaitForSingleObjectEx, WaitForMultipleObjectsEx

-OVERLAPPED 구조체의 파일 위치 정보

CreateFile 함수를 통해 생성한 핸들이 가리키는 파일의 커널 오브젝트 안에는 파일의 위치 정보를 담고 있는 멤버변수가 존재한다(파일 포인터) 이 파일 포인터는 동기화된 입.출력 함수 호출이 완료될 때마다 완료된 크기만큼 갱신이 이뤄진다. 때문에 순차적으로 데이터를 읽거나 쓸 수 있따. 그러나 이것은 어디까지나 동기화된 파일 I/O에만 해당하는 말이다.

"비동기 I/O에서 커널 오브젝트에 존재하는 파일의 위치 정보는 의미가 없다."

OVERLAPPED 구조체 멤버 중에서 일부는 데이터 입.출력 시작 위치를 지정하는 용도로 사용된다. 이 멤버들을 활용하는 것이 해결책이다. 물론 데이터 입출력 시작 위치를 개발자가 직접 계산해야 하는 불편함은 있다.

-타이머에서의 완료 루틴

BOOL SetWaitableTimer (
HANDLE hTimer
const LARGE_INTEGER pDueTime
LONG lPeriod
PTIMERAPCROUTINE pfnCompletionRoutine // 완료루틴을 지정한다. 타이머의 완료루틴은 다음과 같은 형태로 선언되어야 한다.
VOID CALLBACK TimerAPCProc (
LPVOID lpArgToCompletionRoutine,
DWORD dwTimerLowValue,
DWORD dwTimerHighValue,
);
전달인자 dwTimerLowValue,dwTimerHighValue 통해서 타이머가 Siganaled 상태가 된 시간 정보가 전달되며, 첫 번째 전달인자 lpArgToCompletionRoutine은 SetWaitableTimer 함수의 다섯 번째 전달인자가 그대로 전달된다.
lpArgToCompletionRoutine // 타이머 완료루틴의 첫 번째 전달인자로 그대로 전달된다.

-지금까지의 내용 정리 -

Windows는 기본적으로 두 가지 방식의 비동기 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가 완료될 때 지정된 함수가 호출된다는 특징을 지니고 중첩 I/O는 그러한 장치가 없기 때문에 중간에 I/O가 완료되었음을 확인하는 과정이 별도로 필요하다.

02 . APC

APC는 Asynchronout Procedure Call 의 약자로서 비동기 함수 호출 메커니즘을 의미한다. 앞서 소개했던 완료루틴도 내부적으로는 이 APC를 활용하여 구현되어 있다. 조금 더 정확히 말하자면 완료루틴 구성과정에서 호출하는 ReadFileEx,WriteFileEx 함수와 SetWaitableTimer 함수는 APC 메커니즘을 기반으로 구현된 함수들이다.

-APC의 구조

APC는 크게 두 가지 종류로 나뉜다. 하나는 "User-mode APC" 이고 다른 하나는 "Kernel-mode APC"이다. Kernel-mode APC는 다시 "Normal Kernel-mode APC"와 "Special Kernel-mode APC"로 나뉘는데, 커널모드 APC는 이책에서 언급하고자 하는 범위를 넘어서므로 유저모드 APC에 대해서만 언급한다.

모든 쓰레든느 자신만의 APC 큐라는것을 가지고있다. 그리고 APC 큐에는 다음과 같은 내용이 저장된다.

완료루틴을 구성하는 과정에서 WriteFileEx 함수가 호출되고 이때 인자로 전달된 함수 포인터와 매개변수 정보가 쓰레드의 APC 큐라는 곳에 저장되고있다(I/O완료시) APC 큐는 쓰레드별로 독립적이다. 다시 말하면 모든쓰레드는 자신만의 APC 큐를 지니고 있다.

이처럼 APC 큐에는 비동기적으로 호출되어야 할 함수들과 매개변수 정보가 저장된다. 그러나 저장되었다고 해서 함수가 바로 호출되는 것은 아니다. 쓰레드가 알림 가능 상태에 놓이게 될 때에 비로소 호출되는 것이다. APC 큐가 쓰레드 별로 독립적이라고 하였는데, 이를 조금 더 넓게 이해하면 완료루틴 이라는 것 자체가 쓰레드별로 독립적인 메커니즘이라는 결론도 내릴 수 있다.

  • APC 큐의 접근

APC 큐에 함수 정보를 전달할 수 있는 방법은 총 세 가지다. WriteFileEx,ReadFileEx 그리고 SetWaitableTimer 함수가 그것이다. 그러나 다음 함수를 이용한다면 직접적으로 APC 큐에 호출하고자 하는 함수 정보를 전달할 수도 있다.

DWORD QueueUserAPC (
PAPCFUNC pfnAPC // 비동기로 호출될 함수를 지정한다. 이 함수를 통해서 지정할 수 있는 함수의 타입은 다음과 같은 형태로 제한된다.

VOID CALLBACK APCProc(ULING_PTR dwParam);

HANDLE hThread // 비동기 함수 정보를 추가할 APC 큐를 지정한다 APC 큐를 소유하는 쓰레드 핸들을 지정한다.

ULONG_PTR dwData // APC 큐에 등록된 함수 호출 시 전달할 인자를 지정한다.

0개의 댓글