윈도우즈 시스템 프로그래밍이라는 책과 해당 책의 저자이신 윤성우님의 강의를 통해 공부한 내용을 정리하는 글입니다.
CloseHandle 함수에 대한 정확한 이해
⦁ 프로세스가 소멸된다고 해서 커널 오브젝트가 소멸된다고 말할 수 없다.
⦁ 커널 오브젝트의 소멸 시기를 결정하는 기준을 알아보기 위해 CloseHandle 함수를 알아보자.
⦁ KernelObjProb1.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
TCHAR command[] = _T("KernelObjProb2.exe");
CreateProcess(NULL, command, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
CloseHandle(pi.hProcess);
return 0;
}
⦁ KernelObjProb2.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
DWORD n = 0;
while (n < 100)
{
for (DWORD i = 0; i < 10000; i++)
for (DWORD i = 0; i < 10000; i++);
_fputts(_T("KenrelObjProb2.exe \n"), stdout);
n++;
}
return 0;
}
⦁ CloseHandle 함수의 선언
BOOL CloseHandle (
HANDLE hObject
);
//If the function succeeds, the return value is nonzero.
CloseHandle 함수는 핸들을 반환할 때 사용하는 함수이다.
자칫 커널 오브젝트를 소멸시킨다고 단정지을 수 있지만 위 예제의 실행 결과를 보면 그렇지 않다는 것을 알 수 있다.
⦁ 실행 결과
실행 결과를 살펴보면 CloseHandle 함수 호출에 의해서 KernelObjProb2.exe 프로세스가 소멸되지 않았음을 확인할 수 있다.
CloseHandle 함수와 프로세스 종료코드
그렇다면 커널 오브젝트의 소멸 시점을 어떻게 결정지을까?
일단 프로세스가 종료되어야 한다. 그러나 프로세스 종료 시 해당 커널 오브젝트를 소멸시키면 무문제가 발생할 수 있다.
아래 예제에서는 자식 프로세스가 문제가 발생하면 -1을, 정상적으로 실행이 완료되면 1을 부모 프로세스로 반환하게 구현하였다.
⦁ OperationStateParent.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi;
DWORD state;
si.cb = sizeof(si);
si.dwFlags = STARTF_USEPOSITION | STARTF_USESIZE;
si.dwX = 100;
si.dwY = 200;
si.dwXSize = 300;
si.dwYSize = 200;
si.lpTitle = _T("return & exit");
TCHAR command[] = _T("OperationStateChild.exe");
CreateProcess(NULL, command, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &state);
if (state == STILL_ACTIVE)
_tprintf(_T("STILL_ACTIVE \n\n"));
else
_tprintf(_T("state : %d \n\n"), state);
return 0;
}
⦁ WaitForSingleObject 함수는 인자로 전달된 핸들이 가리키는 프로세스가 종료될 때까지 기다리는 함수이다.
⦁ GetExitCodeProcess 함수는 첫 번째 인자로 전달된 핸들이 가리키는 프로세스가 반환하는 종료코드를 얻기 위해 사용되는 함수이다. 만일 프로세스가 종료되지 않고 실행중이라면 STILL_ACTIVE를 반환한다.
⦁ OperationStateChild.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
float num1, num2;
_fputts(_T("Return Value Test \n"), stdout);
_tscanf(_T("%f %f"), &num1, &num2);
if (num2 == 0)
{
exit(-1);
}
_tprintf(_T("Operation Result: %f \n"), num1 / num2);
return 1;
}
두 번째 입력된 숫자로 나눗셈을 진행하므로 0이 전달되면 오류가 발생한다. 이럴 경우 exit(-1)이 실행되고, 문제 없이 연산 될 경우는 return 1로 마무리 된다. 이러한 값들이 종료코드이다.
⦁ 실행결과
첫 번째 실행결과는 정상적인 연산이 진행된 결과고, 두 번째 실행결과는 두 번째 인자로 0이 전달되어 비정상 종료가 일어난 결과이다. 이를 보면 알 수 있듯이 부모 프로세스는 자식 프로세스의 종료코드를 얻게 된다.
커널 오브젝트와 Usage Count
자식 프로세스의 종료코드는 자식 프로세스의 커널 오브젝트에 저장된다. 그렇기 때문에 자식 프로세스가 종료될때 커널 오브젝트도 동시에 소멸된다면 부모 프로세스는 위 예제와 같이 종료코드를 얻을수 없게 된다.
때문에, 위에서 커널 오브젝트 소멸 시기가 프로세스 종료시기와 일치하면 안된다고 이야기했다.
그렇다면 커널 오브젝트의 소멸 시기는 언제가 좋을까?
바로, 해당 커널 오브젝트를 참조하는 대상이 하나도 없을 때 소멸시키는 것이다.
그렇기 때문에 Windows는 Usage Count(참조 횟수)를 관리하여 이것이 0이 되면 해당 커널 오브젝트를 소멸시킨다.
프로세스는 생성과 동시에 커널 오브젝트의 Usage Count가 1이 된다.
그 뒤로 접근 가능 대상이 늘어날 때마다 Usage Count가 1씩 증가한다.
그러므로 부모 프로세스가 자식 프로세스를 생성하게되면 자식 프로세스의 Usage Count는 2가 된다.
자기 자신과 부모 프로세스 모두 자식 프로세스의 커널 오브젝트에 접근이 가능하기 때문이다.
Usage Count와 CloseHandle
⦁ OperationStateParent.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi;
DWORD state;
si.cb = sizeof(si);
si.dwFlags = STARTF_USEPOSITION | STARTF_USESIZE;
si.dwX = 100;
si.dwY = 200;
si.dwXSize = 300;
si.dwYSize = 200;
si.lpTitle = _T("return & exit");
TCHAR command[] = _T("OperationStateChild.exe");
CreateProcess(NULL, command, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &state);
if (state == STILL_ACTIVE)
_tprintf(_T("STILL_ACTIVE \n\n"));
else
_tprintf(_T("state : %d \n\n"), state);
return 0;
}
OperationStateParent.cpp에서 자식 프로세스를 생성해주었는데, 이것이 종료되면 자식 프로세스의 커널 오브젝트에 접근하는 대상이 하나 줄었다. 따라서 UsageCount가 1이 되므로 소멸되지 않는다.만약 이어서 CloseHandle 함수를 호출(인자로 자식 프로세스 핸들 전달)한다고 가정하면, 자식 프로세스의 커널 오브젝트에 더 이상 접근하지 않는다는 의미이므로 Usage Count가 하나 감소하고, 자식 프로세스의 커널 오브젝트가 소멸된다.
이로 미루어보아, CloseHandle 함수는 핸들을 반환하면서 커널 오브젝트의 Usage Count를 하나 감소시키는 기능을 한다는 것을 알 수 있다.
또, 프로세스가 종료되는 시점에도 Usage Count가 하나 감소하게 된다.
프로세스 강제 종료
CloseHandle 함수와 프로세스 종료는 별개의 문제임을 위 예제를 통해 알았다.
프로세스 종료를 요청하는 함수는 TerminateProcess이다.
이 함수는 프로세스를 강제로 종료시키기 때문에 사용에 주의를 기울여야 한다.
Calculator.cpp의 문제점 그리고 해결책
사용자가 ELSE에 해당하는 5를 입력하게되면, 계산기 프로그램이 실행되는 프로그램이었다.
계산기가 실행되게 되면 Usage Count가 2가 된다. 그 상태에서 계산기 프로세스를 종료시키면 Usage Count는 1이 된다. 그렇게 되면, 계산기 프로그램은 종료되었어도 계산기 프로세스의 커널 오브젝트는 소멸되지 않고 계속 남아있게 된다는 뜻이다. 그 뒤로 계속해서 ELSE에 해당하는 5를 입력하게 되면 커널 오브젝트는 소멸되지 않은 채 계속 누적될 것이다. 이러한 문제는 CloseHandle 함수를 호출하면 해결된다.
⦁ Calculator2.cpp
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
enum { DIV = 1, MUL, ADD, MIN, ELSE, EXIT };
DWORD ShowMenu();
void Divide(double, double);
void Multiple(double, double);
void Add(double, double);
void Min(double, double);
int _tmain(int argc, TCHAR* arvg[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi;
si.cb = sizeof(si);
TCHAR command[] = _T("calc.exe");
SetCurrentDirectory(_T("C:\\WINDOWS\\system32"));
DWORD sel;
double num1, num2;
while (true)
{
sel = ShowMenu();
if (sel == EXIT)
return 0;
if (sel != ELSE)
{
_fputts(_T("Input Num1, Num2: "), stdout);
_tscanf(_T("%lf %lf"), &num1, &num2);
}
switch (sel)
{
case DIV:
Divide(num1, num2);
break;
case MUL:
Multiple(num1, num2);
break;
case ADD:
Add(num1, num2);
break;
case MIN:
Min(num1, num2);
break;
case ELSE:
ZeroMemory(&pi, sizeof(pi));
CreateProcess(NULL, command, NULL, NULL,
TRUE, 0, NULL, NULL, &si, &pi);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
break;
}
}
return 0;
}
DWORD ShowMenu()
{
DWORD sel;
_fputts(_T("-----Menu----- \n"), stdout);
_fputts(_T("num 1: Divide \n"), stdout);
_fputts(_T("num 2: Multiple \n"), stdout);
_fputts(_T("num 3: Add \n"), stdout);
_fputts(_T("num 4: Minus \n"), stdout);
_fputts(_T("num 5: Any other operations, \n"), stdout);
_fputts(_T("num 6: Exit \n"), stdout);
_fputts(_T("SELECTION >>"), stdout);
_tscanf(_T("%d"), &sel);
return sel;
}
void Divide(double a, double b)
{
_tprintf(_T("%f / %f = %f \n\n"), a, b, a / b);
}
void Multiple(double a, double b)
{
_tprintf(_T("%f * %f = %f \n\n"), a, b, a * b);
}
void Add(double a, double b)
{
_tprintf(_T("%f + %f = %f \n\n"), a, b, a + b);
}
void Min(double a, double b)
{
_tprintf(_T("%f - %f = %f \n\n"), a, b, a - b);
}
CreateProcess 함수 호출 뒤에 CloseHandle을 두 번 호출하여 프로세스와 쓰레드의 커널 오브젝트를 소멸시켜준다. 쓰레드에 관해서는 차후에 등장한다.
바탕화면 프로세스
바탕화면에 있는 아이콘을 더블클릭하여 프로세스를 생성할 경우, 이 프로세스의 Usage Count는 2가 된다.
이는 바탕화면도 독립적으로 실행 중에 있는 프로세스이기 때문이다. 더블클릭이라는 이벤트를 발생시켜 바탕화면 프로세스에게 프로세스 생성을 요청하는 것이다.
그렇다면 바탕화면도 Calculator.cpp 예제와 같은 문제점이 발생하지 않을까 걱정이 될 수 있다.
하지만 Calculator2.cpp에서 보여주는 처리 방식과 동일한 처리를 하므로 문제가 발생하지 않는다.
즉, 바탕화면 프로세스는 자식 프로세스 생성과정에서 얻게 되는 핸들을 그 즉시 반환해 버린다.
'윈도우즈 시스템 프로그래밍' 카테고리의 다른 글
윈도우즈 시스템 프로그래밍 7장 - Signaled vs Non-Signaled (0) | 2021.07.25 |
---|---|
윈도우즈 시스템 프로그래밍 7장 - 프로세스간 통신(IPC) (0) | 2021.07.25 |
윈도우즈 시스템 프로그래밍 6장 - 커널 오브젝트와 핸들의 종속관계 (0) | 2021.07.24 |
윈도우즈 시스템 프로그래밍 6장 - 커널 오브젝트에 대한 이해 (0) | 2021.07.24 |
윈도우즈 시스템 프로그래밍 5장 - 프로그래밍을 통한 프로세스 생성 (0) | 2021.07.23 |