다음을 통해 공유


.NET Framework 2.0에서 프로파일러 스택 걷기: 기본 사항 및 그 이상

 

2006년 9월

데이비드 브로먼
Microsoft Corporation

적용 대상:
   Microsoft .NET Framework 2.0
   CLR(공용 언어 런타임)

요약: 프로파일러를 프로그래밍하여 .NET Framework CLR(공용 언어 런타임)에서 관리되는 스택을 안내하는 방법을 설명합니다. (14페이지 인쇄)

콘텐츠

소개
동기 및 비동기 호출
그것을 혼합
최상의 행동
더 이상은 안 된다
크레딧이 만기되는 크레딧
저자 정보

소개

이 문서는 관리되는 애플리케이션을 검사하기 위해 프로파일러를 빌드하는 데 관심이 있는 사용자를 대상으로 합니다. 프로파일러를 프로그래밍하여 .NET Framework CLR(공용 언어 런타임)에서 관리되는 스택을 안내하는 방법을 설명합니다. 주제 자체가 때때로 무거울 수 있기 때문에 기분을 밝게 유지하려고 노력할 것입니다.

CLR 버전 2.0의 프로파일링 API에는 프로파일러가 프로파일링 중인 애플리케이션의 호출 스택을 걸을 수 있는 DoStackSnapshot 이라는 새 메서드가 있습니다. CLR 버전 1.1은 In-process 디버깅 인터페이스를 통해 유사한 기능을 노출했습니다. 그러나 DoStackSnapshot을 사용하면 호출 스택을 걷는 것이 더 쉽고 정확하며 안정적입니다. DoStackSnapshot 메서드는 가비지 수집기, 보안 시스템, 예외 시스템 등에서 사용하는 것과 동일한 스택 워커를 사용합니다. 그래서 당신은 그것이 옳아야 있다는 것을 알고 있습니다.

전체 스택 추적에 액세스하면 프로파일러 사용자에게 흥미로운 일이 발생할 때 애플리케이션에서 발생하는 일에 대한 큰 그림을 얻을 수 있습니다. 애플리케이션 및 사용자가 프로파일링하려는 항목에 따라 개체가 할당될 때, 클래스가 로드될 때, 예외가 throw될 때 호출 스택을 원하는 사용자를 상상할 수 있습니다. 애플리케이션 이벤트(예: 타이머 이벤트)가 아닌 다른 항목에 대한 호출 스택을 가져오는 경우에도 샘플링 프로파일러에 흥미로울 수 있습니다. 핫스폿이 포함된 함수를 호출한 함수를 호출한 함수를 호출한 사람을 볼 수 있을 때 코드에서 핫스폿을 보는 것이 더 쉬워집니다.

DoStackSnapshot API를 사용하여 스택 추적을 가져오는 데 집중하겠습니다. 스택 추적을 가져오는 또 다른 방법은 섀도 스택을 빌드하는 것입니다. FunctionEnterFunctionLeave 를 후크하여 현재 스레드에 대한 관리형 호출 스택의 복사본을 유지할 수 있습니다. 섀도 스택 빌드는 애플리케이션을 실행하는 동안 항상 스택 정보가 필요하고 관리되는 모든 호출에서 프로파일러의 코드를 실행하고 반환하는 데 드는 성능 비용을 신경 쓰지 않는 경우에 유용합니다. DoStackSnapshot 메서드는 이벤트에 대한 응답과 같이 스택에 대한 약간 스파서 보고가 필요한 경우에 가장 적합합니다. 몇 밀리초마다 스택 스냅샷을 만드는 샘플링 프로파일러조차도 섀도 스택을 빌드하는 것보다 훨씬 희박합니다. 따라서 DoStackSnapshot은 샘플링 프로파일러에 적합합니다.

와일드 사이드에서 스택 워크를 수행합니다.

원할 때마다 호출 스택을 가져올 수 있는 것이 매우 유용합니다. 그러나 권력에는 책임이 있습니다. 프로파일러 사용자는 스택이 탐색되어 AV(액세스 위반) 또는 런타임 교착 상태가 발생하지 않도록 합니다. 프로파일러 작성기는 주의하여 힘을 발휘해야 합니다. DoStackSnapshot을 사용하는 방법과 신중하게 수행하는 방법에 대해 설명합니다. 여기서 볼 수 있듯이 이 메서드를 더 많이 사용하려고 할수록 올바르게 만들기가 더 어려워집니다.

우리의 주제를 살펴보겠습니다. 프로파일러가 호출하는 항목은 다음과 같습니다(Corprof.idl의 ICorProfilerInfo2 인터페이스에서 찾을 수 있음).

HRESULT DoStackSnapshot( 
  [in] ThreadID thread, 
  [in] StackSnapshotCallback *callback, 
  [in] ULONG32 infoFlags, 
  [in] void *clientData, 
  [in, size_is(contextSize), length_is(contextSize)] BYTE context[], 
  [in] ULONG32 contextSize); 

다음 코드는 CLR이 프로파일러에서 호출하는 코드입니다. (Corprof.idl에서도 찾을 수 있습니다.) 이전 예제의 콜백 매개 변수에서 이 함수의 구현에 대한 포인터를 전달합니다.

typedef HRESULT __stdcall StackSnapshotCallback( 
  FunctionID funcId, 
  UINT_PTR ip, 
  COR_PRF_FRAME_INFO frameInfo, 
  ULONG32 contextSize, 
  BYTE context[], 
  void *clientData); 

그것은 샌드위치처럼. 프로파일러가 스택을 연습하려는 경우 DoStackSnapshot을 호출합니다. CLR이 해당 호출에서 반환되기 전에 각 관리되는 프레임 또는 스택에서 관리되지 않는 프레임의 각 실행에 대해 한 번씩 StackSnapshotCallback 함수를 여러 번 호출합니다. 그림 1은 이 샌드위치를 보여줍니다.

그림 1. 프로파일링 중 호출의 "샌드위치"

내 표기법에서 볼 수 있듯이 CLR은 프레임이 스택에 푸시된 방식(리프 프레임 먼저 푸시(마지막에 푸시), 기본 프레임 마지막(먼저 푸시됨)에서 역순으로 프레임을 알릴 수 있습니다.

이러한 함수에 대한 모든 매개 변수는 무엇을 의미합니까? 나는 아직 그들 모두를 논의 할 준비가되지 않았습니다, 하지만 난 그들 중 몇 가지를 논의 할 것이다, DoStackSnapshot으로 시작. (나는 몇 분 안에 나머지에 도착합니다.) infoFlags 값은 Corprof.idl의 COR_PRF_SNAPSHOT_INFO 열거형에서 가져온 것이며 CLR이 보고하는 프레임에 대한 등록 컨텍스트를 제공할지 여부를 제어할 수 있습니다. clientData에 대해 원하는 값을 지정할 수 있으며 CLR은 StackSnapshotCallback 호출에서 다시 제공합니다.

StackSnapshotCallback에서 CLR은 funcId 매개 변수를 사용하여 현재 안내된 프레임의 FunctionID 값을 전달합니다. 현재 프레임이 관리되지 않는 프레임의 실행인 경우 이 값은 0이며, 나중에 살펴보겠습니다. funcId가 0이 아닌 경우 funcIdframeInfoGetFunctionInfo2GetCodeInfo2와 같은 다른 메서드에 전달하여 함수에 대한 자세한 정보를 얻을 수 있습니다. 스택 워크 중에 이 함수 정보를 바로 얻거나 funcId 값을 저장하고 나중에 함수 정보를 가져와 실행 중인 애플리케이션에 미치는 영향을 줄일 수 있습니다. 나중에 함수 정보를 가져오는 경우 frameInfo 값은 사용자에게 제공하는 콜백 내에서만 유효합니다. 나중에 사용할 수 있도록 funcId 값을 저장해도 괜찮지만 나중에 사용할 수 있도록 frameInfo 를 저장하지 마세요.

StackSnapshotCallback에서 돌아오면 일반적으로 S_OK 반환되고 CLR은 스택을 계속 걷습니다. 원하는 경우 스택 워크를 중지하는 S_FALSE 반환할 수 있습니다. 그러면 DoStackSnapshot 호출이 CORPROF_E_STACKSNAPSHOT_ABORTED 반환됩니다.

동기 및 비동기 호출

DoStackSnapshot을 동기 및 비동기식으로 두 가지 방법으로 호출할 수 있습니다. 동기 호출은 가장 쉽게 바로 호출할 수 있습니다. CLR이 프로파일러의 ICorProfilerCallback(2) 메서드 중 하나를 호출할 때 동기 호출을 수행하고, 이에 대한 응답으로 DoStackSnapshot 을 호출하여 현재 스레드의 스택을 안내합니다. 이는 ObjectAllocated와 같은 흥미로운 알림 지점에서 스택의 모양을 확인하려는 경우에 유용합니다. 동기 호출을 수행하려면 ICorProfilerCallback(2) 메서드 내에서 DoStackSnapshot을 호출하여 말하지 않은 매개 변수에 대해 0 또는 null을 전달합니다.

비동기 스택 워크는 다른 스레드의 스택을 걸거나 스레드를 강제로 중단하여 스택 워크(자체 또는 다른 스레드)를 수행할 때 발생합니다. 스레드를 중단하려면 스레드의 명령 포인터를 하이재킹하여 임의 시간에 사용자 고유의 코드를 강제로 실행해야 합니다. 이것은 여기에 나열하는 너무 많은 이유로 미친 듯이 위험합니다. 제발, 그냥 하지 마십시오. 비동기 스택 워크에 대한 설명을 DoStackSnapshot 의 비 하이재킹 사용으로 제한하여 별도의 대상 스레드를 안내합니다. 스택 워크가 시작될 때 대상 스레드가 임의 지점에서 실행 중이었기 때문에 이 "비동기"라고 합니다. 이 기술은 일반적으로 샘플링 프로파일러에서 사용됩니다.

다른 사람을 통해 모든 걷기

크로스 스레드, 즉 비동기 스택 워크를 약간 세분화해 보겠습니다. 현재 스레드와 대상 스레드의 두 스레드가 있습니다. 현재 스레드는 DoStackSnapshot을 실행하는 스레드입니다. 대상 스레드는 DoStackSnapshot에서 스택을 안내하는 스레드입니다. 스레드 매개 변수의 스레드 ID를 DoStackSnapshot에 전달하여 대상 스레드를 지정합니다. 다음에 일어나는 일은 희미한 마음을 위한 것이 아닙니다. 대상 스레드는 스택을 안내하도록 요청했을 때 임의의 코드를 실행하고 있었습니다. 따라서 CLR은 대상 스레드를 일시 중단하고, 해당 스레드가 실행되는 전체 시간 동안 일시 중단된 상태로 유지됩니다. 이 작업을 안전하게 수행할 수 있나요?

난 당신이 물어 기뻐요. 이것은 참으로 위험하고, 나는 이것을 안전하게 하는 방법에 대해 나중에 이야기 할 것이다. 하지만 먼저 혼합 모드 스택에 들어가겠습니다.

그것을 혼합

관리되는 애플리케이션은 관리 코드에서 모든 시간을 소비하지 않을 수 있습니다. PInvoke 호출 및 COM interop을 사용하면 관리 코드가 비관리 코드로 호출되고 경우에 따라 대리자를 사용하여 다시 호출할 수 있습니다. 관리 코드는 CLR(관리되지 않는 런타임)으로 직접 호출하여 JIT 컴파일을 수행하고, 예외를 처리하고, 가비지 수집을 수행하는 등의 작업을 수행합니다. 따라서 스택 워크를 수행하면 혼합 모드 스택이 발생할 수 있습니다. 일부 프레임은 관리되는 함수이고 다른 프레임은 관리되지 않는 함수입니다.

이미 자라!

내가 계속하기 전에, 짧은 중간. 모든 사람은 최신 PC의 스택이 더 작은 주소로 증가한다는 것을 알고 있습니다(즉, "푸시"). 그러나 이러한 주소를 마음속이나 화이트보드에서 시각화할 때는 세로로 정렬하는 방법에 동의하지 않습니다. 우리 중 일부는 스택 성장 상상 (상단에 작은 주소); 일부는 아래로 성장 참조하십시오 (하단에 작은 주소). 우리 팀에서도 이 문제에 대해 분열되어 있습니다. 지금까지 사용한 디버거와 함께 선택했습니다. 호출 스택 추적 및 메모리 덤프는 작은 주소가 큰 주소를 "위"라고 알려줍니다. 따라서 스택이 증가합니다. 기본 맨 아래에 리프 호출 수신자가 맨 위에 있습니다. 당신이 동의하지 않는 경우, 당신은 문서의이 부분을 통해 얻을 수있는 몇 가지 정신 재배열을해야합니다.

웨이터, 내 스택에 구멍이 있습니다.

이제 동일한 언어를 사용했으므로 혼합 모드 스택을 살펴보겠습니다. 그림 2에서는 혼합 모드 스택 예제를 보여 줍니다.

그림 2. 관리되는 프레임과 관리되지 않는 프레임이 있는 스택

조금 뒤로 물러나면 DoStackSnapshot 이 애초에 존재하는 이유를 이해하는 것이 좋습니다. 스택에서 관리 되는 프레임을 걷는 데 도움이 됩니다. 관리되는 프레임을 직접 연습하려고 하면 관리 코드에서 사용되는 몇 가지 엉뚱한 호출 규칙 때문에 특히 32비트 시스템에서 신뢰할 수 없는 결과를 얻을 수 있습니다. CLR은 이러한 호출 규칙을 이해하므로 DoStackSnapshot 이 이를 디코딩하는 데 도움이 될 수 있습니다. 그러나 관리되지 않는 프레임을 포함하여 전체 스택을 걸을 수 있게 하려는 경우 DoStackSnapshot 은 완전한 솔루션이 아닙니다.

선택할 수 있는 위치는 다음과 같습니다.

옵션 1: 아무 작업도 수행하지 않고 사용자에게 "관리되지 않는 구멍"이 있는 스택을 보고하거나...

옵션 2: 관리되지 않는 자체 스택 워커를 작성하여 해당 구멍을 채웁니다.

DoStackSnapshot이 관리되지 않는 프레임 블록을 가로질러 오면 앞에서 설명한 것처럼 funcId가 0으로 설정된 StackSnapshotCallback 함수를 호출합니다. 옵션 1을 사용하려는 경우 funcId 가 0일 때 콜백에서 아무 작업도 수행하지 않습니다. CLR은 다음 관리형 프레임에 대해 다시 호출하고 해당 지점에서 절전 모드를 해제할 수 있습니다.

관리되지 않는 블록이 둘 이상의 관리되지 않는 프레임으로 구성된 경우 CLR은 여전히 StackSnapshotCallback 을 한 번만 호출합니다. CLR은 관리되지 않는 블록을 디코딩하기 위해 노력하지 않습니다. 블록을 다음 관리되는 프레임으로 건너뛰는 데 도움이 되는 특별한 내부자 정보가 있으며, 이것이 진행됩니다. CLR은 관리되지 않는 블록 내에 무엇이 있는지 반드시 알지는 못합니다. 따라서 옵션 2를 파악할 수 있습니다.

첫 번째 단계는 Doozy입니다.

어떤 옵션을 선택하든 관리되지 않는 구멍을 채우는 것이 유일한 어려운 부분은 아닙니다. 산책을 시작하는 것만으로도 어려울 수 있습니다. 위의 스택을 살펴보세요. 맨 위에는 관리되지 않는 코드가 있습니다. 경우에 따라 운이 좋을 수 있으며 관리되지 않는 코드는 COM 또는 PInvoke 코드가 됩니다. 이 경우 CLR은 건너뛰는 방법을 알 수 있을 만큼 똑똑하고 첫 번째 관리되는 프레임(예제의 D)에서 연습을 시작합니다. 그러나 가능한 한 스택을 완료하도록 보고하기 위해 가장 관리되지 않는 최상위 블록을 걷는 것이 좋습니다.

최상위 블록을 걷고 싶지 않더라도 운이 좋지 않다면 관리 되지 않는 코드는 COM 또는 PInvoke 코드가 아니라 JIT 컴파일 또는 가비지 수집을 수행하는 코드와 같이 CLR 자체의 도우미 코드가 될 수 있습니다. 이 경우 CLR은 사용자의 도움 없이 D 프레임을 찾을 수 없습니다. 따라서 DoStackSnapshot 에 대한 시드되지 않은 호출은 오류 CORPROF_E_STACKSNAPSHOT_UNMANAGED_CTX 또는 CORPROF_E_STACKSNAPSHOT_UNSAFE 발생합니다. (그건 그렇고, corerror.h를 방문하는 것은 정말 가치가 있습니다.)

"시드되지 않은"이라는 단어를 사용했습니다. DoStackSnapshot컨텍스트contextSize 매개 변수를 사용하여 시드 컨텍스트 를 사용합니다. "context"라는 단어는 많은 의미로 오버로드됩니다. 이 경우 레지스터 컨텍스트에 대해 이야기하고 있습니다. 아키텍처 종속 Windows 헤더(예: nti386.h)를 정독하는 경우 CONTEXT라는 구조체를 찾을 수 있습니다. CPU 레지스터에 대한 값을 포함하고 특정 시점의 CPU 상태를 나타냅니다. 이것이 제가 말하는 컨텍스트의 유형입니다.

컨텍스트 매개 변수에 대해 null을 전달하면 스택 워크가 시드되지 않으며 CLR이 맨 위에서 시작됩니다. 그러나 컨텍스트 매개 변수에 대해 null이 아닌 값을 전달하여 스택의 아래쪽 일부 지점에서 CPU 상태를 나타내는 경우(예: D 프레임을 가리키는 경우) CLR은 컨텍스트를 사용하여 시드된 스택 워크를 수행합니다. 스택의 실제 상단을 무시하고 가리킬 때마다 시작됩니다.

좋아, 꽤 사실이 아니다. DoStackSnapshot에 전달하는 컨텍스트는 완전히 지시문보다 힌트에 가독성이 더 큽니다. CLR이 첫 번째 관리되는 프레임을 찾을 수 있다고 확신하는 경우(가장 관리되지 않는 최상위 블록이 PInvoke 또는 COM 코드이기 때문에) 이를 수행하고 시드를 무시합니다. 하지만 개인적으로 받아들이지 마십시오. CLR은 가장 정확한 스택 워크를 제공하여 도움을 주려고 합니다. 시드는 가장 관리되지 않는 최상위 블록이 CLR 자체의 도우미 코드인 경우에만 유용합니다. 건너뛰는 데 도움이 되는 정보가 없기 때문입니다. 따라서 CLR이 워크를 시작할 위치를 스스로 확인할 수 없는 경우에만 시드가 사용됩니다.

당신은 당신이 처음에 우리에게 씨앗을 제공 할 수있는 방법을 궁금해 할 수 있습니다. 대상 스레드가 아직 일시 중단되지 않은 경우 대상 스레드의 스택을 따라 D 프레임을 찾아 시드 컨텍스트를 계산할 수 없습니다. 그럼에도 불구하고 DoStackSnapshot을 호출하기 전에 그리고 DoStackSnapshot이 대상 스레드를 일시 중단하기 전에 관리되지 않는 산책을 수행하여 시드 컨텍스트를 계산하라고 말하고 있습니다. 사용자 CLR에서 대상 스레드를 일시 중단해야 합니까? 사실, 그래.

이 발레를 안무할 때가 된 것 같아요. 그러나 너무 깊어지기 전에 스택 워크를 시드하는 방법과 방법에 대한 문제는 비동기 워크에만 적용됩니다. 동기 연습을 수행하는 경우 DoStackSnapshot 은 시드 없이도 항상 가장 관리되는 최상위 프레임으로 가는 길을 찾을 수 있습니다.

모두 함께 지금

관리되지 않는 구멍을 채우는 동안 비동기, 스레드 간 시드 스택 워크를 수행하는 진정한 모험 프로파일러의 경우 스택 워크는 다음과 같습니다. 여기에 표시된 스택이 그림 2에서 본 스택과 동일한 스택이라고 가정합니다.

스택 콘텐츠 프로파일러 및 CLR 작업

1. 대상 스레드를 일시 중단합니다. (대상 스레드의 일시 중단 횟수는 이제 1입니다.)

2. 대상 스레드의 현재 레지스터 컨텍스트를 가져옵니다.

3. 레지스터 컨텍스트가 관리되지 않는 코드를 가리키는지 여부를 결정합니다. 즉, ICorProfilerInfo2::GetFunctionFromIP를 호출하고 FunctionID 값 0을 다시 가져올지 여부를 검사.

4. 이 예제에서 레지스터 컨텍스트는 관리되지 않는 코드를 가리키기 때문에 가장 관리되는 최상위 프레임(함수 D)을 찾을 때까지 관리되지 않는 스택 워크를 수행합니다.

5. 시드 컨텍스트를 사용하여 DoStackSnapshot 을 호출하면 CLR이 대상 스레드를 다시 일시 중단합니다. (일시 중단 횟수는 이제 2입니다.) 샌드위치가 시작됩니다.
a. CLR은 D용 FunctionID를 사용하여 StackSnapshotCallback 함수를 호출합니다.
b. CLR은 FunctionID가 0인 StackSnapshotCallback 함수를 호출합니다. 이 블록을 직접 걸어야 합니다. 첫 번째 관리되는 프레임에 도달하면 중지할 수 있습니다. 또는 다음 콜백이 다음 관리 프레임이 시작되는 위치와 관리되지 않는 워크가 종료되어야 하는 위치를 정확히 알려주기 때문에 다음 콜백 후 언젠가까지 관리되지 않는 워크를 속이고 지연시킬 수 있습니다.
다. CLR은 C용 FunctionID를 사용하여 StackSnapshotCallback 함수를 호출합니다.
d. CLR은 B용 FunctionID를 사용하여 StackSnapshotCallback 함수를 호출합니다.
e. CLR은 FunctionID가 0인 StackSnapshotCallback 함수를 호출합니다. 다시 말하지만, 당신은이 블록을 직접 걸어야합니다.
f. CLR은 A용 FunctionID를 사용하여 StackSnapshotCallback 함수를 호출합니다.
g. CLR은 Main용 FunctionID를 사용하여 StackSnapshotCallback 함수를 호출합니다.

h. DoStackSnapshot Win32 ResumeThread() API를 호출하여 대상 스레드를 "다시 시작"합니다. 이 API는 스레드의 일시 중단 횟수를 감소시키고(일시 중단 수는 이제 1임) 를 반환합니다. 샌드위치가 완성되었습니다.
6. 대상 스레드를 다시 시작합니다. 이제 일시 중단 횟수가 0이므로 스레드가 물리적으로 다시 시작됩니다.

최상의 행동

좋아, 이것은 몇 가지 심각한 주의없이 너무 많은 힘이다. 가장 고급 사례에서는 타이머 인터럽트 에 응답하고 애플리케이션 스레드를 임의로 일시 중단하여 스택을 안내합니다. 저런!

좋은 것은 어렵고 처음에는 명확하지 않은 규칙을 포함합니다. 그래서 잠수하자.

나쁜 씨앗

쉬운 규칙으로 시작하자 : 나쁜 씨앗을 사용하지 마십시오. DoStackSnapshot을 호출할 때 프로파일러가 잘못된(null이 아닌) 시드를 제공하는 경우 CLR은 잘못된 결과를 제공합니다. 여기서 가리키는 스택을 살펴보고 스택의 값이 무엇을 나타내야 하는지를 가정합니다. 그러면 CLR이 스택에서 주소로 간주되는 것을 역참조하게 됩니다. 잘못된 시드가 있는 경우 CLR은 값을 메모리의 알 수 없는 위치로 역참조합니다. CLR은 프로파일링 중인 프로세스를 중단하는 모든 2차 AV를 방지하기 위해 가능한 모든 작업을 수행합니다. 하지만 당신은 정말 바로 씨앗을 얻기 위해 노력해야한다.

현탁액의 비애

스레드 일시 중단의 다른 측면은 여러 규칙이 필요할 정도로 복잡합니다. 스레드 간 보행을 하기로 결정한 경우 최소한 CLR에 사용자를 대신하여 스레드를 일시 중단하도록 요청하기로 결정했습니다. 또한 스택 맨 위에 있는 관리되지 않는 블록을 걷고 싶다면 현재 이것이 좋은 생각인지에 대한 CLR의 지혜를 호출하지 않고 직접 스레드를 일시 중단하기로 결정했습니다.

컴퓨터 과학 수업을 수강했다면 아마도 "식사 철학자" 문제를 기억할 것입니다. 철학자 그룹이 테이블에 앉아 있으며, 각각 오른쪽에는 포크, 다른 하나는 왼쪽에 있습니다. 문제에 따르면, 그들은 각각 먹을 두 포크가 필요합니다. 각 철학자는 오른쪽 포크를 집어 들지만, 각 철학자가 왼쪽의 철학자가 필요한 포크를 내려 놓기를 기다리고 있기 때문에 아무도 왼쪽 포크를 선택할 수 없습니다. 그리고 철학자가 원형 테이블에 앉아 있다면, 당신은 대기의 주기와 공복의 많은있어. 그들이 모두 굶어 죽는 이유는 교착 상태 회피의 간단한 규칙을 깨뜨리기 때문입니다 : 여러 잠금이 필요한 경우 항상 동일한 순서로 가져 가라. 이 규칙을 따르면 A가 B에서 대기하고 B가 C에서 대기하고 C가 A에서 대기하는 주기를 방지합니다.

애플리케이션이 규칙을 따르고 항상 동일한 순서로 잠금을 취한다고 가정해 보겠습니다. 이제 구성 요소가 함께 제공되고(예: 프로파일러) 스레드를 임의로 일시 중단하기 시작합니다. 복잡성이 크게 증가했습니다. 서스펜더가 일시 중단자가 보유한 잠금을 가져와야 하는 경우 어떻게 해야 할까요? 또는 일시 중단자가 보유하는 잠금을 기다리는 다른 스레드에서 잠금을 기다리는 스레드가 일시 중단자가 보유한 잠금이 필요한 경우 어떻게 해야 할까요? 일시 중단은 스레드 종속성 그래프 새 에지를 추가하여 주기를 도입할 수 있습니다. 몇 가지 특정 문제를 살펴보겠습니다.

문제 1: suspendee는 일시 중단자가 필요하거나 일시 중단자가 의존하는 스레드에 필요한 잠금을 소유합니다.

문제 1a: 잠금이 CLR 잠금입니다.

상상할 수 있듯이 CLR은 많은 스레드 동기화를 수행하므로 내부적으로 사용되는 여러 잠금이 있습니다. DoStackSnapshot을 호출하면 CLR은 대상 스레드가 스택 워크를 수행하기 위해 현재 스레드(DoStackSnapshot을 호출하는 스레드)가 필요로 하는 CLR 잠금을 소유하고 있음을 검색합니다. 해당 조건이 발생하면 CLR은 일시 중단을 수행하지 않으며 DoStackSnapshot 은 오류 CORPROF_E_STACKSNAPSHOT_UNSAFE 함께 즉시 반환됩니다. 이 시점에서 DoStackSnapshot을 호출하기 전에 스레드를 직접 일시 중단한 경우 스레드를 직접 다시 시작하고 문제를 방지했습니다.

문제 1b: 잠금은 사용자 고유의 프로파일러 잠금입니다.

이 문제는 정말 상식적인 문제입니다. 여기 저기에서 수행할 고유한 스레드 동기화가 있을 수 있습니다. 애플리케이션 스레드(스레드 A)가 프로파일러 콜백을 발견하여 프로파일러의 잠금 중 하나를 사용하는 프로파일러 코드 중 일부를 실행한다고 상상해 보세요. 그런 다음 스레드 B는 스레드 A를 걸어야 합니다. 즉, 스레드 B는 스레드 A를 일시 중단합니다. 스레드 A가 일시 중단되는 동안 스레드 B는 스레드 A가 소유할 수 있는 프로파일러의 자체 잠금을 시도하지 않아야 합니다. 예를 들어 스레드 B는 스택 워크 중에 StackSnapshotCallback 을 실행하므로 스레드 A가 소유할 수 있는 콜백 중에 잠금을 사용하면 안 됩니다.

문제 2: 대상 스레드를 일시 중단하는 동안 대상 스레드가 일시 중단을 시도합니다.

"그런 일은 일어나지 않습니다!" 라고 말할 수 있습니다. 믿거나 말거나, 다음과 같은 경우 가능합니다.

  • 애플리케이션은 다중 프로세서 상자에서 실행되며,
  • 스레드 A는 한 프로세서에서 실행되고 스레드 B는 다른 프로세서에서 실행되며,
  • 스레드 B가 스레드 A를 일시 중단하려고 시도하는 동안 스레드 A는 스레드 B를 일시 중단하려고 합니다.

이 경우 두 일시 중단이 모두 승리하고 두 스레드가 모두 일시 중단될 수 있습니다. 각 스레드는 다른 스레드가 절전 모드를 해제하기를 기다리고 있으므로 일시 중단된 상태로 유지됩니다.

이 문제는 스레드가 서로 일시 중단될 DoStackSnapshot을 호출하기 전에 CLR을 사용하여 검색할 수 없기 때문에 문제 1보다 더 당황스럽습니다. 그리고 서스펜션을 수행한 후 너무 늦었습니다!

대상 스레드가 프로파일러를 일시 중단하려고 하는 이유는 무엇인가요? 가상의 잘못 작성된 프로파일러에서 스택 보행 코드와 일시 중단 코드는 임의 시간에 임의의 수의 스레드에 의해 실행될 수 있습니다. 스레드 A가 스레드 B가 스레드 A를 걷려고 하는 동시에 스레드 B를 걷려고 한다고 상상해 보세요. 둘 다 프로파일러의 스택 워킹 루틴의 SuspendThread 부분을 실행하기 때문에 동시에 서로를 일시 중단하려고 합니다. 둘 다 승리하고 프로파일되는 애플리케이션이 교착 상태에 빠졌습니다. 여기서 규칙은 분명합니다. 프로파일러가 두 스레드에서 스택 보행 코드(따라서 일시 중단 코드)를 동시에 실행할 수 없도록 합니다.

대상 스레드가 보행 스레드를 일시 중단하려고 시도할 수 있는 덜 명백한 이유는 CLR의 내부 작동 때문입니다. CLR은 가비지 수집과 같은 작업에 도움이 되도록 애플리케이션 스레드를 일시 중단합니다. 워커가 가비지 수집기 스레드가 워커를 일시 중단하려고 하는 동시에 가비지 수집을 수행하는 스레드를 걷다가 일시 중단하려고 하면 프로세스가 교착 상태가 됩니다.

그러나 문제를 피하는 것은 쉽습니다. CLR은 작업을 수행하려면 일시 중단해야 하는 스레드만 일시 중단합니다. 스택 워크에 두 개의 스레드가 있다고 상상해 보십시오. 스레드 W는 현재 스레드(워크를 수행하는 스레드)입니다. 스레드 T는 대상 스레드(스택이 실행되는 스레드)입니다. 스레드 W가 관리 코드를 실행한 적이 없으므로 CLR 가비지 수집이 적용되지 않는 한 CLR은 스레드 W를 일시 중단하지 않습니다. 즉, 프로파일러에서 스레드 W가 스레드 T를 일시 중단하는 것이 안전합니다.

샘플링 프로파일러를 작성하는 경우 이 모든 것을 보장하는 것은 매우 자연스러운 일입니다. 일반적으로 타이머 인터럽트 및 다른 스레드의 스택을 안내하는 별도의 고유한 만들기 스레드가 있습니다. 샘플러 스레드를 호출합니다. 샘플러 스레드를 직접 만들고 실행 내용을 제어할 수 있으므로(따라서 관리 코드를 실행하지 않음) CLR은 이를 일시 중단할 이유가 없습니다. 모든 스택 걷기를 수행하도록 자체 샘플링 스레드를 만들도록 프로파일러를 디자인하면 앞에서 설명한 "잘못 작성된 프로파일러"의 문제도 방지할 수 있습니다. 샘플러 스레드는 다른 스레드를 걷거나 일시 중단하려는 프로파일러의 유일한 스레드이므로 프로파일러가 샘플러 스레드를 직접 일시 중단하려고 시도하지 않습니다.

이것은 우리의 첫 번째 사소한 규칙, 그래서 강조에 대 한 그것을 반복 하자:

규칙 1: 관리 코드를 실행하지 않은 스레드만 다른 스레드를 일시 중단해야 합니다.

아무도 시체를 걷는 것을 좋아하지 않습니다.

스레드 간 스택 워크를 수행하는 경우 워크 기간 동안 대상 스레드가 활성 상태로 유지되도록 해야 합니다. 대상 스레드를 DoStackSnapshot 호출에 매개 변수로 전달해도 모든 종류의 수명 참조를 암시적으로 추가한 것은 아닙니다. 애플리케이션은 스레드가 언제든지 사라지도록 할 수 있습니다. 스레드를 연습하는 동안 발생하는 경우 액세스 위반이 쉽게 발생할 수 있습니다.

다행히 CLR은 ICorProfilerCallback(2) 인터페이스로 정의된 적절한 이름의 ThreadDestroyed 콜백을 사용하여 스레드가 제거될 때 프로파일러에 알립니다. ThreadDestroyed를 구현하고 스레드를 걷는 프로세스가 완료될 때까지 기다려야 합니다. 다음 규칙으로 자격을 얻을 수 있을 만큼 흥미롭습니다.

규칙 2: ThreadDestroyed 콜백을 재정의하고 삭제할 스레드 스택을 탐색할 때까지 구현이 대기하도록 합니다.

다음 규칙 2는 해당 스레드의 스택을 걷는 작업을 완료할 때까지 CLR이 스레드를 삭제하지 못하도록 차단합니다.

가비지 수집은 주기를 만드는 데 도움이 됩니다.

이 시점에서 상황이 약간 혼란 스러울 수 있습니다. 다음 규칙의 텍스트로 시작하여 여기에서 해독해 보겠습니다.

규칙 3: 가비지 수집을 트리거할 수 있는 프로파일러 호출 중에 잠금을 유지하지 마세요.

앞에서 프로파일러가 소유 스레드가 일시 중단될 수 있고 동일한 잠금이 필요한 다른 스레드에서 스레드를 걸을 수 있는 경우 자체 잠금이 있는 경우 프로파일러가 하나를 보유하는 것은 좋지 않은 생각이라고 언급했습니다. 규칙 3은 더 미묘한 문제를 방지하는 데 도움이 됩니다. 여기서는 소유 스레드가 가비지 수집을 트리거할 수 있는 ICorProfilerInfo(2) 메서드를 호출하려고 하는 경우 자체 잠금을 보유해서는 안 된다고 말합니다.

몇 가지 예제가 도움이 될 것입니다. 첫 번째 예제에서는 스레드 B가 가비지 수집을 수행한다고 가정합니다. 시퀀스는 다음과 같습니다.

  1. 스레드 A는 프로파일러 잠금 중 하나를 사용하고 소유합니다.
  2. 스레드 B는 프로파일러의 GarbageCollectionStarted 콜백을 호출합니다.
  3. 1단계에서 프로파일러 잠금의 스레드 B 블록.
  4. 스레드 A는 GetClassFromTokenAndTypeArgs 함수를 실행합니다.
  5. GetClassFromTokenAndTypeArgs 호출은 가비지 수집을 트리거하려고 하지만 가비지 수집이 이미 진행 중임을 검색합니다.
  6. 스레드 A 블록- 현재 진행 중인 가비지 수집(스레드 B)이 완료 될 때까지 기다립니다. 그러나 스레드 B는 프로파일러 잠금으로 인해 스레드 A를 기다리고 있습니다.

그림 3에서는 이 예제의 시나리오를 보여 줍니다.

그림 3. 프로파일러와 가비지 수집기 간의 교착 상태

두 번째 예제는 약간 다른 시나리오입니다. 시퀀스는 다음과 같습니다.

  1. 스레드 A는 프로파일러 잠금 중 하나를 사용하고 소유합니다.
  2. 스레드 B는 프로파일러의 ModuleLoadStarted 콜백을 호출합니다.
  3. 1단계에서 프로파일러 잠금의 스레드 B 블록.
  4. 스레드 A는 GetClassFromTokenAndTypeArgs 함수를 실행합니다.
  5. GetClassFromTokenAndTypeArgs 호출은 가비지 수집을 트리거합니다.
  6. 현재 가비지 수집을 수행하는 스레드 A는 스레드 B가 수집될 준비가 될 때까지 기다립니다. 그러나 스레드 B는 프로파일러 잠금으로 인해 스레드 A를 기다리고 있습니다.
  7. 그림 4에서는 두 번째 예제를 보여 줍니다.

그림 4. 프로파일러와 보류 중인 가비지 수집 간의 교착 상태

당신은 광기를 소화 했나요? 문제의 핵심은 가비지 수집에 자체 동기화 메커니즘이 있다는 것입니다. 첫 번째 예제의 결과는 한 번에 하나의 가비지 수집만 발생할 수 있기 때문에 발생합니다. 가비지 수집은 일반적으로 너무 자주 발생하지 않으므로 스트레스가 많은 조건에서 작업하지 않는 한 다른 가비지 수집을 기다려야하기 때문에 이는 명백한 프린지 사례입니다. 그럼에도 불구하고 충분히 오랫동안 프로파일을 작성하면 이 시나리오가 발생하고 이에 대비해야 합니다.

가비지 수집을 수행하는 스레드가 다른 애플리케이션 스레드를 수집할 준비가 될 때까지 기다려야 하기 때문에 두 번째 예제의 결과가 발생합니다. 문제는 혼합에 자신의 잠금 중 하나를 도입 할 때 발생, 따라서 주기를 형성. 두 경우 모두 스레드 A가 프로파일러 잠금 중 하나를 소유한 다음 GetClassFromTokenAndTypeArgs를 호출하도록 허용하여 규칙 3이 손상됩니다. (실제로 가비지 수집을 트리거할 수 있는 메서드를 호출하는 것으로 프로세스를 종료하기에 충분합니다.)

당신은 아마 지금까지 몇 가지 질문이 있습니다.

17. 가비지 수집을 트리거할 수 있는 ICorProfilerInfo(2) 메서드를 어떻게 알 수 있나요?

A. MSDN 또는 적어도 내 블로그 또는 Jonathan Keljo의 블로그에서 이를 문서화할 계획입니다.

17. 스택 걷기와 어떤 관련이 있나요? DoStackSnapshot의 멘션 없습니다.

A. 정답입니다. 또한 DoStackSnapshot은 가비지 수집을 트리거하는 ICorProfilerInfo(2) 메서드 중 하나도 아닙니다. 여기서 규칙 3에 대해 논의하는 이유는 자신의 프로파일러 잠금을 구현할 가능성이 가장 높기 때문에 이 트랩에 빠지기 쉬운 임의의 샘플에서 스택을 비동기적으로 걷는 모험적인 프로그래머이기 때문입니다. 실제로 규칙 2는 기본적으로 프로파일러에 동기화를 추가하도록 지시합니다. 샘플링 프로파일러에도 다른 동기화 메커니즘이 있을 가능성이 높으며, 아마도 임의의 시간에 공유 데이터 구조 읽기 및 쓰기를 조정할 수 있습니다. 물론 DoStackSnapshot 을 건드리지 않는 프로파일러에서 이 문제가 발생할 수 있습니다.

더 이상은 안 된다

하이라이트에 대한 간략한 요약으로 마무리하겠습니다. 기억해야 할 중요한 사항은 다음과 같습니다.

  • 동기 스택 워크에는 프로파일러 콜백에 대한 응답으로 현재 스레드를 걷는 작업이 포함됩니다. 여기에는 시드, 일시 중단 또는 특수 규칙이 필요하지 않습니다.
  • 스택의 맨 위가 PInvoke 또는 COM 호출의 일부가 아닌 관리되지 않는 코드인 경우 비동기 워크에는 시드가 필요합니다. 대상 스레드를 직접 일시 중단하고 가장 관리되는 최상위 프레임을 찾을 때까지 직접 탐색하여 시드를 제공합니다. 이 경우 시드를 제공하지 않으면 DoStackSnapshot 에서 오류 코드를 반환하거나 스택 맨 위에 있는 일부 프레임을 건너뛸 수 있습니다.
  • 스레드를 일시 중단해야 하는 경우 관리 코드를 실행하지 않은 스레드만 다른 스레드를 일시 중단해야 합니다.
  • 비동기 워크를 수행할 때 항상 ThreadDestroyed 콜백을 재정의하여 해당 스레드의 스택 워크가 완료될 때까지 CLR이 스레드를 삭제하지 못하도록 차단합니다.
  • 프로파일러가 가비지 수집을 트리거할 수 있는 CLR 함수를 호출하는 동안 잠금을 유지하지 마세요.

프로파일링 API에 대한 자세한 내용은 MSDN 웹 사이트의 프로파일링(관리되지 않음) 을 참조하세요.

크레딧이 만기되는 크레딧

이러한 규칙을 작성하는 것이 팀의 노력이므로 CLR 프로파일링 API 팀의 나머지 부분에 감사의 메모를 포함하고 싶습니다. 숀 셀리트렌니코프에게 특별한 감사, 누가이 콘텐츠의 많은 이전 화신을 제공.

 

작성자 정보

David는 제한된 지식과 성숙도를 감안할 때 생각보다 오랫동안 Microsoft에서 개발자로 일해 왔습니다. 더 이상 코드에서 검사 수 없지만, 그는 여전히 새로운 변수 이름에 대한 아이디어를 제공합니다. 데이비드는 초쿨라 백작의 열렬한 팬이며 자신의 차를 소유하고 있습니다.