Computer Science 기본 지식/운영체제

[Windows] Overlapped I/O

로파이 2021. 10. 4. 15:59

비동기 I/O를 위한 Windows에서 제공하는 API를 정리한다.

 

※ 동기 I/O와 비동기 I/O

프로그램에서 수반되는 입출력을 현재 실행중인 프로세스나 스레드에서 어떻게 처리할 것 인지에 대한 방법이다. 여러 작업을 동시에 처리하여 다중 CPU를 사용하는 컴퓨터에서 CPU의 이용률을 최대화하는 것이 유리하다. 만약 프로그램의 주 쓰레드에서 I/O 입출력 동작을 필요로 하는 상황에서 입출력이 끝날때까지 블로킹 상태로 놓인다면 입출력이 완료되는 시점에서 다음 실행이 진행되기 때문에 동기적 (Synchronous) I/O를 수행하는 것이다. 하지만 이는, I/O 작업동안 주 쓰레드는 아무것도 하지 않기 때문에 CPU를 최대한으로 이용한다고 볼 수 없다. 반면에 비동기적 I/O는 입출력 동작을 요청하기만 하고 블로킹 없이 즉시 이 후 작업을 진행할 수 있기 때문에 둘 이상의 작업을 동시에 할 수 있다.

 

중첩(Overlapped) I/O

비동기적 I/O를 이용하여 많은 수의 입출력 동작을 동시에 수행할 수 있다. 이러한 모습을 중첩한다고 해서 중첩 I/O라고 한다. 비동기적 I/O를 지원하기 위해 기존 입출력 동작을 수행하는 블로킹 함수 fwrite/fread/send/recv와 비슷한 논블로킹 함수를 사용해야 한다.

 

Overlapped 구조체

typedef struct _OVERLAPPED {
    ULONG_PTR Internal;
    ULONG_PTR InternalHigh;
    union {
        struct {
            DWORD Offset;
            DWORD OffsetHigh;
        } DUMMYSTRUCTNAME;
        PVOID Pointer;
    } DUMMYUNIONNAME;

    HANDLE  hEvent;
} OVERLAPPED, *LPOVERLAPPED;

중첩 I/O 기능을 지원하는 함수에 주로 인자형태로 전달하여 Event 객체가 hEvent에 할당되어 있다면 입출력이 끝났음을 알림(Singaled)이 설정된다. (※ Completion Routine을 사용하면 Event 객체를 생성하여 할당할 필요가 없다.)

 

중첩 I/O기반 파이프 통신

서버 - 클라이언트 구조에서 프로세스 간 통신을 지원하는 파이프 객체를 통해 중첩 I/O를 실행해보자.

 

1) 서버에서 이름있는 파이프를 만들고 이때 Overlapped 기능을 지원하는 플래그(FILE_FLAG_OVERLAPPED)를 설정한다. 

// 파이프 통신을 이용한 중첩 I/O
hPipe = CreateNamedPipe(
        pipeName,
        PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, // 읽기, 쓰기 모드 / 중첩 I/O를 위한 플래그
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, // 메시지 기반
        PIPE_UNLIMITED_INSTANCES, // 최대 파이프 개수
        BUF_SIZE / 2, // 출력 버퍼 사이즈
        BUF_SIZE / 2, // 입력 버퍼 사이즈
        20000, // 타임 아웃 시간
        NULL); // 보안 속성

2) 클라이언트에서 파이프 연결 요청을 보낸다.

hPipe = CreateFile(
			pipeName, // 연결할 파이프 이름
			GENERIC_READ | GENERIC_WRITE, // 읽기 쓰기 모드 동시 지정
			0,
			NULL,
			OPEN_EXISTING,
			0,
			NULL
		);

3) 서버에서 클라이언트의 파이프 연결을 수락한다.

BOOL isSuccess = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);

4) 클라이언트는 파이프 연결 요청이 수락되었는 지 확인한다.

		// 연결 요청이 실패하였다.
		if (hPipe != INVALID_HANDLE_VALUE)
			break;
	
		// 파이프 연결 요청이 바쁜 상태가 아니다.
		if (GetLastError() != ERROR_PIPE_BUSY)
		{
			_tprintf(_T("Could not open pipe\n"));
			return 0;
		}

		// 연결 요청이 수락되었는지 확인한다.
		if (!WaitNamedPipe(pipeName, 20000))
		{
			_tprintf(_T("Could not open pipe \n"));
			return 0;
		}

5) 클라이언트는 연결 요청이후 파이프 속성을 설정하고 보내야할 파일 이름을 파이프를 통해 전송한다.

	// 파이프 속성 변경
	DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_WAIT; // 메세지 기반 파이프
	BOOL isSuccess = SetNamedPipeHandleState(hPipe, &pipeMode, NULL, NULL); // 서버 파이프와 연결된 핸들

	if (!isSuccess)
	{
		_tprintf(_T("SetNamedPipeHandleState failed\n"));
		return 0;
	}
	
    // 전송해야할 파일 이름을 전송
	LPCWSTR fileName = _T("news.txt");
	DWORD bytesWritten = 0;

	isSuccess = WriteFile(hPipe, fileName, (_tcslen(fileName) + 1) * sizeof(TCHAR), &bytesWritten, NULL);

6) 서버는 연결 수락 이후 파일 이름을 수신받아 해당 파일을 연다.

	isSuccess = ReadFile(hPipe, fileName, MAX_PATH * sizeof(TCHAR), &fileNameSize, NULL);

	if (!isSuccess || fileNameSize == 0)
	{
		_tprintf(_T("Pipe read message error! \n"));
		return -1;
	}

	FILE* filePtr = NULL;
	_tfopen_s(&filePtr, fileName, _T("r"));
	if (filePtr == NULL)
	{
		_tprintf(_T("File open fault!\n"));
		return -1;
	}

7) 파일 데이터를 읽어들여 비동기 I/O로 파이프를 통해 버퍼 데이터를 전송한다.

- Overlapped 구조체에 Event를 생성한다. (전송이 완료되었을 때, 다시 파일을 읽어 전송을 계속하기 위해)

	OVERLAPPED overlappedInst;
	memset(&overlappedInst, 0, sizeof(overlappedInst));
	overlappedInst.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); // manual-reset / signaled 상태

- 실제 파일 내용을 전송하는 로직

	while (!feof(filePtr))
	{
		bytesRead = (DWORD)fread(dataBuf, 1, BUF_SIZE, filePtr);
		bytesWrite = bytesRead;
		// WriteFile 호출시 중첩 객체의 이벤트 객체가 자동으로 non-signaled 상태가 된다.
		isSuccess = WriteFile(hPipe, dataBuf, bytesWrite, &bytesWritten, &overlappedInst);

		if (!isSuccess && GetLastError() != ERROR_IO_PENDING)
		{
			_tprintf(_T("Pipe Write Message Error!\n"));
			break;
		}

		/*
			다른 작업을 할 수 있는 영역
		*/

		WaitForSingleObject(overlappedInst.hEvent, INFINITE);
		// 중첩 I/O의 결과를 얻는 함수
		GetOverlappedResult(hPipe, &overlappedInst, &bytesTransfer, FALSE);

		_tprintf(_T("Transferred Data Size : %d\n"), bytesTransfer);
	}
  • Event 객체를 Signaled 상태로 생성하였지만 WriteFile 출력 동작으로 자동으로 Non-Signaled 상태로 초기화 된다.
  • 비동기 I/O로 동시에 다음 작업을 할 수 있는 영역이 생긴다.
  • WaitForSingleObject를 통해 전송이 끝날 때까지 기다리고 중첩 결과를 얻을 수 있다.
  • 파일 포인터로 부터 다음 전송해야할 버퍼를 읽어들이고 파일의 끝에 도달할 때까지 데이터 전송을 계속한다.

8) 클라이언트는 전송된 데이터를 읽어 출력한다.

	DWORD bytesRead = 0;
	while (1)
	{
		isSuccess = ReadFile(hPipe, readDataBuf, BUF_SIZE * sizeof(TCHAR), &bytesRead, NULL);

		if (!isSuccess && GetLastError() != ERROR_MORE_DATA)
		{
			break;
		}

		readDataBuf[bytesRead / sizeof(TCHAR)] = 0;
		_tprintf(_T("%s\n"), readDataBuf);
	}

	CloseHandle(hPipe);

 

동기 I/O에서 WriteFile의 연산은 출력 버퍼로의 데이터 복사와 실제 전송까지 끝날때 까지 블로킹 상태에 놓일 것이다. 비동기 I/O에서 WriteFile의 연산은 출력 버퍼로의 데이터 복사가 끝나면 반환이 되고 다른 일을 할 수 있게 딘다.

 

GetOverlappedResult

BOOL GetOverlappedResult(
  HANDLE       hFile,
  LPOVERLAPPED lpOverlapped,
  LPDWORD      lpNumberOfBytesTransferred,
  BOOL         bWait
);
  • hFile : 파일이나 이름있는 파이프, 통신가능한 개체의 핸들을 전달한다. 이 핸들은 중첩 I/O를 실행하는 함수, ReadFile, WriteFile 등을 호출할 때 전달하는 핸들이다.
  • lpOverlapped : 중첩 작업이 시작할 때 할당할 중첩 구조체의 포인터
  • lpNmberOfBytesTransferred : 실제 읽기나 쓰기 작업에서 작업된 바이트의 크기를 받기 위해 전달한다.
  • bWait : TRUE이고 lpOverapped 구조체의 Internal 멤버 변수가 STATUS_PENDING이라면, 함수는 입출력이 완료될 때까지 반환하지 않는다. FALSE이고 아직 펜딩 중이라면, FALSE를 반환하고 GetLastError()는 ERROR_IO_INCOMPLETE를 반환한다.

- 전체 코드

NamedPipe_Server.cpp

더보기
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>

#define BUF_SIZE 1024
int CommToClient(HANDLE);

int _tmain(int argc, TCHAR* argv[])
{
	LPCWSTR pipeName = _T("\\\\.\\pipe\\simple_pipe");

	HANDLE hPipe;

	while (1)
	{
		// 파이프 통신을 이용한 중첩 I/O
		hPipe = CreateNamedPipe(
			pipeName,
			PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, // 읽기, 쓰기 모드 / 중첩 I/O를 위한 플래그
			PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, // 메시지 기반
			PIPE_UNLIMITED_INSTANCES, // 최대 파이프 개수
			BUF_SIZE / 2, // 출력 버퍼 사이즈
			BUF_SIZE / 2, // 입력 버퍼 사이즈
			20000, // 타임 아웃 시간
			NULL); // 보안 속성

		if (hPipe == INVALID_HANDLE_VALUE)
		{
			_tprintf(_T("CreateNamedPipe Failed\n"));
			return -1;
		}

		BOOL isSuccess = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED);

		if (isSuccess)
			CommToClient(hPipe);
		else
			CloseHandle(hPipe);
	}

	return 1;
}

int CommToClient(HANDLE hPipe)
{
	TCHAR fileName[MAX_PATH];
	TCHAR dataBuf[BUF_SIZE];
	BOOL isSuccess;
	DWORD fileNameSize;

	isSuccess = ReadFile(hPipe, fileName, MAX_PATH * sizeof(TCHAR), &fileNameSize, NULL);

	if (!isSuccess || fileNameSize == 0)
	{
		_tprintf(_T("Pipe read message error! \n"));
		return -1;
	}

	FILE* filePtr = NULL;
	_tfopen_s(&filePtr, fileName, _T("r"));
	if (filePtr == NULL)
	{
		_tprintf(_T("File open fault!\n"));
		return -1;
	}


	OVERLAPPED overlappedInst;
	memset(&overlappedInst, 0, sizeof(overlappedInst));
	overlappedInst.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); // manual-reset / signaled 상태

	DWORD bytesWritten = 0, bytesRead = 0;
	DWORD bytesWrite = 0, bytesTransfer = 0;

	while (!feof(filePtr))
	{
		bytesRead = (DWORD)fread(dataBuf, 1, BUF_SIZE, filePtr);
		bytesWrite = bytesRead;
		// WriteFile 호출시 중첩 객체의 이벤트 객체가 자동으로 non-signaled 상태가 된다.
		isSuccess = WriteFile(hPipe, dataBuf, bytesWrite, &bytesWritten, &overlappedInst);

		if (!isSuccess && GetLastError() != ERROR_IO_PENDING)
		{
			_tprintf(_T("Pipe Write Message Error!\n"));
			break;
		}

		/*
			다른 작업을 할 수 있는 영역
		*/

		WaitForSingleObject(overlappedInst.hEvent, INFINITE);
		// 중첩 I/O의 결과를 얻는 함수
		GetOverlappedResult(hPipe, &overlappedInst, &bytesTransfer, FALSE);

		_tprintf(_T("Transferred Data Size : %d\n"), bytesTransfer);
	}

	FlushFileBuffers(hPipe);
	DisconnectNamedPipe(hPipe);
	CloseHandle(hPipe);
	return 1;
}

NamedPipe_Client.cpp

더보기
#include <cstdio>
#include <tchar.h>
#include <Windows.h>

#define BUF_SIZE 1024


int _tmain(int argc, TCHAR* argv[])
{
HANDLE hPipe;
TCHAR readDataBuf[BUF_SIZE + 1];
LPCWSTR pipeName = _T("\\\\.\\pipe\\simple_pipe");

while (1)
{
hPipe = CreateFile(
pipeName, // 연결할 파이프 이름
GENERIC_READ | GENERIC_WRITE, // 읽기 쓰기 모드 동시 지정
0,
NULL,
OPEN_EXISTING,
0,
NULL
);

// 연결 요청이 실패하였다.
if (hPipe != INVALID_HANDLE_VALUE)
break;

// 파이프 연결 요청이 바쁜 상태가 아니다.
if (GetLastError() != ERROR_PIPE_BUSY)
{
_tprintf(_T("Could not open pipe\n"));
return 0;
}

// 연결 요청이 수락되었는지 확인한다.
if (!WaitNamedPipe(pipeName, 20000))
{
_tprintf(_T("Could not open pipe \n"));
return 0;
}
}

DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_WAIT; // 메세지 기반 파이프
BOOL isSuccess = SetNamedPipeHandleState(hPipe, &pipeMode, NULL, NULL); // 서버 파이프와 연결된 핸들

if (!isSuccess)
{
_tprintf(_T("SetNamedPipeHandleState failed\n"));
return 0;
}

LPCWSTR fileName = _T("news.txt");
DWORD bytesWritten = 0;

isSuccess = WriteFile(hPipe, fileName, (_tcslen(fileName) + 1) * sizeof(TCHAR), &bytesWritten, NULL);

if (!isSuccess)
{
_tprintf(_T("WriteFile Failed"));
return 0;
}

DWORD bytesRead = 0;
while (1)
{
isSuccess = ReadFile(hPipe, readDataBuf, BUF_SIZE * sizeof(TCHAR), &bytesRead, NULL);

if (!isSuccess && GetLastError() != ERROR_MORE_DATA)
{
break;
}

readDataBuf[bytesRead / sizeof(TCHAR)] = 0;
_tprintf(_T("%s\n"), readDataBuf);
}

CloseHandle(hPipe);

_tprintf(_T("Trasfer Finished\n"));
getchar();
return 0;
}

 

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