Computer Science 기본 지식/운영체제

[Windows] 완료 루틴 (Completion Routine)과 APC (Asynchronous Procedure Call)

로파이 2021. 10. 4. 18:11

앞 포스트에서 비동기 중첩 I/O 작업이 완료가 되면 등록한 Overlapped 구조체의 이벤트 핸들이 Signaled 상태가 되고 그 결과를 GetOverlappedResult를 통해 확인할 수 있었다.

 

완료 루틴이란 이벤트 핸들을 따로 생성할 필요없이 I/O 작업이 완료되면 자동으로 실행되는 루틴을 가리킨다. 이는 Overlapped 구조체를 등록하는 과정과 마찬가지로 비동기 입출력을 실행하는 함수에 등록하여 사용한다.

 

ex) WriteFileEx

BOOL WriteFileEx(
  HANDLE                          hFile,
  LPCVOID                         lpBuffer,
  DWORD                           nNumberOfBytesToWrite,
  LPOVERLAPPED                    lpOverlapped,
  LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

파일 출력 함수 WriteFile의 완료 루틴을 등록할 수 있는 확장 버전의 함수이다. 5번째 인자에 LPVOERLAPPED_COMPLETION_ROUTINE 타입의 인자를 전달할 수 있는데, 이 타입은 다음과 같이 정의가 되어있다.

 

- LPOVERLAPPED_COMPLETION_ROUTINE

typedef
VOID
(WINAPI *LPOVERLAPPED_COMPLETION_ROUTINE)(
    _In_    DWORD dwErrorCode,
    _In_    DWORD dwNumberOfBytesTransfered,
    _Inout_ LPOVERLAPPED lpOverlapped
    );
  • dwErrorCode : 에러가 발생했을 때 (0이아닌 값) 해당 인자를 통해 에러 코드를 확인할 수 있다.
  • dwNumerOfBytesTrasfered : 실제 전송된 바이트 크기를 알 수 있다.
  • lpOverlapped :  해당 함수는 I/O 작업이 완료된 이후의 시점이기 때문에 GetOverlappedResult를 생략하고 바로 결과를 확인할 수 있으며 주로 Overlapped 구조체에 완료된 이후에 사용할 사용자 데이터를 사용하게 된다.

- 예제

#include <cstdio>
#include <tchar.h>
#include <Windows.h>

TCHAR strData[] = _T("Shaun Paul Runyon killed his coworkers inside the rental house they shared in Davenport, in central Florida,")
_T("Runyon, 39, is charged with three counts of first-degree murder and one count of aggravated battery in connection with the incident. He is being held in Polk County Jail with no bond, according to jail records. \
					  CNN has been unable to determine whether Runyon has obtained legal counsel.")
	_T("Runyon, who is temporarily in Florida as a contract electrician for Pennsylvania-based company");

VOID WINAPI FileIOCompletionRoutine(DWORD, DWORD, LPOVERLAPPED);

int _tmain(int argc, TCHAR* argv[])
{
	TCHAR fileName[] = _T("data.txt");
	HANDLE hFile = CreateFile(fileName, GENERIC_WRITE, FILE_SHARE_WRITE, 0, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, 0);

	if (hFile == INVALID_HANDLE_VALUE)
	{
		_tprintf(_T("File Creation Fault!\n"));

		return -1;
	}

	OVERLAPPED overlappedInst;
	memset(&overlappedInst, 0, sizeof(overlappedInst));
	overlappedInst.hEvent = (HANDLE)1234;
	WriteFileEx(hFile, strData, sizeof(strData), &overlappedInst, FileIOCompletionRoutine);

	SleepEx(INFINITE, TRUE);
	CloseHandle(hFile);
	return 1;
}

VOID WINAPI FileIOCompletionRoutine(DWORD errorCode, DWORD numOfBytesTransfered, LPOVERLAPPED overlapped)
{
	_tprintf(_T("********** File Write Result **************\n"));
	_tprintf(_T("Error Code: %d\n"), errorCode);
	_tprintf(_T("Transfered Bytes Len: %d\n"), numOfBytesTransfered);
	_tprintf(_T("The other info : %d\n"), (DWORD)overlapped->hEvent);
}

출력 결과

위 예제를 실행하면 strData 문자열이 파일에 잘 출력된 것을 확인할 수 있다.

 

SleepEx를 사용한 이유

DWORD SleepEx(
  DWORD dwMilliseconds,
  BOOL  bAlertable
);

해당 라인 없이 실행하면 프로그램은 I/O 완료 루틴이 실행되기도 전에 종료할 수 도 있다. 이를 위해 Sleep 함수로 잠시 현재 쓰레드를 Suspend시키는데 이는 남은 타임 슬라이스를 반환하고 dwMilliseconds 시간동안 실행할 수 없는 상태에 놓이게 된다. 이후에는 준비 상태가 되지만 언제 스레드가 실행될지는 알 수 없다. 두번째 인자 bAleratble은 Suspend된 상태에서 알림받으면 함수를 반환하여 계속 실행할 수 있게끔 할 수 있다. dwMilliseconds가 경과해도 함수는 반환하게 된다.

 

알림이 가능한 SleepEx가 함수를 반환하는 시기

1) I/O 완료 루틴 함수가 호출되었을 때

2) 비동기 Procedure Call (APC)가 쓰레드에 큐 되었을 때

3) 시간 만료로 인한 반환

 

시간 만료로 인해 함수를 반환한다면 0을 반환하고 I/O 완료 루틴으로 반환을 한다면 WAIT_IO_COMPLETION을 반환한다.

Aleratble 상태를 사용하는 함수로 WaitForSingleObjectEx, WaitForMultipleObjectsEx가 있다.

 

- Overlapped 구조체의 오프셋 변수의 활용

다음과 같이 문장 3개를 각각의 3번의 호출로 중첩 I/O를 통해 파일을 쓰도록 하자.

	OVERLAPPED overlappedInstOne;
	memset(&overlappedInstOne, 0, sizeof(overlappedInstOne));
	overlappedInstOne.hEvent = (HANDLE)_T("First I/O");
	WriteFileEx(hFile, strData1, sizeof(strData1), &overlappedInstOne, FileIOCompletionRoutine);

	OVERLAPPED overlappedInstTwo;
	memset(&overlappedInstTwo, 0, sizeof(overlappedInstTwo));
	overlappedInstTwo.hEvent = (HANDLE)_T("Second I/O");
	WriteFileEx(hFile, strData2, sizeof(strData2), &overlappedInstTwo, FileIOCompletionRoutine);

	OVERLAPPED overlappedInstThree;
	memset(&overlappedInstThree, 0, sizeof(overlappedInstThree));
	overlappedInstThree.hEvent = (HANDLE)_T("Third I/O");
	WriteFileEx(hFile, strData3, sizeof(strData3), &overlappedInstThree, FileIOCompletionRoutine);

위의 결과로 출력된 파일을 확인하면 제대로 쓰여지 않은 것을 볼 수 있는데, 이는 WriteFileEx의 hFile 파일의 포인터가 각각의 쓰기가 완료되고 다음 문장의 시작점을 가리키고 있지 않아서 발생한다. 3번의 중첩 I/O로 3문장이 동시에 파일에 쓰이고 있는 상황인 것이다.

이런 상황에 쓰이는 Overlapped 구조체의 Offset 변수는 I/O를 진행할 시작 위치를 지정할 수 있다. 따라서 각각의 비동기 I/O 함수를 호출할 때 Overlapped 구조체 Offset을 알맞게 계산하여 지정할 수 있도록 한다.

	UINT iFileOffset = 0;
	OVERLAPPED overlappedInstOne;
	memset(&overlappedInstOne, 0, sizeof(overlappedInstOne));
	overlappedInstOne.hEvent = (HANDLE)_T("First I/O");
	WriteFileEx(hFile, strData1, sizeof(strData1), &overlappedInstOne, FileIOCompletionRoutine);
	iFileOffset += lstrlen(strData1) * sizeof(TCHAR); // 오프셋 정보 업데이트

	OVERLAPPED overlappedInstTwo;
	memset(&overlappedInstTwo, 0, sizeof(overlappedInstTwo));
	overlappedInstTwo.hEvent = (HANDLE)_T("Second I/O");
	overlappedInstTwo.Offset = iFileOffset;
	WriteFileEx(hFile, strData2, sizeof(strData2), &overlappedInstTwo, FileIOCompletionRoutine);
	iFileOffset += lstrlen(strData2) * sizeof(TCHAR);

	OVERLAPPED overlappedInstThree;
	memset(&overlappedInstThree, 0, sizeof(overlappedInstThree));
	overlappedInstThree.hEvent = (HANDLE)_T("Third I/O");
	overlappedInstThree.Offset = iFileOffset;
	WriteFileEx(hFile, strData3, sizeof(strData3), &overlappedInstThree, FileIOCompletionRoutine);
	iFileOffset += lstrlen(strData3) * sizeof(TCHAR);

 

타이머에서 완료루틴

타이머 객체에도 완료 루틴을 설정할 수 있다.

SetWaitableTimer : https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-setwaitabletimer

BOOL SetWaitableTimer(
  HANDLE              hTimer,
  const LARGE_INTEGER *lpDueTime,
  LONG                lPeriod,
  PTIMERAPCROUTINE    pfnCompletionRoutine,
  LPVOID              lpArgToCompletionRoutine,
  BOOL                fResume
);
  • pfnCompletionRoutine : 완료 루틴을 지정한다. 
  • lpArgToCompletionRoutine : 완료 루틴에 사용될 인자

 

PTIMERAPCROUTINE

https://docs.microsoft.com/en-us/windows/win32/api/synchapi/nc-synchapi-ptimerapcroutine

typedef
VOID
(APIENTRY *PTIMERAPCROUTINE)(
    _In_opt_ LPVOID lpArgToCompletionRoutine,
    _In_     DWORD dwTimerLowValue,
    _In_     DWORD dwTimerHighValue
    );
  • lpArgToCompletionRoutine : SetWaitableTimer에서 전달한 사용자 데이터이다.
  • dwTimerLowValue, dwTimerHighValue : Singaled 상태가 되어 완료된 시간을 알 수 있다.

 

APC (Asynchronouse Procedure Call)

비동기 함수 호출 메커니즘을 의미하며 APC 메커니즘을 통해 I/O의 완료로 지정된 완료 루틴이 실행될 수 있게 된다.

APC는 유저 모드 APC와 커널 모드 APC가 있다.

모든 쓰레드는 자신만의 APC 큐가 있으며 APC 큐에는 등록된 완료 루틴에 관련된 함수 포인터와 인자들이 저장된다.

APC 큐에 완료 루틴이 등록된다고 해서 바로 호출되는 것은 아니고 현재 쓰레드가 알림 가능 상태(Alertable)에 놓일 때 호출된다. 이는 SleepEx와 같은 함수로 알림 가능 상태를 설정할 수 있다.

APC 메커니즘

QueueUserAPC 함수를 통해 사용자 모드의 APC에 호출할 함수를 직접 등록할 수 있다.

DWORD QueueUserAPC(
  PAPCFUNC  pfnAPC,
  HANDLE    hThread,
  ULONG_PTR dwData
);
  • pfnAPC : 쓰레드가 Alertable Wait이 될 때 호출할 함수를 지정한다.
  • VOID CALLBACK APCPROC(ULONG_TPR dwParam) 타입의 PAPCFUNC 함수 포인터이다.
  • hThread : 등록할 APC 큐에 해당하는 쓰레드의 핸들을 지정한다.
  • dwData : 사용자 데이터를 지정한다.

- 예제

#define _WIN32_WINNT 0x4000
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>

VOID CALLBACK APCProc(ULONG_PTR);

int _tmain(int arc, TCHAR* argv[])
{
	HANDLE hThread = GetCurrentThread();

	QueueUserAPC(APCProc, hThread, (ULONG_PTR)1);
	QueueUserAPC(APCProc, hThread, (ULONG_PTR)2);
	QueueUserAPC(APCProc, hThread, (ULONG_PTR)3);
	QueueUserAPC(APCProc, hThread, (ULONG_PTR)4);
	QueueUserAPC(APCProc, hThread, (ULONG_PTR)5);

	Sleep(500);
	SleepEx(INFINITE, TRUE); // Set This Thread Alertable
	return 0;
}

VOID CALLBACK APCProc(ULONG_PTR dwParam)
{
	_tprintf(_T("Asynchronous Procedure Call Num %d\n"), (DWORD)dwParam);
}

 

참고 : 뇌를 자극하는 윈도우즈 시스템 프로그래밍 (윤성우 저)