7장. 프로세스 간 통신(IPC) (1)

유니야·2023년 1월 8일

✏️ 프로세스 간 통신(IPC)의 의미와 메일 슬롯 방식의 IPC

💻 IPC (Inter-Process Communication)

  • 프로세스 간 통신

    • 프로세스 간 데이터 송수신. 메모리 공유
    • 프로세스 A가 프로세스 B에게 데이터를 보내는 경우, 공유되는 메모리 공간을 마련해두고 해당 공간에 A가 데이터를 가져다 두면 B가 데이터를 가져가는 흐름. 따라서 프로세스 간 통신은 내부적으로 메모리 공유라고 할 수 있다.
    • 기본적으로 하나의 프로그램은 하나의 프로세스를 형성하지만, 여러 프로세스를 형성하는 프로그램도 있다. 이런 경우 프로세스는 서로 통신할 수 있어야 한다.
  • IPC가 필요한 이유
    각 프로세스는 OS에 의해 완전히 독립된 메모리를 공간을 가지고, 다른 메모리 영역에 접근할 수 없도록 막혀 있다. 만약 프로세스가 다른 메모리 영역에 접근하는 것을 허용한다면, 대부분 하나의 프로그램은 하나의 프로세스를 두기 때문에 어떤 프로그램이 다른 프로그램에 영향을 미치는 문제가 발생할 수 있다. 이는 안정성의 문제를 초래한다.
    이에 대한 해결책으로 OS는 IPC를 제공하며 이는 운영체제 별로 다르다. 윈도우즈에는 대표적인 세 가지 IPC가 있다.

💻 메일 슬롯

  • 우체통의 역할

  • 단방향. 따라서 각 프로세스는 자신의 역할에 따라 데이터를 보내거나, 받는 것만 가능하다.

  • Sender 프로세스는 여러 곳에 데이터를 보낼 수 있다.

  • 브로드 캐스팅

    • Sender 프롯세스는 한 번의 송신으로 같은 주소 값을 가진 여러 Receiver 프로세스에게 데이터를 보낼 수 있다.
  • 데이터 흐름

    1) Receiver 프로세스가 데이터 수신 목적의 우체통을 생성하고 주소 부여. 이는 Receiver 프로세스가 접근가능한 영역. 어떤 프로세스건 해당 주소를 알면 이 영역에 접근할 수 있다.
    2) Sender 프로세스는 이 우체통에 데이터를 보내기 위해 주소를 알아야 한다.
    3) Receiver 프로세스는 우체통을 확인해서 데이터가 있으면 가져감.
    4) 해당 메일 슬롯은 Receiver 프로세스와 Sender 프로세스가 공유하는 영역. 즉 메모리 공유

  • 프로그래밍 모델


    CreateFile(): Receiver 프로세스가 만들어둔 우체통과 연결하는 함수. 파일을 생성하거나, 생성된 파일은 열 때 사용하는 함수
    WriteFile(): 파일에 데이터를 쓰는 함수
    ReadFile(): 파일에서 데이터를 읽어 들이는 함수

파일을 이용하는 것 또한 메모리 공유를 이용한 IPC 통신. 이처럼 데이터를 읽고 쓰는 기능이 파일을 읽고 쓰는 과정과 내부 구현이 유사하기 때문에 시스템 함수명 또한 파일 시스템 관련 함수와 비슷하게 둔 것. 그러나 단순히 파일을 이용한 통신은 불안정. 이런 점이 개선된 방법이 메일 슬롯 방식이다.

// MailSlotReciever.cpp

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

#define SLOT_NAME _T("\\\\.\\mailslot\\mailbox")
// 위 경로상에 .은 현재 컴퓨터 이름으로 치환됨. 이건 로컬 상의 통신.
// . 대신해서 네트워크 상으로 연결된 다른 컴퓨터의 이름을 적을 수도 있음. 
// 그럼 완전히 독립된 두 컴퓨터 상의 프로세스 간 통신도 가능함. 
// 근데 잘 안씀. 소켓을 이용한 TCP/IP 통신 기법을 씀.

int _tmain(int argc, LPTSTR argv[])
{
    HANDLE hMailSlot; // mailslot 핸들
    TCHAR messageBox[50];
    LPDWORD bytesRead;

    // 1. 메일슬롯 생성(우체통 생성)
    hMailSlot = CreateMailslot(
        SLOT_NAME,             // 메일 슬롯 명
        0,                     // 메일 슬롯 버퍼 크기. 0은 허용최대치
        MAILSLOT_WAIT_FOREVER, // ReadFile() 함수 특성. 이 특성은 데이터가 들어올 때까지 기다림.
        NULL                   // 보안설정
    );
    
    // MAILSLOT_WAIT_FOREVER 특성의 ReadFile() 함수는 데이터가 들어올 때까지 Blocking 상태가 됨.
    ReadFile(
        hMailSlot,        // 메일 슬롯 지정
        messageBox,       // 데이터 수신버퍼(배열지정)
        sizeof(TCHAR)*50, // 읽어 들일 데이터 최대크기
        bytesRead,        // 읽어 들인 데이터 실제크기를 얻기 위한 변수
        NULL
    );
    
    CloseHandle(hMailSlot);

    return 0;
}

// MailSlotSender.cpp

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

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

int _tmain(int argc, LPTSTR argv[])
{
    HANDLE hMailSlot; // mailslot 핸들
    TCHAR messageBox[50] = _T("Hello, MailSlot!");
    LPDWORD lpTransmitResult;

    // 2. 연결작업
    hMailSlot = CreateFile(
        SLOT_NAME,             // 메일 슬롯 명 
        GENERIC_WRITE,         // 메일 슬롯 용도. 샌더이기에, write 밖에 안됨.
        FILE_SHARE_READ,       
        NULL,                  
        OPEN_EXISTING,         // 생성방식. 샌더이기에 기존에 만들어져 있을 메일 슬롯을 여는 것.
        FILE_ATTRIBUTE_NORMAL,  
        NULL                   
    );

    WriteFile(
        hMailSlot,                           // 메일 슬롯 지정
        messageBox,                          // 전송 데이터 버퍼
        _tcslen(messageBox) * sizeof(TCHAR), // 전송 크기
        lpTransmitResult,                    // 전송 결과
        NULL
    );

    CloseHandle(hMailSlot);

    return 0;
}

✏️ Signaled vs Non-Signaled

💻 커널 오브젝트의 상태

커널 오브젝트: 커널에 의해 관리되는 리소스 정보를 담고 있는 데이터 블록.
상태를 반영하기 위한 boolean타입 멤버 변수가 있을 것.
ex) true -> Signaled, false -> Non-Signaled
커널 오브젝트가 의미하는건 리소스 별로 다르기 때문에, 커널 오브젝트의 상태 또한 리소스에 따라 다르다.
1) 부모 프로세스가 자식 프로세스를 생성하면 커널 오브젝트가 생성됨. 커널 오브젝트는 Non-Signaled 상태 -> 즉, 프로세스가 현재 실행 중
2) 프로세스가 종료 -> 커널 오브젝트는 Signaled 상태로 바뀜.

  • 커널 오브젝트의 상태가 필요한 이유
    • 현재 상태를 참조함으로서 해당 커널 오브젝트의 핸들을 가지고 있는 다른 프로세스가 이 프로세스의 상태를 알 수 있다.
    • 즉 현재 자신의 상태 정보를 통해 프로세스 상태를 알려주기 위함.


상태가 바뀌는 시점

💻 프로세스 관점에서의 상태 관찰 시나리오

1) 부모 프로세스가 자식 프로세스를 생성. 핸들 값을 얻게 됨
2) WaitForSingleObject(): 해당 커널 오브젝트가 Signaled 상태가 되기를 기다리는 함수. 부모 프로세스는 현재 자식 프로세스가 종료됐는지, 실행 중인지 알고 싶을 때 이 함수의 반환 값을 통해 알 수 있다. 종료(Signaled)됐다면 바로 반환되고, 실행 중이라면(Non-Signaled) 블로킹된다.

  • 블로킹: 함수를 빠져나오지 않고 잠시 멈춰있는 상태.
    어떤 리소스가 Signaled 상태가 되기를 기다릴 때, 해당 리소스의 커널 오브젝트를 함수 인자로 전달하면 실행 중일 때 반환된다.

  • 예제 모델

    1) 하나의 프로그램 내에서 3개의 프로세스를 생성.
    2) 부모 프로세스가 자식인 A, B 프로세스에게 1~10의 합 계산을 나눠서 요청하는 형태.
    3) 계산이 종료된다면 자식 프로세스들은 종료코드를 반환.
    4) 부모 프로세스는 자식들이 반환한 결과를 합해서 최종 결과를 도출.
    이 때 부모 프로세스는 자식 프로세스들이 종료될 때까지 연산을 진행해서는 안 된다. 그러나 보통은 프로세스 생성하는데 연산이 많이 필요하기 때문에 부모 프로세스의 결과 도출 연산이 자식 프로세스가 종료되기도 전에 진행되어서 엉뚱한 결과가 나온다.

  • 해결 방법

    • WaitForSingleObject() 함수를 이용.
    • 부모 프로세스는 각 자식 프로세스가 종료되기를, 즉 커널 오브젝트의 상태가 Signaled 상태가 되기를 기다려야 한다.
    • 자식 프로세스의 핸들 값을 해당 함수에 전달하면 자식 프로세스가 Signaled 상태가 되어 함수가 반환되기 전까지 부모 프로세스는 블로킹 상태에 놓인다.
    • 자식 프로세스가 종료되어 함수를 빠져나오면 부모 프로세스의 연산이 진행된다.
  • 위와 같은 멀티 프로세스 구조의 사용 이유

    • 모든 코어가 연산에 참여하게 만들어 최적화된 성능을 내기 위해.
    • 계산이 복잡해질 경우 연산량이 많아진다. 코어가 4개일 경우 동시에 실행 가능한 프로세스 수가 4개. 위 과정처럼 나눠서 연산하면 모든 코어가 연산에 참여하게 할 수 있다.

0개의 댓글