Notice
Recent Posts
Recent Comments
Link
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

codingfarm

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

Windows/윈도우즈 시스템 프로그래밍

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

scarecrow1992 2021. 5. 14. 23:07

학습 목표

  • IPC 기법에 대해 이해한다.
  • 커널 오브젝트의 두가지 상태(signaled & Non-Signaled)와 핸들 테이블에 대해 이해한다.

 

 


1. 프로세스간 통신(IPC; Inter Process Communication)

  • 프로세스는 각각 독립된 메모리 공간을 소유하므로 IPC가 어렵다.

  • 프로세스는 자신에게 할당된 메모리 공간 이외에는 접근이 불가능하다.
  • 그러므로 IPC를 위한 별도의 통신수단이 필요하다

 

 

 


2. 메일슬롯 방식의 IPC

메일 슬롯(Mail Slot)의 원리

  • 대표적인 IPC 기법중 하나
  • 프로세스 간에 데이터를 주고 받기 위해 접선 장소를 마련한다.
  • 단방향 통신만 가능

mailslot의 생성과 연결은 CreateMailSlot과 CreateFile 함수를 통해 이루어지며

데이터 수신 및 송신은 ReadFile과 WriteFile을 통해 이루어진다.

 

 

mailslot 정의

  • 프로세스간 데이터를 받기 위한 역할
  • 커널에 의해 관리되는 resource이므로 kernel object가 있으며, handle로 접근함
  • 단방향 통신
  • broadcasting 불가
  • 주소를 부여하여 외부 proces가 접근 가능
  • 각기 다른 프로세스에 동일한 주소의 mail slot 제작이 가능
    • broad cast 방식의 송신 가능
  • 생성과 동시에 usage count가 1이다
    1. mailslot을 생성한 프로세스 하나만 참조한다
    2. process와 thread를 제외한 다른 모든 resource의 kernel object는 생성과 동시에 usage count가 하나임

 

 

mailslot 구성을 위해 필요한 요소

1. Receiver가 준비할것

CreateMailSlot 함수로 mailslot을 생성

1
2
3
4
5
6
HANDLE CreateMailslot(
  LPCTSTR               lpName,
  DWORD                 nMaxMessageSize,
  DWORD                 lReadTimeout,
  LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
cs
인수 기능
LPCTSTR  lpName ▶ 생성하는 메일슬롯의 이름을 결정
▶ 주소의 기본 형식
\\computername\mailslot\[path]name
$\rightarrow$ "\\\\.\\mailslot\\mailbox"
DWORD  nMaxMessageSize ▶ mailslot의 버퍼 크기를 지정
  - 0 : 시스템이 허용하는 최대 크기로 지정
DWORD   lReadTimeout ▶ 최대 blocking 시간을 millisecond 단위로 지정하는데 사용
  - 0 : blocking 상태 없이 빠져나옴
  - MAILSLOT_WAIT_FOREVER : 읽어들일 데이터가 존재하게 될 때까지 브로킹 상태에 놓임
LPSECURITY_ATTRIBUTES
lpSecurityAttributes
▶ 핸들의 상속 여부를 결정
  - NULL : 상속X
return value  생성된 mailslot kernel object의 handle

생성된 메일슬롯에 들어온 데이터는 ReadFile 함수를 통해 읽을 수 있다.

CreateMailSlot과 ReadFile을 이용하는 뼈대코드

1
2
3
4
5
6
7
8
9
10
// Recevier의 skeleton code
 
HANDLE hMailSlot;
hMailSlot = CreateFile("\\\\.\\mailslot\\mailbox", ...);
 
...
...
 
CHAR message[50];
WriteFile(hMailSlot, message, ...);
cs

 

 

2. sender가 준비할 것

  • CreateFile 함수로 mail slot에 접근하고 WriteFile을 통해 데이터를 전송
  • Receiver측에서 만든 mailslot의 이름(주소)를 알아야 한다.

주소체계

"\\Computername\mailslot\[path]name"
ex) "\\.\mailslot\abc\def\mailbox"

. : local computer(현재 자신이 쓰는 컴퓨터)

computer name : 동일한 네트워크 도메인에 존재하는 호스트

* : 네트원크로 연결된 모든 같은 조소로 생성된 mailslot

 

데이터 스트림

  • mailslot에 data 전송을 위해 필요
  • 개념적인 표현임
  • sender에서 receiver의 mailslot과 연결하게끔 해주는 다리 역할
  • resource이므로 kernel object와 handle의 생성을 동반함

 

 

BroadCasting 방식의 통신

하나의 sender가 한번에 여러개의 receiver에게 동일한 메시지를 전송

 

 

예제 코드

receiver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// MailReceiver.cpp
 
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
 
#define    SLOT_NAME    _T("\\\\.\\mailslot\\mailbox")
 
int _tmain(int argc, TCHAR* argv[]) {
    HANDLE hMailSlot;
    TCHAR messageBox[50];
    DWORD bytesRead;
 
    hMailSlot = CreateMailslot(
                SLOT_NAME,                // 주소
                0,                        // 버퍼 크기를 허용 최대치로
                MAILSLOT_WAIT_FOREVER,    // ReadFile 함수 특성
                NULL                    // 보안 설정(handle 미상속)
                );
 
    if (hMailSlot == INVALID_HANDLE_VALUE) {
        _fputts(_T("Unable to create mailslot!\n"), stdout);
        return 1;
    }
 
    // Message 수신
    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;    // NULL 문자 삽입
        _fputts(messageBox, stdout);
    }
 
    CloseHandle(hMailSlot);
    return 0;
}
cs

 

sender

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// MailSender.cpp
 
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
 
#define SLOT_NAME    _T("\\\\.\\mailslot\\mailbox")
 
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;
    }
 
    // Message 송신
    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;
        }
 
        // exit 입력시 종료
        if (!_tcscmp(message, _T("exit\n"))) {
            _fputts(_T("Good Bye!"), stdout);
            break;
        }
    }
    CloseHandle(hMailSlot);
    return 0;
}
 
cs

 

실행 결과

receiver를 먼저 실행하여 mailslot을 생성하고, sender를 실행하여 연결을 시도해야한다.

sender에서 입력한대로 receiver측에서 출력이 발생하는 프로그램이며, exit를 입력하면 연결이 종료된다.

특이한점은 sender측의 _tcslen 함수에서 "exit\n"과 비교하는 부분에서 \n이 빠지면 제대로 종료가 안되지먄

receiver측에서는 "exit\n" 에 \n이 빠저도 제대로 종료된다. 아직 이유는 모르겠다.

 

 

 

 


3. Signaled vs Non-Signaled

kernel object state

  • signaled(TRUE), non-signaled(FALSE) 2가지로 나뉨
  • resource에 특정 상황이 발생했음을 알려주기 위해 존재
    • 특정상황은 resource에 따라 다름
    • 그러므로 kernel object의 상태가 변하는 시점은 그 종류에 따라 달라짐

 

process kernel object의 상태 변화

  • process가 생성되면서 process kernel object가 만들어지며, 최초의 상태는 Non-signaled가 된다.
    • Non-Signaled : 프로세스 실행 중
    • Signaeld : 프로세스 종료

프로세스의 커널 오브젝트는 Signaled 에서 Non-Signaled로 전환 불가능 하다

(이는 리소스의 종류마다 서로 상이한 특성임)

 

 

커널 오브젝트의 상태 확인하기

WaitForSingleObject 함수를 통해 알 수 있다.

1
2
3
4
DWORD WaitForSingleObject(
  HANDLE hHandle,
  DWORD  dwMilliseconds
);
cs
  • hHandle : 상태를 확인하기 원하는 커널 오브젝트의 핸들
  • dwMilliseconds : 상태를 확인중인 커널 오브젝트가 Signaled가 될 때까지 기다리는 시간
    • INFINITE : 무한정 대기
    • 정수 값 : milliseconds 시간만큼 대기
  • return value
    • WAIT_OBJECT_0 : 커널 오브젝트가 Signaled 상태가 되었을 때 반환하는 값
    • WAIT_TIMEOUT : 설정된 시간이 다 될 경우 반환하는 값
    • WAIT_ABANDONED : 소유 관계와 관련하여 함수가 정상적이지 못한 오류 발생에 의해 반환되는 경우(13장에서 배움)

 

둘 이상의 kernel object를 확인하고 싶을 경우 WaitForMultipleObjects 함수로 판별 가능

1
2
3
4
5
6
DWORD WaitForMultipleObjects(
  DWORD        nCount,
  const HANDLE *lpHandles,
  BOOL         bWaitAll,
  DWORD        dwMilliseconds
);
cs
  • nCount : 배열의 크기
  • lpHandles : 배열의 시작 주소
  • bWaitAll
    • TRUE : 관찰 대상이 모두 Signaled 상태가 되기를 기다린다.
    • FALSE : 하나라도 signaled 상태가 되면 반환
  • dwMilliseconds : 타임아웃 설정

 

 

프로세스의 종료 코드(Exit Code)

프로세스는 main 함수의 return 값을 통해 자신이 어떻게 종료되었는지에 대한 정보를 담은 exit code를 남길 수 있다.

이 exit code는 해당 프로세스의 kernel object 내에 남게 되며, GetExitCodeProcess를 호출하여 값을 얻어올 수 있다.

 

 

 

예제 코드

커널 오브젝트의 상태 확인이 필요한 상황을 연출해보자

부모 프로세스에서 A와 B 2개의 자식프로세스를 생성한다.

각 자식 프로세스는 1~5, 6~10 의 합연산을 처리하며

연산이 끝나면 결과값을 부모 프로세스에서 받아 두 수의 합을 출력하는 프로그램이다.

위 프로그램 작성시 주의할 부분은

 

프로세스 A와 B의 연산이 끝난후에 부모프로세스에서 합 연산이 진행되어야 한다는 점이다.

이를 위해 프로세스를 동기화할 필요가 있으며, 여기에서 앞서 배운 WaitForSingleObject나 WaitForMultipleObject를 사용하겠다

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// PardAdder.cpp
 
#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;
}
cs

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// WaitForObject.cpp
 
#include<stdio.h>
#include<tchar.h>
#include<windows.h>
 
using namespace std;
 
int _tmain(int argc, TCHAR* argv[]) {
    STARTUPINFO si1 = { 0, };
    STARTUPINFO si2 = { 0, };
 
    PROCESS_INFORMATION pi1, 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,
        NULLNULL, TRUE,
        0NULLNULL,
        &si1, &pi1);
 
    CreateProcess(NULL, command2,
        NULLNULL, TRUE,
        0NULLNULL,
        &si2, &pi2);
 
    CloseHandle(pi1.hThread);
    CloseHandle(pi2.hThread);
 
    /*
    HANDLE handles[2];
    handles[0] = pi1.hProcess;
    handles[1] = pi2.hProcess;
 
    WaitForMultipleObjects(2, handles, TRUE, INFINITE);
    */
    
    WaitForSingleObject(pi1.hProcess, INFINITE);
    WaitForSingleObject(pi2.hProcess, INFINITE);
 
    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);
 
    system("pause");
    return 0;
}
cs

 

 

 

 

 

 

 

 

Comments