본문 바로가기
C/[서적] 뇌를 자극하는 윈도우즈 시프

Chapter 08. 프로세스간 통신(IPC) 2

by GameStudy 2022. 2. 4.

8.1 핸들 테이블과 오프젝트 핸들의 상속

 

8.1-1 핸들 테이블의 이해 점검

  Note) 핸들 테이블과 메일 슬롯

    1. 프로세스가 메일 슬롯을 생성함.

      메일 슬롯 또한 커널 오브젝트의 생성을 동반하는 리소스.

      그럼 메일 슬롯의 커널 오브젝트 핸들값과 주소정보가 

      프로세스의 핸들 테이블에 등록이 됨.

    2. 가장 중요한 것은, 핸들 테이블은 프로세스에 종속 적이란 것.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

8.1-2 핸들 테이블의 상속

  Note) 핸들 테이블의 상속 여부 항목 추가.

    - 이전까지 핸들 테이블에는 핸들값과 주소값 밖에 없었음.

      이제 하나 더 추가되어서, 상속 여부라는 항목까지 추가됨.

    - 상속 여부라 함은, 조건에 맞다면 부모 프로세스의 핸들 테이블로부터

      자식 프로세스의 핸들 테이블로 상속이 됨을 의미함.

      즉, 부모 프로세스의 핸들 테이블 특정 정보를 자식 프로세스에게 

      상속 되게끔 할 수 있다는 뜻.

    - 일단 상속이 되면, 핸들값/주소값/상속여부 모두 같은 값이 

      자식 프로세스에게 상속됨.

    - 그렇다면 두 가지 궁금증이 생김.

      1. 어떠한 경우에 상속여부가 Yes가 되느냐, 즉, 어떻게 설정하는 것이냐.

        이때, 상속 여부가 결정되는 핸들값이

        어떤 커널 오브젝트가 무엇인지는 안 중요함. 프로세스든, 메일 슬롯이든..

      2. 상속이 가능하려면, 어떤 조건이 필요하느냐.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 다시보는 CreateProcess() 함수

    일단, 부모 프로세스의 핸들 테이블을 자식 프로세스에게 

    상속해 줄거냐 말거냐가 좀 더 우선적인 사항.

    이건 자식 프로세스를 생성할 때, bInheritHandles 매개변수의

    인자값으로 설정가능.

BOOL CreateProcess(
    LPCSTR                lpApplicationName,   // 실행 파일의 이름
    LPSTR                 lpCommandLine,       // 실행 파일에게 전달할 인자값
    LPSECURITY_ATTRIBUTES lpProcessAttributes,
    LPSECURITY_ATTRIBUTES lpThreadAttributes,
    BOOL                  bInheritHandles,     // 핸들 테이블 상속 여부
    DWORD                 dwCreationFlags,
    LPVOID                lpEnvironment,
    LPCSTR                lpCurrentDirectory,  
    LPSTARTUPINFOA        lpStartupInfo,       // 프로세스의 특성 정보
    LPPROCESS_INFORMATION lpProcessInformation // 생성된 프로세스의 특정 정보
);

 

8.1-3 핸들의 상속

  Note) 핸들의 상속

    핸들 테이블에 핸들값의 상속 여부를 설정하는 방법.

    대부분의 리소스들이 아래와 같은 방법으로 설정함.

    SECURITY_ATTRIBUTES는 보안 관리자를 설정하기 위한 구조체임.

    지금까지는 NULL을 넣었던 자리임. NULL을 넣으면 자동으로 상속 여부는 No.

    그러나 이를 적절히 설정하면 핸들값의 상속 여부를 Yes로도 설정 가능.

    즉, 잘 설정된 sa 구조체 변수를 리소스 생성할 때

    전달하면 상속 여부도 설정 가능하단 뜻.

... 중략 ...
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;
... 중략 ...
CreateMailSlot(..., &sa); // &sa는 4번째 전달인자
... 중략 ...

 

  Note) 다른 리소스들은 어떻게 설정하나?

    대부분의 리소스들은 생성함수에서 SA 구조체의 포인터를 요구함.

 

8.1-4 핸들의 상속과 UC

  Note) 핸들 상속 이전 UC

    다시 이야기하지만, 어떤 리소스의 커널 오브젝트를 가리키는지는 안중요.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 핸들 상속 이후 UC

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Ex8-1,Ex8-2)

    1. 리시버 프로세스가 메일 슬롯 리소스를 만듦.

    2. 샌더 프로세스는 일단 리시버 프로세스와 사이에

      연결 고리를 파일 생성 함수로 만들어냄.

      사실 이 연결 고리도 OS가 관리하는 리소스이기 때문에

      커널 오브젝트 생성을 동반함.

      그게 0x1700번지에 있는 커널 오브젝트.

      이 연결 고리 리소스의 커널 오브젝트 속 UC는 현재 1임.

      그리고 이 연결 고리 리소스를 생성할 때, 상속 여부는 Y로 설정.

    3. 샌더 프로세스는 자식 프로세스를 생성하면서

      핸들 테이블 상속 여부를 Y로 결정.

      따라서 자식 프로세스의 핸들 테이블에는 샌더 프로세스가 생성한

      연결 고리 리소스의 커널 오브젝트 핸들값도 상속되게됨.

      이 덕분에 연결 고리 리소스의 커널 오브젝트 속 UC는 현재 2가 됨.

    4. 결론적으로 샌더 프로세스도 리시버 프로세스에 데이터를 전송할 수 있고,

      샌더 프로세스의 자식 프로세스도 리시버 프로세스에 데이터를 전송할 수 있게됨.

    5. 한 가지 확인해야 할 점. 부모 프로세스를 통해서 127이라는 핸들값이

      자식 프로세스로 상속이 되었는데, 정작 자식 프로세스는 이를 모를수도 있음.

      이를 어떻게 자식 프로세스에게 상속되었음을 알릴 것인가가 이슈.

      간단하게는 파일을 오픈해서 알릴 수도 있고,

      자식 프로세스가 생성될 때, _tmain() 함수의 인자를 통해서 전달할 수도 있음.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

 

8.1-5 Pseudo 핸들과 핸들의 중복 시나리오

  Note) 시나리오 1

    A 프로세스가 생성되면 A 프로세스의 커널 오브젝트 핸들값이

    핸들 테이블에 등록이 되지 않음. 대신 GetCurrentProcess() 함수를

    호출하면 반환되어 지는 값에 가짜 핸들값. 이 반환되는 특정값은

    항상 일치함. 이 특정 값을 커널 즉, OS가 프로세스 자신의 

    커널 오브젝트 핸들값으로 인식해 줄 뿐임. 핸들 테이블에 등록되는

    핸들 값이 아니기에 가짜 핸들값임. 핸들이려면 핸들 테이블에 등록되어야 함.

   

  Note) 그런데, 가끔 이 가짜 핸들값을 진짜 핸들값으로 만들어야 할 때가 있음.

    A 프로세스가 B 프로세스를 생성함. 이때 A 프로세스의 커널 오브젝트 핸들값을

    자식 프로세스인 B에게 전달하고 싶음. 가능할까? 어떻게 해야할까?

    지금 배운 내용으로는 핸들 테이블을 상속해서 해결 해 보자.

    근데 쟁점들이 있음.

    1. A 프로세스의 핸들 테이블에는 A 프로세스의 커널 오브젝트 핸들값이 없음.

    2. 도대체 왜 전달해야만 할까?

 

  Note) 2번 쟁점 먼저 고민 해 보자.

    부모 프로세스의 커널 오브젝트 핸들값을 자식 프로세스가 안다면,

    부모 프로세스가 종료되기를 기다릴 수가 있음.

 

  Note) 1번 쟁점의 해결 방법: DuplicateHandle() 함수의 호출.

    이 함수의 중요 인자값 4개만 먼저 이해해 보자.

    - A 핸들: 프로세스의 핸들 정보

    - 256: 복사 하고픈 핸들 정보

    - B 핸들: 프로세스의 핸들 정보

    여기까지 정리하면,

    A 프로세스의 핸들 테이블에 등록된 256이라는 핸들을

    B 프로세스의 핸들 테이블에 복사해라 라는 뜻.

    - &val: A 프로세스의 핸들 테이블에 등록된 256이라는 핸들이

      그대로 B프로세스의 핸들테이블에 복사될 수도 있지만,

      다른 값으로 복사가 될 수도 있음. 이때 이 달라진 값을 val에 적어줌.

    - 결론적으로, 프로세스 A는 256이라는 핸들로 특정 커널 오브젝트에

      접근을 할 것이고, 프로세스 B는 364라는 핸들로 접근할 수 있게 됨.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 시나리오 2

    DuplicateHandle( 프로세스 A 핸들, 256, 프로세스 A 핸들, &val, ...)

    위 명령어는 결국 "프로세스 A의 핸들 테이블에 등록된 256이라는 핸들을

    프로세스 A의 핸들 테이블에 등록하고, 실제 등록된 핸들값을 val에 저장해라"임.

    따라서, 프로세스 A의 핸들 테이블에는 핸들 256이 가리키는 커널 오브젝트와 

    똑같은 커널 오브젝트를 가리키는 또 다른 값의 핸들 284(값 중복 금지)가 복사됨.

    따라서 해당 커널 오브젝트 속 UC는 2가 됨.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

  Note) 시나리오 3. 자식 프로세스에게 부모 프로세스의 핸들 전달 방법 최종.

    부모 프로세스가 종료하기를 기다리기 원한다면 아래와 같이 작성해야 함.

    "자기 자신 프로세스의 핸들 테이블에 등록된 자기 자신 프로세스의 핸들을

    자기 자신 프로세스의 핸들 테이블에 등록해라."라는 명령에 의해

    결국 자신의 핸들 테이블에 자신의 커널 오브젝트 핸들을 등록할 수 있게 됨.

    즉, 사용 가능한 핸들이 됨.

    관련 예제: Ex8-3, Ex8-4

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

8.2 파이프 방식의 IPC

 

8.2-1 IPC별 특성

  Note) IPC별 특성 표

    방향성은 만약 채팅을 구현한다고 했을때, 단방향의 경우 번거로움.

    양방향의 경우 한 번 서로 연결되면 서로 주거니 받거니 가능.

    통신 범위라 함은, 동일한 컴퓨터 상에서만 통신 가능하냐 

    아니면 네트워크 상에 연결된 독립된 컴퓨터 상에서도 통신 가능하냐

  메일 슬롯 이름 없는 파이프 이름 있는 파이프
방향성 단방향, 브로드캐스팅 단방향 양방향
통신범위 제한 없음 부모-자식 프로세스 제한 없음

 

  Note) 이름 있는 Vs. 이름 없는

    이름이라 함은 주소를 의미함. 이 주소는 네트워크 상에 연결 되어 있는

    PC를 구분하기 위한 고유 주소를 의미함.

    즉, 이름이 있다는 것은 네트워크 상의 외부 PC와 통신 가능하다는 뜻.

    이름이 없다는 것은 네트워크 상의 외부 PC와 통신이 불가능하다는 뜻.

    이름이 없을때, 통신의 두 대상 사이에는 특별한 연결 고리가 있어야 함.

    이는 다음에 배우게 됨.

 

  Note) 언뜻보기에 이름 없는 파이프는 상대적으로 덜 중요해 보이지만

    이름 있는 파이프 만큼 중요한 기법임.

 

  Note) 위에서 브로드캐스팅의 특성을 가진 방식은 메일 슬롯이 유일함.

    그러나 추후에 TCP/IP의 UDP 통신 기법을 배우면 브로드캐스팅이 가능함.

    또한 메일 슬롯 기반 방식으로 브로드캐스팅을 거의 구현하진 않음

    소켓 통신을 이용해서 브로드캐스팅을 구현하는 경우가 대부분.

 

 

8.2-2 이름 없는 파이프

  Note) 아래 예제는 이름 없는 파이프의

    기본 특성만 설명하는 예제. 활용 예제가 아님.

    그럼 활용은 어디서 가능할까? 일단은 기본 특성만 잘 파악하자.

    그리고 나중에 프로젝트에서 활용할 때 느낌이 오면 됨.

 

  Ex8-5)

    프로세스에서 이름 없는 파이프를 생성함. 

    이름 없는 파이프는 단방향성이므로,

    파이프에 입구와 출구가 정해져있다고 해보자.

    즉, 이 프로세스는 파이프의 입구와 출구를 갖고 있음.

    다시말해, 프로세스 혼자서 입구에 데이터를 넣고 출구로 받을수 있음.

    이걸 해보는 예제. 이때 출구가 hReadPipe, 입구가 hWritePipe.

    CreatePipe()를 하게 되면 두 개의 입구를 얻게 되는 것.

    WriteFile()을 통해서 입구로 데이터를 송신.

    ReadFile()을 통해서 출구로 데이터를 수신.

<hide/>

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

int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hReadPipe, hWritePipe; // pipe handle
    TCHAR sendString[50] = _T("Hello, Pipe!");
    TCHAR recvString[50] = _T("\0");
    DWORD bytesWritten = 0;
    DWORD bytesRead = 0; 

    CreatePipe(&hReadPipe, &hWritePipe, NULL, 0);

    WriteFile(
        hWritePipe,
        sendString,
        lstrlen(sendString) * sizeof(TCHAR),
        &bytesWritten,
        NULL
    );
    _tprintf(_T("String send: %s\n"), sendString);

    ReadFile(
        hReadPipe,
        recvString,
        bytesWriten,
        &bytesRead,
        NULL
    );

    // ... 중략 ...

    return 0;
}

 

  Note) 위에서 언급된 특성 표를 보면

    이름 없는 파이프의 경우 부모-자식 프로세스 간의 통신 범위를 갖는다고 되어 있음.

    이게 어떻게 가능하냐면, 핸들 테이블의 상속을 통해서 가능한 것.

    풀어서 말하자면, 부모 프로세스가 이름 없는 파이프를 만들면 

    hReadPipe와 hWritePipe가 부모 프로세스의 핸들 테이블에 등록이 됨.

    이때 부모 프로세스는 자식 프로세스를 생성하고 상속이 이뤄지면

    자식 프로세스도 hReadPipe/hWritePipe 정보를 알 수 있음.

    즉, 부모 프로세스가 hWritePipe를 통해 WriteFile() 하면

    자식 프로세스가 hReadPipe를 통해 ReadFile() 가능함. 

    반대의 경우도 충분히 가능해지기 때문에 부모-자식 프로세스 간의 통신 가능.

 

8.2-3 이름 있는 파이프 프로그래밍 모델

  Note) Server와 Client 도식화

    1. 먼저 Server와 Client가 있음.

      이때 단순히 Server는 데이터를 보내기만,

      Client는 단순히 데이터를 받기만 하는게 아님.

      이름 있는 파이프는 양방향 통신이 가능하다고 했기 때문.

    2. Server에서는 이름 있는 파이프를 생성함.

      이때 CreateNamedPipe() 함수를 호출

      다만 이는 Server 프로세스가 이름 있는 파이프를 가지고만 있는 상태.

    3. Server 프로세스는 ConnectNamedPipe() 함수를 호출

      이로써 이름 있는 파이프가 외부에서 누구나 연결할 수 있도록 노출 한 것.

    4. Client는 노출된 이름 있는 파이프를 통해서 연결을 시도하는 외부 프로세스.

      이때 CreateFile() 함수를 호출함. 이를 통해서 이름 있는 파이프 오픈(연결)

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

8.2-4 이름 있는 파이프

  Note) 관련 예제: Ex8-6(Server), Ex8-7(Client)

    서버는 무한 루프를 통해서 계속 돌고 있음.

    클라이언트는 무한 루프를 통해 계속 연결 요청을 함.

    연결이 된 상태에서 서비스 받고 클라이언트가 나가면

    또다시 다른 클라이언트가 연결 요청을 할 수 있는 예제.

    즉, 하나의 서버에 여러 클라이언트가 작업을 할 수 있는 상태.

    여러 클라이언트가 순차 접속해서 작업할 순 있지만,

    동시에 작업할 순 없음.

 

  Note) CreateNamedPipe() 함수 설명

<hide/>

HANDLE hPipe = CreateNamedPipe(
    pipeName,                  // 파이프 이름
    PIPE_ACCESS_DUPLEX,        // 읽기,쓰기 모드 지정
    PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
    PIPE_UNLIMITED_INSTANCES,  // 최대 생성 가능한 인스턴스(여기선 파이프) 개수.
    /*
    - 이 인자를 대신해서 10이라는 인자값을 전달하면
      동시에 생성가능한, 즉 현재 보유할 수 있는 최대 파이프의 갯수가 10개라는 것.
      그럼 클라이언트가 서비스 받고 종료되거나 혹은 서버가 종료된다면
      하나가 줄어서 현재 파이프의 갯수는 9개가 되는 것.
    - 근데 CreateNamedPipe() 함수는 파이프 하나 생성하는 함수.
      왜 하나 생성할 때마다 최대 인스턴스 개수를 지정해줘야하나?
    - 이 인자값은 CreateNamedPipe() 함수의 첫 호출시에만 의미를 갖음.
      즉, 내부에 static 변수가 있을듯. 합리적 의심.
      그럼 두 번째 호출시에는 0으로 둘까? 그게 더 깔끔할거같은데?
      싶지만 MS사에선 그냥 첫 호출시의 값을 유지해주라고 권고함.
    - 또한 이 최대 인스턴스 개수는 파이프 이름별 최대 개수임.
      즉 파이프 A를 만들면 파이프 A에 접근 가능한 최대 파이프의 갯수가 10개.
    */
    BUF_SIZE,                  // 출력버퍼 사이즈
    BUF_SIZE,                  // 입력버퍼 사이즈
    /*
    - 내가 생성한 파이프의 입력 버퍼와 출력 버퍼의 크기를 지정함.
    - 데이터를 상대방 클라이언트에 보내주고 싶은데,
      상대방 클라이언트가 받을 여력이 안됨. 
      근데 프로그램 상에선 Write 해야함. 그때는 출력 버퍼에 넣어둠(write)
    - 마찬가지로 데이터를 서버에 데이터를 보내주고 싶은데,
      서버가 받을 여력이 안됨. 그럴땐 입력 버퍼에 넣어둠(Write)
    */
    20000,                     // 클라이언트 타임-아웃  
    /*
    - 클라이언트가 연결 요청을 했는데 현재 파이프이름으로 생성된
      파이프의 인스턴스 개수가 최대치에 도달함.
      즉, 더이상의 연결 요청을 받을 수가 없음.
    - 그럼 클라이언트는 기다려야 함.
      그럴 때, 서버가 클라이언트에게 특정 ms 만큼 기다리라고 지정해 주는 것.
      근데 클라이언트가 따로 어느정도 기다리겠다고 지정할 수도 있음.
    */
    NULL                       // 디폴트 보안 속성
);

 

 Note) FlushFileBuffers(hPipe) 함수와 DisconnectNamedPipe(hPipe) 함수

<hide/>

FlushFileBuffers(hPipe);
/*
  - FlushFileBuffers() 함수는 Write buffer를 비우는 함수.
    read buffer는 다 읽으면 그냥 끝임. 비우고 자시고 할게 없음.
  - 버퍼는 모아서 보내는 특징을 지님.
    멀리까지 가는데 조금씩 보내면 성능면에서 떨어지게 됨.
    한번에 모아서 보내는게 좋음.
  - 근데 마지막 데이터를 보내는데, 이게 1byte라면 전송이 안될 수도 있음.
    즉 전송 실패시 버퍼를 비워주는 역할.
  - 근데 항상 마지막에만 하는게 아님. 도중에도 비울 필요가 있다면 호출 가능한 함수.
    ex. fflush(stdin);
*/
DisconnectNamedPipe(hPipe);
/*
  - CloseHandle(hPipe) 함수는 파이프를 소멸시키는 함수가 아님.
    해당 핸들이 가리키는 커널 오브젝트 속 UC를 하나 감소시키는 역할일 뿐.
  - 따라서 누군가는 명시적으로 "더이상 통신 안하겠다"라고 클라이언트쪽에 알려줘야함.
    그게 DisconnectNamedPipe(hPipe) 함수.
  - 이 함수가 호출된 후부터는 클라이언트 쪽에서 해당 파이프로 Read-Write 했을때
    에러가 발생하게됨.
*/
CloseHandle(hPipe);

 

  Note) WaitNamedPipe() 함수

<hide/>

while (1)
{
    hPipe = CreateFile(
        pipeName,             // 파이프 이름 
        GENERIC_READ | GENERIC_WRITE, // 읽기, 쓰기 모드 동시 지정 
        0,
        NULL,
        OPEN_EXISTING,
        0,
        NULL
    );
    /*
    CreateFile() 함수는 파일 생성 함수이다 보니, 파이프의 특성을 설정하는데 부족함.
    예로들어, 파이프를 생성한 다음에 메세지 기반으로 통신할 것인지 바이너리 기반으로 통신할 것인지 설정 불가.
    그래서 뒤이어 나올 코드에서 SetNamedPipeHandleState() 함수를 통해서 특정을 추가적으로 설정함.
    */

    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;
    }
    /* 전체적인 While(1) 내의 시나리오
    - CreateFile()이 While(1) 안에 있음.
      이건 연결 요청을 무한정 하겠다는 뜻은 아님.
      일단 연결 요청을 함.
    - hPipe != INVALID_HANDLE_VALUE
      연결 요청한 파이프가 유효하지 않은 값이 아니라면 탈출해서 다음 작업 실행.
    - GetLastError() != ERROR_PIPE_BUSY
      여유분의 파이프 인스턴스 개수가 없는건 아닌데
      다른 이유라면 그냥 종료.
    - !WaitNamedPipe(pipeName, 20000)
      해당 파이프의 여유 인스턴스 개수가 생길때까지 기다린다.
      20000ms 동안만 기다리겠다.
      근데 20000이 아니라 default로 설정하면 
      서버에서 정해준 시간만큼만 기다리게되는 것.
      서버가 클라이언트의 특성을 지정하는 유일한 경우. 대부분 그러지 못함.
      어찌되었든 WaitNamedPipe() 함수는 만약 연결 가능한 상태가 된다면
      즉, 파이프 인스턴스 여유분이 생긴다면 true를 반환함.
    */
}

 

   Note) SetNamedPipeHandleState() 함수

<hide/>

DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_WAIT; // 메시지 기반으로 모드 변경.
/*
  - 여기서 readmode만 메세지 기반으로 설정하는 이유는? 
    CreateFile()할 때 결정되기 때문.
  - 일단 이게 중요한게 아니고, 만약 바이너리 방식으로 통신한다고 가정해 보자.
    어찌되었든 받는 쪽에서 메세지 형식으로 받으면 메세지 타입으로 받게되는거임.
    일반적으론 통일을 시켜주는게 맞긴하지만, 문제될건 없음.
    클라이언트가 어떤 방식으로 보내든 서버 입장에선 바이너리 방식으로 읽겠다하면
    그냥 바이너리 방식으로 받는거임.
  - 즉 결론적으로 서버의 설정과 클라이언트의 설정은 사실상 별개의 내용.
    파이프의 특성 설정에 있어서의 특징.
*/
BOOL isSuccess = SetNamedPipeHandleState(
    hPipe,     // 파이프 핸들
    &pipeMode, // 변경할 모드 정보.  
    NULL,      // 설정하지 않는다. 
    NULL       // 설정하지 않는다. 
);

 

  Ex8-6(Server), Ex8-7(Client))

<hide/>

/*
    namedpipe_server.cpp
    프로그램 설명: 이름 있는 파이프 서버.
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h> 

#define BUF_SIZE 1024

int CommToClient(HANDLE);

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

    HANDLE hPipe;

    while (1)
    {
        hPipe = CreateNamedPipe(
            pipeName,                  // 파이프 이름
            PIPE_ACCESS_DUPLEX,        // 읽기,쓰기 모드 지정
            PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
            PIPE_UNLIMITED_INSTANCES,  // 최대 생성 가능한 인스턴스(여기선 파이프) 개수.
            BUF_SIZE,                  // 출력버퍼 사이즈.
            BUF_SIZE,                  // 입력버퍼 사이즈 
            20000,                     // 클라이언트 타임-아웃  
            NULL                       // 디폴트 보안 속성
        );

        if (hPipe == INVALID_HANDLE_VALUE)
        {
            _tprintf(_T("CreatePipe failed"));
            return -1;
        }

        BOOL isSuccess;
        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,                 // read 버퍼 지정.
        MAX_PATH * sizeof(TCHAR), // read 버퍼 사이즈 
        &fileNameSize,            // 수신한 데이터 크기
        NULL);

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

    FILE* filePtr = _tfopen(fileName, _T("r"));

    if (filePtr == NULL)
    {
        _tprintf(_T("File open fault! \n"));
        return -1;
    }

    DWORD bytesWritten = 0;
    DWORD bytesRead = 0;

    while (!feof(filePtr))
    {
        bytesRead = fread(dataBuf, 1, BUF_SIZE, filePtr);

        WriteFile(
            hPipe,				// 파이프 핸들
            dataBuf,			// 전송할 데이터 버퍼  
            bytesRead,		    // 전송할 데이터 크기 
            &bytesWritten,	    // 전송된 데이터 크기 
            NULL);

        if (bytesRead != bytesWritten)
        {
            _tprintf(_T("Pipe write message error! \n"));
            break;
        }
    }

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

 

<hide/>

/*
    namedpipe_client.cpp
    프로그램 설명: 이름 있는 파이프 클라이언트.
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

#define BUF_SIZE 1024

int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hPipe;
    TCHAR readDataBuf[BUF_SIZE + 1];
    LPTSTR 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"));
        return 0;
    }

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

    isSuccess = WriteFile(
        hPipe,                // 파이프 핸들
        fileName,             // 전송할 메시지 
        (lstrlen(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] = 0;
        _tprintf(_T("%s \n"), readDataBuf);
    }

    CloseHandle(hPipe);
    return 0;
}

 

8.3 프로세스 환경변수

Prologue) 일반적으로 프로세스의 환경변수는 IPC로 안봄.

  가끔 환경 변수를 IPC로 취급할 때가 있음.

  그냥 프로세스의 독립적인 기능이라고 생각해도됨.

  근데 왜 IPC의 관점으로 보냐면,

  환경변수가 상속 가능한 프로세스의 데이터 블록이기 때문.

  즉, 통신으로 활용 가능한 부분이 있음. 일반적으론 통신으로 안씀.

  일반적으로는 그냥 자신만의 고유 정보를 담았다 뺏다하는 용도로만 씀.

  부모-자식 프로세스 사이의 통신은 부가적인 관점인거임.

 

8.3-1 프로세스 환경변수의 구성

  Note) 도식화

    - 환경변수는 프로세스 별로 독립적인 메모리 공간에 할당됨.

      어떻게 저장이 되냐면, 키-벨류 쌍으로 저장을 해야 함.

    - 사용을 할때는 SetEnvironmentVariable() 함수와

      GetEnvironmentVariable() 함수를 통해서 사용 하게 됨.

      첫 인자의 문자열이 키, 두 번째 인자의 문자열이 벨류

    - 그럼 어떻게 IPC로 활용가능한가?

      프로세스 A가 부모 프로세스이고, 프로세스 B가 자식 프로세스라면

      부모 프로세스는 자신의 환경 변수 테이블을 자식 프로세스에게 상속할 수 있음.

      즉, 부모 프로세스에서 자식 프로세스로 통신이 된 것.

    - 실제로 부모 프로세스에서 자식 프로세스로 정보를 전달해줘야하는 경우는 많음.

      그리고 그 방법도, 매개변수를 통해서 혹은 파일을 통해서 넘겨줄 수도 있음.

      그럴때 환경변수를 통해서 넘겨주는 기법도 잘 활용 가능하다는 뜻.

      그럼 상당히 안정적인 프로그램 작성 가능.

출처. 윤성우, "뇌를 자극하는 윈도우즈 시스템 프로그래밍"

 

8.3-2 환경변수 전달

  Note) 환경변수 전달

    관련 예제: Ex8-8, Ex8-9

<hide/>

/*
    EnvParent.cpp
    프로그램 설명: 환경 변수 설정하는 부모 프로세스.
*/

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

int _tmain(int argc, TCHAR* argv[])
{
    SetEnvironmentVariable(_T("Good"), _T("morning"));
    SetEnvironmentVariable(_T("Hey"), _T("Ho!"));
    SetEnvironmentVariable(_T("Big"), _T("Boy"));

    STARTUPINFO si = { 0, };
    PROCESS_INFORMATION pi = { 0, };
    si.cb = sizeof(si);

    CreateProcess(
        NULL, _T("EnvChild"), NULL, NULL, FALSE,
        CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
        NULL,    // 부모 프로세스의 환경 변수 등록여부. NULL일 경우, 부모의 환경 변수 테이블이 자식의 환경 변수 테이블로 그대로 등록됨.
        NULL, &si, &pi
    );

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    return 0;
}

 

<hide/>

/*
    EnvChild.cpp
    프로그램 설명: 환경 변수 참조하는 자식 프로세스.
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h> 

#define BUFSIZE 1024

int _tmain2(int argc, TCHAR* argv[])
{
    TCHAR value[BUFSIZE];

    if (GetEnvironmentVariable(_T("Good"), value, BUFSIZE) > 0)
        _tprintf(_T("[%s = %s] \n"), _T("Good"), value);

    if (GetEnvironmentVariable(_T("Hey"), value, BUFSIZE) > 0)
        _tprintf(_T("[%s = %s] \n"), _T("Hey"), value);

    if (GetEnvironmentVariable(_T("Big"), value, BUFSIZE) > 0)
        _tprintf(_T("[%s = %s] \n"), _T("Big"), value);

    Sleep(10000);

    return 0;
}

 

 

 

 

댓글