뇌를 자극하는 윈도우즈 시스템 프로그래밍(저자, 윤성우)
01. 핸들 테이블과 오브젝트 핸들의 상속
I believe that a competent Windows Programmer must understand how a process's handle table is managed
제프리 리처Jeffrey Richter는 본인의 저서 『Programming Application for Microsoft Windows』에 위와 같은 문구를 작성했다. 유능한 프로그래머는 핸들 테이블이 어떻게 관리되는지 이해하고 있어야만 한다는 뜻으로 해석할 수 있다. 이 대목에서 핸들 테이블의 중요성에 대해서 파악할 수 있다.
01. A. 프로세스의 커널 오브젝트 핸들 테이블
앞서 MailReceiver.cpp의 예제에서는 CreateMailslot 함수를 통해 메일슬롯을 생성하고, 동시에 생겨난 메일슬롯의 커널 오브젝트 핸들을 반환했다. 그러면 이때, 우리는 핸들을 통해서 메일슬롯에 접근이 가능함을 알 수 있다. 그런데 이 부분을 조금 더 들어가면, 생성된 핸들이 커널 오브젝트를 의미한다는 정보는 없다. 핸들이 메모리 공간의 위치에 존재하는 커널 오브젝트임을 알 방법이 없는 것이다. 이러한 과정에 핸들 테이블을 추가하여 이해를 수월하게 할 수 있다.
- 프로세스의 핸들 테이블 도입
핸들 테이블은 핸들 정보를저 장하고 있는 테이블로서 프로세스별로 독립적이다. 각각의 프로세스가 자신만의 핸들 테이블을 가지고 관리한다. CreateProcess 함수 혹은 CreateMailslot 함수와 같은 호출을 통해서 생성된 핸들 정보를 프로세스는 자신이 가지고 있는 핸들 테이블에 등록한다.
- 핸들의 상속
부모 프로세스와 자식 프로세스간의 핸들 테이블 공유는 가능하다. CreateProcess 함수의 5번째 인자값에 따라 부모 프로세스가 가지고 있는 핸들 테이블 데이터를 자식 프로세스의 핸들 테이블에 상속할 수 있다. 핸들 테이블의 모든 데이터를 상속해야만 하는 것은 아니다. 핸들 테이블 내부에 상속 여부를 결정짓는 컬럼이 존재하기 때문이다.
BOOL CreateProcess(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);
위 내용은 CreateProcess 함수의 정의다. 여기에서 5번째 매개변수 bInheritHandle이 자식 프로세스에게 핸들 테이블에 등록된 핸들 정보의 상속 여부를 결정짓는 요소다. 이렇듯 핸들의 상속 여부는 리소스 생성 순간에 프로그래머에 의해서 결정된다.
▶ 핸들 테이블의 관점
: 프로세스가 핸들을 얻게 되었다는 의미는 핸들 테이블에 해당 핸들에 대한 정보가 갱신(추가)되었음을 의미한다.
핸들의 상속 여부는 리소스 생성 순간에 프로그래머에 의해서 결정된다. CreateMailslot 함수의 정의에서 다음과 같은 매개변수가 있다.
LPSECURITY_ATTRIBUTES lpSecurityAttributes
이 전달인자에 NULL이 전달되면 여기에서 생성된 메일슬롯의 핸들은 자식 프로세스에게 상속되지 않는다. 반대로 적절한 값으로 초기화해서 변수의 주소값을 전달하면 메일슬롯의 핸들은 상속될 수 있다.
typedef struct _SECURITY_ATTRIBUTES (
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
) SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTE;
위 내용은 SECURITY_ATTRIVBUTES 구조체의 선언을 나타낸다. nLength에는 구조체 변수 크기를 바이트 단위로 설정해주어야 한다. 내부적으로 사용되는 멤버기에 반드시 구조체 변수 크기로 초기화를 해야 한다.
두 번째 멤버 lpSecurityDescriptor에는 NULL로 초기화 한다. 핸들 상속 관점에서는 의미를 지니지 않는다. 세 번째 멤버 bInheritHandle은 TRUE로 설정한다. 상속 여부를 결정짓기 때문이다.
01. B. Pseudo 핸들과 핸들의 중복Duplicate
GetCurrentProcess 함수를 통해서 프로세스 자신의 커널 오브젝트에 접근이 가능하다. 그런데 이 함수 호출을 통해 얻은 핸들을 가짜 핸들Pseudo Handle이라고 한다. 이러한 이유는 핸들 테이블에 등록되지 않은, 실행 중인 프로세스를 참조하기 위한 약속된 상수가 반환되는 것이기 때문이다.
그렇기 때문에 자식 프로세스에게 상속되지도 않으며, CloseHandle에 추가하더라도 아무런 일이 일어나지 않는다. 그렇다면 진짜 핸들을 얻는 방법을 보자.
BOOL DuplicateHandle (
HANDLE hSourceProcessHandle,
HANDLE hSourceHandle,
HANDLE hTargetProcessHandle,
LPHANDLE lpTargetHandle,
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwOptions
);
매개변수 명 | 설명 |
hSourceProcessHandle | 복제할 핸들을 소유하는 프로세스를 지정한다. |
hSourceHandle | 복제할 핸들을 지정한다. |
hTargetProcessHandle | 복제된 핸들을 소유할 프로세스를 지정한다. |
lpTargetHandle | 복제된 핸들값을 저장할 변수의 주소를 지정한다. |
dwDesiredAccess | 복제된 핸들의 접근 권한을 설정한다. - DUPLICATE_SAME_ACCESS가 전달될 경우, 전달 인자가 무시된다. |
bInheritHandle | 복제된 핸들의 상속 여부를 결정한다. (TRUE - 상속 가능, FALSE - 상속불가) |
dwOptions | - DUPLICATE_SAME_ACCESS를 전달될 경우, 원본 핸들과 동일한 접근 권한을 가진다. - DUPLICATE_CLOSE_SOURCE를 전달할 경우, 원본 핸들을 종료 시킨다. 위 내용은 비트 연산자를 통해서 동시 전달이 가능하다. |
위 내용은 DuplicateHandle 함수의 정의와 매개변수에 대한 설명이다. 사용 방법은 다음과 같다.
DuplicateHandle (
프로세스 A 핸들, // 프로세스 A에 존재하는
256, // 핸들 256 정보를
프로세스 B 핸들, // 프로세스 B 핸들 테이블에 등록한다.
&val, // 등록된 핸들의 값은 변수 val에 저장한다.
...
...
);
참고로 프로세스 B의 위치에 프로세스 A가 들어갈 수 있다. 이렇게 되면 중복으로 핸들 테이블에 등록이 되면서 Usage Count가 하나 상승한다. 그렇기 때문에 관리에 있어서 CloseHandle 함수 호출을 통해서 핸들을 반환해야만 한다.
02. 파이프 방식의 IPC
앞서서 Anonymous Pipe와 Named Pipe에 대해서 가볍게 언급한 적이 있다. 이 둘을 메일슬롯과 비교해 보자. 메일슬롯은 서로 관련이 없는 프로세스들 사이에서 통신 시에 유용한 IPC 기법이다. 반면에 Anonymous Pipe는 부모 <-> 자식 관계 혹은 형제 관계와 같이 관계가 있는 프로세스 사이에서 통신할 때 유용하다. 또 다른 Named Pipe는 이름이 있는 파이프라는 뜻이다. 이름이 있다는 것은 주소 정보가 있다는 뜻이다.
메일슬롯과 같이 주소 정보를 공유함으로써 데이터를 주고받을 수 있다는 뜻이 된다. 단, 이는 단방향만 가능했던 메일슬롯과 다르게 양방향 소통이 가능하다.
02. A. 익명 파이프Anonymous Pipe
BOOL CreatePipe(
PHANDLE hReadPipe,
PHANDLE hWritePipe,
LPSECURITY_ATTRIBUTES lpPipeAttributes,
DWORD nSize
);
매개변수 명 | 설명 |
hReadPipe | 파이프는 두 개의 끝을 가진다. 따라서 생성 시, 각각 끝에 접근하는 두 개의 핸들을 얻게 되는데 이 인자를 통해서 데이터를 읽기 위한 파이프 끝에 해당하는 핸들을 얻게 된다. |
hWritePipe | 데이터를 쓰기 위한 다른 한쪽 끝에 해당하는 핸들을 얻는다. |
lpPipeAttributes | 보안 관련 정보를 전달할 경우 사용된다. 핸들의 상속 특성을 지정할 때 사용할 수 있다. |
nSize | 파이프의 버퍼 사이즈를 지정하는 용도로 사용된다. 참고로 전달 값으로 버퍼의 크기가 지정되는 것이 아니라 내부의 버퍼링 메커니즘에 의해서 버퍼 사이즈가 결정된다. |
위 내용은 CreatePipe 함수의 정의와 매개변수에 대한 설명이다. 다음과 같은 예제를 통해서 익명 파이프를 확인할 수 있다.
// anonymous_pipe.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
HANDLE hReadPipe, hWritePipe;
TCHAR sendString[] = _T("anonymous pipe");
TCHAR recvString[100];
DWORD bytesWritten;
DWORD bytesRead;
CreatePipe(&hReadPipe, &hWritePipe, NULL, 0);
WriteFile (
hWritePipe, sendString,
lstrlen(sendString) * sizeof(TCHAR), &bytesWritten, NULL
);
ReadFile (
hReadPipe, recvString,
bytesWritten, &bytesRead, NULL
);
recvString[bytesRead/sizeof(TCHAR)] = 0;
_tprintf(_T("string recv : %s \n"), recvString);
CloseHandle(hReadPipe);
CloseHandle(hWritePipe);
return 0;
}
02. B. 이름있는 파이프Named Pipe
① : CreateNamedPipe 함수에 의해 생성
② : ConnectNamedPipe 함수에 의해 연결 대기 상태로 전환
③ : Client의 CreateFile 함수 호출에 의한 파이프 연결
이름있는 파이프를 CreateNamePipe 함수에 의해 생성하고 ConnectNamePipe 함수를 호출하면 연결 요청을 기다리는 파이프로 상태 변경된다.
HANDLE CreateNamedPipe (
LPCTSTR lpName,
DWORD dwOpenMode,
DWORD dwPipeMode,
DWORD nMaxInstances,
DWORD nOutBufferSize,
DWORD nInBufferSize,
DWORD nDefaultTImeOut,
LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
매개변수 명 | 설명 |
lpName | 파이프 이름을 지정한다. 이름은 다음과 같이 구성된다. \\. \pipe\pipename |
dwOpenMode | - PIPE_ACCESS_DUPLEX : 읽기, 쓰기 모두 가능 - PIPE_ACCESS_INBOUND : 읽기만 가능 - PIPE_ACCESS_OUTBOUND : 쓰기만 가능 위 3개 중에서 하나를 지정한다. |
dwPipeMode | 데이터 전송 타입, 수신 타입, 블로킹 모드를 설정한다. |
nMaxInstances | 생성할 수 있는 파이프의 최대 개수를 지정한다. 지정 개수 만큼 클라이언트 연결 요청을 수용할 수 있다. 허용 범위는 1부터 PIPE_UNLIMITED_INSTANCES(255)까지다. |
nOutBufferSize | 출력 버퍼 사이즈를 지정한다. 0을 입력할 경우, Windows Default 값으로 설정된다. |
nInBufferSize | 입력 버퍼 사이즈를 지정한다. 0을 입력할 경우, Windows Default 값으로 설정된다. |
nDefaultTimeOut | WaitNamePipe 함수에 적용할 기본 만료 시간을 밀리세컨드 단위로 지정한다. |
lpSecurityAttributes | 보안 속성을 지정한다. |
BOOL ConnectNamedPipe (
HANDLE hNamePipe,
LPOVERLAPPED lpOverlapped
);
매개변수 명 | 설명 |
hNamedPipe | CreateNamedPipe 함수 호출을 통해 생성된 파이프의 핸들을 전달한다. |
lpOverlapped | 중첩 I/O를 위한 전달인자다. |
위 내용은 CreateNamedPipe 함수와 ConnectNamedPipe 함수의 정의와 설명이다. 두 함수를 통해서 NamedPipe를 관리하고 활용할 수 있다.
'독서 > 뇌를 자극하는 윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
[스터디] 시스템 프로그래밍 - Chapter 10. 컴퓨터 구조에 대한 세 번째 이야기 (2) | 2023.10.25 |
---|---|
[스터디] 시스템 프로그래밍 - Chapter 9. 스케줄링 알고리즘과 우선순위 (2) | 2023.10.04 |
[스터디] 시스템 프로그래밍 - Chapter 7. 프로세스간 통신(IPC) 1 (2) | 2023.10.04 |
[스터디] 시스템 프로그래밍 - Chapter 6. 커널 오브젝트와 오브젝트 핸들 (2) | 2023.10.03 |
[스터디] 시스템 프로그래밍 - Chapter 5. 프로세스의 생성과 소멸 (0) | 2023.10.02 |