프로파일러 연결 및 분리
.NET Framework 버전 4 이전 버전의 .NET Framework에서는 응용 프로그램과 프로파일러가 동시에 로드되어야 했습니다. 응용 프로그램이 시작되면 런타임에서는 환경 변수, 레지스트리 설정을 검사하여 응용 프로그램이 프로파일링되어야 하는지 확인한 후 요청된 프로파일러를 로드했습니다. 그런 다음 프로파일러는 응용 프로그램이 종료될 때까지 프로세스 공간에 유지되었습니다.
.NET Framework 4부터는 응용 프로그램이 시작된 이후에 프로파일러를 응용 프로그램에 연결하고 응용 프로그램을 프로파일링한 다음, 응용 프로그램 종료 이전에 프로파일러를 분리할 수 있습니다. 프로파일러가 분리된 이후 응용 프로그램은 프로파일러가 연결되기 이전과 동일하게 계속 실행됩니다. 즉, 프로파일러의 임시 상태에 대한 추적 정보가 프로세스 공간에 남아 있지 않습니다.
또한 .NET Framework 4부터는 프로파일링 연결 및 분리 메서드와 관련 프로파일링 API가 개선되어 프로파일러 기반 도구를 just-in-time 기본 제공 진단 도구로 사용할 수 있습니다.
다음 단원에서 이러한 개선 사항에 대해 설명합니다.
프로파일러 연결
연결 세부 정보
단계별 연결
AttachProfiler 메서드 찾기
런타임 로드 이전에 대상 응용 프로그램 연결
연결 제한 사항
프로파일러 연결 예제
연결 후 프로파일러 초기화
프로파일러 연결 완료
프로파일러 분리
분리 과정의 스레드 고려 사항
단계별 분리
분리 제한 사항
디버깅
프로파일러 연결
프로파일러를 실행 중인 응용 프로그램에 연결하려면 두 개의 개별 프로세스, 즉 트리거 프로세스 및 대상 프로세스가 필요합니다.
트리거 프로세스는 실행 중인 응용 프로그램의 프로세스 공간으로 프로파일러 DLL이 로드되도록 하는 실행 파일입니다. 트리거 프로세스는 ICLRProfiling::AttachProfiler 메서드를 사용하여 CLR(공용 언어 런타임)에게 프로파일러 DLL을 로드하도록 요청합니다. 프로파일러 DLL이 대상 프로세스에 로드된 이후 트리거 프로세스가 유지되는지 아니면 종료되는지는 프로파일러 개발자의 결정에 따라 달라집니다.
대상 프로세스에는 프로파일링하려는 응용 프로그램과 CLR이 포함됩니다. AttachProfiler 메서드가 호출된 이후 프로파일러 DLL은 대상 응용 프로그램과 함께 이 프로세스 공간으로 로드됩니다.
연결 세부 정보
AttachProfiler는 지정된 프로파일러를 지정된 프로세스에 연결하고, 선택적으로 일부 데이터를 프로파일러에 전달합니다. 그런 다음 지정된 제한 시간 동안 연결이 완료되기를 기다립니다.
대상 프로세스 내에서 연결 시 프로파일러를 로드하는 절차는 시작 시 프로파일러를 로드하는 절차와 유사합니다. CLR에서는 해당 CLSID를 CoCreates하고, ICorProfilerCallback3 인터페이스를 쿼리하고, ICorProfilerCallback3::InitializeForAttach 메서드를 호출합니다. 할당된 제한 시간 내에 이러한 작업이 성공하면 AttachProfiler에서는 S_OK HRESULT를 반환합니다. 따라서 트리거 프로세스는 프로파일러가 초기화를 완료하는 데 충분하다고 알려진 제한 시간을 선택해야 합니다.
참고 |
---|
시간 초과는 대상 프로세스의 종료자가 제한 시간 값보다 오래 실행되면 발생할 수 있습니다. 이 경우 HRESULT_FROM_WIN32(ERROR_TIMEOUT)가 반환됩니다.이 오류가 발생하면 연결 작업을 다시 시도할 수 있습니다. |
연결 완료 이전에 제한 시간이 만료되면 AttachProfiler에서 HRESULT 오류를 반환하지만 연결에는 성공할 수도 있습니다. 이 경우 성공 여부를 확인할 수 있는 다른 방법이 있습니다. 프로파일러 개발자는 종종 트리거 프로세스와 대상 응용 프로그램 간에 사용자 지정 통신 채널이 있도록 합니다. 이러한 통신 채널은 성공적인 연결을 검색하는 데 사용될 수 있습니다. 또한 트리거 프로세스는 AttachProfiler를 다시 호출하고 CORPROF_E_PROFILER_ALREADY_ACTIVE를 받아 성공을 감지할 수도 있습니다.
또한 대상 프로세스에는 프로파일러의 DLL을 로드할 수 있는 충분한 액세스 권한이 있어야 합니다. 따라서 액세스 권한이 제한된 NETWORK SERVICE 같은 계정으로 실행될 수 있는 서비스(예: W3wp.exe 프로세스)에 연결할 경우 문제가 될 수 있습니다. 이러한 경우 파일 시스템의 일부 디렉터리에는 대상 프로세스에 의한 액세스를 방지하는 보안상 제한 사항이 있을 수 있습니다. 따라서 프로파일러 개발자는 적절한 액세스를 허용하는 위치에 프로파일러 DLL을 설치해야 합니다.
실패는 응용 프로그램 이벤트 로그에 기록됩니다.
단계별 연결
트리거 프로세스는 AttachProfiler를 호출하고, 대상 프로세스 및 연결될 프로파일러를 식별하고, 연결 이후 프로파일러에 제공될 데이터를 선택적으로 전달합니다.
대상 프로세스 내에서 CLR은 연결 요청을 받습니다.
CLR은 대상 프로세스 내에서 프로파일러 개체를 만들고 이 개체로부터 ICorProfilerCallback3 인터페이스를 가져옵니다.
그런 다음 CLR은 InitializeForAttach 메서드를 호출하고, 트리거 프로세스가 연결 요청에 포함한 데이터를 전달합니다.
프로파일러는 자신을 초기화하고, 필요한 콜백을 활성화하고, 성공적으로 반환합니다.
트리거 프로세스 내에서 AttachProfiler는 프로파일러의 성공적 연결을 알리는 S_OK HRESULT를 반환합니다. 트리거 프로세스는 더 이상 관련되지 않습니다.
이전의 모든 버전과 마찬가지로 프로파일링은 이 시점 이후 계속됩니다.
AttachProfiler 메서드 찾기
트리거 프로세스는 다음 단계에 따라 AttachProfiler 메서드를 찾을 수 있습니다.
LoadLibrary 메서드를 호출하여 mscoree.dll을 로드하고 CLRCreateInstance 메서드를 찾습니다.
CLSID_CLRMetaHost 및 IID_ICLRMetaHost 인수를 사용하여 CLRCreateInstance 메서드를 호출합니다. 이 메서드는 ICLRMetaHost 인터페이스를 반환합니다.
ICLRMetaHost::EnumerateLoadedRuntimes 메서드를 호출하여 프로세스의 각 CLR 인스턴스에 대한 ICLRRuntimeInfo 인터페이스를 검색합니다.
프로파일러를 연결할 인터페이스를 찾을 때까지 ICLRRuntimeInfo 인터페이스를 반복합니다.
IID_ICLRProfiling 인수를 사용해 ICLRRuntimeInfo::GetInterface 메서드를 호출하여 ICLRProfiling 인터페이스를 가져옵니다. 이 인터페이스는 AttachProfiler 메서드를 제공합니다.
런타임 로드 이전에 대상 응용 프로그램 연결
이 기능은 지원되지 않습니다. 런타임을 아직 로드하지 않은 프로세스를 지정하여 트리거 프로세스에서 AttachProfiler 메서드를 호출하려고 하면 AttachProfiler 메서드는 실패 HRESULT를 반환합니다. 런타임이 로드되는 즉시 응용 프로그램을 프로파일링하려는 사용자는 대상 응용 프로그램을 시작하기 전에 해당하는 환경 변수를 직접 설정하거나 프로파일러 개발자가 작성한 응용 프로그램을 실행하여 설정해야 합니다.
연결 제한 사항
프로파일러 연결에는 ICorProfilerCallback 및 ICorProfilerInfo 메서드의 하위 집합만 사용할 수 있습니다. 이에 대해서는 다음 단원에서 설명합니다.
ICorProfilerCallback 제한 사항
프로파일러가 ICorProfilerInfo::SetEventMask 메서드를 호출하는 경우 프로파일러는 COR_PRF_ALLOWABLE_AFTER_ATTACH 열거형에 있는 이벤트 플래그만 지정해야 합니다. 다른 콜백은 프로파일러 연결에 사용할 수 없습니다. COR_PRF_ALLOWABLE_AFTER_ATTACH 비트마스크에 없는 플래그를 프로파일러 연결에서 지정하려고 하면 ICorProfilerInfo::SetEventMask에서 CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT를 반환합니다.
ICorProfilerInfo 제한 사항
AttachProfiler 메서드를 호출하여 로드된 프로파일러에서 제한된 ICorProfilerInfo 또는 ICorProfilerInfo2 메서드 중 하나를 호출하려고 하면 해당 메서드에서 CORPROF_E_UNSUPPORTED_FOR_ATTACHING_PROFILER HRESULT를 반환합니다.
제한된 ICorProfilerInfo 메서드:
제한된 ICorProfilerInfo2 메서드:
제한된 ICorProfilerInfo3 메서드:
프로파일러 연결 예제
이 예제에서는 트리거 프로세스가 대상 프로세스의 PID(프로세스 식별자), 대기할 제한 시간(밀리초), 로드할 프로파일러의 CLSID 및 프로파일러 DLL 파일의 전체 경로를 이미 알고 있는 것으로 가정합니다. 또한 프로파일러의 구성 데이터를 저장하는 MyProfilerConfigData 구조체 및 구성 데이터를 해당 구조체에 삽입하는 PopulateMyProfilerConfigData 함수를 프로파일러 개발자가 정의한 것으로 가정합니다.
HRESULT CallAttachProfiler(DWORD dwProfileeProcID, DWORD dwMillisecondsTimeout,
GUID profilerCLSID, LPCWSTR wszProfilerPath)
{
// This can be a data type of your own choosing for sending configuration data
// to your profiler:
MyProfilerConfigData profConfig;
PopulateMyProfilerConfigData(&profConfig);
LPVOID pvClientData = (LPVOID) &profConfig;
DWORD cbClientData = sizeof(profConfig);
ICLRMetaHost * pMetaHost = NULL;
IEnumUnknown * pEnum = NULL;
IUnknown * pUnk = NULL;
ICLRRuntimeInfo * pRuntime = NULL;
ICLRProfiling * pProfiling = NULL;
HRESULT hr = E_FAIL;
hModule = LoadLibrary(L"mscoree.dll");
if (hModule == NULL)
goto Cleanup;
CLRCreateInstanceFnPtr pfnCreateInterface =
(CLRCreateInstanceFnPtr)GetProcAddress(hModule, "CLRCreateInterface");
if (pfnCreateInterface == NULL)
goto Cleanup;
hr = (*pfnCreateInterface)(CLSID_CLRMetaHost, IID_ICLRMetaHost, (LPVOID *)&pMetaHost);
if (FAILED(hr))
goto Cleanup;
hr = pMetaHost->EnumerateLoadedRuntimes(hProcess, &pEnum);
if (FAILED(hr))
goto Cleanup;
while (pEnum->Next(1, &pUnk, NULL) == S_OK)
{
hr = pUnk->QueryInterface(IID_ICLRRuntimeInfo, (LPVOID *) &pRuntime);
if (FAILED(hr))
{
pUnk->Release();
pUnk = NULL;
continue;
}
WCHAR wszVersion[30];
DWORD cchVersion = sizeof(wszVersion)/sizeof(wszVersion[0]);
hr = pRuntime->GetVersionString(wszVersion, &cchVersion);
if (SUCCEEDED(hr) &&
(cchVersion >= 3) &&
((wszVersion[0] == L'v') || (wszVersion[0] == L'V')) &&
((wszVersion[1] >= L'4') || (wszVersion[2] != L'.')))
{
hr = pRuntime->GetInterface(CLSID_CLRProfiling, IID_ICLRProfiling, (LPVOID *)&pProfiling);
if (SUCCEEDED(hr))
{
hr = pProfiling->AttachProfiler(
dwProfileeProcID,
dwMillisecondsTimeout,
profilerCLSID,
wszProfilerPath,
pvClientData,
cbClientData
);
pProfiling->Release();
pProfiling = NULL;
break;
}
}
pRuntime->Release();
pRuntime = NULL;
pUnk->Release();
pUnk = NULL;
}
Cleanup:
if (pProfiling != NULL)
{
pProfiling->Release();
pProfiling = NULL;
}
if (pRuntime != NULL)
{
pRuntime->Release();
pRuntime = NULL;
}
if (pUnk != NULL)
{
pUnk->Release();
pUnk = NULL;
}
if (pEnum != NULL)
{
pEnum->Release();
pEnum = NULL;
}
if (pMetaHost != NULL)
{
pMetaHost->Release();
pMetaHost = NULL;
}
if (hModule != NULL)
{
FreeLibrary(hModule);
hModule = NULL;
}
return hr;
}
연결 후 프로파일러 초기화
.NET Framework 4부터는 ICorProfilerCallback3::InitializeForAttach 메서드가 ICorProfilerCallback::Initialize 메서드의 연결 대응 메서드로 제공됩니다. CLR에서는 InitializeForAttach를 호출하여 연결 이후 상태를 초기화할 수 있는 기회를 프로파일러에게 제공합니다. 이 콜백에서 프로파일러는 ICorProfilerInfo::SetEventMask 메서드를 호출하여 하나 이상의 이벤트를 요청합니다. ICorProfilerCallback::Initialize 메서드에서의 호출과 달리 InitializeForAttach 메서드에서 SetEventMask 메서드를 호출하면 COR_PRF_ALLOWABLE_AFTER_ATTACH 비트마스크에 있는 비트만 설정할 수 있습니다(연결 제한 사항 참조).
InitializeForAttach에서 오류 코드를 받게 되면 CLR은 응용 프로그램 이벤트 로그에 실패를 기록하고, 프로파일러 콜백 인터페이스를 해제하고, 프로파일러를 언로드합니다. AttachProfiler는 InitializeForAttach와 동일한 오류 코드를 반환합니다.
프로파일러 연결 완료
.NET Framework 4부터는 InitializeForAttach 메서드 이후에 ICorProfilerCallback3::ProfilerAttachComplete 콜백이 발생합니다. ProfilerAttachComplete은 프로파일러에서 요청한 콜백이 InitializeForAttach 메서드에서 활성화되어 있고 이제 프로파일러는 손실된 알림에 신경 쓰지 않고 연결된 ID에서 후속 작업을 수행할 수 있음을 나타냅니다.
예를 들어, 프로파일러가 InitializeForAttach 콜백 중에 COR_PRF_MONITOR_MODULE_LOADS를 설정한 경우를 가정해 봅니다. 프로파일러가 InitializeForAttach에서 돌아오면 CLR은 ModuleLoad 콜백을 활성화한 다음 ProfilerAttachComplete 콜백을 발생시킵니다. 그런 다음 프로파일러는 ICorProfilerInfo3::EnumModules 메서드를 사용하여 ProfilerAttachComplete 콜백 중에 현재 로드되어 있는 모든 모듈의 ModuleID를 열거할 수 있습니다. 또한 열거 중에 로드되는 모든 새 모듈에 대해 ModuleLoad 이벤트가 발생합니다. 프로파일러는 발견되는 모든 중복 ModuleID를 올바로 처리해야 합니다. 예를 들어, 프로파일러가 연결하는 동안 모듈이 로드되면 해당 ModuleID가 두 번 나타날 수 있습니다. 즉, ICorProfilerInfo3::EnumModules에서 반환하는 열거형 및 ModuleLoadFinished 콜백에서 두 번 나타날 수 있습니다.
프로파일러 분리
연결 요청은 트리거 프로세스에 의해 프로세스 외부에서 시작되어야 합니다. 그러나 분리 요청은 ICorProfilerInfo3::RequestProfilerDetach 메서드를 호출하는 프로파일러 DLL에 의해 프로세스 내부에서 시작됩니다. 프로세스 외부(예: 사용자 지정 UI)에서 분리 요청을 시작하려는 프로파일러 개발자는 대상 응용 프로그램과 함께 실행 중인 프로파일러 DLL에게 RequestProfilerDetach를 호출하도록 신호를 보내는 프로세스 간 통신 메커니즘을 만들어야 합니다.
RequestProfilerDetach에서는 반환 이전에 일부 작업(예: 이벤트를 프로파일러 DLL로 보내지 않도록 내부 상태를 설정)을 동기적으로 수행합니다. 나머지 작업은 RequestProfilerDetach가 반환한 이후에 비동기적으로 수행됩니다. 모든 응용 프로그램 스레드의 스택에서 모든 프로파일러 코드가 팝되도록 하고 폴링을 수행하는 등의 나머지 작업은 개별 스레드(DetachThread)에서 실행됩니다. RequestProfilerDetach가 완료되면 CLR에서 프로파일러의 인터페이스 및 코드 힙을 해제하고 프로파일러의 DLL을 언로드하기 전에 프로파일러는 마지막 콜백(ICorProfilerCallback3::ProfilerDetachSucceeded)을 받습니다.
분리 과정의 스레드 고려 사항
실행 컨트롤은 여러 가지 방법으로 프로파일러에 전환될 수 있습니다. 그러나 컨트롤을 프로파일러 언로드 이후에 프로파일러에 전달하면 안 됩니다. 또한 프로파일러와 런타임은 이 동작이 발생하지 않도록 해야 하는 책임을 공유해야 합니다.
런타임은 프로파일러 코드를 스택에 포함하거나 포함할 수 있는 프로파일러에서 만들거나 도용한 스레드에 대해 알지 못합니다. 따라서 프로파일러는 자신이 만든 모든 스레드에서 종료되도록 해야 하며, ICorProfilerInfo3::RequestProfilerDetach를 호출하기 전에 모든 샘플링 또는 도용을 중지해야 합니다. 그러나 ICorProfilerInfo3::RequestProfilerDetach를 호출하기 위해 프로파일러가 만든 스레드를 프로파일러가 사용할 수 있는 경우에는 이 규칙이 적용되지 않습니다. 이 경우 해당 스레드는 LoadLibrary 및 FreeLibraryAndExitThread 함수를 통해 프로파일러 DLL에 대한 고유한 참조를 유지해야 합니다. 자세한 내용은 다음 섹션을 참조하십시오.
프로파일러가 RequestProfilerDetach를 호출한 이후 런타임에서는 프로파일러를 완전히 언로드하려고 할 때 ICorProfilerCallback 메서드 때문에 스레드 스택에 프로파일러 코드가 남아 있지 않도록 해야 합니다.
단계별 분리
프로파일러가 분리될 때 다음 단계가 수행됩니다.
예외적인 경우가 아니라면 프로파일러는 자신이 만든 모든 스레드를 종료하고 ICorProfilerInfo3::RequestProfilerDetach를 호출하기 전에 모든 샘플링 및 도용을 중지합니다.
프로파일러는 CLR에서 만든 스레드를 사용하는 대신 예외적으로 자체 스레드를 중 하나를 사용해 ICorProfilerInfo3::RequestProfilerDetach 메서드를 호출하여 분리를 구현할 수 있습니다. RequestProfilerDetach 메서드를 호출하는 것은 프로파일러 스레드이기 때문에 프로파일러가 이 동작을 구현하는 경우 이 메서드가 호출될 때 프로파일러 스레드가 종료되도록 할 수 있습니다. 그러나 프로파일러가 이 구현을 선택하는 경우에는 다음과 같은 동작이 보장되어야 합니다.
RequestProfilerDetach를 호출하기 위해 유지될 스레드는 LoadLibrary 함수를 자신에 대해 호출하여 프로파일러 DLL에 대한 자체 참조를 보존해야 합니다.
스레드는 RequestProfilerDetach를 호출한 직후 FreeLibraryAndExitThread 함수를 호출하여 프로파일러 DLL에 대한 보유를 해제하고 종료되어야 합니다.
RequestProfilerDetach는 프로파일러가 비활성화된 것으로 간주하기 위해 런타임의 내부 상태를 설정합니다. 이렇게 하면 나중에 콜백 메서드를 통해 프로파일러를 호출하지 못합니다.
RequestProfilerDetach는 모든 스레드가 스택에서 나머지 콜백 메서드를 모두 팝했는지 확인하도록 DetachThread에 신호를 보냅니다.
RequestProfilerDetach는 분리가 성공적으로 시작되었는지 여부를 나타내는 상태 코드와 함께 반환됩니다.
이 시점에서 CLR은 ICorProfilerInfo, ICorProfilerInfo2 및 ICorProfilerInfo3 인터페이스 메서드를 통해 프로파일러가 더 이상 CLR을 호출하지 못하도록 합니다. 이와 같이 호출할 경우 즉시 실패하고 CORPROF_E_PROFILER_DETACHING HRESULT가 반환됩니다.
프로파일러가 반환되거나 스레드를 종료합니다. 자체 스레드 중 하나를 사용하여 RequestProfilerDetach를 호출한 프로파일러는 이제 이 스레드에서 FreeLibraryAndExitThread를 호출해야 합니다. 프로파일러가 콜백 내에서 CLR 스레드를 사용하여 RequestProfilerDetach를 호출한 경우 프로파일러는 CLR에 제어를 다시 반환합니다.
한편 DetachThread는 모든 스레드가 스택에서 나머지 콜백 메서드를 모두 팝했는지 계속 확인합니다.
DetachThread는 스레드의 스택에 콜백이 남아 있지 않음을 확인한 후 ICorProfilerCallback3::ProfilerDetachSucceeded 메서드를 호출합니다. 프로파일러는 ProfilerDetachSucceeded 메서드에서 최소 작업만 수행하고 신속히 반환되어야 합니다.
DetachThread는 프로파일러의 ICorProfilerCallback 인터페이스에 있는 마지막 Release()를 수행합니다.
DetachThread는 프로파일러의 DLL에 있는 FreeLibrary 함수를 호출합니다.
분리 제한 사항
다음과 같은 시나리오에서는 분리가 지원되지 않습니다.
변경 불가능한 이벤트 플래그를 프로파일러에서 설정하는 경우
프로파일러에서 ELT(Enter/Leave/Tailcall)프로브를 사용하는 경우
프로파일러에서 MSIL(Microsoft Intermediate Language) 계측을 사용하는 경우(예: SetILFunctionBody 메서드 호출)
이러한 시나리오에서 프로파일러 분리가 시도되면 RequestProfilerDetach에서 오류 HRESULT를 반환합니다.
디버깅
프로파일러 연결 및 분리 기능 때문에 응용 프로그램 디버깅이 방해받지는 않습니다. 응용 프로그램은 디버깅 세션 중 어느 때라도 프로파일러와 연결되거나 분리될 수 있습니다. 반대로, 프로파일러와 연결되거나 분리된 응용 프로그램 역시 어느 때라도 디버거와 연결되거나 분리될 수 있습니다. 그러나 디버거에 의해 중지된 응용 프로그램의 경우 프로파일러 연결 요청에 응답할 수 없기 때문에 프로파일러와 연결되지 못합니다.