#include "stdafx.h"
#include <Windows.h>
/////////////////////////////////////////////////////////////////////////
//파일 쓰기가 완료되면 역호출되는 함수.
void CALLBACK FileIoComplete(
DWORD dwError, //에러코드
DWORD dwTransfered, //입/출력이 완료된 데이터 크기
LPOVERLAPPED pOl) //OVERLAPPED 구조체
{
printf("FileIoComplete() Callback - [%d 바이트] 쓰기완료 - %s\n",
dwTransfered, (char*)pOl->hEvent);
//hEvent 멤버를 포인터로 전용했으므로 가리키는 대상 메모리 해제한다.
//이 메모리는 IoThreadFunction() 함수에서 동적 할당한 것들이다!
delete[] pOl->hEvent;
delete pOl;
puts("FileIoComplete() - return\n");
}
/////////////////////////////////////////////////////////////////////////
DWORD WINAPI IoThreadFunction(LPVOID pParam)
{
//메모리를 동적 할당하고 값을 채운다.
//※이 메모리는 완료 함수에서 해제한다.
char *pszBuffer = new char[16];
memset(pszBuffer, 0, sizeof(char) * 16);
strcpy_s(pszBuffer, sizeof(char) * 16, "Hello IOCP");
//※OVERLAPPED 구조체의 hEvent 멤버를 포인터 변수로 전용한다!
LPOVERLAPPED pOverlapped = NULL;
pOverlapped = new OVERLAPPED;
memset(pOverlapped, NULL, sizeof(OVERLAPPED));
pOverlapped->Offset = 1024 * 1024 * 512; //512MB
pOverlapped->hEvent = pszBuffer;
//비동기 쓰기를 시도한다. 쓰기가 완료되면 완료 함수가 역호출된다.
puts("IoThreadFunction() - 중첩된 쓰기 시도");
::WriteFileEx((HANDLE)pParam,
pszBuffer,
sizeof(char) * 16,
pOverlapped,
FileIoComplete);
//비동기 쓰기 시도에 대해 Alertable wait 상태로 대기한다.
for ( ; ::SleepEx(1, TRUE) != WAIT_IO_COMPLETION; );
puts("IoThreadFunction() - return");
return 0;
}
/////////////////////////////////////////////////////////////////////////
int _tmain(int argc, _TCHAR* argv[])
{
//중첩된 쓰기 속성을 부여하고 파일을 생성한다.
HANDLE hFile = ::CreateFile(
_T("TestFile.txt"),
GENERIC_WRITE, //쓰기 모드
0, //공유하지 않음.
NULL,
CREATE_ALWAYS, //무조건 생성.
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, //중첩된 쓰기
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
puts("ERROR: 대상 파일을 열 수 없습니다.");
return 0;
}
//비동기 쓰기를 위한 스레드를 생성한다.
HANDLE hThread = NULL;
DWORD dwThreadID = 0;
dwThreadID = 0;
hThread = ::CreateThread(
NULL, //보안속성 상속.
0, //스택 메모리는 기본크기(1MB).
IoThreadFunction, //스래드로 실행할 함수이름.
hFile, //함수에 전달할 매개변수.
0, //생성 플래그는 기본값 사용.
&dwThreadID); //생성된 스레드ID 저장.
//작업자 스레드가 종료될 때까지 기다린다.
::WaitForSingleObject(hThread, INFINITE);
//파일을 닫고 종료한다.
puts("main() thread 종료");
::CloseHandle(hFile);
return 0;
}
Windows에서 비동기 I/O(Overlapped I/O) 요청은 크게 다음 단계를 거칩니다:
- 사용자 모드(유저모드)에서 WriteFileEx 같은 함수를 호출하여 비동기 작업을 요청.
- 운영체제(커널)가 이 요청을 내부 I/O 큐(혹은 APC 큐)에 등록.
- 디바이스 드라이버(디스크, 네트워크 어댑터 등)가 실제 하드웨어 작업을 수행.
- 작업이 완료되면 OS가 지정된 콜백(Completion Routine)을 호출하거나, 이벤트를 SET하는 등으로 결과를 알림.
- 사용자 모드는 콜백 내에서 메모리를 정리하거나 후속 로직을 실행.
이 중 "큐를 통해 어떻게 I/O가 스케줄링되고, 콜백이 어떻게 호출되며, 메모리를 어디서 어떻게 해제하는지"가 핵심입니다.
1. 운영체제(커널) 내부의 I/O 큐
1.1 Overlapped I/O 요청 구조
- WriteFileEx( hFile, pBuffer, size, pOverlapped, FileIoComplete ) 호출 시:
- 유저모드에서 OVERLAPPED 구조체와 버퍼(pBuffer)를 준비.
- 커널은 이 요청을 I/O 요청 큐(디바이스 드라이버나 커널 내)로 등록.
- 함수는 즉시 반환(비동기). 실제 작업(디스크에 쓰기)은 OS가 백그라운드에서 스케줄링.
1.2 APC 큐(Asynchronous Procedure Call)
- 콜백 방식을 사용하는 WriteFileEx는, I/O 완료 시점에 해당 스레드의 APC 큐에 "이 콜백을 호출하라"는 항목을 등록합니다.
- 스레드가 Alertable Wait 상태(SleepEx, WaitForSingleObjectEx(..., TRUE) 등)에 들어가 있으면, OS가 APC 큐의 항목을 찾아 콜백을 호출.
1.3 순서 요약
- 사용자 함수(WriteFileEx) → 커널 I/O 큐
- 커널: 디바이스 드라이버와 하드웨어 협업으로 실제 파일 쓰기 진행
- 작업 완료
- 커널: "모든 바이트를 디스크에 기록 완료!"
- 스레드의 APC 큐에 "이 콜백 호출" 항목 추가
- 스레드 Alertable Wait → 콜백 호출(APC 실행)
2. 메모리 할당과 해제
2.1 메모리 할당(스레드에서 수행)
예제에서 스레드 함수가 new로 다음을 동적 할당:
- 문자열 버퍼(예: char* pszBuffer = new char[16])
- OVERLAPPED 구조체(예: LPOVERLAPPED pOverlapped = new OVERLAPPED;)
왜 스레드에서 할당?
- Overlapped I/O를 요청하는 순간, OS는 이 구조체와 버퍼 주소를 그대로 사용할 것이므로, 요청이 끝나기 전까지 메모리가 유효해야 함.
- 스택 변수를 사용하면 함수 탈출 시 소멸해버려서 문제 발생.
2.2 콜백에서 해제(함수 FileIoComplete)
void CALLBACK FileIoComplete(
DWORD dwError,
DWORD dwTransferred,
LPOVERLAPPED pOl
) {
// pOl->hEvent를 '포인터'로 썼으므로 문자열 버퍼 주소가 들어 있음
delete[] pOl->hEvent; // 문자열 해제
delete pOl; // Overlapped 구조체 해제
}
- Overlapped I/O가 완료되면, OS가 이 콜백을 호출하고, pOverlapped 포인터를 인자로 넘겨줌.
- 콜백 내에서 pOl->hEvent(원래 이벤트 필드지만 예제에선 버퍼 포인터로 전용)와 pOl 자체를 해제(delete) 처리.
- 장점: 메모리를 딱 I/O가 끝난 시점에 안전하게 해제 가능.
- 주의: 콜백이 호출되기 전, 스레드가 메모리를 해제하면 참조 오류 발생.
3. Alertable Wait와 SleepEx
3.1 Alertable Wait의 의미
- SleepEx(ms, TRUE):
- 스레드를 ms 밀리초 동안 Alertable 상태로 재움.
- TRUE = Alertable: APC(Asynchronous Procedure Call) 발생 시 즉시 깨어나 콜백 실행.
- WaitForSingleObjectEx(hEvent, ms, TRUE) 등도 같은 기법.
- 결론: "콜백 기반의 비동기 I/O"는 해당 스레드가 Alertable Wait 상태일 때만 콜백이 동작.
3.2 스레드 진행 흐름
- 스레드가 WriteFileEx(비동기 요청) 후 SleepEx(1, TRUE) 같은 작은 단위로 반복(wait).
- I/O가 완료되면 OS가 "이 스레드에 APC 콜백을 실행하라" 통보.
- 스레드는 WAIT_IO_COMPLETION이 반환될 때까지 반복.
- 콜백 실행 → 메모리 해제 → 콜백 리턴 → 스레드가 다시 SleepEx → 최종적으로 WAIT_IO_COMPLETION 받아 탈출.
4. 내부 큐를 이용한 스케줄링 과정
- I/O Request Queue(커널 레벨)
- WriteFileEx가 호출되면, I/O 요청을 커널(파일시스템 드라이버/디스크 드라이버) 큐에 등록.
- Driver/Kernel은 각 요청(Overlapped)마다 실제 디스크 연산을 수행.
- 요청 완료 시점
- Driver/Kernel: "이제 해당 Overlapped I/O 완료"
- OS는 "APC Queue"에 "이 Overlapped에 대한 콜백" 등록
- APC Queue
- Thread별로 존재.
- 스레드가 alertable wait 상태일 때, APC Queue의 콜백 함수가 실행.
- 메모리/자원 반환
- 콜백 함수가 Overlapped 구조체와 버퍼를 해제.
5. Q&A: "왜 이 구조가 필요한가?"
- 단일 스레드 vs 다중 I/O
- 하나의 스레드가 여러 개의 비동기 I/O를 동시에 요청 가능.
- I/O 완료를 콜백으로 받고, 그 시점에만 처리 → CPU와 I/O 병렬 활용.
- 큐와 이벤트
- "이벤트" 기반 모델처럼 WaitForMultipleObjects로 처리하는 방식도 있음(Overlapped + 이벤트).
- 콜백(APC) 기반은 Alertable Wait 상태를 유지해야 하며, 완료 시 함수가 호출됨.
- 매칭:
- Overlapped 구조체가 요청을 구분(Offset, pOl->hEvent 등).
- 콜백 함수에서 어떤 요청이 끝났는지 식별 가능.
6. 결론 및 요약
- 콜백 기반 비동기 파일 I/O에서, OS 내부 큐(I/O Request Queue + APC Queue)가 핵심 역할.
- WriteFileEx: Overlapped 구조체를 OS에 전달 → OS가 I/O 완료 시 APC 큐에 콜백 호출 등록.
- Alertable Wait: 스레드가 SleepEx(…, TRUE) 상태여야 콜백이 실제로 실행됨.
- 메모리 정리: Overlapped 구조체와 버퍼를 콜백 내에서 안전하게 해제.
요컨대, "비동기로 I/O를 요청하고, OS가 I/O를 끝낸 순간에 콜백 함수를 호출해준다"라는 흐름이 콜백 기반 Overlapped I/O의 모든 것입니다. 큐 안에 쌓이는 I/O 요청들은 OS가 처리 완료 시점을 결정하고, 그 결과를 스레드의 APC로 알려줍니다. 이런 구조 덕분에, 애플리케이션은 I/O 대기 없이 고성능, 고동시성 작업을 수행할 수 있습니다.
'프로그래밍 > 소켓 프로그래밍 입문' 카테고리의 다른 글
APC(Asynchronous Procedure Call) 알아보기 (0) | 2025.01.25 |
---|---|
Windows 비동기 I/O의 두가지 방식, 이벤트와 콜백 (0) | 2025.01.25 |
콜백 기반 비동기 파일 I/O: 간략하게 살펴보기 (0) | 2025.01.24 |
Windows Overlapped I/O로 비동기 파일 쓰기: 이벤트 방식 (0) | 2025.01.24 |
윈도우 운영체제에서의 파일 I/O 구조: 요청과 처리 (0) | 2025.01.24 |