<커널 오브젝트와 오브젝트 핸들>
[커널 오브젝트에 대한 이해]
저를 포함해서 운영체제와 관련된 공부를 하면서 아마 '커널(Kernel)'이라는 단어를 한 번쯤은 들어봤을겁니다.
우리가 지금 쓰는 운영체제는 기본적인 요소 외에도 여러가지 기능들을 제공합니다.
메모장이라던가, 웹 브라우저, 그림판이나 계산기 등등의 잡다한 것들도 있습니다.
그런데 그 중에서도 컴퓨터를 운영하는 데에 있어 핵심이 되는 부분을 '커널'이라고 합니다.
요즘에는 이런걸 세세하게 따지는지는 잘 모르겠습니다.
그렇지만 이번에는 '커널 오브젝트'라는 것을 주제로 다룰 것이기 때문입니다.
일단 '커널 오브젝트'라는 놈이 뭐냐?
한 문장으로 정리하면 다음과 같습니다.
"커널에서 관리하는 중요한 정보를 담아둔 데이터 블록을 가리켜 '커널 오브젝트'라 한다."
여기서 커널이 관리하는 중요한 정보라 함은 커널이 관리하는 리소스 정보를 뜻합니다.
그리고 이 리소스는 운영체제에 의해서 생성/소멸되는 정보와 데이터들을 의미합니다.
[커널 오브젝트의 이해]
https://sevenshards.tistory.com/43
이전 글인 '프로세스 생성과 소멸'에서 CreateProcess 함수를 이용해서 프로세스를 직접 생성해봤습니다.
그런데 엄밀히 따지면, 프로세스를 직접 만든 것이 아니라 '운영체제한테 프로세스를 만들어달라'라고 '요구'를 한겁니다.
그래서 Windows 운영체제에서는 우리의 요구에 맞게 프로세스를 만들어서 준 것이고요.
정리하자면, 프로세스를 생성하는 실질적인 주체는 코드를 작성하는 프로그래머가 아니라 운영체제라는 겁니다.
더 나아가서 프로세스를 관리하는 것도 운영체제가 합니다.
그러니까 프로세스 생성부터 소멸까지, 그리고 그 사이에 일어나는 일(프로세스의 상태변화와 같은)을 하는거죠.
근데 운영체제는 프로세스를 하나만 관리하는 것이 아니라 여러 개의 프로세스를 관리합니다.
그러다보면 운영체제의 입장에서는 얘네를 관리하려면 뭔가 고정적으로 저장하고 갱신해야할 정보들이 생기게 됩니다.
예를 들자면 우선 순위 정보라던가, 프로세스 상태 정보와 같은 것들은 운영체제 내부에서 관리가 되는 것들이죠.
정보가 바뀌면 계속 갱신을 해줘야하고, 이 정보를 토대로 운영체제의 스케줄러가 프로세스를 관리할 것이니까요.
그래서 운영체제가 프로세스를 관리하기 위해서는 프로세스와 관련된 정보를 저장할 수 있어야 합니다.
그리고 갱신을 하려면 정보에 대해서 참조와 변경도 할 수 있어야 합니다.
그래서 Windows 운영체제 개발자들은 어떤 구조체를 하나 만들게 됩니다.
이 구조체에는 프로세스에 대한 정보를 담기 위한 구조체이며 일단은 '프로세스 관리 구조체'라고 하겠습니다.
이제 프로세스가 생성될 때마다 '프로세스 관리 구조체의 변수'가 하나씩 생성되게 됩니다.
그리고 이 구조체에는 새롭게 생성된 프로세스 정보들로 초기화가 됩니다.
이 변수가 바로 '커널 오브젝트(Kernel Object)'라는 놈입니다.
근데 여기서 '프로세스 관리 구조체'라는 것이 있을 것이라고는 했지, 어떻게 생겼는지는 아무도 모릅니다.
Windows 운영체제 개발자가 아니고서는요.
실제로 MS에서도 이와 관련된 정보는 절대 오픈하지 않습니다.
그래서 어떤 멤버 변수들이 있는지는 정확히는 모르지만, 대략적으로 감을 잡을 수는 있습니다.
이 말이 무슨 말인가 하면, 커널 오브젝트는 프로그래머가 직접 생성하거나 조작할 수 없다는 겁니다.
"아니 직접 생성도 못하고 조작도 못하는걸 뭐하러 배워요!" 라고 하시는 분들이 있을거라고 생각합니다.
저도 그랬거든요.
그래도 커널 오브젝트를 왜 알아야 되느냐?
Windows에서 제공하는 시스템 함수를 이해하려면 이 개념을 알아야 하거든요.
[그 이외의 커널 오브젝트들]
일단은 프로세스가 생성될 때마다 커널 오브젝트가 생성된다고 했습니다.
근데 프로세스가 생성될 때에만 커널 오브젝트가 생성되는 것은 아닙니다.
앞에서 '커널이 관리하는 리소스 정보가 담기는 데이터 블록이 커널 오브젝트다' 라고 했었죠.
그리고 '리소스'라는 것은 '운영체제에 의해서 관리가 되는 대상'을 말합니다.
이 리소스라는 것에는 프로세스도 포함됩니다.
그리고 나중에 배우게 될 쓰레드나 메일슬롯, 파이프라는 놈도 리소스고, 심지어 파일도 리소스입니다.
그래서 요약하자면 다음과 같습니다.
"Windows OS는 프로세스, 쓰레드, 파일과 같은 리소스(Resource)들을 원활히 관리하기 위해 필요한 정보를 저장한다.
이때 데이터를 저장하는 메모리 블록을 가리켜 커널 오브젝트(Kernel Object)라 한다."
그러면 Windows에서 만드는 모든 종류의 커널 오브젝트들은 동일한 구조체로부터 만들어질까요?
정답은 "No"입니다.
프로세스는 프로세스에 맞는, 쓰레드에는 쓰레드에 맞는, 파일에는 파일에 맞는 커널 오브젝트가 생성됩니다.
리소스별로 관리되어야 하는 사항들이 다 다르기 때문에 그렇습니다.
그래서 커널 오브젝트는 위 그림처럼 리소스별로 따로따로 생성됩니다.
이 그림의 포인트는 Windows 커널에 의해서 관리되는 리소스의 수 만큼 커널 오브젝트도 생성된다는 것입니다.
[오브젝트 핸들(Handle)을 이용한 커널 오브젝트의 조작]
제목에서도 보면 아시겠지만, 이제 커널 오브젝트를 조작하는 부분에 대해서 이야기를 할까 합니다.
아까 커널 오브젝트는 프로그래머가 생성도 못하고, 접근도 못한다고 했습니다.
네, 맞습니다. 얼씬도 못합니다.
하지만! Windows에서 제공하는 시스템 함수의 호출을 통해서 간접적인 조작은 가능합니다.
MS에서는 프로그래머가 괜히 커널 오브젝트를 직접 건드리는 것보다 자기들이 제공하는 방식으로 접근하길 원합니다.
괜히 직접 조작했다가 뭔가 잘못되면 큰일이 날 수도 있는 것이고, 그리고 무엇보다도 오픈하기 싫은 것이 클겁니다.
그래도 프로그래머들이 이걸 조작해야하는 상황은 있습니다.
그래서 최소한도 내에서, 안전하게 접근하라고 시스템 함수를 제공한겁니다.
[프로세스의 우선 순위 변경]
일단 커널 오브젝트를 조작은 해보려고 하는데, 도대체 조작할 것이 뭐가 있을까요?
가장 먼저 프로세스의 우선 순위를 변경시켜봅시다.
우선순위와 관련해서는 꽤 뒤에 가서 공부하게 될 쓰레드라는 놈을 만나면 좀 더 자세하게 다루게 될 것 같습니다.
사용할 함수는 SetPriorityClass라는 놈입니다.
들어가서 함수의 정의를 보시면 첫 번째 인자에 핸들(HANDLE)이라는 놈이 있습니다.
핸들? 뭐 자동차 핸들 말하는건가?
대충 그렇게 생각하시면 이해하기 빠르실겁니다.
맞습니다. 우리가 커널 오브젝트를 조작하기 위한 '핸들'의 개념으로 이해하면 됩니다.
일단 이 '핸들'이라는 놈은 커널 오브젝트에 할당되는 숫자입니다.
[커널 오브젝트에 할당되는 숫자! 핸들(Handle)]
일단 우선순위 정보를 변경해야되는데, 이건 커널 오브젝트에 있는 정보입니다.
그리고 우리는 이 커널 오브젝트를 조작하고 싶은 상황이고요.
그래서 SetPriorityClass라는 함수를 가지고 운영체제에 다음과 같이 요구합니다.
"내가 지금 이 커널 오브젝트의 우선 순위를 바꾸고 싶은데 바꿔줘요"
여기서 문제는 '이 커널 오브젝트'를 뭐로 가리킬까요?
위 그림처럼 Windows에서는 커널 오브젝트를 생성할 때마다 '핸들'이라는 정수값을 하나씩 부여합니다.
그래서 이 정수값만 알 수 있다면, 커널 오브젝트를 가리키는 것이 가능하게 됩니다.
SetPriorityClass의 첫 번째 인자는 내가 우선순위를 변경하고 싶은 커널 오브젝트의 핸들을 주면 되는겁니다.
이처럼 '핸들'은 커널 오브젝트를 가리키는(지시하는) 용도로 사용됩니다.
아까 자동차 핸들을 비유로 들었죠.
"이 커널 오브젝트의 핸들을 내가 쥐고 있다!" 라고 생각하면 좀 이해하기 쉽지 않을까 합니다.
근데... 핸들 정보는 어디서 갖고와요?
[핸들 정보는 어디서?]
우선순위를 변경시키는 시스템 함수도 알았고, 핸들이라는 개념도 알았습니다.
그럼 이제 남은건?
'핸들 정보를 어디서 어떻게 갖고 올거냐'가 남았습니다.
핸들 정보를 얻는 방법은 커널 오브젝트의 종류, 다시 말해서 리소스의 종류별로 다릅니다.
여기서는 프로세스만 이야기를 했으니, 코드를 통해서 프로세스의 핸들을 얻어오는 것을 보겠습니다.
[Operation1.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: Opertaion1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-03
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트 핸들을 이용한 커널 오브젝트의 조작 예제
* 이전 버전 내용:
*/
#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("Operation2.exe");
CreateProcess(
NULL, command, NULL, NULL,
TRUE, 0, NULL, NULL, &si, &pi);
while (1)
{
for (DWORD i = 0; i < 10000; i++)
for (DWORD i = 0; i < 10000; i++); // Busy Waiting 상태
_fputts(_T("Operation1.exe\n"), stdout);
}
return 0;
}
코드 자체는 어려울 부분이 없습니다.
다만, 여기서 Busy Waiting이라고 주석을 달아놓은 부분이 있습니다.
보시면 별 하는 것도 없으면서 중첩 for문을 통해 10000x10000이라는 괴상한 작업을 하고 있습니다.
이 부분은 아래에 있는 출력문이 나오는 속도를 늦추기 위해서 들어간 구문입니다.
실제 프로세스는 이 루프를 완전히 돌 때까지는 아래의 출력문이 나오지 않게 됩니다.
그래서 우리가 보기에는 아무것도 안하는 것 같지만, 내부에서는 미친듯이 뺑이(?)를 치고 있는 상황입니다.
아마 공부를 좀 하신 분이라면 Sleep 함수를 써도 되지 않겠냐라고 생각하는 분들도 있으실 겁니다.
그런데 예제에서 의도하는 결과를 보려면 Sleep은 답이 아닙니다.
Busy Waiting과 다르게 Sleep은 프로세스가 Blocked 상태로 들어가기 때문입니다.
[Operation2.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: Opertaion2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-03
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트 핸들을 이용한 커널 오브젝트의 조작 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
SetPriorityClass(GetCurrentProcess(), HIGH_PRIORITY_CLASS);
while (1)
{
for (DWORD i = 0; i < 10000; i++)
for (DWORD i = 0; i < 10000; i++); // Busy Waiting 상태
_fputts(_T("Operation2.exe\n"), stdout);
}
return 0;
}
여기서 SetPriorityClass 함수가 사용되었습니다.
그리고 핸들을 얻는 방법 중 하나인 GetCurrentProcess()라는 함수를 사용했습니다.
이는 현재 프로세스의 핸들을 얻을 때 사용하는 함수입니다.
그리고 HIGH_PRIORITY_CLASS라는 것은 상수값이 두 번째 인자로 전달됩니다.
지금은 '일반적인 프로세스보다는 높은 우선순위를 부여하라' 정도로 알고 있으면 됩니다.
그리고 실행해보면 결과는 아마 대부분 'Operation2가 우선순위가 높으니까 얘만 실행되겠지' 라고 생각하실겁니다.
하지만 애석하게도 현 시점에서는 그런 결과를 보신 분은 없을겁니다.
이 책에서 나오는 실행 결과는 '싱글코어 CPU'를 기준으로 진행해서 그렇거든요.
요즘 어지간한 CPU들은 다 코어가 여럿인데.
다만, Operation2.exe가 아주 근소하게나마 Operation1.exe보다 한 번 더 출력되는 것을 보실 수는 있을겁니다.
일단 듀얼코어 이상일 경우에는 프로세스 우선순위랑은 무관하게 둘 다 같이 번갈아가면서 나옵니다.
이 부분에 대해서는 저도 나름대로 생각해봤습니다.
듀얼코어 이상부터는 병렬처리가 가능하기에 이런 결과가 나온다고 보고 있습니다.
그리고 듀얼코어 이상이라서 볼 수 없는 결과지만, 책을 가지고 있으신 분들은 뒤에 다른 결과가 있습니다.
거기서 도출할 수 있는 결론이 하나가 더 있습니다.
"함수가 호출되어 실행되는 중간에는 절대로 CPU의 할당 시간을 다른 프로세스에게 넘기지 않을 것이다"
→ 응 아니야
완전히 잘못 생각하고 있는겁니다.
운영체제의 기준에서는 그딴거 없습니다.
우선순위가 제일 먼저 고려되어야 하고, 우선순위가 높은 놈이 깡패가 되는겁니다.
그래서 함수가 호출되어 실행하는 도중에도 얼마든지 우선순위가 높은 프로세스가 먼저 실행될 수 있다는 것.
이 정도까지가 이번 예제에서 확인할 수 있는 부분입니다.
[커널 오브젝트와 핸들의 종속 관계]
앞에서 커널 오브젝트와 핸들이 왜 있는지, 그리고 이 둘이 어떤 관계를 가지고 있는지도 정리를 해봤습니다.
그러면 이제 종속관계는 어떻게 되어있는지 한 번 알아봅시다.
"앞의 개념도 어려워 죽겠는데 종속관계는 또 뭔 소리냐?"
저도 처음에는 이 챕터 들어오면서부터 뭔 소리인가 싶었습니다.
처음에 읽을 때는 그냥 그러려니 했고, 강의를 들으면서 두 번째로 내용을 정리했습니다.
그리고 지금 이 글을 쓰면서 세 번째로 읽을 때가 되니까 이해가 됐고요.
한 번에 이해가 안된다면 여러번 보시면 됩니다.
[커널 오브젝트의 종속 관계]
일단 결론부터 먼저 쓰겠습니다.
"커널 오브젝트는 Windows 운영체제에 종속적이다."
이 말이 무슨 말인고 하면, 책에서도 들었던 비유를 그대로 들어보겠습니다.
도서관에 가면 많은 책들이 있습니다.
그리고 이 책들은 도서관에 등록된 고객들에게 대출이 가능합니다.
그렇다면 책들은 고객들에게 종속된 것일까요? 도서관에 종속된 것일까요?
별 문제가 없다면 책은 도서관에 종속되었다고 생각하실겁니다.
도서관에 있는 책은 어떤 고객이 소유하고 있는 것이 아니라 도서관에서 소유하는 것입니다.
그래서 도서관에서 새 책을 사서 넣을 수도 있고, 오래된 책이면 폐기할 수도 있는 것이고요.
이걸 도서관을 이용하는 고객이 하면 말이 안되는 것이죠.
비유를 들었던 것을 하나하나 풀어보겠습니다.
'도서관 = 운영체제'
'책들 = 커널 오브젝트'
'고객 = 리소스(프로세스, 쓰레드 등등)'
'새 책을 사서 넣는다 = 커널 오브젝트의 생성'
'책을 폐기한다 = 커널 오브젝트의 소멸'
그래서 위에서 들었던 비유를 토대로 다음과 같은 결론이 나옵니다.
1) 커널 오브젝트는 리소스에 종속적인 것이 아니라, 운영체제에 종속적인 관계다.
따라서 커널 오브젝트의 생성 및 소멸 시점은 운영체제에 의해서 결정된다.
2) 커널 오브젝트는 리소스에 종속적인 것이 아니라 운영체제에 종속적인 관계다.
따라서 커널 오브젝트는 여러 리소스에 의해서 접근이 가능하다.
[핸들(핸들 테이블)의 종속 관계]
그렇다면 커널 오브젝트가 생성되면서 만들어지는 핸들의 종속관계는 어떻게 될까요?
여기는 반대로 핸들(핸들 테이블)은 운영체제에 종속적인 관계가 아니라 리소스에 종속적인 관계가 됩니다.
일단 핸들(정확히는 핸들 테이블)은 리소스가 생성되면서 만들어지기 때문입니다.
이 부분은 좀 더 뒤에 가서 다루게 될 것 같습니다.
현 시점에서는 커널 오브젝트와 핸들의 종속 관계는 상대적인 소유관계가 있다는 것을 알아두시면 됩니다.
[예제를 통한 종속 관계의 이해]
[KerObjShare.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: KernelObjShare.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: 커널 오브젝트는 여러 프로세스에서 접근 가능함을 확인하는 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi = { 0, };
si.cb = sizeof(si);
TCHAR command[] = _T("Operation2.exe");
CreateProcess(
NULL, command, NULL, NULL, TRUE,
0, NULL, NULL, &si, &pi);
// 타이밍이라는 변수를 둔다.
DWORD timing = 0;
while (1)
{
for (DWORD i = 0; i < 10000; i++)
for (DWORD i = 0; i < 10000; i++); // Busy Waiting
_fputts(_T("Parent\n"), stdout);
// Busy waiting을 하는 사이에 증가한다.
timing += 1;
if (timing == 2) // 2가 되면
SetPriorityClass(pi.hProcess, NORMAL_PRIORITY_CLASS); // 우선순위를 낮춘다.
}
return 0;
}
앞에서 만들었던 Operation2는 그대로 써먹습니다.
여기서 주목하셔야 될 부분은 우선 순위를 낮춘다고 주석을 달아둔 부분입니다.
pi.hProcess가 무엇인가 하면, 내가 새로 생성한 자식 프로세스의 핸들입니다.
PROCESS_INFORMATION이라는 구조체에는 내가 생성한 자식 프로세스의 정보가 담기게 됩니다.
그래서 프로세스를 생성하면 자식 프로세스의 프로세스와 쓰레드 핸들 정보가 들어가 있습니다.
자식 프로세스의 핸들 정보를 가지고 있기 때문에 부모 프로세스에서 우선순위를 낮추는 것이 가능합니다.
그리고 위에서 말했던 것과 같이, 커널 오브젝트는 여러 리소스에 의해 접근이 가능하다는 것을 보여주는 예제입니다.
[PROCESS_INFORMATION 구조체]
구조체의 정의를 보시면 첫 번째는 자식 프로세스의 핸들, 두 번째는 자식 프로세스의 쓰레드 핸들입니다.
그럼 3, 4번째는 뭐냐?
핸들이랑 ID의 차이를 둔 이유가 뭐냐라고 하실텐데, 이렇게 생각하시면 됩니다.
"프로세스(쓰레드) 핸들은 프로세스의 커널 오브젝트를 가리키기(구분짓기) 위한 것이다.
프로세스(쓰레드) ID는 커널 오브젝트가 아니라 프로세스(쓰레드) 자체를 구분짓기 위한 것이다."
뭔가 말해놓고도 좀 이상하긴 한데, 뒤에 가서 공부하게 될 '핸들 테이블'이라는 개념을 알고 나면 정리가 될 겁니다.
[커널 오브젝트와 Usage Count]
이제 이번 글의 마지막 주제입니다.
아까 앞에서 커널 오브젝트와 핸들의 종속 관계에 대해서 이야기를 했었습니다.
그리고 이전 예제는 "커널 오브젝트는 여러 리소스에 의해서 접근이 가능하다"는 사실을 확인했고요.
"커널 오브젝트의 생성과 소멸의 주체는 운영체제다."
라는 결론을 내렸었는데, 여기서는 이걸 확인하려고 합니다.
[CloseHandle 함수에 대한 정확한 이해]
예를 들어, A라는 프로세스가 생성되었다고 합시다.
그러면 A라는 프로세스에 대한 커널 오브젝트도 생성이 됩니다.
A라는 프로세스에 대한 커널 오브젝트는 프로세스 A를 대표한다고 볼 수 있습니다.
그렇다면 그 역인 프로세스 A는 프로세스 A의 커널 오브젝트를 대표한다고 볼 수 있을까요?
안됩니다.
프로세스가 소멸한다고 해서 커널 오브젝트가 소멸한다고는 말할 수 없습니다.
소멸될 수도 있지만, 아닐 수도 있습니다.
왜냐고요? 운영체제가 결정할 일이지 우리가 결정할 수 있는 일이 아니니까요.
그럼 운영체제가 커널 오브젝트 소멸시기를 결정하는 것은 기준은 무엇인지 확인해봅시다.
[CloseHandle 함수와 프로세스 소멸]
[KernelObjProb1.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: KernelObjProb1.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: CloseHandle 함수 사용 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int _tmain(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi = { 0, };
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;
}
코드 자체는 특별할 것은 없습니다.
다만, CloseHandle이라는 함수가 새롭게 쓰였습니다.
CloseHandle 함수는 이름 그대로 핸들을 닫는 기능을 수행합니다.
다른 표현으로는 핸들을 반환한다라고 볼 수도 있습니다.
이 함수와 관련해서는 'Usage Count'라는 개념이 함께 들어가야 정확한 이해가 가능합니다.
지금 시점에서 이렇게 해석하는 분들도 있을겁니다.
"핸들이 가리키는 리소스가 더 이상 필요하지 않으니 이에 해당하는 리소스를 해제하고 커널 오브젝트도 소멸시켜라"
리소스 해제와 커널 오브젝트의 소멸은 좀 다른 이야기입니다.
완전히 틀린 말은 아니지만, 엄밀히 따지면 틀린 말입니다.
왜 그런지는 뒤의 예제 코드를 이어서 확인해보겠습니다.
[KernelObjProb2.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: KernelObjProb2.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: CloseHandle 함수 사용 예제
* 이전 버전 내용:
*/
#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++); // Busy waiting
_fputts(_T("KernelObjProb2.exe\n"), stdout);
n++;
}
return 0;
}
KernelObjProb1.cpp에서 사용한 CloseHandle 함수에 대한 정확한 해석은 다음과 같습니다.
"내가 생성한 자식 프로세스에 대해서 더 이상 내가 관여할 것이 없다. 자식 프로세스의 핸들을 반환하라."
여기서는 핸들의 반환만 이야기를 했을 뿐, 리소스의 해제에 대한 이야기는 없습니다.
만약 리소스의 해제까지 된다면 프로세스도 종료가 될 것이고 위에서 했던 말이 맞는 말이 됩니다.
그런데 실행하면? 프로세스는 종료되지 않고 계속 실행됩니다.
다시 말해서 CloseHandle 함수는 리소스의 해제와는 무관한 함수라는 것입니다.
[CloseHandle 함수와 프로세스 종료코드]
앞선 결과를 통해서 CloseHandle 함수가 리소스의 해제와는 무관한 함수라는 것을 알았습니다.
이제 다시 운영체제에 의한 커널 오브젝트 관리로 이야기를 넘어가볼까 합니다.
운영체제는 커널 오브젝트의 소멸 시점은 과연 언제일까요?
일단 생각해볼 수 있는 것은 첫 번째로는 프로세스가 종료될 때입니다.
그런데 프로세스 종료 시 해당 프로세스의 커널 오브젝트를 소멸시키게 되면 문제가 생길 수 있습니다.
그걸 예제 코드를 통해서 한 번 확인해볼까 합니다.
[OperationParent.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: OperationStateParent.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: CloseHandle 함수와 프로세스 종료코드 예제
* 이전 버전 내용:
*/
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
int main(int argc, TCHAR* argv[])
{
STARTUPINFO si = { 0, };
PROCESS_INFORMATION pi = { 0, };
DWORD state;
si.cb = sizeof(si);
si.dwFlags = STARTF_USEPOSITION | STARTF_USESIZE;
si.dwX = 100;
si.dwY = 200;
si.dwXSize = 300;
si.dwYSize = 200;
TCHAR title[] = _T("OperationStateChild.exe");
si.lpTitle = title;
TCHAR command[] = _T("OperationStateChild.exe");
CreateProcess(
NULL, command, NULL, NULL, TRUE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
for (DWORD i = 0; i < 10000; i++)
for (DWORD i = 0; i < 10000; i++); // busy waiting, 자식 프로세스의 종료를 기다림
// WaitForSingleObject(pi.hProcess, INFINITE);
// 위 함수를 사용할 경우에는 자식 프로세스가 종료 코드를 보낼때까지 기다린다.
// 위의 busy waiting은 생각보다 빨리 끝나므로 이걸 사용해서 확인하는 것이 나음
// 자식 프로세스의 종료 코드를 받기 위해 사용한 함수
GetExitCodeProcess(pi.hProcess, &state);
if (state == STILL_ACTIVE)
_tprintf(_T("STILL_ACTIVE\n\n"));
else
_tprintf(_T("state: %d\n\n"), state);
// CloseHandle(pi.hProcess);
// 커널 오브젝트의 Usage Count를 1 감소시키는 기능.
// 실제로 프로세스의 커널 오브젝트가 소멸되는 것은 커널 오브젝트의 Usage Count가 0이 되었을 때.
// 그리고 프로세스의 경우에는 프로세스가 종료되는 시점에서 Usage Count가 1 감소한다.
return 0;
}
[OperationChild.cpp]
/*
* Windows System Programming - 커널 오브젝트와 핸들
* 파일명: OperationStateChild.cpp
* 파일 버전: 0.1
* 작성자: Sevenshards
* 작성 일자: 2023-12-04
* 이전 버전 작성 일자:
* 버전 내용: CloseHandle 함수와 프로세스 종료코드 예제
* 이전 버전 내용:
*/
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <tchar.h>
#include <Windows.h>
int main(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); // 또는 return -1;
_tprintf(_T("Operation Result: %f\n"), num1 / num2);
return 1;
}
코드 자체는 굉장히 간단합니다.
부모 프로세스에서 자식 프로세스를 생성하고, 자식 프로세스에서 반환하는 값을 기다리는 코드입니다.
여기서 GetExitCodeProcess는 자식 프로세스가 종료된 이후 반환하는 종료코드를 받아오는 함수입니다.
우리는 지금까지 main함수 내에서 return이나 비정상 종료 시 사용하는 exit를 쓰면서 0이나 -1을 붙여왔습니다.
이 둘은 전달하는 값이 0이건 -1이건 프로그램 종료가 되는 문장입니다.
그렇다면 프로세스가 종료되고 난 뒤에 반환되는 값은 어디에 저장이 될까요?
바로 커널 오브젝트에 저장이 되는 것입니다.
return 1, exit(-1)처럼 이 뒤에 붙는 값이 종료 코드, 즉 종료 상태를 알리는 값입니다.
[커널 오브젝트와 Usage Count]
위의 결과로 알게 된 사실은 자식 프로세스의 종료 코드는 자식 프로세스의 커널 오브젝트에 저장된다는 것입니다.
만약 프로세스 종료 시에 커널 오브젝트도 같이 소멸된다면 부모 프로세스에서는 종료코드의 값을 얻을 수 없게 됩니다.
그래서 프로세스가 종료되었다고 커널 오브젝트까지 동시에 소멸되지 않는 것입니다.
그렇다면 커널 오브젝트를 소멸시키는 시점은 언제가 가장 좋을까요?
바로 커널 오브젝트를 참조하는 대상이 하나도 없을 때 소멸시키는 것입니다.
그리고 이 방법이 Windows가 커널 오브젝트 소멸 시기를 결정하는 방법입니다.
이것을 달리 말한다면 커널 오브젝트를 참조하는 프로세스가 단 하나라도 있다고 하면 소멸되지 않는다는 말입니다.
그래서 Windows에서는 소멸 시기를 결정짓기 위한 방법으로 'Usage Count(참조 횟수)'라는 것을 사용합니다.
Usage Count가 0이 되면 더 이상 참조하는 프로세스가 없는 것으로, 해당 커널 오브젝트를 소멸시킵니다.
프로세스는 생성과 동시에 커널 오브젝트의 Usage Count는 1이 됩니다.
생성과 동시에 Usage Count가 0으로 초기화 되면 커널 오브젝트는 바로 소멸되기 때문입니다.
이후 커널 오브젝트에 접근 가능한 대상이 늘어날 때마다 Usage Count는 1 증가하게 됩니다.
여기서 접근 가능한 대상이 늘어난다는 것은 프로세스 커널 오브젝트에 접근 가능한 핸들 갯수의 증가를 말합니다.
위의 예제를 그림으로 표현하면 다음과 같이 표현할 수 있습니다.
여기서 자식 프로세스가 생성되면서 Usage Count는 1이 됩니다.
그리고 부모 프로세스에서는 자식 프로세스의 핸들을 얻었기 때문에 Usage Count는 2가 됩니다.
그래서 결론적으로는 자식 프로세스가 생성됐을 때의 Usage Count = 2가 되는 것입니다.
이제 커널 오브젝트가 소멸되는 시점을 설명해볼까 합니다.
예는 위의 예를 그대로 사용하겠습니다.
우선 자식 프로세스의 커널 오브젝트에서 Usage Count는 2인 상황입니다.
여기서 자식 프로세스가 종료가 됩니다.
그러면 자식 프로세스는 커널 프로젝트를 더 이상 참조하지 않으므로 Usage Count는 1 줄어들게 됩니다.
그리고 종료 시 코드를 반환하여 커널 오브젝트에 저장하게 됩니다.
마지막으로 부모 프로세스도 종료가 됩니다.
그러면 커널 오브젝트의 Usage Count는 또 하나 줄어들어 0이 됩니다.
여기서 Usage Count = 0이 되었으므로 운영체제는 해당 커널 오브젝트를 소멸시킵니다.
이제 CloseHandle 함수에 대한 정확한 설명을 하면 다음과 같습니다.
"CloseHandle 함수는 핸들을 반환하면서 커널 오브젝트의 Usage Count를 1 감소시킨다."
그리고 프로세스(+ 쓰레드) 종료 시에도 Usage Count가 1 감소하게 됩니다.
[Calculator.cpp의 문제점 그리고 해결책]
이전에 구현했던 Calculator.cpp에는 큰 문제가 있다고 했습니다.
그 문제는 바로, Calc.exe가 실행되는 부분입니다.
Calc.exe가 실행되면 부모 프로세스와 자식 프로세스인 Calc가 참조하므로 Usage Count는 2가 됩니다.
그런데 계산기 프로그램을 종료하면, 자식 프로세스의 Usage Count만 1이 감소가 됩니다.
그래서 커널 오브젝트는 소멸되지 않습니다.
여기서 계산기 프로그램을 다시 실행하면 또 새로운 커널 오브젝트가 생성됩니다.
그리고 이걸 또 종료하면? 아까와 같은 Usage Count가 1이 되어 커널 오브젝트가 남아있게 됩니다.
계속해서 Usage Count가 1인 커널 오브젝트가 누적이 됨으로 인해 문제가 되는 것입니다.
그래서 이 문제를 해결하기 위해서는 CloseHandle 함수를 이용하여 커널 오브젝트를 소멸시키면 됩니다.