서버 스텁 메모리 관리
Server-Stub 메모리 관리 소개
MIDL 생성 스텁은 클라이언트 프로세스와 서버 프로세스 간의 인터페이스 역할을 합니다. 클라이언트 스텁은 [in] 특성으로 표시된 매개 변수에 전달된 모든 데이터를 마샬링하고 서버 스텁으로 보냅니다. 서버 스텁은 이 데이터를 수신하면 호출 스택을 다시 구성한 다음, 해당 사용자 구현 서버 함수를 실행합니다. 또한 서버 스텁은 [out] 특성으로 표시된 매개 변수 데이터를 마샬링하고 클라이언트 애플리케이션에 반환합니다.
MSRPC에서 사용하는 32비트 마샬링된 데이터 형식은 NDR(네트워크 데이터 표현) 전송 구문의 규격 버전입니다. 이 형식에 대한 자세한 내용은 Open Group 웹 사이트를 참조하세요. 64비트 플랫폼의 경우 성능 향상을 위해 NDR64라는 NDR 전송 구문에 대한 Microsoft 64비트 확장을 사용할 수 있습니다.
인바운드 데이터 경계 해제
MSRPC에서 클라이언트 스텁은 서버 스텁으로 전송하기 위해 하나의 연속 버퍼 에서 [in] 으로 태그가 지정된 모든 매개 변수 데이터를 마샬링합니다. 마찬가지로 서버 스텁은 클라이언트 스텁으로 돌아가기 위해 연속 버퍼에 [out] 특성으로 표시된 모든 데이터를 마샬링합니다. RPC 아래의 네트워크 프로토콜 계층은 전송을 위해 버퍼를 조각화하고 패킷화할 수 있지만 조각화는 RPC 스텁에 투명합니다.
서버 호출 프레임을 만들기 위한 메모리 할당은 비용이 많이 드는 작업일 수 있습니다. 서버 스텁은 가능한 경우 불필요한 메모리 사용을 최소화하려고 시도하며, 서버 루틴은 [in] 또는 [in, out] 특성으로 표시된 데이터를 해제하거나 재할당하지 않는다고 가정합니다. 서버 스텁은 불필요한 중복을 방지하기 위해 가능하면 버퍼의 데이터를 다시 사용하려고 시도합니다. 일반적으로 마샬링된 데이터 형식이 메모리 형식과 동일한 경우 RPC는 동일한 형식의 데이터에 대한 추가 메모리를 할당하는 대신 마샬링된 데이터에 대한 포인터를 사용합니다.
예를 들어 다음 RPC 호출은 마샬링된 형식이 메모리 내 형식과 동일한 구조체로 정의됩니다.
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
이 경우 RPC는 plInStructure에서 참조하는 데이터에 대한 추가 메모리를 할당하지 않습니다. 오히려 단순히 서버 쪽 함수 구현에 마샬링된 데이터에 대한 포인터를 전달합니다. RPC 서버 스텁은 스텁이 "강력한" 플래그(MIDL 컴파일러의 최신 버전에서 기본 설정)를 사용하여 컴파일되는 경우 경계 해제 프로세스 중에 버퍼를 확인합니다. RPC는 서버 쪽 함수 구현에 전달된 데이터가 유효하다는 것을 보장합니다.
메모리는 서버에 전달되지 않으므로 plOutStructure에 할당됩니다.
인바운드 데이터에 대한 메모리 할당
서버 스텁이 [in] 또는 [in, out] 특성으로 표시된 매개 변수 데이터에 대한 메모리를 할당하는 경우에 발생할 수 있습니다. 이는 마샬링된 데이터 형식이 메모리 형식과 다르거나 마샬링된 데이터를 구성하는 구조체가 충분히 복잡하고 RPC 서버 스텁에서 원자성으로 읽어야 하는 경우에 발생합니다. 다음은 서버 스텁에서 받은 데이터에 메모리를 할당해야 하는 몇 가지 일반적인 경우입니다.
데이터는 다양한 배열 또는 규칙적인 다양한 배열입니다. 다음은 [length_is()] 또는 [first_is()] 특성이 설정된 배열(또는 배열에 대한 포인터 ) 입니다. NDR에서는 이러한 배열의 첫 번째 요소만 마샬링되고 전송됩니다. 예를 들어 아래 코드 조각에서 매개 변수 pv 에 전달된 데이터에는 메모리가 할당됩니다.
void RpcFunction ( [in] long size, [in, out] long *pLength, [in, out, size_is(size), length_is(*pLength)] long *pv );
데이터는 크기가 큰 문자열 또는 비규격 문자열입니다. 이러한 문자열은 일반적으로 [size_is()] 특성으로 태그가 지정된 문자 데이터에 대한 포인터입니다. 아래 예제에서는 SizedString 서버 쪽 함수에 전달된 문자열에 메모리가 할당된 반면 NormalString 함수에 전달된 문자열은 재사용됩니다.
void SizedString ( [in] long size, [in, size_is(size), string] char *str ); void NormalString ( [in, string] char str );
데이터는 enum16 및 __int3264 같은 마샬링된 크기와 메모리 크기가 다른 간단한 형식입니다.
데이터는 메모리 맞춤이 자연 맞춤보다 작거나, 위의 데이터 형식을 포함하거나, 후행 바이트 패딩이 있는 구조체에 의해 정의됩니다. 예를 들어 다음 복잡한 데이터 구조는 강제로 2바이트 맞춤을 적용하고 끝에 안쪽 여백이 있습니다.
#pragma pack(2) typedef 구조체 ComplexPackedStructure { char c;
long l; 맞춤은 두 번째 바이트 char c2에서 강제 적용됩니다. 2바이트 맞춤 } ''을(를) 유지하기 위한 후행 1바이트 패드가 있습니다.
- 데이터에는 필드별로 마샬링해야 하는 구조체가 포함되어 있습니다. 이러한 필드에는 DCOM 인터페이스에 정의된 인터페이스 포인터가 포함됩니다. 무시된 포인터; 정수 값은 [range] 특성으로 설정됩니다. [wire_marshal], [user_marshal], [transmit_as] 및 [represent_as] 특성으로 정의된 배열의 요소 및 포함된 복잡한 데이터 구조입니다.
- 데이터에는 공용 구조체, 공용 구조체를 포함하는 구조체 또는 공용 구조체 배열이 포함됩니다. 공용 구조체의 특정 분기만 와이어에 마샬링됩니다.
- 데이터에는 고정된 차원이 하나 이상 있는 다차원 규칙 배열이 있는 구조체가 포함되어 있습니다.
- 데이터에는 복잡한 구조의 배열이 포함되어 있습니다.
- 데이터에는 enum16 및 __int3264 같은 간단한 데이터 형식의 배열이 포함되어 있습니다.
- 데이터에는 ref 및 인터페이스 포인터의 배열이 포함되어 있습니다.
- 데이터에는 포인터에 적용된 [force_allocate] 특성이 있습니다.
- 데이터에는 포인터에 적용된 [allocate(all_nodes)] 특성이 있습니다.
- 데이터에는 포인터에 적용된 [byte_count] 특성이 있습니다.
64비트 데이터 및 NDR64 전송 구문
앞에서 설명한 것처럼 64비트 데이터는 NDR64라는 특정 64비트 전송 구문을 사용하여 마샬링됩니다. 이 전송 구문은 포인터가 32비트 NDR에서 마샬링되고 64비트 플랫폼의 서버 스텁으로 전송될 때 발생하는 특정 문제를 해결하기 위해 개발되었습니다. 이 경우 마샬링된 32비트 데이터 포인터가 64비트 데이터 포인터와 일치하지 않으며 메모리 할당이 변함없이 발생합니다. 64비트 플랫폼에서 보다 일관된 동작을 만들기 위해 Microsoft는 NDR64라는 새로운 전송 구문을 개발했습니다.
이 문제를 보여 주는 예제는 다음과 같습니다.
typedef struct PtrStruct
{
long l;
long *pl;
}
이 구조체는 마샬링될 때 32비트 시스템의 서버 스텁에서 재사용됩니다. 그러나 서버 스텁이 64비트 시스템에 있는 경우 NDR 마샬링된 데이터의 길이는 4바이트이지만 필요한 메모리 크기는 8입니다. 따라서 메모리 할당이 강제로 수행되고 버퍼 재사용이 거의 발생하지 않습니다. NDR64는 포인터의 마샬링된 크기를 64비트로 만들어 이 문제를 해결합니다.
32비트 NDR과 달리 enum16 및 __int3264 같은 간단한 데이터 타이드는 NDR64에서 구조 또는 배열을 복잡하게 만들지 않습니다. 마찬가지로 후행 패드 값은 구조체를 복잡하게 만들지 않습니다. 인터페이스 포인터는 최상위 수준에서 고유한 포인터로 처리됩니다. 따라서 인터페이스 포인터를 포함하는 구조체 및 배열은 복잡한 것으로 간주되지 않으며 사용하기 위해 특정 메모리 할당이 필요하지 않습니다.
아웃바운드 데이터 초기화
모든 인바운드 데이터가 중단되지 않은 후 서버 스텁은 [out] 특성으로 표시된 아웃바운드 전용 포인터를 초기화해야 합니다.
typedef struct RpcStructure
{
long val;
long val2;
}
void ProcessRpcStructure
(
[in] RpcStructure *plInStructure;
[out] RpcStructure *plOutStructure;
);
위의 호출에서 서버 스텁은 마샬링된 데이터에 없기 때문에 plOutStructure 를 초기화해야 하며 서버 함수 구현에 사용할 수 있어야 하는 암시적 [ref] 포인터입니다. RPC 서버 스텁은 [out] 특성을 사용하여 최상위 참조 전용 포인터를 초기화하고 0으로 표시합니다. 그 아래에 있는 모든 [out] 참조 포인터도 재귀적으로 초기화됩니다. 재귀는 [고유] 또는 [ptr] 특성이 설정된 포인터에서 중지됩니다.
서버 함수 구현은 최상위 포인터 값을 직접 변경할 수 없으므로 다시 할당할 수 없습니다. 예를 들어 위의 ProcessRpcStructure 구현에서 다음 코드가 잘못되었습니다.
void ProcessRpcStructure(RpcStructure *plInStructure, rpcStructure *plOutStructure)
{
plOutStructure = MIDL_user_allocate(sizeof(RpcStructure));
Process(plOutStructure);
}
plOutStructure 는 스택 값이며 변경 내용이 RPC로 다시 전파되지 않습니다. 서버 함수 구현은 plOutStructure를 해제하여 할당을 방지하려고 시도할 수 있으며, 이로 인해 메모리가 손상될 수 있습니다. 그런 다음 서버 스텁은 메모리의 최상위 포인터(포인터 대 포인터의 경우)와 스택의 크기가 예상보다 작은 최상위 단순 구조체에 대한 공간을 할당합니다.
클라이언트는 특정 상황에서 서버 쪽의 메모리 할당 크기를 지정할 수 있습니다. 다음 예제에서 클라이언트는 인바운드 크기 매개 변수에서 아웃바운드 데이터의 크기를 지정합니다.
void VariableSizeData
(
[in] long size,
[out, size_is(size)] char *pv
);
크기를 포함하여 인바운드 데이터를 경계 해제한 후 서버 스텁은 "sizeof(char)*size"의 크기로 pv에 대한 버퍼를 할당합니다. 공간이 할당된 후 서버 스텁은 버퍼를 0으로 설정합니다. 이 특정 경우 스텁은 버퍼의 크기가 런타임에 결정되므로 MIDL_user_allocate()를 사용하여 메모리를 할당합니다.
DCOM 인터페이스의 경우 클라이언트와 서버가 동일한 COM 아파트를 공유하거나 ICallFrame 이 구현된 경우 MIDL 생성 스텁이 전혀 관련되지 않을 수 있습니다. 이 경우 서버는 할당 동작에 의존할 수 없으며 클라이언트 크기 메모리를 독립적으로 확인해야 합니다.
서버 쪽 함수 구현 및 아웃바운드 데이터 마샬링
인바운드 데이터에 대한 경계 해제 및 아웃바운드 데이터를 포함하도록 할당된 메모리 초기화 직후 RPC 서버 스텁은 클라이언트에서 호출한 함수의 서버 쪽 구현을 실행합니다. 현재 서버는 [in, out] 특성으로 특별히 표시된 데이터를 수정할 수 있으며 아웃바운드 전용 데이터에 할당된 메모리( [out]로 태그가 지정된 데이터)를 채울 수 있습니다.
마샬링된 매개 변수 데이터 조작에 대한 일반적인 규칙은 간단합니다. 서버는 새 메모리만 할당하거나 서버 스텁에서 특별히 할당한 메모리를 수정할 수 있습니다. 데이터에 대한 기존 메모리를 재할당하거나 해제하면 함수 호출의 결과 및 성능에 부정적인 영향을 미칠 수 있으며 디버그하기가 매우 어려울 수 있습니다.
논리적으로 RPC 서버는 클라이언트와 다른 주소 공간에 있으며 일반적으로 메모리를 공유하지 않는다고 가정할 수 있습니다. 따라서 서버 함수 구현은 클라이언트 메모리 주소에 영향을 주지 않고 [in] 특성으로 표시된 데이터를 "스크래치" 메모리로 사용하는 것이 안전합니다. 즉, 서버 는 데이터를 재 할당하거나 해제하려고 시도해서는 안 되며, 이러한 공간을 RPC 서버 스텁 자체에 제어할 수 있습니다.
일반적으로 서버 함수 구현은 [in, out] 특성으로 표시된 데이터를 다시 할당하거나 해제할 필요가 없습니다. 고정 크기 데이터의 경우 함수 구현 논리는 데이터를 직접 수정할 수 있습니다. 마찬가지로 변수 크기 데이터의 경우 함수 구현은 [size_is()] 특성에 제공된 필드 값을 수정해서는 안 됩니다. 데이터 크기를 조정하는 데 사용되는 필드 값을 변경하면 클라이언트에 반환된 더 작거나 더 큰 버퍼가 비정상 길이를 처리할 수 없습니다.
서버 루틴이 [in, out] 특성으로 표시된 데이터에서 사용하는 메모리를 재할당해야 하는 경우 스텁에서 제공하는 포인터가 MIDL_user_allocate() 또는 마샬링된 와이어 버퍼로 할당된 메모리에 대한 포인터인지 서버 쪽 함수 구현에서 알 수 없습니다. 이 문제를 해결하기 위해 MS RPC는 데이터에 [force_allocate] 특성이 설정된 경우 메모리 누수 또는 손상이 발생하지 않도록 할 수 있습니다. [force_allocate]가 설정되면 서버 스텁은 항상 포인터에 대한 메모리를 할당하지만, 주의해야 할 점은 모든 사용 시 성능이 저하된다는 것입니다.
호출이 서버 쪽 함수 구현에서 반환되면 서버 스텁은 [out] 특성으로 표시된 데이터를 마샬링하고 클라이언트에 보냅니다. 서버 쪽 함수 구현에서 예외를 throw하는 경우 스텁은 데이터를 마샬링하지 않습니다.
할당된 메모리 해제
RPC 서버 스텁은 예외 발생 여부에 관계없이 서버 쪽 함수에서 호출이 반환된 후 스택 메모리를 해제합니다. 서버 스텁은 스텁에서 할당한 모든 메모리와 MIDL_user_allocate()로 할당된 모든 메모리를 해제합니다. 서버 쪽 함수 구현은 예외를 throw하거나 오류 코드를 반환하여 항상 RPC에 일관된 상태를 제공해야 합니다. 복잡한 데이터 구조의 채우기 중에 함수가 실패하는 경우 모든 포인터가 유효한 데이터를 가리키거나 NULL로 설정되어 있는지 확인해야 합니다.
이 단계에서 서버 스텁은 [in] 데이터를 포함하는 마샬링된 버퍼의 일부가 아닌 모든 메모리를 해제합니다. 이 동작의 한 가지 예외는 [allocate(dont_free)] 특성이 설정된 데이터입니다. 서버 스텁은 이러한 포인터와 연결된 메모리를 해제하지 않습니다.
서버 스텁이 스텁 및 함수 구현에서 할당한 메모리를 해제한 후 스텁은 특정 데이터에 대해 [notify_flag] 특성이 지정된 경우 특정 알림 함수를 호출합니다.
RPC를 통해 연결된 목록 마샬링 -- 예제
typedef struct _LINKEDLIST
{
long lSize;
[size_is(lSize)] char *pData;
struct _LINKEDLIST *pNext;
} LINKEDLIST, *PLINKEDLIST;
void Test
(
[in] LINKEDLIST *pIn,
[in, out] PLINKEDLIST *pInOut,
[out] LINKEDLIST *pOut
);
위의 예제에서 LINKEDLIST 의 메모리 형식은 마샬링된 와이어 형식과 동일합니다. 따라서 서버 스텁은 pIn에서 데이터 포인터의 전체 체인에 대한 메모리를 할당하지 않습니다. 대신 RPC는 전체 연결된 목록에 대해 와이어 버퍼를 다시 사용합니다. 마찬가지로 스텁은 pInOut에 대한 메모리를 할당하지 않고 대신 클라이언트에 의해 마샬링된 와이어 버퍼를 다시 사용합니다.
함수 서명에 아웃바운드 매개 변수 pOut이 포함되어 있으므로 서버 스텁은 반환된 데이터를 포함하도록 메모리를 할당합니다. pNext가 NULL로 설정된 상태에서 할당된 메모리는 처음에 0으로 설정됩니다. 애플리케이션은 새 연결된 목록에 대한 메모리를 할당하고 pOut-pNext> 를 가리킬 수 있습니다. pIn 및 포함된 연결된 목록을 스크래치 영역으로 사용할 수 있지만 애플리케이션은 pNext 포인터를 변경하지 않아야 합니다.
애플리케이션은 pInOut에서 가리키는 연결된 목록의 콘텐츠를 자유롭게 변경할 수 있지만 최상위 링크 자체는 물론 pNext 포인터를 변경해서는 안 됩니다. 애플리케이션이 연결된 목록을 단축하기로 결정한 경우 지정된 pNext 포인터가 RPC 내부 버퍼 또는 MIDL_user_allocate()로 특별히 할당된 버퍼에 연결되는지 알 수 없습니다. 이 문제를 해결하려면 아래 코드와 같이 사용자 할당을 강제하는 연결된 목록 포인터에 대한 특정 형식 선언을 추가합니다.
typedef [force_allocate] PLINKEDLIST;
이 특성은 서버 스텁이 연결된 목록의 각 노드를 개별적으로 할당하도록 강제하고 애플리케이션은 MIDL_user_free()를 호출하여 연결된 목록의 단축된 부분을 해제할 수 있습니다. 그러면 애플리케이션은 새로 단축된 연결된 목록의 끝에 있는 pNext 포인터를 NULL로 안전하게 설정할 수 있습니다.