프로그래밍/Win32 Deep Inside

WriteFile/ReadFile의 overlapped i/o 사용하기 (asynchronous, non-block, iocp)

초록생선 2013. 3. 21. 13:07
ReadFile이나 WriteFile과 같은 i/o 함수들은 일반적으로 i/o가 완료될 때 까지 blocked 상태로 유지됩니다. 즉, 예를 들어 100MB를 한꺼번에 읽는다고 한다면, 읽기가 완료될 때 가지 해당 함수는 blocked 즉, wait상태로 됩니다.
만일, read -> cpu 사용 코드 -> write -> cpu 사용 코드 -> read -> cpu 사용 코드 -> write 와 같이 반복되는 작업인 경우, 각 과정이 1초가 사용된다면, 위 과정은 일반적인 i/o 함수 사용시 총 7초가 사용됩니다. 허나, non-blocked, asynchronous, iocp, 혹은 overlapped i/o를 사용한다면, i/o 함수는 즉각적으로 리턴이 되어, 이후의 코드를 진행할 수 있습니다. 그렇다면, 위 과정은 운이 좋다면 총 3초(cpu 사용코드만 지체됨), 일반적으로는 4초(i/o와 cpu 사용코드가 동시 실행하여 1초가 걸린다고 가정)로 가정할 수 있습니다.

말로 이해하기 어렵다면, 아래 msdn에서 알려준 그림을 참고하세요. (출처 : msdn)



즉, cpu 사용(User mode)과 i/o를 동시에 진행할 수 있다는 장점이 있습니다.

이러한 overlapped i/o를 사용할 때 주의할 점은,
  • GetOverlappedResult를 통해 Overlapped i/o의 수행 결과를 체크해야 한다.
  • File Pointer를 수동으로 설정해야 한다.
    ; 일반적인 block i/o는 i/o 함수 호출후 File Pointer가 자동으로 뒤로 넘어가는데, overlapped i/o는 수동으로 함수 호출전에 설정해야 함
  • OVERLAPPED 구조체의 event를 사용자가 생성한다면, auto-reset으로 하기를 추천한다.
  • ReadFile / WriteFile 사용시, 쓰기 버퍼의 크기와 File Pointer 위치는 sector 크기의 배수여야 한다. 일반적으로 512byte이며, 4096byte도 있다.
  • FILE_FLAG_NO_BUFFERING를 가지고 CreateFile된 경우, ReadFile / WriteFile 사용시, File Pointer 위치와 쓰려는 버퍼 크기는 disk의 sector 단위, 즉, 일반적으로 512byte(혹은 4096byte) 정수배가 되어야 한다. 그렇지 않는 경우, 87(=ERROR_INVALID_PARAMETER)오류가 발생한다.
  • EOF 혹은 그 이후의 위치를 ReadFile하면 I/O Pending은 발생하나, Overlapped Result가 ERROR_HANDLE_EOF가 전달된다.

와 같습니다.

Overlapped i/o 사용시의 주요한 tip은 다음과 같습니다.

1. File pointer 계산시에는 LARGE_INTEGER를 활용하면 편리합니다.

2. I/O completion, 즉 I/O 완료때 까지 기다릴 때, MsgWaitForMultipleObjects를 호출하면, UI가 깨지지 않게 할 수 있습니다. 다시말해, 만일, MFC의 OnButton등에서 100MB의 파일을 읽는다고 하면, I/O 함수에 의해 Primary thread가 wait되기 때문에 UI Message Pumping이 발생하지 않아 화면이 깨지게 됩니다. 그래서 보통 I/O Thread를 만들어서 해당 함수를 수행하게 되는데, 그러다 보면 Thread 관리라는 어렵고 귀찮은 문제가 발생합니다. 이와 대조적으로, overlapped i/o로 Read한뒤, MsgWaitForMultipleObjects를 호출하여 완료때 까지 기다리면, Message Pumping이 수행되어 UI가 깨지지 않게 됩니다.

3.
OVERLAPPEDhEventRegisterWaitForSingleObject와 연결하면, I/O가 완료될때 OS가 관리하는 thread pool에서 사용자가 등록한 callback 함수를 호출 받을 수 있습니다. 이는 File 관련 I/O외에도 Network 관련 I/O에서 잘 활용하면 매우 편리하게 사용할 수 있습니다. 보통 경험상 WT_EXECUTELONGFUNCTION 옵션이 가장 적합했습니다.

4. Overlapped i/o를 사용한다면, CreateFile에서 FILE_FLAG_NO_BUFFERING을 조합하면 최적화된 성능을 가질 수 있다고 합니다. 다만, 해당 경우에는, 앞서 설명한 sector 크기의 배수 조건을 만족해야 합니다.

아래는 Overlapped i/o로 이뤄지는 ReadFile, WriteFile 호출 예제 입니다.

// overlapped_write.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>
#include <assert.h> int _tmain(int argc, _TCHAR* argv[]) { HANDLE h = INVALID_HANDLE_VALUE; char buf[1024] = {0,}; char buf2[1024] = {0,}; char buf3[1024] = {0,}; int i = 0; LARGE_INTEGER nLarge = {0,}; char temppath[MAX_PATH+1] = {0,}; OVERLAPPED ov = {0,}; DWORD dwValue = 0; // abc...xy가 반복 for (i=0; i<1024; i++) { buf[i] = 'a' + i % ('z' - 'a'); } // ABC...XY가 반복 for (i=0; i<1024; i++) { buf2[i] = 'A' + i % ('Z' - 'A'); } // 임시 경로 생성 ::GetTempPathA(MAX_PATH, temppath); strncat_s(temppath, "\\_test_.txt", MAX_PATH); // %temp%_test_.txt 생성 // FILE_FLAG_NO_BUFFERING이 OR되면 최적의 성능으로 된다고 한다. h = ::CreateFileA(temppath, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_FLAG_OVERLAPPED, // | FILE_FLAG_NO_BUFFERING 가능
NULL); if (INVALID_HANDLE_VALUE == h) { assert(FALSE); exit(1); } // i/o completion 발생시 event가 signal 됨 // auto-reset mode이기 때문에, WaitForSingleObject등을 한번 통과하면, // 자동으로 non-signaled 상태로 전환됨 ov.hEvent = ::CreateEvent(NULL, FALSE, FALSE, NULL); ////////////////////////////////////////////////////////////////////////// // Overlapped i/o로 파일 쓰기 // Overlapped i/o로 쓴다. 즉시 리턴된다. // FILE_FLAG_NO_BUFFERING 사용시 sector 크기의 배수 (512 x 2 = 1024)로 Write해야 한다. if (FALSE == ::WriteFile(h, buf, 1024, NULL, &ov)) { dwValue = ::GetLastError(); if (dwValue == ERROR_IO_PENDING) { // i/o가 완료되지 않았음 // 정상 } else { // 에러 상황 assert(FALSE); exit(1); } } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } // 이곳에서 CPU 사용되는 코드를 실행하면 된다. { } // I/O가 완료될 때 가지 기다린다. ::WaitForSingleObject(ov.hEvent, INFINITE); // I/O의 결과를 보고받는다. if (FALSE == ::GetOverlappedResult(h, &ov, &dwValue, TRUE)) { // 에러 상황 assert(FALSE); exit(1); } // 다음 File Pointer를 계산한다. // LARGE_INTER의 QuadPart를 이용하면 편리하다. nLarge.QuadPart += 1024; // LARGE_INTEGER 값을 기반으로 File Pointer 위치를 전달한다. ov.Offset = nLarge.LowPart; ov.OffsetHigh = nLarge.HighPart; // Overlapped i/o로 쓴다. 즉시 리턴된다. // FILE_FLAG_NO_BUFFERING 사용시 sector 크기의 배수 (512 x 2 = 1024)로 Write해야 한다. if (FALSE == ::WriteFile(h, buf, 1024, NULL, &ov)) { dwValue = ::GetLastError(); if (dwValue == ERROR_IO_PENDING) { // i/o가 완료되지 않았음 // 정상 } else { // 에러 상황 assert(FALSE); exit(1); } } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } // 이곳에서 CPU 사용되는 코드를 실행하면 된다. { } // I/O가 완료될 때 가지 기다린다. // 만일, MsgWaitForMultipleObjects(...)를 사용하면, // UI의 block을 막을 수 있어 유용한다. // 만일 취소를 원한다면, ::CancelIo(...)를 사용할 수 있다. ::WaitForSingleObject(ov.hEvent, INFINITE); // I/O의 결과를 보고받는다. if (FALSE == ::GetOverlappedResult(h, &ov, &dwValue, TRUE)) { // 에러 상황 assert(FALSE); exit(1); } // 여기까지, // abc....xyabc...xy... 가 1024 byte // ABC....XYABC...XY... 가 1024 byte // 총 2048 byte가 저장되었다. printf("2048 bytes written.\r\n"); ////////////////////////////////////////////////////////////////////////// // Overlapped i/o로 파일 읽기 // 1byte 위치 부터 읽어본다. // 읽기는 WriteFile 처럼, sector 배수의 조건이 필요 없다. // 여기서는 File Pointer는 선두 1byte, 읽을 크기는 10byte로 한다. // Overlapped i/o로 하기 때문에, 바로 리턴된다. // 만일, FILE_FLAG_NO_BUFFERING이 OR되었다면, 아래는 87번 오류(=ERROR_INVALID_PARAMETER) // 가 들어온다.
nLarge.QuadPart = 1; ov.Offset = nLarge.LowPart; ov.OffsetHigh = nLarge.HighPart; if (FALSE == ::ReadFile(h, buf3, 10, NULL, &ov)) { dwValue = ::GetLastError(); if (dwValue == ERROR_IO_PENDING) { // i/o가 완료되지 않았음 // 정상 } else { // 에러 상황 assert(FALSE); exit(1); } } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } // 이곳에서 CPU 사용되는 코드를 실행하면 된다. { } // I/O가 완료될 때 까지 기다린다. ::WaitForSingleObject(ov.hEvent, INFINITE); // I/O의 결과를 보고받는다. if (FALSE == ::GetOverlappedResult(h, &ov, &dwValue, TRUE)) { // 에러 상황 assert(FALSE); exit(1); } printf("Readed : %s\r\n", buf3); ZeroMemory(buf3, sizeof(buf3)); // EOF 근처에서 한번 읽어 본다. // 즉시 리턴된다. nLarge.QuadPart = 2048 - 5; ov.Offset = nLarge.LowPart; ov.OffsetHigh = nLarge.HighPart; if (FALSE == ::ReadFile(h, buf3, 1024, NULL, &ov)) { dwValue = ::GetLastError(); if (dwValue == ERROR_IO_PENDING) { // i/o가 완료되지 않았음 // 정상 } else { // 에러 상황 assert(FALSE); exit(1); } } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } // I/O가 완료될 때 까지 기다린다. ::WaitForSingleObject(ov.hEvent, INFINITE); // I/O의 결과를 보고받는다. if (FALSE == ::GetOverlappedResult(h, &ov, &dwValue, TRUE)) { // 에러 상황 assert(FALSE); exit(1); } printf("Readed : %s, Byte Transfered : %d\r\n", buf3, dwValue); // EOF 위치에서 읽어 본다. nLarge.QuadPart = 2048; ov.Offset = nLarge.LowPart; ov.OffsetHigh = nLarge.HighPart; if (FALSE == ::ReadFile(h, buf3, 1024, NULL, &ov)) { dwValue = ::GetLastError(); if (dwValue == ERROR_IO_PENDING) { // EOF 영역에서 읽어서, 실제로 읽을 것이 없겠지만, // I/O Pending이 들어온다. // 정상 } else { // 에러 상황 assert(FALSE); exit(1); } } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } // I/O가 완료될 때 까지 기다린다. ::WaitForSingleObject(ov.hEvent, INFINITE); // I/O의 결과를 보고받는다. if (FALSE == ::GetOverlappedResult(h, &ov, &dwValue, TRUE)) { dwValue = ::GetLastError(); if (dwValue == ERROR_HANDLE_EOF) { // EOF로 인해 실패함 // 정상 } else { // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } } else { // 성공할 수 없음 // 예측되지 않은 에러 상황 assert(FALSE); exit(1); } printf("Read Seek at %d, ERROR_HANDLE_EOF returned, Byte Transfered : %d\r\n", nLarge.QuadPart, dwValue); if (NULL != ov.hEvent) { ::CloseHandle(ov.hEvent); ov.hEvent = NULL; } if (INVALID_HANDLE_VALUE != h) { ::CloseHandle(h); h = INVALID_HANDLE_VALUE; } return 0; }

위 코그를 실행하면 다음과 같습니다.

2048 bytes written.
Readed : bcdefghijk
Readed : tuvwx, Byte Transfered : 5
Read Seek at 2048, ERROR_HANDLE_EOF returned, Byte Transfered : 0
계속하려면 아무 키나 누르십시오 . . .