본문 바로가기

03. 프로세스

by 식 2009. 8. 21.
프로세스란 일반적으로 수행 중인 프로그램의 인스턴스 라고 정의한다.

프로세스의 구성요소
 ① 프로세스 커널 오브젝트 : 프로세스를 관리하기 위한 목적으로 운영체제가 사용하는 커널 오브젝트, 시스템은 프로세스에 대한 각종 통계 정보를 프로세스 커널 오브젝트에 저장하기도 한다.
 ② 주소 공간 : 실행 모듈이나 DLL(Dynamic-Link Library)의 코드와 데이터를 수용하는 주소 공간, 이러한 주소 공간은 스레드 스택이나 힙 할당과 같은 동적 메모리 할당에 사용되는 공간도 포함된다.

- 스레드
 프로세스는 자력으로 수행될 수 없다. 프로세스가 무언가를 수행하기 위해서는 반드시 프로세스의 컨텍스트(Context) 내에서 수행되는 스레드(Thread)가 있어야 한다. 스레드는 프로세스의 주소 공간 상에 위치하고 있는 코드를 수행할 책임이 있다. 하나의 프로세스는 다수의 스레드를 가질 수 있으며, 이러한 스레드들은 프로세스 주소 공간 내에서 "동시에" 코드를 수행한다. 이렇게 되려면 각 스레드들은 자신만의 CPU 레지스터 집합과 스택을 가져야만 한다. 각 프로세스는 프로세스 주소 공간 내의 코드를 수행 하기 위해 적어도 한 개의 스레드를 가지고 있다.

 프로세스가 생성되면 시스템은 자동적으로 첫 번째 스레드를 생성해 주는데, 이0를 주 스레드(Primary Thread)라고 부른다. 이 스레드는 추가적인 스레드를 생성할 수 있고, 이렇게 생성된 스레드들이 더욱더 많은 스레드를 만들어낼 수도 있다. 만일 프로세스의 주소 공간 내의 코드를 수행할 스레드가 없다면 프로세스는 계속해서 존재해야 할 이유가 없고, 따라서 시스템은 자동적으로 프로세스와 프로세스 주소 공간을 파괴한다.


- 스케줄링
 모든 스레드가 동시에 수행될 수 있도록 하기 위해 운영체제는 CPU 시간을 조금씩 나누어준다. 각 스레드들은 라운드 로빈(Round-Robin) 방식으로 주어지는 단위 시간(Quantum)만큼만 수행 될 수 있다. 이렇게 되면 마치 모든 스레드들이 동시에 수행되고 있는것처럼 보이게 된다.
다수의 CPU를 가지고 있는 머신의 경우 각 스레드에게 공평하게 CPU 시간을 나누어주는 알고리즘은 상당히 복잡하다. 마이크로소프트 윈도우의 경우 다수의 스레드를 동시에 수행시키기 위해 각 CPU별로 서로 다른 스레드를 수행하도록 스케줄링하고 있다. 윈도우 운영체제에서는 스레드에 대한 모든 관리와 스케줄링을 윈도우 커널이 담당한다. 따라서 다수의 CPU를 가지고 있는 머신의 장점을 사용하기 위해 코드를 변경해야 할 필요는 없다. 하지만 여러 개의 CPU를 가진 머신의 장점을 최대한 살리기 위해 애플리케이션의 알고리즘을 적절히 변경하는 것은 여전히 유효한 방법이다.


1. 윈도우 어플리케이션 작성

- 윈도우가 지원하는 애플리케이션

 ① GUI(Graphic User Interface) : 그래픽 폰트를 사용하며, 윈도우를 만들 수 있고, 메뉴를 가지기도 하고, 다이얼로그 박스를 통해 사용자와의 상호작용을 수행하기도 한다. 그리고 윈도우스러운 표준화된 모든 요소들을 사용하게 된다. 윈도우와 함께 배포되는 보조 프로그램(메모장, 계산기, 워드패드등)들은 대부분 GUI 기반의 애플리케이션이다.

 ② CUI(Console User Interface) : 텍스트를 기반으로 한다. 일반적으로 윈도우를 생성하지도 않고, 메시지를 처리하지도 않으며, 그래픽 유저 인터페이스를 필요로 하지도 않는다. 비록 CUI 기반의 애플리케이션도 화면 상에 단일의 윈도우 생성하고 그 안에서 수행되긴 하지만, 이 윈도우는 단순히 텍스트만을 출력한다. 명령 프롬프트(Command Promt)가 전형적인 CUI 기반 애플리케이션의 예이다.

 ※ 두 가지 형태의 애플리케이션은 경계가 명확하지 않다. CUI 기반의 애플리케이션도 다이얼로그 박스를 사용하는 것이 가능하다. 예를 들어 명령 쉘은 쉘이 제공하는 모든 명령어를 기억하는 대신에 그래픽 다이얼로그 박스에서 명령을 선택하여 수행할 수 있다 . 뿐만 아니라 GUI 기반의 애플리케이션도 텍스트 문자열을 콘솔 창에 출력할 수 있다.

- 링커 스위치(Linker Switch)
마이크로소프트트의 Visual Studio를 이용하여 애플리케이션 프로젝트를 생성하면, Visual Studio 통합 환경은 실행 파일의 형태에 알맞은 서브시스템 타입을 실행 파일에 포함시킬 수 있도록 다양한 링커 스위치를 설정한다.
CUI 기반 애플리케이션을 위한 링커 스위치 : /SUBSYSTEM:CONSOLE 
GUI 기반 애플리케이션을 위한 링커 스위치 : /SUBSYSTEM:WINDOWS

사용자가 애플리케이션을 수행하면 운영체제의 로더(Loader)는 실행 파일의 헤더를 확인하여 서브시스템 값을 가져온다. 만일 이 값이 CUI 기반의 애플리케이션이 콘솔 윈도우를 사용할 수 있도록 조치한다. 만일 CUI기반의 애플리케이션이 명령 프롬프트에서 수행되면 명령 프롬프트가 사용중인 윈도우를 사용하고, 윈도우 익스플로어에서 수행되면 새로운 콘솔 윈도우를 생성한다
 헤더로부터 확인한 서브시스템 값이 GUI기반의 애플리케이션을 의미하는 값이면 로더는 콘솔 윈도우를 생성하지 않고 애플리케이션을 바로 로드한다. 애플리케이션이 수행되면 운영체제는 애플리케이션이 어떤 형태인지에 대해 더 이상 신경 쓰지 않는다.
 윈도우 애플리케이션은 애플리케이션이 수행을 시작할 진입점 함수를 반드시 가져야 한다. C/C++ 개발자는 두 가지 형태의 진입점 함수를 사용할 수 있다.
 어떤 진입점 함수를 사용할지는 유니코드 문자열의 사용 여부에 달려 있다. 사실 운영체제는 우리가 작성한 진입점 함수를 직접 호출하지는 않으며, C/C++ 런타임에 의해 구현된 C/C++ 런타임 시작 함수(C++ Runtime Startup Function)를 호출한다. 이러한 함수는 링크 시 -entry:명령행 옵션(Command-Line Option)을 통해 설정된다. C/C++ 런타임 시작 함수는 malloc이나 free와 같은 함수가 호출될 수 있도록 C/C++ 런타임 라이브러리에 대한 초기화를 수행한다. 또한 개발자가 코드 상에서 선언한 각종 전역 오브젝트나 static 으로 선언된 C++  오브젝트들을 코드가 수행되기 전에 적절히 생성하는 역할을 수행한다.

[표] 애플리케이션 타입과 진입점
 애플리케이션 타입  진입점  실행 파일에 포함되는 런타임 시작 함수
 ANSI 문자(열) ,GUI 어플리케이션  _tWinMain(WinMain)  WinMainCRTStartup
 유니코드 문자(열), GUI 애플리케이션  _tWinMain(wWinMain)  wWinMainCRTStartup
 ANSI 문자(열), CUI 애플리케이션  _tmain(main)  mainCRTStartup
 유니코드 문자(열), CUI 애플리케이션   _tmain(wmain)  wmainCRTStartup

링커는 실행 파일을 링크하는 단계에서 적절한 C/C++ 런타임 시작 함수를 선택해야 한다.
① /SUBSYSTEM:WINDOWS : 링커 스위치가 설정되어 있으면 링커는 WInMain 이나 wWinMain 함수를 찾게 된다.
함수를 찾을 수 있는 경우 :  WinMainCRTStartup이나 wWinMainCRTStartup 함수를 호출하도록 설정한다.
함수가 찾을 수 없는 경우 :  외부 기호를 확인할 수 없습니다(unresolved external symbol) 에러를 반환한다.

 ② /SUBSYSTEM:CONSOLE : 링커 스위치가 지정되면 링커는 main 이나 wmain 함수를 찾는다.
 함수를 찾을 수 있는 경우 : mainCRTStartup이나 wmainCRTStartup 함수를 호출하도록 설정한다.
 함수가 찾을 수 없는 경우 :  외부 기호를 확인할 수 없습니다(unresolved external symbol) 에러를 반환한다.

프로젝트 설정에서 /SUBSYSTEM 링커 스위치를 완전히 제거할 수도 있다는 것은 잘 알려져 있지 않은 사실이다. 만일 이러한 링커 스위치를 제거하게 되면 링커는 자동적으로 애플리케이션에 적합한 설정 값을 찾아낸다. 링크 단계에서 링커는 코드에서 4개의 함수 중(WinMain, wWinMain, main, wmain) 어떤 것이 구현되었는지를 확인하고 적절한 서브시스템 설정을 추정한다. 이렇게 결정된 정보를 근간으로 어떤 C/C++ 시작함수가 실행 파일에 포함되어야 하는지를 결정하게 된다.

 모든 C/C++ 런타임 시작함수는 기본적으로 동일한 작업을 수행한다. 차이점이라면 C 런타임 라이브러리의 초기화 이후에 수행해야 할 진입점 함수가 어떤 것이냐에 따라 ANSI 문자열이나 유니코드 문자열을 처리해야 한다는 점 정도일 것이다. Visual C++에는 C/C++ 런타임 라이브러리의 소스 코드가 포함되어 있다. crtexe.c 파일을 살펴보면 4개의 시작 함수에 대한 구현 내용을 찾아볼 수 있는다.

- 시작 함수가 수행하는 작업
 ① 새로운 프로세스의 전체 명령행을 가리키는 포인터를 획득

 ② 새로운 프로세스의 환경변수를 가리키는 포인터를 획득

 ③ C/C++ 런타임 라이브러리의 전역변수를 초기화. 사용자 코드가 StdLib.h 파일을 인클루드하면 이 변수에 접근할 수 있다.
 변수명 타입   설명과 이 변수를 대체하는 윈도우 함수
 _osver  unsigned int  운영체제의 빌드 버전.
 예) 비스타 RTM은 6000이므로 _osver는 6000값을 가진다
 _winmajor  unsigned int  16비트로 나타낸 윈도우 major버전.
 윈도우 비스타의 경우 6.
 GetVersionEx 대체 사용 가능
 _winminor  unsigned int  16비트로 나타낸 윈도우 minor 버전.
 윈도우 비스타의 경우 0.
 GetVersionEx 대체 사용 가능
 _winver  unsinged int  (_winmajor << 8) + _winminor.
 GetVersionEx 대체 사용 가능
 __argc  unsigned int  명령행을 통해 전달된 인자의 개수.
 GetCommandLine을 대체 사용 가능
 __argv
 _wargv
 char
 wchar_t
 ANSI / 유니코드 문자열을 가리키는 __argc 크기의 배열
 배열의 각 요소는 명령행 인자를 가리킨다.
 _UNICODE가 정의되어 있으면 __argv가 NULL, 정의되어 있지 않으면 __wargv가 NULL이다.
 GetCommandLine을 대체 사용 가능
 _environ
 _wenviron
 char
 wchar_t
 ANSI/유니코드를 가리키는 배열. 각 배열 요소는 환경변수 문자열을 가리킨다. _UNICODE가 정의되어 있으면 _environ이 NULL, 정의되어 있지 않으면 _wenviron 이 NULL이다.
 GetEnviromentStrings, GetEnvironmentVariable 대체 사용 가능
 _pgmptr
 _wpgmptr
 char
 wchar_t
 ANSI/유니코드로 표현되는 수행 중인 프로그램의 전체 경로와 이름.
 _UNICODE가 정의도어 있으면 _pgmptr이 NULL, 정의되어 있지 않으면 _wpgmptr이 NULL이다.
 GetModuleFileName의 첫 번째 매개변수로 NULL을 전달하는 형태로 대체 사용 가능

④ 모든 전역 오브젝트와 static C++ 클래스 오브젝트의 생성자를 호출한다.


(1) 프로세스의 인스턴스 핸들

 모든 실행 파일과 DLL 파일은 프로세스의 메모리 공간 상에 로드될 때 고유의 인스턴스 핸들을 할당 받는다. 이러한 인스턴스 핸들은 (w)WinMain의 첫 번째 매개변수인 hInstanceExe를 통해 전달된다. 이 핸들 값은 보통 리소스를 로드할 때 사용된다.
ex) 실행 파일 이미지에 포함되어 있는 아이콘 리소스를 로드하려는 경우 다음의 함수를 호출해야 한다.
HICON LoadIcon(HINSTANCE hInstance, PCSTR pszIcon);

LoadIcon의 첫 번째 매개변수로는 리소스가 포함되어 있는 파일(실행 파일이나 DLL)의 인스턴스 핸들을 지정하면 된다. 많은 애플리케이션에서 (w)WinMain의 hInstanceExe 매개변수를 전역변수에 저장해 두어 실행 파일의 전체소스에서 이 값에 손쉽게 접근할 수 있도록 하곤 한다.

플랫폼 SDK 문서를 살펴보면 HMODULE 형 인자를 요구하는 함수들이 있음을 알 수 있다. 예를 들면 다음에 나오는 GetModuleFileName과 같은 함수가 있다.
DWORD GetModuleFileName(HMOUDLE hInstModule, PTSTR pszPath, DWORD cchPath);

WinMain의 hInstanceExe 매개변수의 실제 값은 시스템이 프로세스의 메모리 주소 공간 상에 실행 파일을 로드할 시작 메모리 주소(base memory address)다. 예를 들어 시스템이 실행 파일을 열어서 그 내용을 0x00400000에 로드하고자 한다면 (w)WinMain의 hInstanceExe 매개변수는 0x00400000을 가지게 된다.

실행 파일이 로드될 시작 주소는 링커에 의해 결정된다. 서로 다른 링커는 서로 다른 기본 시작 주소를 가질 수 있다. Visual Studio의 링커는 역사적인 이유로 0x00400000을 기본 시작 주소로 사용하고 있는데, 0x00400000은 윈도우 98에서 실행 파일을 로드할 수 있는 가장 하단의 메모리 주소였다. 애플리케이션이 로드되는 시작 주소는 마이크로소프트 링커의 경우 /BASE:address 옵션을 사용하여 변경할 수 있다.

 아래의 GetModuleHandle 함수는 실행 파일이나 DLL 파일이 프로세스의 메모리 공간 상의 어디에 로드되어 있는지를 가리키는 핸들/시작 주소를 반환한다.

HMODULE GetModuleHandle(PCTSTR pszModule);

이 함수를 호출할 때에는 호출하는 프로세스의 주소 공간에 로드되어 있는 실행 파일명이나 DLL 파일명을 '\0'으로 끝나는 문자열로 전달하면 된다. 시스템이 지정한 실행 파일이나 DLL 파일을 찾아내면 GetModuleHandle 함수는 파일이 로드된 시작 주소를 반환한다. 반면 시스템이 해당 파일을 찾을 수 없다면 NULL을 반환한다. GetModuleHandle을 호출할 때 pszModule 매개변수로 NULL 값을 전달할 수도 있는데, 이 경우 GetModuleHandle은 현재 수행 중인 실행 파일이 로드된 시작 주소를 반환한다. 만일 이 함수가 DLL 내에서 호출된다면 어떤 모듈에 포함되어 코드가 수행 중인지 알아내기 위한 두 가지 방법이 있다.

① 링커에 의해 정의되는 가상변수인 __ImageBase가 현재 수행 중인 모듈의 시작 주소를 가리키고 있다는 사실을 활용할 수 있다. 이 변수 값은 앞서 알아본바와 같이 C 런타임 시작 코드가 (w)WinMain 함수를 호출할 때 사용되는 값이다.

② 첫 번째 매개변수로 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS를 두 번째 매개변수로 현재 수행 중인 함수의 주소를 지정하여 GetModuleHandleEx함수를 호출한다. 마지막 매개변수로 전달되는 값은 HMODULE을 가리키는 포인터 값인데, 두번째 매개변수로 전달한 함수를 포함하고 있는 DLL의 시작주소를 반환해 준다.

※ 실제로 HMODULE과 HINSTANCE는 완전히 동일하다. 어떤 함수가 HMODULE을 요구한다면 HINSTANCE를 넘겨줘도 무방하며, 그 반대도 마찬가지다. 16비트 윈도우에서는 HMODULE과 HINSTANCE가 완전히 구분되는 자로형으로 존재했지만 지금은 혼용하고 있다.
반응형

'' 카테고리의 다른 글

Struct Size  (0) 2009.10.12
04. 프로세스 - CreateProcess  (0) 2009.08.21
02. 커널 오브젝트 下  (0) 2009.08.18
01. 커널 오브젝트 上  (0) 2009.08.18
한 줄에 여러개의 숫자를 입력받아 배열에 할당  (0) 2009.07.13