커널 오브젝트

1. 커널 오브젝트란?

커널 오브젝트는 운영체제 커널이 시스템 리소스를 관리하기 위해 사용하는 내부 데이터 블록이다.

운영체제는 여러 프로세스와 리소스를 동시에 관리해야 하므로, 각 리소스의 상태와 속성을 저장하고 갱신할 관리 구조가 필요하다.
이때 커널 내부에서 생성되고 관리되는 구조체를 커널 오브젝트라고 한다.

즉, 커널 오브젝트는 운영체제가 리소스를 추적하고 제어하기 위해 사용하는 커널 영역의 관리용 데이터 구조라고 볼 수 있다.


2. 커널 오브젝트가 필요한 이유

운영체제는 다음과 같은 정보를 지속적으로 관리해야 한다.

  • 프로세스의 실행 상태
  • 스레드의 상태
  • 파일의 접근 위치
  • 동기화 객체의 신호 상태
  • 메모리 매핑 정보
  • 보안 속성
  • 참조 횟수

이러한 정보들은 사용자 프로그램이 직접 관리하는 것이 아니라, 운영체제가 커널 영역에서 관리한다.

따라서 커널 오브젝트는 운영체제가 리소스를 안정적으로 관리하기 위한 핵심 구조체라고 할 수 있다.


3. 커널 오브젝트의 종류

커널 오브젝트는 하나의 고정된 구조체가 아니라, 리소스 종류에 따라 서로 다른 형태로 생성된다.

예를 들어 다음과 같다.

커널 오브젝트 종류저장되는 정보 예시
프로세스 커널 오브젝트프로세스 ID, 상태, 우선순위, 핸들 테이블, 주소 공간 정보
스레드 커널 오브젝트스레드 ID, 레지스터 상태, 스케줄링 정보, 스택 정보
파일 커널 오브젝트파일 위치, 접근 권한, 파일 속성
이벤트 커널 오브젝트신호 상태, 대기 중인 스레드 목록
뮤텍스 커널 오브젝트소유 스레드, 잠금 상태, 대기 스레드 목록

즉, 파일을 관리하기 위한 커널 오브젝트와 프로세스를 관리하기 위한 커널 오브젝트는 서로 다른 정보를 가진다.

1이벤트 커널 오브젝트


4. 커널 오브젝트와 사용자 프로그램

커널 오브젝트는 커널 영역에 존재하기 때문에, 사용자 프로그램이 직접 접근할 수 없다.

사용자 프로그램은 커널 오브젝트를 직접 수정하는 대신, 운영체제가 제공하는 함수를 통해 간접적으로 조작한다.

예를 들어 Windows에서는 다음과 같은 함수들이 커널 오브젝트를 생성하거나 조작한다.

함수역할
CreateProcess프로세스 커널 오브젝트 생성
CreateThread스레드 커널 오브젝트 생성
CreateFile파일 커널 오브젝트 생성 또는 접근
CreateEvent이벤트 커널 오브젝트 생성
CloseHandle핸들 참조 해제

5. 핸들이란?

핸들은 사용자 프로그램이 커널 오브젝트를 간접적으로 사용하기 위한 식별자이다.

커널 오브젝트는 커널 영역에 있으므로 사용자 프로그램이 직접 주소를 알 수 없다.
대신 운영체제는 사용자 프로그램에게 핸들을 반환하고, 프로그램은 이 핸들을 이용해 커널 오브젝트를 조작한다.

예를 들어 다음과 같은 흐름이다.

HANDLE fileHandle = CreateFile(...);

이때 fileHandle은 파일 커널 오브젝트 자체가 아니라, 해당 커널 오브젝트에 접근하기 위한 식별자이다.


6. 핸들은 단순한 주소가 아니다

핸들은 커널 오브젝트의 실제 메모리 주소가 아니다.

핸들은 보통 프로세스별 핸들 테이블에서 커널 오브젝트를 찾기 위한 값이다.

즉, 사용자 프로그램은 핸들을 통해 다음과 같은 방식으로 커널 오브젝트에 접근한다.

사용자 프로그램→ 핸들 전달→ 운영체제 함수 호출→ 커널이 핸들 테이블에서 커널 오브젝트 검색→ 커널 오브젝트 조작

따라서 핸들은 커널 오브젝트에 직접 접근하기 위한 포인터라기보다는, 운영체제에게 특정 커널 오브젝트를 가리키도록 요청하기 위한 식별자에 가깝다.


7. 커널 오브젝트의 수명

커널 오브젝트는 보통 참조 횟수를 기반으로 관리된다.

프로세스가 커널 오브젝트에 대한 핸들을 얻으면, 해당 커널 오브젝트의 참조 횟수가 증가한다.
반대로 CloseHandle을 호출하면 참조 횟수가 감소한다.

참조 횟수가 0이 되면 운영체제는 해당 커널 오브젝트를 제거할 수 있다.

CloseHandle(fileHandle);

CloseHandle은 커널 오브젝트 자체를 무조건 삭제하는 함수가 아니라, 현재 프로세스가 가지고 있던 핸들 참조를 해제하는 함수이다.


8. 커널 오브젝트와 스케줄링에 대한 오해

다음 문장은 틀린 설명이다.

함수가 호출되어 실행되는 중간에는 절대로 CPU의 할당 시간을 다른 프로세스에게 넘겨주지 않는다.

함수 실행 중에도 CPU의 할당 시간은 다른 프로세스나 스레드에게 넘어갈 수 있다.

운영체제는 2선점형 스케줄링을 사용하기 때문에, 현재 스레드가 어떤 함수를 실행 중이더라도 3타임 슬라이스가 끝나거나 더 높은 우선순위의 스레드가 준비되면 CPU를 다른 스레드에게 넘길 수 있다.

즉, 함수 호출은 원자적인 실행 단위가 아니다.


9. 함수 호출 중에도 컨텍스트 스위칭이 가능한 이유

CPU는 함수 단위로 실행을 보장하지 않는다.
운영체제는 스레드 단위로 CPU 실행 시간을 배분한다.

따라서 다음과 같은 상황에서 함수 실행 중에도 컨텍스트 스위칭이 발생할 수 있다.

  • 타임 슬라이스가 끝난 경우
  • 더 높은 우선순위의 스레드가 준비 상태가 된 경우
  • 현재 스레드가 I/O 요청으로 대기 상태에 들어간 경우
  • 동기화 객체를 기다리게 된 경우
  • 인터럽트가 발생한 경우

예를 들어 다음 코드가 있다고 해도,

void SomeFunction(){    int value = 10;    value += 20;    value *= 3;}

운영체제는 이 함수가 끝날 때까지 반드시 기다려주지 않는다.
함수의 중간 지점에서도 현재 스레드를 멈추고 다른 스레드를 실행할 수 있다.


커널 오브젝트의 종속 관계

1. 커널 오브젝트는 운영체제에 종속적이다

커널 오브젝트는 특정 프로세스에 직접 소속된 데이터가 아니라, 운영체제 커널이 관리하는 리소스 관리 구조체이다.

따라서 다음과 같이 표현할 수 있다.

커널 오브젝트는 프로세스에 종속적인 것이 아니라, 운영체제에 종속적이다.

즉, 어떤 프로세스가 커널 오브젝트를 생성했다고 해서 그 커널 오브젝트의 수명이 해당 프로세스의 수명과 반드시 같지는 않다.


2. 프로세스 종료와 커널 오브젝트 소멸은 다르다

프로세스가 종료되면 해당 프로세스의 실행은 끝난다.
하지만 그 프로세스를 나타내는 프로세스 커널 오브젝트가 즉시 소멸되는 것은 아니다.

예를 들어 부모 프로세스가 자식 프로세스를 생성했다고 해보자.

PROCESS_INFORMATION processInfo = {};CreateProcess(    nullptr,    commandLine,    nullptr,    nullptr,    FALSE,    0,    nullptr,    nullptr,    &startupInfo,    &processInfo);

이때 processInfo.hProcess는 자식 프로세스의 커널 오브젝트를 가리키는 핸들이다.

자식 프로세스가 종료되면 자식 프로세스의 실행은 끝나지만, 자식 프로세스 커널 오브젝트에는 여전히 중요한 정보가 남아 있다.

대표적으로 다음과 같은 정보가 저장된다.

  • 프로세스 종료 코드
  • 프로세스의 종료 상태
  • 프로세스에 대한 대기 가능 상태
  • 일부 통계 정보

따라서 자식 프로세스가 종료되는 순간 프로세스 커널 오브젝트까지 바로 제거해버리면, 부모 프로세스는 GetExitCodeProcess 같은 함수를 통해 자식 프로세스의 종료 코드를 확인할 수 없게 된다.


3. 커널 오브젝트의 소멸 시점

커널 오브젝트는 해당 오브젝트를 참조하는 대상이 더 이상 없을 때 소멸된다.

Windows는 커널 오브젝트를 참조 횟수 기반으로 관리한다.
커널 오브젝트를 참조하는 핸들이 존재하면 해당 오브젝트는 유지된다.
반대로 모든 핸들이 닫히고 더 이상 참조하는 대상이 없으면 운영체제가 커널 오브젝트를 제거할 수 있다.

이 구조는 C++의 shared_ptr과 비슷하게 이해할 수 있다.

CloseHandle(processInfo.hProcess);CloseHandle(processInfo.hThread);

CloseHandle은 커널 오브젝트를 즉시 삭제하는 함수가 아니다.
현재 프로세스가 가지고 있던 핸들 참조를 해제하는 함수이다.

참조 횟수가 0이 되는 순간, 운영체제가 해당 커널 오브젝트를 정리할 수 있다.


4. 커널 오브젝트는 여러 프로세스에서 접근할 수 있다

커널 오브젝트는 특정 프로세스의 메모리 공간에 직접 존재하는 것이 아니라 커널 영역에서 관리된다.

따라서 하나의 커널 오브젝트를 여러 프로세스가 접근할 수 있다.

예를 들어 다음과 같은 방식이 가능하다.

  • 핸들 상속
  • 핸들 복제
  • 이름 있는 커널 오브젝트 사용

대표적인 예시는 다음과 같다.

HANDLE eventHandle = CreateEvent(    nullptr,    FALSE,    FALSE,    L"MyEvent");

이름 있는 이벤트 객체를 만들면, 다른 프로세스도 같은 이름을 통해 해당 커널 오브젝트에 접근할 수 있다.

HANDLE eventHandle = OpenEvent(    EVENT_ALL_ACCESS,    FALSE,    L"MyEvent");

즉, 커널 오브젝트는 운영체제가 관리하므로 여러 프로세스가 공유하거나 접근할 수 있다.


핸들의 종속 관계

1. 핸들은 프로세스에 종속적이다

핸들은 커널 오브젝트 자체가 아니라, 프로세스가 커널 오브젝트에 접근하기 위해 사용하는 식별자이다.

중요한 점은 다음과 같다.

핸들은 프로세스에 종속적이다.

즉, 같은 커널 오브젝트를 가리키더라도 프로세스마다 핸들 값은 다를 수 있다.


2. 핸들은 프로세스의 핸들 테이블에 저장된다

Windows에서 각 프로세스는 자신만의 핸들 테이블을 가진다.

핸들은 이 핸들 테이블에서 특정 커널 오브젝트를 찾기 위한 값이다.

프로세스 A의 핸들 테이블 : 핸들 값 100 → 이벤트 커널 오브젝트
프로세스 B의 핸들 테이블 : 핸들 값 220 → 같은 이벤트 커널 오브젝트

위 예시처럼 프로세스 A에서는 핸들 값이 100이고, 프로세스 B에서는 핸들 값이 220일 수 있다.

하지만 두 핸들이 같은 커널 오브젝트를 가리킬 수 있다.

따라서 핸들을 단순히 커널 오브젝트의 주소라고 생각하면 안 된다.
핸들은 해당 프로세스의 핸들 테이블 안에서만 의미 있는 값이다.


3. 핸들을 다른 프로세스에 그대로 넘기면 안 되는 이유

핸들은 프로세스별 핸들 테이블에 종속된 값이기 때문에, 한 프로세스의 핸들 값을 다른 프로세스에 그대로 전달해도 의미가 없다.

예를 들어 프로세스 A에서 다음 핸들을 얻었다고 하자.

HANDLE handle = CreateEvent(...);

이 핸들 값이 100이라고 해서, 프로세스 B에서 100이라는 값을 그대로 사용하면 같은 커널 오브젝트에 접근할 수 있는 것이 아니다.

프로세스 B의 핸들 테이블에서 100은 전혀 다른 오브젝트를 가리키거나, 유효하지 않은 값일 수 있다.

다른 프로세스가 같은 커널 오브젝트에 접근하려면 다음과 같은 방식이 필요하다.

  • 핸들 상속
  • DuplicateHandle
  • 이름 있는 커널 오브젝트 생성 후 OpenEvent, OpenMutex, OpenFileMapping 등으로 접근

프로세스 커널 오브젝트와 종료 코드

1. 자식 프로세스의 종료 코드는 어디에 저장되는가?

자식 프로세스의 종료 코드는 자식 프로세스의 커널 오브젝트에 저장된다.

자식 프로세스가 종료되면 실행은 끝나지만, 부모 프로세스는 여전히 다음과 같은 작업을 할 수 있다.

DWORD exitCode = 0;GetExitCodeProcess(processInfo.hProcess, &exitCode);

또는 자식 프로세스가 종료될 때까지 기다릴 수도 있다.

WaitForSingleObject(processInfo.hProcess, INFINITE);

이것이 가능한 이유는 자식 프로세스가 종료되어도 자식 프로세스 커널 오브젝트가 즉시 소멸되지 않기 때문이다.


2. 자식 프로세스 종료 직후 커널 오브젝트가 소멸되면 생기는 문제

만약 자식 프로세스가 종료되는 순간 프로세스 커널 오브젝트까지 바로 소멸된다면, 부모 프로세스는 다음 작업을 할 수 없다.

  • 자식 프로세스의 종료 코드 확인
  • 자식 프로세스 종료 여부 대기
  • 자식 프로세스와 관련된 상태 확인

예를 들어 다음 코드가 정상적으로 동작할 수 없게 된다.

WaitForSingleObject(processInfo.hProcess, INFINITE);DWORD exitCode = 0;GetExitCodeProcess(processInfo.hProcess, &exitCode);

따라서 Windows는 프로세스가 종료되었다고 해서 해당 프로세스 커널 오브젝트를 즉시 제거하지 않는다.


부모 프로세스가 자식 프로세스 핸들을 곧바로 반환하는 이유

여기서 “반환”은 보통 CloseHandle을 의미한다고 보는 것이 자연스럽다.

즉, 부모 프로세스가 CreateProcess로 자식 프로세스를 만든 뒤, 더 이상 자식 프로세스를 제어하거나 상태를 확인할 필요가 없다면 자식 프로세스 핸들을 곧바로 닫는 것이 좋다.

CloseHandle(processInfo.hProcess);CloseHandle(processInfo.hThread);

1. 곧바로 닫는 이유

부모 프로세스가 자식 프로세스 핸들을 계속 가지고 있으면, 자식 프로세스 커널 오브젝트의 참조 횟수가 유지된다.

즉, 자식 프로세스가 이미 종료되었더라도 부모 프로세스가 핸들을 닫지 않으면 커널 오브젝트가 계속 남아 있을 수 있다.

이는 불필요한 커널 리소스 점유로 이어진다.

따라서 부모 프로세스가 자식 프로세스에 대해 더 이상 다음 작업을 하지 않는다면 핸들을 바로 닫는 것이 맞다.

  • 종료 대기
  • 종료 코드 확인
  • 강제 종료
  • 우선순위 변경
  • 기타 프로세스 제어

2. 핸들을 닫아도 자식 프로세스가 종료되는 것은 아니다

중요한 점은 다음과 같다.

CloseHandle(processInfo.hProcess);

이 코드는 자식 프로세스를 종료시키는 코드가 아니다.

단지 부모 프로세스가 자식 프로세스 커널 오브젝트에 대해 가지고 있던 핸들 참조를 해제하는 것이다.

자식 프로세스는 계속 실행될 수 있다.

프로세스를 실제로 종료시키려면 TerminateProcess 같은 별도의 함수를 사용해야 한다.


3. 자식 프로세스 핸들을 늦게 닫아야 하는 경우

부모 프로세스가 자식 프로세스의 상태를 나중에 확인해야 한다면 핸들을 바로 닫으면 안 된다.

대표적인 경우는 다음과 같다.

자식 프로세스 종료를 기다려야 하는 경우

WaitForSingleObject(processInfo.hProcess, INFINITE);

부모 프로세스가 자식 프로세스가 끝날 때까지 기다려야 한다면, 자식 프로세스 핸들을 유지해야 한다.


자식 프로세스의 종료 코드를 확인해야 하는 경우

DWORD exitCode = 0;GetExitCodeProcess(processInfo.hProcess, &exitCode);

자식 프로세스가 정상 종료되었는지, 오류 코드로 종료되었는지 확인해야 한다면 핸들을 유지해야 한다.


자식 프로세스를 제어해야 하는 경우

부모 프로세스가 자식 프로세스에 대해 다음과 같은 제어를 해야 한다면 핸들이 필요하다.

  • TerminateProcess로 강제 종료
  • SetPriorityClass로 우선순위 변경
  • GetProcessTimes로 실행 시간 확인
  • GetExitCodeProcess로 종료 상태 확인

자식 프로세스와 동기화해야 하는 경우

프로세스 핸들은 대기 가능한 커널 오브젝트다.

즉, 부모 프로세스는 자식 프로세스 핸들을 이용해 자식 프로세스 종료 시점을 동기화할 수 있다.

WaitForSingleObject(processInfo.hProcess, INFINITE);

따라서 부모가 자식의 종료 시점에 맞춰 후속 작업을 해야 한다면 핸들을 유지해야 한다.


커널 오브젝트의 상태

1. 커널 오브젝트의 두 가지 상태

커널 오브젝트는 내부적으로 상태 정보를 가진다.

대표적인 상태는 다음 두 가지다.

  1. Signaled 상태
  2. Non-Signaled 상태

이 상태 정보는 커널 오브젝트 내부의 멤버 변수 중 하나로 관리된다고 이해할 수 있다.


2. 상태 정보가 존재하는 이유

커널 오브젝트의 상태는 특정 리소스에 어떤 상황이 발생했는지를 알려주기 위해 존재한다.

다만, 어떤 상황에서 Signaled 상태가 되는지는 커널 오브젝트의 종류마다 다르다.

커널 오브젝트 종류Signaled가 되는 대표 상황
프로세스프로세스가 종료됨
스레드스레드가 종료됨
이벤트SetEvent가 호출됨
뮤텍스뮤텍스를 획득할 수 있는 상태가 됨
세마포어사용 가능한 카운트가 1 이상이 됨

즉, Signaled 상태의 의미는 오브젝트 종류에 따라 다르게 해석해야 한다.


3. 프로세스 커널 오브젝트의 상태

프로세스 커널 오브젝트는 프로세스가 생성될 때 함께 만들어진다.

처음 생성된 프로세스는 실행 중이므로, 이때 프로세스 커널 오브젝트는 Non-Signaled 상태이다.

이후 프로세스가 종료되면 해당 프로세스 커널 오브젝트는 Signaled 상태로 변경된다.

정리하면 다음과 같다.

프로세스 상태프로세스 커널 오브젝트 상태
실행 중Non-Signaled
종료됨Signaled

즉, 프로세스 커널 오브젝트에서 Signaled 상태는 “프로세스가 종료되었다”는 의미이다.


4. 프로세스 커널 오브젝트의 상태 변화

프로세스 커널 오브젝트는 다음 흐름을 가진다.

프로세스 생성→ 프로세스 커널 오브젝트 생성→ Non-Signaled 상태프로세스 종료→ 프로세스 커널 오브젝트가 Signaled 상태로 변경

프로세스 커널 오브젝트는 Non-Signaled 상태에서 Signaled 상태로 바뀔 수 있다.

하지만 일반적으로 Signaled 상태에서 다시 Non-Signaled 상태로 돌아가지는 않는다.

이미 종료된 프로세스를 다시 실행 중인 상태로 되돌릴 수 없기 때문이다.


5. WaitForSingleObject의 의미

WaitForSingleObject는 특정 커널 오브젝트가 Signaled 상태가 될 때까지 기다리는 함수이다.

예를 들어 스레드 핸들을 넘기면, 해당 스레드 커널 오브젝트가 Signaled 상태가 될 때까지 기다린다.

WaitForSingleObject(threadHandle, INFINITE);

이 경우 의미는 다음과 같다.

스레드가 종료될 때까지 기다린다.

왜냐하면 스레드 커널 오브젝트는 스레드가 종료되었을 때 Signaled 상태가 되기 때문이다.


6. 프로세스 종료 대기

프로세스 핸들을 WaitForSingleObject에 전달하면, 해당 프로세스가 종료될 때까지 기다릴 수 있다.

WaitForSingleObject(processHandle, INFINITE);

이 코드의 의미는 다음과 같다.

processHandle이 가리키는 프로세스 커널 오브젝트가 Signaled 상태가 될 때까지 대기한다.

프로세스 커널 오브젝트는 프로세스가 종료되면 Signaled 상태가 되므로, 결과적으로 이 코드는 프로세스 종료를 기다리는 코드가 된다.


7. 종료 코드와 커널 오브젝트

프로세스가 종료될 때 전달하는 값을 종료 코드라고 한다.

프로세스의 종료 코드는 해당 프로세스의 커널 오브젝트에 저장된다.

따라서 부모 프로세스는 자식 프로세스가 종료된 뒤, 자식 프로세스 커널 오브젝트에 저장된 종료 코드를 가져올 수 있다.

DWORD exitCode = 0;WaitForSingleObject(processHandle, INFINITE);GetExitCodeProcess(processHandle, &exitCode);

여기서 WaitForSingleObject를 먼저 호출하는 이유는 자식 프로세스가 완전히 종료된 뒤 안전하게 종료 코드를 가져오기 위해서다.


8. 종료 코드를 이용한 데이터 전달

자식 프로세스는 종료 시 종료 코드를 남길 수 있다.

부모 프로세스는 이 종료 코드를 확인하여 자식 프로세스의 실행 결과를 판단할 수 있다.

예를 들어 다음과 같은 식으로 사용할 수 있다.

종료 코드의미
0정상 종료
1일반 오류
2특정 작업 실패
기타 값프로그램이 정의한 종료 결과

즉, 종료 코드는 부모 프로세스에게 최소한의 결과 정보를 전달하는 수단으로 사용할 수 있다.

다만 종료 코드는 정수 값 하나에 가깝기 때문에, 복잡한 데이터 전달용으로는 적합하지 않다.
복잡한 데이터를 주고받으려면 파이프, 공유 메모리, 소켓 같은 IPC 기법을 사용하는 것이 적절하다.


핸들 테이블

핸들 테이블은 핸들 정보를 저장하고 있는 테이블이다.

각 프로세스는 자신만의 핸들 테이블을 가지며, 이 테이블은 프로세스별로 독립적으로 관리된다. 핸들 테이블을 통해 운영체제는 다음 정보를 관리한다.

  • 특정 핸들값이 어떤 커널 오브젝트를 가리키는지
  • 해당 핸들에 어떤 접근 권한이 있는지
  • 해당 핸들이 상속 가능한 핸들인지

예를 들어 부모 프로세스의 핸들 테이블에 다음과 같은 정보가 있을 수 있다.

127 -> 메일슬롯 커널 오브젝트

이때 127은 단순한 숫자값처럼 보이지만, 실제로는 부모 프로세스의 핸들 테이블 안에서 특정 커널 오브젝트를 가리키는 값이다.


핸들 상속

부모 프로세스의 핸들 정보는 자식 프로세스에게 상속될 수 있다. 여기서 말하는 상속은 C++의 클래스 상속이나 다형성과 관련된 상속이 아니다.

즉, 다음과 같은 의미의 상속이다.

부모 프로세스의 핸들 테이블에 등록된 핸들 정보가 자식 프로세스의 핸들 테이블에도 등록되는 것

예를 들어 부모 프로세스가 메일슬롯 핸들을 가지고 있고, 그 핸들이 상속 가능하도록 설정되어 있다면 자식 프로세스도 같은 커널 오브젝트를 가리키는 핸들 정보를 가질 수 있다.

부모 프로세스 핸들 테이블 127 -> 메일슬롯 커널 오브젝트
자식 프로세스 핸들 테이블 127 -> 메일슬롯 커널 오브젝트

이렇게 되면 부모와 자식 프로세스는 서로 다른 핸들 테이블을 가지지만, 같은 커널 오브젝트를 참조할 수 있다.


커널 오브젝트의 참조 카운트

커널 오브젝트는 자신을 참조하는 핸들이 몇 개인지 관리하기 위해 참조 카운트를 가진다.

어떤 프로세스의 핸들 테이블에 특정 커널 오브젝트를 가리키는 핸들 정보가 등록되면, 해당 커널 오브젝트의 참조 카운트가 증가한다.

따라서 핸들 상속이 일어나면 자식 프로세스의 핸들 테이블에도 같은 커널 오브젝트를 가리키는 정보가 등록된다.

그 결과 커널 오브젝트의 참조 카운트도 증가한다.

정리하면 다음과 같다.

부모 프로세스가 커널 오브젝트 생성-> 부모 핸들 테이블에 핸들 정보 등록-> 커널 오브젝트 참조 카운트 증가

자식 프로세스가 핸들 상속-> 자식 핸들 테이블에도 핸들 정보 등록-> 커널 오브젝트 참조 카운트 증가

이 때문에 부모 프로세스가 핸들을 닫더라도, 자식 프로세스가 아직 해당 핸들을 가지고 있다면 커널 오브젝트는 바로 제거되지 않는다.

커널 오브젝트는 자신을 참조하는 핸들이 모두 닫혀 참조 카운트가 0이 되었을 때 제거될 수 있다.


핸들 상속 가능 여부

핸들의 상속 가능 여부는 보통 리소스를 생성하는 순간 결정된다.

Windows에서는 리소스를 생성할 때 SECURITY_ATTRIBUTES 구조체를 전달할 수 있다. 이 구조체 안의 bInheritHandle 값을 통해 핸들 상속 가능 여부를 설정한다.

SECURITY_ATTRIBUTES securityAttributes = {};securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);securityAttributes.bInheritHandle = TRUE;

이렇게 설정하면 해당 리소스를 생성하면서 반환되는 핸들은 상속 가능한 핸들이 된다. 반대로 bInheritHandleFALSE이면 자식 프로세스에게 상속되지 않는다.


핸들이 상속되어도 자식이 자동으로 아는 것은 아니다

핸들이 상속되었다고 해서 자식 프로세스가 그 핸들값을 자동으로 알 수 있는 것은 아니다.

핸들 테이블은 운영체제가 관리하는 내부 자료구조이기 때문에, 자식 프로세스가 자신의 핸들 테이블을 직접 조회해서 어떤 핸들이 상속되었는지 확인할 수 없다.

즉, 자식 프로세스의 핸들 테이블에 다음 정보가 등록되어 있더라도,

127 -> 메일슬롯 커널 오브젝트

자식 프로세스 코드 입장에서는 127이라는 핸들값을 알 방법이 없다.

따라서 부모 프로세스는 자식 프로세스에게 다음과 같은 정보를 별도로 전달해주어야 한다.

너에게 상속된 메일슬롯 핸들값은 127이다.이 값을 사용해서 메일슬롯에 접근해라.

이 핸들값 전달은 다양한 방식으로 할 수 있다.

  • 파일에 핸들값 저장
  • 명령행 인자로 전달
  • 환경 변수로 전달
  • 파이프 등 다른 IPC 수단으로 전달

예제에서 파일을 사용하는 이유는, 상속된 핸들값을 자식 프로세스에게 알려주기 위한 가장 단순한 전달 방식이기 때문이다.


Pseudo 핸들과 핸들의 복제

Pseudo 핸들

Pseudo 핸들은 말 그대로 가짜 핸들이다.

일반적인 핸들은 프로세스의 핸들 테이블에 등록되어 있고, 해당 핸들값을 통해 특정 커널 오브젝트를 참조한다.

하지만 Pseudo 핸들은 핸들 테이블에 등록된 실제 핸들이 아니다.

대표적인 예시는 다음 함수에서 반환되는 핸들이다.

HANDLE processHandle = GetCurrentProcess();

GetCurrentProcess()는 현재 실행 중인 프로세스를 가리키는 핸들을 반환하는 것처럼 보이지만, 실제로는 핸들 테이블에 등록된 Real 핸들이 아니다.

이 값은 운영체제가 특별하게 해석하는 상수값이다.

즉, 다음과 같이 이해할 수 있다.

GetCurrentProcess()

는 현재 프로세스를 가리키는 실제 핸들을 새로 생성해서 반환하는 것이 아니라,

현재 실행 중인 프로세스를 의미하는 약속된 값

을 반환하는 것이다.


Pseudo 핸들의 특징

Pseudo 핸들은 핸들 테이블에 등록되지 않는다.

따라서 다음과 같은 특징을 가진다.

  • 핸들 테이블에 등록되지 않는다.
  • 커널 오브젝트의 참조 카운트를 증가시키지 않는다.
  • CloseHandle로 반환할 필요가 없다.
  • 다른 프로세스에게 상속할 수 없다.
  • 다른 프로세스에게 그대로 전달해도 의미가 없다.

예를 들어 부모 프로세스에서 GetCurrentProcess()로 얻은 Pseudo 핸들을 자식 프로세스에게 전달한다고 해도, 자식 프로세스 입장에서는 그 값이 부모 프로세스를 의미하지 않는다.

Pseudo 핸들은 항상 현재 실행 중인 프로세스를 기준으로 해석되기 때문이다.

HANDLE pseudoHandle = GetCurrentProcess();

이 코드가 부모 프로세스에서 실행되면 부모 프로세스를 의미하고, 자식 프로세스에서 실행되면 자식 프로세스를 의미한다.

따라서 Pseudo 핸들은 프로세스 간 전달용으로 사용할 수 없다.


Pseudo 핸들을 닫을 필요가 없는 이유

일반적인 핸들은 핸들 테이블에 등록되므로, 사용이 끝나면 CloseHandle을 호출해야 한다.

CloseHandle(realHandle);

그래야 핸들 테이블에서 해당 핸들 정보가 제거되고, 커널 오브젝트의 참조 카운트도 감소한다.

하지만 Pseudo 핸들은 애초에 핸들 테이블에 등록된 핸들이 아니다.

따라서 제거할 핸들 테이블 항목도 없고, 감소시킬 참조 카운트도 없다.

그래서 Pseudo 핸들은 CloseHandle을 호출할 필요가 없다.


핸들의 복제

핸들 복제가 필요한 이유

Pseudo 핸들이 아닌 실제 핸들을 얻고 싶다면 핸들 복제를 해야 한다.

핸들 복제는 DuplicateHandle 함수를 통해 수행한다.

DuplicateHandle(...)

이 함수는 특정 프로세스의 핸들 테이블에 등록된 핸들을 다른 프로세스의 핸들 테이블에도 등록해준다.

즉, 단순히 핸들값 숫자만 복사하는 것이 아니라, 운영체제에게 요청해서 새로운 핸들 테이블 항목을 만들어주는 것이다.


핸들값 복사와 핸들 복제의 차이

핸들은 겉으로 보면 숫자값처럼 보인다.

그래서 다음처럼 단순히 값을 복사하면 될 것처럼 보일 수 있다.

HANDLE copiedHandle = originalHandle;

하지만 이것은 진짜 핸들 복제가 아니다.

이 코드는 같은 프로세스 안에서는 같은 핸들값을 한 번 더 들고 있는 것에 불과하다.

핸들 테이블에 새로운 항목이 생긴 것이 아니므로 커널 오브젝트의 참조 카운트도 증가하지 않는다.

진짜 핸들 복제는 DuplicateHandle을 사용해야 한다.

DuplicateHandle(    sourceProcessHandle,    sourceHandle,    targetProcessHandle,    &duplicatedHandle,    0,    FALSE,    DUPLICATE_SAME_ACCESS);

이렇게 해야 대상 프로세스의 핸들 테이블에 새로운 핸들 정보가 등록된다.


핸들 복제의 결과

핸들 복제가 성공하면 대상 프로세스의 핸들 테이블에 새로운 핸들이 등록된다.

예를 들어 부모 프로세스가 어떤 커널 오브젝트를 가리키는 핸들을 가지고 있다고 하자.

부모 프로세스 핸들 테이블100 -> 커널 오브젝트

이 핸들을 자식 프로세스에게 복제하면 자식 프로세스의 핸들 테이블에도 해당 커널 오브젝트를 가리키는 핸들이 등록된다.

자식 프로세스 핸들 테이블80 -> 같은 커널 오브젝트

주의할 점은 부모의 핸들값과 자식의 핸들값이 반드시 같을 필요는 없다는 것이다.

부모에서는 100번 핸들이었지만, 자식에서는 80번 핸들일 수 있다.

중요한 것은 핸들값 숫자가 같은지가 아니라, 두 핸들이 같은 커널 오브젝트를 참조한다는 점이다.


핸들 복제와 참조 카운트

DuplicateHandle을 통해 핸들이 복제되면 커널 오브젝트를 참조하는 핸들이 하나 더 생긴다.

따라서 커널 오브젝트의 참조 카운트가 증가한다.

부모 핸들 생성-> 참조 카운트 증가DuplicateHandle로 자식에게 핸들 복제-> 참조 카운트 증가자식이 CloseHandle 호출-> 참조 카운트 감소부모가 CloseHandle 호출-> 참조 카운트 감소

따라서 복제된 핸들도 사용이 끝나면 반드시 CloseHandle을 호출해야 한다.


핸들 상속과 핸들 복제의 차이

핸들 상속과 핸들 복제는 모두 다른 프로세스가 같은 커널 오브젝트에 접근할 수 있도록 만든다는 점에서 비슷하다.

하지만 사용 시점과 방식이 다르다.

구분핸들 상속핸들 복제
사용 시점자식 프로세스를 생성할 때프로세스 생성 이후에도 가능
주요 함수CreateProcess + 상속 옵션DuplicateHandle
사전 설정핸들이 상속 가능해야 함원본 핸들과 대상 프로세스 핸들이 필요함
핸들값 전달자식에게 핸들값을 별도로 알려줘야 함복제된 핸들값을 대상 프로세스에 알려줘야 함
참조 카운트증가함증가함

면접 질문 정리

  1. 커널 오브젝트란 무엇인가요?
  2. 운영체제가 커널 오브젝트를 사용하는 이유는 무엇인가요?
  3. 커널 오브젝트는 어떤 정보를 저장하나요?
  4. 커널 오브젝트는 사용자 영역에 존재하나요, 커널 영역에 존재하나요?
  5. 사용자 프로그램이 커널 오브젝트에 직접 접근할 수 없는 이유는 무엇인가요?
  6. 핸들이란 무엇인가요?
  7. 핸들을 커널 오브젝트의 식별 번호라고 표현해도 되나요?
  8. 핸들이 프로세스의 핸들 테이블과 관련 있다는 말은 무슨 뜻인가요?
  9. 커널 오브젝트와 핸들의 차이는 무엇인가요?
  10. “커널 오브젝트는 운영체제에 종속적이다”라는 말은 무슨 의미인가요?
  11. “핸들은 프로세스에 종속적이다”라는 말은 무슨 의미인가요?
  12. 프로세스 커널 오브젝트에는 어떤 정보가 저장될 수 있나요?
  13. 자식 프로세스의 종료 코드는 어디에 저장되나요?
  14. 프로세스 종료와 프로세스 커널 오브젝트 소멸이 다른 이유는 무엇인가요?
  15. 커널 오브젝트의 소멸 시점은 어떻게 결정되나요?
  16. CloseHandle은 커널 오브젝트를 삭제하는 함수인가요?
  17. 함수 호출 중에는 CPU를 다른 프로세스에게 넘기지 않는다는 말이 왜 틀렸나요?
  18. 함수 실행 중에도 컨텍스트 스위칭이 발생할 수 있는 이유는 무엇인가요?
  19. 무한 루프를 도는 스레드가 있어도 시스템 전체가 멈추지 않는 이유는 무엇인가요?

Footnotes

  1. 어떤 일이 끝났다는 신호를 다른 스레드에게 알려주는 객체

  2. 선점형 스케줄링은 운영체제가 실행 중인 스레드의 CPU 사용권을 강제로 회수할 수 있는 스케줄링 방식

  3. 타임 슬라이스는 운영체제가 하나의 스레드에게 CPU를 사용하도록 허용한 짧은 시간 단위