Asynchronous Procedure Call 은 개별 쓰레드에서 실행되는 비동기 함수를 말한다. APC 는 시스템에 의하여 실행되는 Kernel-mode APC 와 어플리케이션에서 프로그래밍 가능한 User-mode APC 가 있다. 이러한 APC 는 주로 비동기 입출력(Asynchronous I/O) 이 완료될 때까지 대기하던 중에 쓰레드를 실행 상태로 되돌려야 할 경우 유용하게 사용될 수 있다. 여기에서는 프로그래밍이 가능한 User-mode APC 를 사용하는 방법에 대하여 설명한다.
참고링크 [Bottom] [Top]
Asynchronous Procedure Calls http://msdn2.microsoft.com/en-us/library/ms681951(VS.85).aspx
QueueUserAPC Function http://msdn2.microsoft.com/en-us/library/ms684954(VS.85).aspx
APCProc Callback Function http://msdn2.microsoft.com/en-us/library/ms681947(VS.85).aspx
HOWTO: 비동기 프로시저 호출을 이용하여 Waitable 타이머 사용 하기 http://support.microsoft.com/default.aspx?scid=kb;ko;kr601487
APC 가 등장한 이유? [Bottom] [Top]
MS 윈도우즈는 우수한 32 레벨의 우선순위 체계 (Scheme) 를 사용한다. 그 중 하위 우선순위 16 은 전형적인 시분할 멀티태스킹 시스템에 의해 사용되는 동적 우선순위이다. 일반 클래스의 우선순위를 지닌 모든 태스크가 사용가능한 CPU 타임 슬라이스를 나눠 사용한다. 그러나 그 일반 클래스 내에서 보다 높은 우선순위를 지닌 태스크가 보다 많은 타임 슬라이스를 얻게 될 것이다. 또한 상위 16 의 우선순위들은 리얼타임 시스템에 의해 사용되는 리얼타임 클래스이다. 이 리얼타임 클래스에서는 오직 가장 높은 우선순위를 지닌 태스크가 주어진 시간에 처리된다. 일반 클래스의 태스크들은 리얼타임 클래스의 태스크들이 실행 중 또는 실행대기 중이 아닐때만 처리 될 수가 있다. 리얼타임 우선순위의 사용을 통해 까다로운 리얼타임의 요구조건도 충족시키는 동시에 데스크탑 컴퓨터를 위한 시분할 시스템도 사용이 가능하기 때문에, 이 체계는 단순하면서도 강력하다고 할 수 있겠다 (Unix 의 우선순위 레벨은 256 까지 가능하다. 하지만, 매우 복잡한 리얼타임 프로젝트 따위를 제외하면 32 레벨의 우선순위로도 충분하다).
이 리얼타임 우선순위에 함정이 도사리고 있다. 하나의 리얼타임 태스크가 지나치게 CPU 타임 슬라이스를 많이 소비하면 시스템이 멈춰버리는 사태가 발생하게 되는 것이다. 서로에게 종속적인 다수의 리얼타임 태스크가 얽히고 설키면 충분히 발생할 수 있는 경우이다. 이런 상황을 가리켜 "우선순위 전도 (Priority Inversion)" 이라 일컫는다. 다시말해, 높은 우선순위를 가진 태스크가 낮은 우선순위가 끝나기를 기다리는 상황 - 고로, 우선순위가 꼬여버린 경우이다. 이것은 프로그래밍을 하는데 있어서 고려해야 할 중요한 요소이지만, 대부분의 프로그래머가 이 리얼타임 우선순위를 할당하는데 있어서 필요한 지식을 갖고 있지 못하다. 왜냐하면, 특별한 상황이 아니고는 프로그래머들이 이러한 리얼타임 우선순위를 사용할 필요가 없기 때문이다. 그 특별한 상황이란? 바로, 디지털 오디오 관련 프로그래밍이다. 이 디지털 오디오 관련 프로그래밍에 있어서 리얼타임 클래스가 있다면 그게 반드시 필요하든 필요하지 않든 태스크에게 할당된 뒤 사용되어 진다.
이러한 Windows 9x 의 단점으로 인한 피해를 최소화 하기 위해, MS 는 자신들이 설계한 우선순위 체계를 완전이 뒤집어 엎는 메카니즘을 사용하기에 이른다. 가장 낮은 일반 클래스의 우선순위를 지닌 태스크가 무작위로 선택되어 다른 모든 우선순위를 무시하고 실행되도록 만든 것이다. 태스크가 선택되는 시간 간격도 무작위로 결정되어 진다. 최악의 경우에는 가장 높은 리얼타임 우선순위를 지닌 태스크도 수백 밀리초의 Latency 가 발생한다. 이런 현상은 리얼타임 시스템에서는 납득할 수 없는 성질의 것이다. 불행스럽게도 - 이러한 일련의 오류를 수정하는 것 대신에 - MS 는 납득할만한 시간의 요구조건을 충족시키키 위해, 새로운 종류의 클래스인 APC (Asynchronous Procedure Calls: 비동기 프로시저 호출) 과 DPC (Deferred Procedure Calls: 지연된 프로시저 호출) 을 사용하기 시작했다. 이것들은 시스템 인터럽트를 제외하면 최고의 우선순위를 지니고 있을 뿐아니라, 일단 태스크의 수행이 시작되면 이 태스크가 끝날 때 까지는 그 어떤 태스크나 APC도 실행이 될 수 없다 (이것을 선점 (preempt) 될 수 없다고 이야기한다). 그러므로 이것은 전술 (前述) 한 "완벽한 우선순위 체계" 를 위반하는 또 하나의 예가 된다. 만약 우선순위 전도 현상의 회피를 위한 메카니즘이 정확히 작동한다면, APC 따위는 애당초 필요하지도 않았을 것이다.
이런 이해하기 힘든 이야기를 이토록 늘어놓은 이유가 무엇일까? 대부분의 주변 장치를 위한 장치 드라이버들이 이 APC 를 사용하여 만들어졌다고 한다. 또한 멀티미디어 프로그램과 같이 리얼타임 클래스가 필요한 프로그래밍에서도 사용되고 있다. 그리고 이런 프로그램의 대부분의 프로그래머들이 우선순위보다는 APC 를 이용한 프로그래밍을 선호한다고 알려져 있기 때문이다. MS 측에서도 이에 대해 어떤 심각한 조치나 권고를 행한 적이 없었다. 그저 설치하고 돌아가면 장땡인 것이다. 보다시피, Windows 2000/XP 도 Windows 9x 만큼은 아니더라도 이러한 심각한 핸디캡을 가진 상태이다.
참고 설명 [Bottom] [Top]
UNIX 의 signal 은 signal 이 특정 프로세스에 전달되면 프로세스가 어떤 상태에 있거나 상관없이, signal handler callback 함수를 실행합니다. 이때 프로세스가 system call 을 수행하기 직전이면 signal handler 를 먼저 실행시키고, 이후에 system call 은 EINTR errno 와 함께 -1 을 리턴합니다. 이와 비슷한 개념이 윈도우즈의 APC ( Asynchronous Procedure Calls ) 이며, 윈도우의 Asynchronous I/O 에서 callback 함수를 사용하는 경우에도 APC 가 사용됩니다.
윈도우에서의 모든 스레드는 APC 를 위한 queue 가 있습니다. APC는 두가지 종류가 있는데, 이는 kernel-mode APC와 user-mode APC 입니다. user-mode APC 는 해당 스레드 큐에 APC 가 도착되면, 이 스레드가 'alertable state' 가 될 때에만 해당 callback 함수를 실행한다고 하며, Asynchronous I/O 에서의 callback 함수 호출은 user-mode APC 를 사용한다고 합니다.
kernel-mode APC 는 해당 스레드 큐에 APC 가 도착하면, 해당 스레드가 다음 스케쥴링이 될 때 해당 callback 함수를 실행한 후에, 원래 진행하던 코드 부분으로 돌아간다고 합니다. 이 방식은 Device driver 와의 통신시에 많이 사용한다.
APC 프로그래밍 [Bottom] [Top]
모든 쓰레드에는 APC 를 처리하기 위한 Queue 가 존재하며 이 Queue 에 APC 가 들어가면, User-mode APC (이하 APC 로 표기) 인 경우 해당 쓰레드가 'Alertable wait state (이하 경고성 대기 상태 로 표기)' 일때 실행된다. 즉, APC 는 무조건적으로 실행되는 것은 아니다. 다음과 같은 함수들로 쓰레드를 경고성 대기 상태로 만들어야 한다.
SignalObjectAndWait()
WaitForSingleObjectEx()
WaitForMultipleObjectsEx()
MsgWaitForMultipleObjectsEx()
- WSAWaitForMultipleEvents()
SleepEx()
이들 함수는 마지막 인자 (Parameter) 로 BOOL 형의 bAlertable 변수를 갖고 있다. 이 인자를 TRUE 로 전달하면 호출한 쓰레드는 경고성 대기 상태로 진입하게 된다. 하지만 FALSE 로 전달하게 되면 같은 계열의 동일한 함수로 동작한다. 예를들어 SleepEx( 1000, FALSE ) 와 Sleep( 1000 ) 는 동일하게 동작한다. 그리고 APC 도 실행되지 않는다.
APC 함수를 쓰레드의 Queue 로 넣는 것은 QueueUserAPC() 함수를 사용한다. 함수 원형은 다음과 같다.
DWORD QueueUserAPC(
PAPCFUNC pfnAPC,
HANDLE hThread,
ULONG_PTR dwData );
QueueUserAPC() 함수의 첫번째 인자 pfnAPC 는 실행될 APC 함수의 포인터 를 전달한다. 두번째 인자는 실행될 쓰레드 핸들 이고, 세번째 인자는 APC 함수로 전달될 인자의 포인터 를 전달한다. 반환값은 에러가 발생한 경우 0( Zero ) 을 반환하며 이때 GetLastError() 함수로 에러 코드를 확인한다.
그리고 첫번째 인자로 전달될 APC 함수의 포인터는 다음과 같이 APCProc 함수의 포인터를 말한다.
VOID CALLBACK APCProc(
ULONG_PTR dwParam );
대략적으로 이런 과정으로 프로그래밍을 할 수 있지만 APC 를 프로그래밍 할 때 주의해야 할 점들이 많다. 먼저, 비동기 입출력 함수를 호출한 쓰레드에서 경고성 대기 상태로 진입해야 한다. 그렇지 않으면 비동기 입출력이 완료된 후 콜백 (Callback) 함수가 실행되지 않는다. 그리고 쓰레드가 종료되면 안된다. 콜백 함수도 실행되지 않지만, APC 함수 역시 실행되지 않는다. 이유는 각 쓰레드별로 가지고 있는 Queue 가 쓰레드 종료와 함께 제거 되기 때문에 APC 를 사용할 수 없게 된다.
참고로, QueueUserAPC() 함수를 사용한 코드를 컴파일 하기 위해서는 _WIN32_WINNT 매크로(Windows NT 이상) 를 0x0400 또는 그 이상 으로 설정해야 한다 (또는 _WIN32_WINDOWS 매크로(Windows 98/ME) 를 0x0400 또는 그 이상 으로 설정). 더 자세한 사항은 Using the Windows Headers 를 참고한다.
APC 예제 [Bottom] [Top]
준비중 입니다.
