[스터디] 시스템 프로그래밍 - Chapter 7. 프로세스간 통신(IPC) 1

2023. 10. 4. 03:36·독서/뇌를 자극하는 윈도우즈 시스템 프로그래밍

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


01. 프로세스간 통신(IPC)의 의미

Inter-Process Communication(IPC)는 프로세스 사이의 통신이라는 뜻을 가진다. 현실에서 우리가 소통을 위해서 사용하는 전화나 메신저, 소포와 같이 통신을 위한 수단들을 컴퓨터 속에서는 IPC라는 개념으로 생각하면 된다.

 

프로세스들은 서로 직접 만나서 데이터를 주고 받을 수 없다. 자기 자신에게 할당된 메모리 공간 이외에는 접근이 불가능하기 때문이다. 이는 프로세스끼리 서로의 영역에 침범하는 것을 방지하기 위해서인데, 서로 접근이 가능하다면 다른 프로세스에 원치 않은 일이 생겨날 수 있기 때문이다. 기본적으로 오늘 날의 운영체제는 프로세스가 본인에게 할당된 메모리 공간 영역에서만 활동할 수 있도록 구현되어 있다.

 

이는 프로그램의 안전성을 높이기 위한 결정이기도 하다.

 


02. 메일슬롯 방식의 IPC

WIndows에서는 다양한 IPC 기법들을 제공하고 있으며, 메일슬롯 기반의 IPC는 그 중 하나이다. 이는 편지를 넣을 수 있는 가느다란 우체통의 입구를 의미하는데, 기본 원리는 '데이터를 주고 받기 위해서 프로세스가 우체통을 마련하는 것'이라고 생각할 수 있다.

 

데이터를 전달하려고 하는 프로세스는 Sender, 그 데이터를 수신하려고 하는 프로세스는 Receiver라고 가정하고 예시를 보자. Receiver는 데이터를 받기 위해서 밖에 우체통을 하나 설치한다. 이때, 이를 가리켜 메일슬롯이라 한다. Sender는 Receiver의 주소를 통해 우체통(메일슬롯)을 향하여 데이터를 전송한다.

 

그러면 Receiver는 메일슬롯을 통해서 데이터를 얻을 수 있다.


02. A. 메일슬롯Mail Slot 구성을 위해 필요한 요소

아래는 메일슬롯을 생성하는 함수의 정의와 매개변수들의 설명이다.

HANDLE CreateMailslot (
    LPCSTR lpName,
    DWORD nMaxMessageSize,
    DWORD lReadTimeout,
    LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
매개변수 명 설명
lpName 생성하는 메일슬롯의 이름에 사용된다.
nMaxMessageSize 메일슬롯의 버퍼 크기를 지정하는데 사용된다. 0일 경우, 시스템이 허용하는 한도에서 최대 크기로 지정된다.
IReadTimeout 전송되는 데이터를 읽기 위해서 ReadFile 함수가 사용된다. 메일슬롯에서 읽을 데이터가 있다면 데이터를 읽으면서 ReadFile 함수를 빠져나온다. 하지만 메일슬롯이 비어있다면 데이터가 채워질 때 까지 ReadFile 함수를 반환하지 않고 Blocking 상태에 놓인다.

IReadTimeout은 최대 블로킹 시간을 밀리세컨드 단위로 지정하는데에 사용된다. 0일 경우, 블로킹 상태 없이 빠져 나와 다음 단계를 수행한다. 상수 MAILSLOT_WAIT_FOREVER를 인자로 전달할 경우, ReadFile 함수는 읽을 데이터가 존재할 때 까지 Blocking 상태에 놓인다.
lpSecurityAttributes 핸들을 상속하기 위한 용도로 사용된다.

해당 함수의 반환 타입은 HANDLE인데, 메일슬롯도 커널에 의해서 관리되는 리소스이기 때문에 커널 오브젝트가 더불어 생성되고, 이때 커널 오브젝트의 핸들이 반환된다.


02. B. Sender의 필요 요소

Receiver가 메일슬롯을 만들었다면, 전달하려는 Sender쪽에서는 메일슬롯의 이름을 가지고 데이터를 전송해야 한다.

// 1단계
HANDLE hMailSlot
hMailSlot = CreateFile("\\\\.\\mailslot\\mailname", ...);

// 2단계
CHAR message[50];
WriteFile(hMailSlot, message, ...);

위 코드는 간략하게 메일슬롯에 데이터를 전송하는 방법에 대해서 작성했다. CreateFile, WriteFile 함수를 통해서 데이터를 주고 받는데 이는 메일슬롯이 Windows 파일 시스템을 기반으로 구현되었기 때문이다. 

 

메일슬롯의 주소는 기본적으로 \\computername\mailslot\[path]name와 같은 구조를 가진다. computername은 local 컴퓨터를 의미하고 [path]name은 실질적으로 메일슬롯이 가지는 이름이다.

 

위에서 CreateFile을 통해서 메일슬롯의 연결을 의미하는 데이터 스트림을 만들었는데 이는 CreateMailslot 함수의 호출을 통해 생성되는 메일슬롯과는 다른 형태의 리소스이며, 운영체제에 의해서 커널 오브젝트와 핸들의 생성을 동반한다.


02. C. 메일슬롯 예제

// MailReceiver.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

#define SLOT_NAME _T("\\\\.\\mailslot\\mailname")

int _tmain(int argc, TCHAR* argv[])
{
	HANDLE hMailSlot;  
	TCHAR messageBox[50];
	DWORD bytesRead; 

	hMailSlot = CreateMailslot(SLOT_NAME, 0, MAILSLOT_WAIT_FOREVER, NULL);

	if (hMailSlot == INVALID_HANDLE_VALUE)
	{
		_fputts(_T("Unable to create mailslot!\n"), stdout);
		return 1;
	}

	_fputts(_T("******** Message ********\n"), stdout);

	while (1)
	{
		if (!ReadFile(hMailSlot, messageBox, sizeof(TCHAR) * 50, &bytesRead, NULL))
		{
			_fputts(_T("Unable to read!"), stdout);
			CloseHandle(hMailSlot);
			return 1;
		}

		if (!_tcsncmp(messageBox, _T("exit"), 4))
		{
			_fputts(_T("Good Bye!"), stdout);
			break;
		}

		messageBox[bytesRead / sizeof(TCHAR)] = 0;
		_fputts(messageBox, stdout);
	}

	CloseHandle(hMailSlot);
	return 0;
}
// MailSender.cpp

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

#define SLOT_NAME _T("\\\\.\\mailslot\\mailname")

int _tmain(int argc, TCHAR* argv[])
{
	HANDLE hMailSlot;
	TCHAR message[50];
	DWORD bytesWritten;

	hMailSlot = CreateFile(
		SLOT_NAME,       // 생성 및 개방하고자하는 파일 이름을 지정한다
		GENERIC_WRITE,   // 개방 모드를 지정한다. (읽기 모드, 쓰기 모드, 읽기/쓰기 모드)
		FILE_SHARE_READ,
		NULL,
		OPEN_EXISTING,   // 파일 생성 방식을 결정짓는다.
		FILE_ATTRIBUTE_NORMAL,
		NULL
	);

	if (hMailSlot == INVALID_HANDLE_VALUE)
	{
		_fputts(_T("Unable to create mailslot!\n"), stdout);
		return 1;
	}

	while (1)
	{
		_fputts(_T("MY CMD > "), stdout);
		_fgetts(message, sizeof(message) / sizeof(TCHAR), stdin);

		if (!WriteFile(hMailSlot, message, _tcslen(message) * sizeof(TCHAR), &bytesWritten, NULL))
		{
			_fputts(_T("Unable to write!"), stdout);
			CloseHandle(hMailSlot);
			return 1;
		}

		if(!_tcscmp(message, _T("exit")))
		{
			_fputts(_T("Good Bye!"), stdout);
			break;
		}
	}

	CloseHandle(hMailSlot);
	return 0;
}

 

MailReceiver.cpp의 실행 파일을 먼저 실행하고 MailSender.cpp의 실행 파일을 실행해야 한다. CreateFile 함수에서 다섯 번째 인자의 값을 OPEN_EXISTING으로 전달하고 있기 때문이다.

 

▶ 메일슬롯과 IPC에 대한 고찰

: 메일슬롯은 단방향 통신만 가능하다. 양쪽 방향으로 메시지를 주고 받는 IPC 기법도 있다. 파이프라는 IPC 기법인데 파이프에는 Anonymous 파이프, Named 파이프가 있다. 이 중에서 Named 파이프는 양방향 데이터 송, 수신을 지원한다.

메일슬롯은 브로드캐스팅Broadcasting 방식의 통식을 지원한다. 하나의 Sender는 한 번의 메시지 전송으로 여러 Receiver에게 동일한 메시지를 동시에 전송이 가능한 것이다.
▶ 메일슬롯의 Usage Count는 생성 시, 1이다.

: 메일슬롯을 참조하고 있는 프로세스는 메일슬롯을 생성한 프로세스 하나다. 뿐만 아니라, 프로세스와 쓰레드를 제외한 다른 모든 커널 오브젝트는 생성과 동시에 Usage Count가 1이 된다.

03. Signaled vs Non-Signaled

커널 오브젝트는 특정 상황하에 변경되는 상태를 가진다. 이는 Sigaled 상태, Non-Signaled 상태가 있다. 상태에 대한 정보는 커널 오브젝트의 내부에 상태를 담당하는 변수를 통해서 저장한다.

 

상태 정보를 통해서 리소스에 특정 상황이 일어났음을 알 수 있다. 특정 상황은 리소스마다 다르며, 그렇기에 커널 오브젝트의 변경 시점은 커널 오브젝트의 종류에 따라 달라진다. 간단하게 훑어보고 지나가자.

 

처음 커널 오브젝트가 생성되면 커널 오브젝트 상태는 Non-Signaled 상태에 놓이게 되며, 프로세스가 종료된다면 Signaled 상태로 변경된다. 이로 인해서 Signaled 상태의 프로세스 커널 오브젝트를 보고 프로세스가 종료되었음을 알 수 있다.

 

반대로 상태가 변경되는 경우는 프로세스가 재시작될 때를 말하는데, 이미 종료된 프로세스를 재실행 할 수 없으므로 Signaled 상태가 Non-Signaled 상태로 변경되는 경우는 없다.


03. A. 커널 오브젝트의 두 가지 상태를 확인하는 용도의 함수

DWORD WaitForSingleObject (
    HANDLE hHandle,
    DWORD dwMilliseconds
);
매개변수 명 설명
hHandle 상태 확인을 원하는 커널 오브젝트의 핸들을 인자로 전달한다.
dwMilliseconds WaitForSingleObject 함수는 인자로 전달된 커널 오브젝트가 Signaled 상태가 되었을 때 반환한다. dwMilliseconds는 커널 오브젝트가 Signaled 상태가 될 때 까지 기다릴 수 있는 최대 시간을 밀리세컨드 단위로 지정할 때 사용된다.

상수 INFINITE를 인자로 전달하면 커널 오브젝트가 Signaled 상태가 될 때 까지 반환하지 않고 무한정 기다린다.

위 내용에서 설명하는 WaitForSingleObject 함수는 커널 오브젝트의 상태를 확인하는데 사용된다. 반환하는 상황이 다양하므로 함수 호출 완료 직후, 반환 값을 확인해야 한다.

 

반환 값 설명
WAIT_OBJECT_0 커널 오브젝트가 Signaled 상태가 되었을 때 반환하는 값
WAIT_TIMEOUT 커널 오브젝트가 Signaled 상태가 되지 않고 dwMilliseconds로 지정한 시간을 넘겼을 때 반환되는 값
WAIT_ABANDONED 소유 관계와 관련하여 함수가 정상적이지 못한 오류 발생에 의해 반환되는 경우에 반환하는 값

 

추가적으로 상태를 확인하고자 하는 커널 오브젝트가 둘 이상이고, 이들의 핸들이 배열로 묶여 있다면 다음으로 소개하는 함수를 활용하는 것이 좋다.

 

DWORD WaitForMultipleObjects(
    DWORD nCount,
    const HANDLE* lpHandles,
    BOOL bWaitAll,
    DWORD dwMilliseconds
);
매개변수 명 설명
nCount 배열에 저장된 핸들의 개수
lpHandles 핸들을 저장하고 있는 배열의 주소 정보를 전달한다.
bWaitAll 관찰 대상이 모두 Signaled 상태가 되는 것을 기다리는 지(TRUE), 하나라도 Signaled 상태가 되는 것을 기다리는지(FALSE) 결정한다.
dwMilliseconds WaitForSingleObject에서의 의미와 동일하다.

03. B. 커널 오브젝트의 상태 확인이 필요한 상황

두 개의 자식 프로세스를 생성해서 각각 1부터 5까지, 그리고 6부터 10까지 덧셈을 시킨 이후 결과를 반환하며, 부모 프로세스는 반환 값을 더해 결과를 출력하는 예제를 생각하며 만들어보자.

 

// NonStopAdderManager.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR* argv[])
{
    STARTUPINFO si1 = { 0, };
    STARTUPINFO si2 = { 0, };

    PROCESS_INFORMATION pi1;
    PROCESS_INFORMATION pi2;

    DWORD return_val1;
    DWORD return_val2;

    TCHAR command1[] = _T("PartAdder.exe 1 5");
    TCHAR command2[] = _T("PartAdder.exe 6 10");

    DWORD sum = 0;

    si1.cb = sizeof(si1);
    si2.cb = sizeof(si2);

    CreateProcess(
        NULL, command1,
        NULL, NULL, TRUE,
        0, NULL, NULL,
        &si1, &pi1
    );

    CreateProcess(
        NULL, command2,
        NULL, NULL, TRUE,
        0, NULL, NULL,
        &si2, &pi2
    );

    CloseHandle(pi1.hThread);
    CloseHandle(pi2.hThread);

    GetExitCodeProcess(pi1.hProcess, &return_val1);
    GetExitCodeProcess(pi2.hProcess, &return_val2);

    if (return_val1 == -1 || return_val2 == -1)
        return -1;

    sum += return_val1;
    sum += return_val2;

    _tprintf(_T("total : %d \n"), sum);

    CloseHandle(pi1.hProcess);
    CloseHandle(pi2.hProcess);

    return 0;
}
// PartAdder.cpp

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR* argv[])
{
	if (argc != 3)
		return -1;

	DWORD start = _ttoi(argv[1]);
	DWORD end = _ttoi(argv[2]);

	DWORD total = 0;

	for (DWORD i = start; i <= end; i++) {
		total += i;
	}
    
	return total;
}
▶ 실행 결과 : 518

실행 결과가 기대하는 것처럼 나오지 않은 이유는 두 개의 자식 프로세스는 연산 결과에 해당하는 종료 코드를 반환하고 종료하지 않아서이다. 

 

WaitForSingleObject(pi1.hProcess, INFINITE);
WaitForSingleObject(pi2.hProcess, INFINITE);

위 코드를 추가함으로서 우가 원하는 기대 값인 55를 얻을 수 있다. WiatForMultipleObjects 함수는 아래와 같이 사용할 수 있다.

    HANDLE handles[2];

    handles[0] = pi1.hProcess;
    handles[1] = pi2.hProcess;

    WaitForMultipleObjects(2, handles, true, INFINITE);

 

'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글

[스터디] 시스템 프로그래밍 - Chapter 9. 스케줄링 알고리즘과 우선순위  (2) 2023.10.04
[스터디] 시스템 프로그래밍 - Chapter 8. 프로세스간 통신(IPC) 2  (2) 2023.10.04
[스터디] 시스템 프로그래밍 - Chapter 6. 커널 오브젝트와 오브젝트 핸들  (2) 2023.10.03
[스터디] 시스템 프로그래밍 - Chapter 5. 프로세스의 생성과 소멸  (0) 2023.10.02
[스터디] 시스템 프로그래밍 - Chapter 4. 컴퓨터 구조에 대한 두 번째 이야기  (1) 2023.09.19
'독서/뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
  • [스터디] 시스템 프로그래밍 - Chapter 9. 스케줄링 알고리즘과 우선순위
  • [스터디] 시스템 프로그래밍 - Chapter 8. 프로세스간 통신(IPC) 2
  • [스터디] 시스템 프로그래밍 - Chapter 6. 커널 오브젝트와 오브젝트 핸들
  • [스터디] 시스템 프로그래밍 - Chapter 5. 프로세스의 생성과 소멸
태역
태역
  • 태역
    RYULAB
    태역
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 언어
        • C
        • C++
        • C#
      • 엔진, 프레임워크
        • Unity
        • Unreal
        • Electron
      • 공부
        • 디자인 패턴
        • 수학
        • CS
        • Git
        • 알고리즘
        • 자료구조
      • 코테
        • 프로그래머스
        • 백준
      • 독서
        • Effective C#
        • CLR via C#
        • 뇌를 자극하는 윈도우즈 시스템 프로그래밍
        • 오브젝트
        • CSAPP
        • OSTEP
      • 프로젝트
        • Unity
      • 개발 일지
        • 퓨처리티
        • 골든타임
      • 활동
        • 게임잼 후기
        • 게임제작동아리 브릿지
        • 크래프톤 정글
        • 기타
      • 기타
  • 블로그 메뉴

    • 링크

    • 공지사항

      • 2024 04 17
    • 인기 글

    • 태그

      인프런 #인프런강의후기 #게임개발 #게임개발강의 #인강후기 #강의후기 #게임개발자 #인프런강의
      티스토리챌린지
      오블완
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.3
    태역
    [스터디] 시스템 프로그래밍 - Chapter 7. 프로세스간 통신(IPC) 1
    상단으로

    티스토리툴바