비동기 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;
}
참고 : 뇌를 자극하는 윈도우즈 시스템 프로그래밍 (윤성우 저)
'Computer Science 기본 지식 > 운영체제' 카테고리의 다른 글
[C++ Thread] 동기화 방법에 따른 성능 비교 (0) | 2021.10.10 |
---|---|
[Windows] 완료 루틴 (Completion Routine)과 APC (Asynchronous Procedure Call) (0) | 2021.10.04 |
[운영체제] OS관련 용어 (0) | 2021.10.04 |
[C++ Thread] Windows API에서 쓰레드 생성 (0) | 2021.06.01 |
[C++ Thread] 스핀 락 Spin Lock (0) | 2021.05.05 |