8장. 프로세스 간 통신(IPC) (2)

유니야·2023년 1월 9일

✏️ 핸들 테이블과 오브젝트 핸들의 상속

💻 핸들 테이블의 이해

1) 프로세스가 메일 슬롯을 생성
메일 슬롯 -> 커널 오브젝트의 생성을 동반하는 리소스.
2) 메일 슬롯에 접근하기 위한 핸들 값과 커널 오브젝트의 주소 정보가 핸들 테이블에 등록된다.
핸들 테이블은 프로세스 별로 종속적.

💻 핸들 테이블의 상속

  • 핸들 테이블의 상속 여부
    • 조건이 맞는다면, 그리고 핸들 테이블의 상속 여부가 Y라면 핸들 테이블은 부모 프로세스에서 자식 프로세스로 상속될 수 있다.
    • 핸들 값과 주소는 동일하게 상속된다.
    • 그렇다면 어떤 경우에 핸들 테이블의 상속 여부가 Y가 되는지.
    • 상속을 위한 조건은 무엇인지.

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 // 생성된 프로세스의 특정 정보
);

💻 핸들의 상속

대부분의 커널 오브젝트의 상속은 아래 코드 형태와 같이 이루어짐.

... 중략 ...
SECURITY_ATTRIBUTES sa;		// 보안 관리자 구조체. 이 구조체가 있는 리소스의 경우 다음과 같이 핸들의 상속 여부를 정해줄 수 있다. 
sa.nLength = sizeof(sa);
sa.lpSecurityDescriptor = NULL;
sa.bInheritHandle = TRUE;	// 핸들의 상속 여부를 Y
... 중략 ...
CreateMailSlot(..., &sa); // &sa는 4번째 전달인자
... 중략 ...
  • 핸들의 상속과 UC

    핸들의 상속 이전 UC

    핸들의 상속 이후 UC

예제

1) Receiver 프로세스는 메일 슬롯을 만들어 데이터가 들어오기를 기다림.
2) Sender 프로세스는 파일 생성 함수를 통해 메일 슬롯과의 연결 고리를 만들고, 핸들을 얻는다. 해당 연결 고리 또한 커널 오브젝트. 이 때 UC는 1.
3) Sender 프로세스는 자식 프로세스를 생성 및 핸들 테이블 정보 복사. 이 때 UC는 2.
4) 자식 프로세스는 해당 핸들 정보를 통해 메일 슬롯에 데이터 송신 가능.
5) 이 때 핸들 값이 127인걸 어떻게 아는지. 가장 기본적인 방법은 파일 이용.

  • Pseudo 핸들과 핸들의 중복 시나리오 (1)

    Pseudo 핸들 = 가짜 핸들
    프로세스 A가 생성 - 프로세스 A의 커널 오브젝트 생성 - 커널 오브젝트의 핸들 정보가 핸들 테이블에 등록됨
    이 때 GetCurrentHandle()함수를 통해 얻어지는 값은 핸들 테이블에 등록된 값이 아닌, Pseudo 핸들. 해당 함수의 반환 값은 항상 일치하는데, 이러한 특정 값을 OS가 프로세스 자신의 커널 오브젝트 핸들 값으로 인식해주는 것.

  • DuplicateHandle()
    부모 프로세스가 자신의 핸들 값을 핸들 테이블에 등록하는 함수.

    • 프로세스 A가 자신의 핸들 값을 자식 프로세스 B에게 상속한다면, 해당 핸들 값은 자식 프로세스의 핸들 테이블에 등록되고 자식 프로세스 입장에서는 부모 프로세스가 먼저 종료되기를 기다릴 수 있다.
    • 그러나 현재 자신의 핸들 값은 핸들 테이블에 등록되지 않은 값이기 때문에 상속할 수 없다.
    • 상속을 위해서는 해당 값을 자신의 핸들 테이블에 등록해야 한다.
    • 이 때 DuplicateHandle() 함수를 이용
    • 인자 -> A 프로세스의 핸들 테이블에 등록된, 256이라는 값을, B 프로세스의 핸들 테이블에 복사
    • 핸들 값은 프로세스 고유의 값이기 때문에 256이라는 값은 프로세스 A에게만 의미가 있음. 복사 시 프로세스 B의 핸들 테이블에는 다른 값으로 복사될 수 있다. 핸들이 가리키는 커널 오브젝트는 같음.
  • Pseudo 핸들과 핸들의 중복 시나리오 (2)

    프로세스가 자신의 핸들 값을 자신의 핸들 테이블에 복사하는 경우, 주소는 똑같지만 핸들 값은 고유 값이기 때문에 다른 값으로 복사가 된다.
    이 때 핸들 테이블에 등록된 것이기 때문에 US는 2. 따라서 CloseHandle() 함수를 두 번 호출해줘야 한다.

  • Pseudo 핸들과 핸들의 중복 시나리오 (3)

    • 자식 프로세스에게 부모 프로세스의 핸들 전달
    • 부모 프로세스가 종료되는걸 기다리기를 원하는 경우.
    • 인자 -> 자신을 가리키는 가짜 핸들 반환받아서, 자신의 핸들 테이블에 복사해라.

✏️ 파이프 방식의 IPC

💻 IPC별 특성

  • 방향성
    메일 슬롯을 통해 두 프로세스가 연결이 되면 리시버 프로세스는 데이터 수신만, 센더 프로세스는 데이터 송신만 가능하기 때문에 통신을 위해서는 두 개의 프로세스가 필요하다.

    • 브로드 캐스팅 -> 리시버 프로세스는 여러 개가 될 수 있고, 센더 프로세스는 한 번의 송신을 통해 여러 프로세스에게 동시에 데이터를 보낼 수 있다.
  • 통신 범위
    동일한 컴퓨터 상에서만 통신이 가능한지, 네트워크 상으로 연결된 다른 컴퓨터 상에서도 통신이 가능한지.
    메일 슬롯의 경우 주소를 기반으로 데이터 통신이 이루어지기 때문에 다른 프로세스 간에도 통신이 가능하다.

  • 이름
    이름 = 네트워크 상 연결되어 있는 PC를 구분할 수 있는 주소. 따라서 이름이 있다면 외부 PC와 통신이 가능하고, 없다면 불가능. 또한 이름이 없다는 건 주소가 없다는 것이기 때문에 통신을 위해서는 다른 연결 고리가 있어야 함.

💻 이름 없는 파이프

한 파이프에는 입구와 출구가 있다. 한 파이프를 소유한다는 것은 입구와 출구 모두 이용할 수 있다는 것.
핸들 정보는 자식 프로세스에게 상속 가능. 부모 프로세스가 자식 프로세스를 만들면 해당 자식 프로세스는 부모 프로세스의 파이프 핸들 정보를 가지게 된다. 두 프로세스 모두 한 파이프를 이용해 데이터 송수신이 가능해지는데 이를 이용해 부모 자식 간 통신이 가능하다.

#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; 

	// 파이프 생성. 입구 & 출구에 접근할 수 있는 핸들정보를 반환받는다. Read = 입구, Write = 출구
    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;
}

💻 이름 있는 파이프

1) CreatNamedPipe() 함수를 이용해 서버 측에서 파이프를 생성. 현재 파이프를 나만 가지고 있는 상황.
2) ConnectNamedPipe() 함수를 통해 파이프를 외부 세상과 연결.
3) 클라이언트 -> 노출되어 있는 파이프에 연결을 시도하는 애. 클라이언트는 CreateFile() 함수를 통해 파이프와 연결
4)

  • CreatNamedPipe() 함수
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)
    - 마찬가지로 데이터를 서버에 데이터를 보내주고 싶은데,
      서버가 받을 여력이 안됨. 그럴땐 입력 버퍼에 넣어둠(Read)
    */
    20000,                     // 클라이언트 타임-아웃  
    /*
    - 클라이언트가 연결 요청을 했는데 현재 파이프이름으로 생성된
      파이프의 인스턴스 개수가 최대치에 도달함.
      즉, 더이상의 연결 요청을 받을 수가 없음.
    - 그럼 클라이언트는 기다려야 함.
      그럴 때, 서버가 클라이언트에게 특정 ms 만큼 기다리라고 지정해 주는 것.
      근데 클라이언트가 따로 어느정도 기다리겠다고 지정할 수도 있음.
    */
    NULL                       // 디폴트 보안 속성
);
 

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


FlushFileBuffers(hPipe);
/*
  - FlushFileBuffers() 함수는 Write buffer를 비우는 함수.
    read buffer는 다 읽으면 그냥 끝임. 비우고 자시고 할게 없음.
  - 버퍼는 모아서 보내는 특징을 지님.
    멀리까지 가는데 조금씩 보내면 성능면에서 떨어지게 됨.
    한번에 모아서 보내는게 좋음.
  - 근데 마지막 데이터를 보내는데, 이게 1byte라면 전송이 안될 수도 있음.
    즉 전송 실패시 버퍼를 비워주는 역할.
  - 근데 항상 마지막에만 하는게 아님. 도중에도 비울 필요가 있다면 호출 가능한 함수.
    ex. fflush(stdin);
*/
DisconnectNamedPipe(hPipe);
/*
  - CloseHandle(hPipe) 함수는 파이프를 소멸시키는 함수가 아님.
    해당 핸들이 가리키는 커널 오브젝트 속 UC를 하나 감소시키는 역할일 뿐.
  - 따라서 누군가는 명시적으로 "더이상 통신 안하겠다"라고 클라이언트쪽에 알려줘야함.
    그게 DisconnectNamedPipe(hPipe) 함수.
  - 이 함수가 호출된 후부터는 클라이언트 쪽에서 해당 파이프로 Read-Write 했을때
    에러가 발생하게됨.
*/
CloseHandle(hPipe);
  • WaitNamedPipe() 함수
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를 반환함.
    */
}
  • SetNamedPipeHandleState() 함수
DWORD pipeMode = PIPE_READMODE_MESSAGE | PIPE_WAIT; // 메시지 기반으로 모드 변경.
/*
  - 여기서 readmode만 메세지 기반으로 설정하는 이유는? 
    CreateFile()할 때 결정되기 때문.
  - 일단 이게 중요한게 아니고, 만약 바이너리 방식으로 통신한다고 가정해 보자.
    어찌되었든 받는 쪽에서 메세지 형식으로 받으면 메세지 타입으로 받게되는거임.
    일반적으론 통일을 시켜주는게 맞긴하지만, 문제될건 없음.
    클라이언트가 어떤 방식으로 보내든 서버 입장에선 바이너리 방식으로 읽겠다하면
    그냥 바이너리 방식으로 받는거임.
  - 즉 결론적으로 서버의 설정과 클라이언트의 설정은 사실상 별개의 내용.
    파이프의 특성 설정에 있어서의 특징.
*/
BOOL isSuccess = SetNamedPipeHandleState(
    hPipe,     // 파이프 핸들
    &pipeMode, // 변경할 모드 정보.  
    NULL,      // 설정하지 않는다. 
    NULL       // 설정하지 않는다. 
);
  • Server
/*
    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);				// UC를 1 줄임.
    return 1;
}
  • Client
/*
    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;
}

✏️ 프로세스 환경변수

IPC의 방법 중 하나이자 프로세스의 독립적인 기능

  • IPC 관점에서의 환경변수
    • 데이터 블록
    • 부모 프로세스가 자식 프로세스에게 상속할 수 있다. 따라서 부모 - 자식 간의 데이터 통신이 가능하다. 그러나 해당 기능은 부가적인 역할.
    • 중심 역할은 부모 프로세스가 자신의 고유 정보를 담고, 빼는 용도.

💻 프로세스 환경변수의 구성

프로세스 별로 독립된 메모리 공간이 있는데, 환경변수는 이러한 메모리 블록에 할당된다. (프로세스 자체가 독립된 메모리 공간)
환경변수는 SetEnvironmentVariable()함수를 통해 문자열 쌍으로 저장된다.
데이터를 찾고자 할 때는 GetEnvironmentVariable()를 이용.

  • 해당 기능을 IPC에 활용하는 방법
    부모 프로세스 A, 자식 프로세스 B가 있을 경우 부모 프로세스는 자신의 환경변수 테이블 정보를 자식 프로세스에게 복사해서 넘겨줄 수 있다. (데이터 통신)

/*
    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;
}
/*
    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;
}

0개의 댓글