CodeSarang.Com
Home | 전체 메뉴 | 질문/답변 Join | Login | 검색   

 

Programming Applications for Windows

등록자 : rawmania (김정수), 2008-09-05

  1. Part |1| 준비하기
    1. Chapter |1| 에러 핸들링
    2. Chapter |2| 유니코드
      1. C 런타임 라이브러리에서 유니코드 지원
      2. 중립 코드의 사용
      3. 윈도우즈에 정의된 유니코드 데이터 타입
      4. 윈도우즈의 유니코드와 ANSI 함수
      5. 윈도우즈 운영체제 문자열 함수
      6. 윈도우즈 문자열 함수
      7. 리소스
      8. 텍스트가 ANSI인지 유니코드인지 결정
      9. 유니코드와 ANSI 간의 문자열 변환
    3. Chapter |3| 커널 오브젝트
      1. 사용 카운트(usage counting)
      2. 보안
      3. 프로세스의 커널 오브젝트 핸들 테이블
      4. 커널 오브젝트 닫기
      5. 프로세스 경게를 넘어선 커널 오브젝트 공유
        1. 오브젝트 핸들 상속
      6. 핸들의 플래그 변경
        1. 네임드 오브젝트
        2. 오브젝트 핸들의 복사
  2. Part |2| 작업 끝내기
    1. Chapter |4| 프로세스
      1. 첫번째 윈도우즈 애플리케이션 만들기
      2. 프로세스의 인스턴스 핸들
      3. 프로세스의 이전 인스턴스 핸들
      4. 프로세스 커맨드라인
      5. 프로세스 환경 변수
      6. 프로세스의 친화력(Affinity)
      7. 프로세스의 에러 모드
      8. 프로세스의 현재 드라이브와 디렉터리
        1. 프로세스의 현재 디렉터리
      9. 시스템 버전
      10. CreateProcess 함수
        1. psaProcess, psaThread 와 bInheritHandles
        2. fdwCreate
        3. pvEnviroment
        4. pszCurDir
        5. psiStartInfo
        6. ppiProcInfo
      11. 프로세스 종료
        1. 프라이머리 스레드의 엔트리 포인트 함수 리턴
        2. ExitProcess 함수
        3. TerminateProcess 함수
        4. 프로세스 내의 모든 스레드가 종료될 때
        5. 프로세스가 종료될 때
      12. 자식 프로세스
        1. 자식 프로세스를 분리해서 실행하기
      13. 시스템에서 실행되는 프로세스 열거
      14. 프로세스 정보 예제 애플리케이션
    2. Chapter |5| 작업(Jobs)
      1. Job내의 프로세스에 제한 가하기
        1. JOBOBJECT_BASIC_LIMIT_INFORMATION
        2. JOBOBJECT_EXTENDED_LIMIT_INFORMATION
        3. JOBOBJECT_BASIC_UI_RESTRICTIONS
        4. JOB_OBJECT_SECURITY_LIMIT_INFORMATION
      2. Job 내에 프로세스 배치하기
      3. Job 내의 모든 프로세스 종료
      4. Job에 대한 통계 질의
        1. 기본적인 실행 통계 정보 알아내기
        2. 기본적인 실행 통계 정보 + I/O 관련 정보 알아내기
        3. 프로세스 ID 집합 알아내기
      5. Job 통지
    3. Chapter |6| 스레드의 기본
      1. 스레드를 생성할 때
      2. 첫번째 스레드 함수 작성하기!
      3. 스레드 함수 생성
        1. psa
        2. cbStack
        3. pfnStartAdd & pvParam
        4. fdwCreate
        5. pdwThreadID
      4. 스레드의 종료
        1. 스레드 함수의 리턴
        2. ExitThread()
        3. TerminateThread()
      5. 프로세스가 종료될 때
      6. 스레드가 종료될 때
      7. Some Thread Internals
      8. C/C++ 런타임 라이브러리 살펴보기
      9. Gaining a Sense of One's Own Identity
      10. 의사 핸들을 실제 핸들로 변환
    4. Chapter |7| 스레드 스케줄링, 우선순위, 친화도
      1. 스레드의 중지(suspending)와 재시작(resuming)
      2. 프로세스의 서스펜드와 재시작
      3. 슬립
      4. 다른 스레드로 스위칭
      5. 스레드의 실행 시간
      6. CONTEXT 다루기
      7. 스레드 우선순위
      8. 우선순위의 추상적 고찰
      9. 프로그래밍 우선순위
      10. 스레드 우선순위 레벨의 동적 상승(boosting)
      11. Foreground 프로세스를 위한 스케줄러 조정(tweaking)
      12. 선호도(Affinity)

Programming Applications for Windows by Jeffrey Richter

Part |1| 준비하기

Chapter |1| 에러 핸들링

윈도우즈 함수들의 리턴형
데이터 형 실패를 가리키는 값
VOID 리턴 값 없음. VOID형 리턴값을 가지는 윈도우 함수는 매우 적다.
BOOL 실패하면 0
HANDLE 실패하면 대개 NULL, 때때로 INVALID_HANDLE_VALUE 를 실패로 보기도 한다. 숫자로는 -1이다. 이 리턴값을 가지는 함수는 확인을 해보고 사용하자.
PVOID 실패하면 NULL
LONG/DWORD 실패는 0이나 -1, 역시 확인해보고 사용한다.

왜 함수가 실패했는지 이해하는 것이 유용할 경우가 많다. 마이크로소프트튼 발생 가능한 모든 에러 코드들을 모아서 리스트를 만들고 각에러코드들을 32비트의 크기의 숫자 중 하나에 할당하였다. 어떤 에러인지 정확히 알고 싶을 때는 GetLastError?()를 호출하면 된다.

 
DWORD GetLastError(); 
이 함수는 해당 스레드의 32비트 에러 코드를 리턴한다.
WinError?.h 헤더 파일에 마이크로소프트에서 정의한 에러 코드를 확인할 수 있다.

에러를 감지할 때, 호출한 스레드에 적합한 에러 코드 번호를 연결시키기 위하여 스레드 로컬 스토리지라는 메커니즘을 사용한다. (21장)

함수 호출이 성공하였을 때에도 성공한 이유를 GetLastError?()를 통해 얻어올 수도 있다.

사용자 애플리케이션을 수행 중 에러를 감지하고, 사용자에게 관련된 텍스트 설명을 보여줄 경우가 발생할때 에러 코드에 해당하는 텍스트 설명으로 바꿀수 있다.

DWORD FormatMessage ( 
 DWORD dwFlags, 
 LPCVOID pSource, 
 DWORD dwMessageId, 
 DWORD dwLanguageId, 
 PTSTR pszBuffer, 
 DWORD nSize,  
 va_list *Argument 
); 
// 사용예 
DWORD dwError = GetLastError(); 
 
HLOCAL hlocal = NULL; // 에러 메시지를 얻기 위한 버퍼 
 
BOOL fOk = FormatMessage( 
 FORMAT_MESSAGE_FROM_SYSTEM|FORMAT_MESSAGE_ALLOCATE_BUFFER, // 에러코드에 대한 문자열을 원하고 | 에러 텍스트 문자열을 메모리 블록에 할당받아 반환할것(LocalFree()로 해지할 것을 약속) 
 NULL,  
 dwError, // 에러코드 
 MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US), // 사용할 언어 
 (LPTSTR)&hlocal, // 반환되는 문자열 
 0,  
 NULL); 
if(hlocal != NULL) 
{ 
 // FormatMessage 처리 
 LocalFree(hlocal); // 반드시 해제시켜줄것 
} 

사용자 지정 에러코드 발생

VOID SetLastError(DWORD dwErrCode); 
비트 31-30 29 28 27-16 15-0
내용 에러 치명도(Severity) 마이크로소프트/사용자 예약됨 마이크로소프트에서 편의를 위해 정의된 코드 예외코드
의미 0 성공, 1 정보, 2 경고, 3 에러 0 마이크로소프트, 1 사용자 0이어야한다 마이크로소프트에 의해 정의됨 마이크로소프트 or 사용자 정의

자세한 내용은 24장에서 다룬다.

Chapter |2| 유니코드

C 런타임 라이브러리에서는 더블 바이트 문자세트를 다루는 함수들이 없다.
DBCS 스트링을 다루는것을 돕기 위해, 윈도우즈는 다음과 같은 함수의 세트를 제공한다.

함수 설명
PTSTR CharNext?(PCTSTR pszCurrentChar?); 문자열에서 다음 문자의 주소를 리턴한다.
PTSTR CharPrev?(PCTSTR pszStart, PCTSTR szCurrentChar?); 문자열에서 이전 문자의 주소를 리턴한다.
BOOL lsDBCSLeadByte?(BYTE bTestChar?); 바이트가 DBCS 문자의 첫 번째 바이트이면 TRUE를 리턴한다.

유니코드(http://www.unicode.org)
1988년 애플과 제록스에 의해 수립된 표준.
1991년 유니코드를 개발하고 권장하기 위해 컨소시엄의 탄생. 이 컨소시엄은 애플, 컴팩, 휴렛팩커드, IBM, 마이크로소프트, 오라클, 실리콘 그래픽, 사이베이스, 유니시스, 제록스로 구성

윈도우 2000부터는 처음부터 유니코드를 염두 해 두고 만들어 졌다.
CreateWindowsEx?를 호출하고 유니코드 문자열이 아닌 클래스 이름과 윈도우 캡션을 인자로 전달한다고 하자. 이때 CreateWindowEx?()는 메모리 블록(프로세스 디폴트 힙)을 할당 받고, 유니코드가 아닌 문자열을 문자열을 유니코드 문자열로 바꾼다. 그리고 유니코드 버전의 CreateWindowsEx?()함수를 호출한다.

윈도우 98이전에는 내부적으로 ANSI 문자열을 사용하므로 CreateWindowsEx?()를 호출할 때, 인자로 ANSI 문자열을 전달하면 바로 호출된다.

윈도우 CE의 경우 윈도우 2000과 마찬가지로 유니코드를 기반으로 개발되었다.

유니코드로 개발하기

#define UNICODE 
#define _UNICODE 

C 런타임 라이브러리에서 유니코드 지원

// 표준 C 헤더 파일 string.h  
//  
typedef unsined short wchar_t; 

char * strcat(char *, const char *); 
wchar_t * wcscat(wchar_t *, const wchar_t *); 
 
char * strchr(const char, int); 
wchar_t * wcschr(const wchar_t *, wchar_t *); 
 
int strcmp(const char *, const char *); 
int wcscmp(const wchar_t *, const wchar_t *); 
 
char * strcpy(char *, const char *); 
wchar_t * wcscpy(wchar_t *, const wchar_t *); 
 
size_t strcpy(const char *); 
size_t * wcscpy(const wchar_t *); 

중립 코드의 사용

string.h 대신에 tchar.h 파일을 헤드파일에 포함시켜야 한다.
_UNICODE 가 정의 되어 있다면 wcs 함수 집합을, _UNICODE가 정의 되어 있지 않다면 str 함수집합을 참조.

// tchar.h 
#ifdef _UNICODE 
 typedef wchar_t TCHAR; 
#else 
 typedef char TCHAR; 
#endif 
 
#ifdef _UNICODE 
 #define _TEXT(x) L##x 
#else 
 #define _TEXT(x) x 
#endif 

윈도우즈에 정의된 유니코드 데이터 타입

_UNICODE 매크로는 C 런타임 헤더 파일에서 사용되고, UNICODE 매크로는 윈도우즈 헤더 파일에서 사용된다.

데이터 타입 설명
WCHAR 유니코드 문자
PWCHAR 유니코드 문자열 포인터
PCWCHAR 상수 유니코드 문자열 포인터

중립코드로는 PTSTR, PCTSTR이 정의 되어 있다. ( LP-, P- 는 똑같이 쓰인다. )

#ifdef UNICODE 
 typedef LPWSTR LPTSTR; 
#else 
 typedef LPSTR LPTSTR; 
#endif 
 
#ifdef UNICODE 
 typedef LPCWSTR LPCTSTR;  
#else 
 typedef LPCSTR LPCTSTR; 
#endif 

윈도우즈의 유니코드와 ANSI 함수

유니코드 문자열을 받는 CreateWindowsEx?()와 ANSI 문자열을 인자를 받는 CreateWindowsEx?()가 있다.

// winuser.h 
 
#ifdef UNICODE 
 #define CreateWindowsEx CreateWindowsExW 
#else 
 #define CreateWindowsEx CreateWindowsExA 
#end if 

유니코드 기반의 윈도우 2000에서, ANSI 버전 CreateWindowsEx?A()에 대한 마이크로소프트 내부 소스 코드는 CreateWindowsEx?W()를 호출하기 위한 하나의 번역 레이어 일 뿐이다.

윈도우즈 98의 경우 CreateWindowsEx?A()의 호출만을 수행한다. CreateWindowsEx?W()의 경우 실패를 리턴한다. GetLastError?()을 호출하면 ERROR_CALL_NOT_IMPLEMEMTED를 리턴한다.

윈도우즈 API 에서 WinExec?와 OpenFile?과 같은 함수는 단지 이미 개발된 16비트 윈도우즈용 프로그램과의 backward compatibility를 위해 존재할 뿐이다.
이 함수들을 호출할 때는 반드시 ANSI 문자열을 전달해야 한다.
반면, 새로운 함수들은 ANSI와 유니코드 두 버전에 대해 모두 구현되어 있다.

윈도우즈 운영체제 문자열 함수

고전적 운영체제 함수 스타일에서 운영체제 문자열 함수이 이름은 대문자와 소문자를 둘 다 가진다.
trCat, StrChr
?, StrCmp?, StrCpy? 와 같은 이름을 사용한다.
ShlWApi.h 헤더 파일을 반드시 인클루드 해야 한다.
이러한 문자열 함수는 StrCat?A와 StrCat?W와 같이 ANSI와 유니코드 두 개의 버전으로 구현되어 있다.

윈도우즈 문자열 함수

함수 설명
lstrcat 한 문자열을 다른 문자열 끝에 연결
lstrcmp 두 문자열을 대소문자를 구별해서 비교한다.
lstrcmpi 두 문자열을 대소문자와 상관없이 비교한다.
lstrcpy 한 문자열을 다른 지역의 메모리에 복사한다.
lstrlen 문자열에 대한 문자단위의 사이즈를 리턴한다.

이 함수들도 마찬가지로 UNICODE 정의 여부에 따라 -A, -W 함수들로 확장된다.

두개의 문자열 비교 함수 lstrcmp와 lstrcmpi는 이들에 대응하는 C 런타임 함수와 동작이 다르다.
C 런타임 함수 strcmp, strcmpi, wcscmp, wcscmpi는 단순히 문자열에서 코드를 비교한다. 즉, 실제 문자가 가지는 의미는 무시하고 단순히 첫 번째 문자열에서 각 문자의 숫자 값과 두 번째 문자열에서 각 문자의 숫자 값을 비교한다. 반면 윈도우즈 함수 lstrcmp, lstrcmpi는 윈도우즈 API인 CompareString
?()을 호출하도록 구현되어 있다.

int CompareString ( 
 LCID lcid, // 지역 ID (32비트 값으로 각 언어들을 구별하기 위한 값), 보통 GetThreadLocale()함수를 호출하여 로케일을 설정한다. 
 DWORD fdwStyle, // 플래그 
 PCWSTR pString1, 
 int cch1, // 문자열의 길이. -1을 보내주면 널문자로 끝남을 보장 
 PCWSTR pString2, 
 int cch2); // 문자열의 길이. -1을 보내주면 널문자로 끝남을 보장 

플래그 설명
NORM_IGNORECASE 대소문자를 구별하지 않는다.
NORM_IGNOREKANATYPE 히라가나와 가타가나 문자를 구별하지 않는다.
NORM_IGNORENONSPACE 공백 문자를 무시한다.
NORM_IGNORESYMBOLS 기호를 무시한다.
NORM_IGNOREWIDTH 싱글 바이트 문자와 같은 문자인 더블 바이트 문자를 구분하지 않는다.
SORT_STRINGSORT 구두점을 기호와 같이 다룬다.

lstrcmp가 호출할때는 fdwStyle 파라미터로 0을 전달하고
lstrcmpi 호추리에는 NORM_IGNORECASE를 전달한다.


C 런타임 함수들은 유니코드 문자열을 다루기 위한 좋은 기능이 없다.
엑센트 있는 문자 등은 대소문자 변경이 불가하다. 이런 결점을 보완한 함수이다. 물론 ANSI와 UNICODE 둘다 지원한다.

// 널문자로 끝나는 문자열에 대한 함수 
PTSTR CharLower(PTSTR pszString); 
PTSTR CharUpper(PTSTR pszString); 
 
// 특정 버퍼내의 널문자로 끝나지 않는 문자열을 변경시에 사용 
DWORD CharLowerBuff(PTSTR pszString, DWORD cchString); 
DWORD CharUpperBuff(PTSTR pszString, DWORD cchString); 
 
BOOL IsCharAlpha(TCHAR ch); 
BOOL IsCharAlphaNumeric(TCHAR ch); 
BOOL IsCharLower(TCHAR ch); 
BOOL IsCharUpper(TCHAR ch); 
 


마이크로소프트의 C 런타임 라이브러리의 printf() 계열의 함수들에 특별히 필트 타입 몇개가 추가되었다.
sprintf에서 %s는 ANSI, %S는 Unicode
swprintf에서 %s는 Unicode, %S는 ANSI

sprintf(szA, "%s", "ANSI Str"); 
sprintf(szA, "%S", L"UNICODE Str"); 
 
swprintf(szW, "%s", L"UNICODE Str"); 
swprintf(szW, "%S", "ANSI Str"); 

리소스

리소스 내의 문자열 값은 항상 유니코드로 쓰여진다.
리소스로 쓰여진 문자열을 읽어올 때, LoadString
?()을 호출하면 각각 LoadString?A(), LoadString?W()로 호출되어 -A()의 경우 반환될 문자열을 ANSI 형으로 바꾸어 반환한다.

텍스트가 ANSI인지 유니코드인지 결정

DWORD IsTextUnicode( 
 CONST PVOID pvBuffer, // 테스트 하고자 하는 버퍼의 주소 
 int cb,  // pvBuffer가 가리키는 버퍼의 바이트 수 
 PINT pResult // 요구할 테스트의 방법. NULL을 전달하면 모든 테스트를 수행 
);  
반환값은 유니코드 텍스트가 있다고 판단되면 TRUE
실패할 가능성도 있음.

유니코드와 ANSI 간의 문자열 변환

int MultiByteToWideChar( 
 UINT uCodePage, // 멀티바이트의 종류 
 DWORD dwFlags, // 액센트와 같은 문자를 구별하기 위해 사용되는 문자에 대한 제어를 추가. 일반적으로 플래그는 사용되지 않고 0을 전달하면 된다. 
 PCSTR pMultiByteStr, 
 int cchMultiByte, // 문자열의 길이. -1을 보내주면 널문자로 끝남을 보장 
 PWSTR pWideCharStr, 
 int cchWideChar // 버퍼에 대한 최대 길이. 0을 전달하면, 함수는 변환을 수행하지 않고 변환이 성공적으로 수행하기 위해 요구되는 버퍼의 크기를 리턴한다. 
); 
이거 그냥 막 쓰면 (strlen()등으로) 마지막에 EOS(0x0000)를 붙여주지 않는다... -1을 활용하자...
뜬금없지만.. fgets()는 엔터로 입력할경우 개행문자까지 붙여서 문자열 만든다-_-;

int WideCharToMultiByte( 
 UINT uCodepage, // 변환될 문자열의 멀티바이트의 종류 
 DWORD dwFlags, // 문자를 구별하기 위한 마크와 시스템이 변환할 수 없는 문자에 대해 영향을 미친다. 일반적으로 이렇게 세밀하게 할 일은 없으므로 0. 
 PCWSTR pWideCharStr, 
 int cchWideChar, // 문자열의 길이. -1을 보내주면 널문자로 끝남을 보장 
 PSTR pMultiByteStr, 
 int cchMultiByte, // 버퍼에 대한 최대 길이. 0을 전달하면, 함수는 변환을 수행하지 않고 변환이 성공적으로 수행하기 위해 요구되는 버퍼의 크기를 리턴한다. 
 PCSTR pDefaultChar, // 와이드 문자를 멀티바이트로 변환할 수 없으면 이 문자로 바꾸어서 표시. 이 파라미터가 NULL이면 디폭트 문자인 ?을 사용.  
 PBOOL pfUsedDefaultChar // 변환의 성공 여부. 단 하나라도 변환에 실패하면 FALSE. NULL을 전달하면 확인하지 않음. 
); 

Chapter |3| 커널 오브젝트

액세스 토큰, 이벤트, 파일, 파일 매핑, I/O 완료 포트, 작업, 메일슬롯, 뮤텍스, 파이프, 프로세스, 세마포어, 스레드, 대기 타이머 등등...

커널 오브젝트는 커널에 의해서만 접근이 가능하므로, 애플리케이션이 자체적으로 이 구조체의 메모리 주소를 알아내어 변경할 수 없다.
다만 커널 오브젝트가 제공하는 함수를 사용는 것이다. 커널 오브젝트를 생성하는 함수는 핸들을 반환하고 이 핸들을 이용해 제어를 하게 된다.

사용 카운트(usage counting)

모든 커널 오브젝트 종류들에서 존재하는 공통적인 멤버중의 하나이다. 오브젝트가 생성되면 1로 설정되고 0으로 내려가면 커널은 이 오브젝트는 삭제한다.

보안

보안기술자(Security Descriptor)를 이용해 보호될 수 있다.
어떤 프로세스가 오브젝트를 생성했는지, 어떤 프로세스가 오브젝트에 접근하고 사용할 수 있는지, 어떤 프로세스에 대해 오브젝트에 대한 접근을 금지해야 하는지 등의 정보가 기록되어 있다.

typedef struct _SECURITY_ATTRIBUTES 
{ 
 DWORD nLength; 
 LPVOID lpSecurityDescriptor; 
 BOOL bInheritHandle; 
} SECURITY_ATTRIBUTES; 

초기화 방법은 다음과 같다.

SECURITY_ATTRIBUTES sa; 
sa.nLength = sizeof(sa); 
sa.lpSecurityDescriptor = 특정 보안 설명자; 
sa.bInHeritHandle = 상속 가능 여부 (TRUE/FALSE); 
 
... 

프로세스의 커널 오브젝트 핸들 테이블

핸들 테이블에 대한 정보는 문서화되어 있지 않다.

프로세스가 처음 초기화될 때 핸들 테이블은 비어 있다.

핸들 값은 실제적으로는 생성한 커널 오브젝트의 정보가 어디에 저장되어있는지를 파악할 수 있는 프로세스 핸들 테이블의 인덱스이다.
(사실 윈도우 2000에서, 핸들 값은 인덱스 값 자체가 아니라 프로세스 핸들 테이블 내에서 해당 오브젝트의 정보가 위치한 곳 까지의 바이트 수이다.)

커널 오브젝트의 핸들을 파라미터로 받는 함수를 호출할 때에는 반드시 이름이 Create-로 시작하는 함수에 의해 리턴된 값을 전달해 주어야 한다.
유효하지 않은 핸들을 전달하면 함수 호출을 실패하고 GetLastError
?()는 6(ERROR_INVALID_HANDLE)을 리턴한다.
핸들 값은 특정 프로세스가 가지고 있는 핸들 테이블내의 임의의 위치를 가리키기 때문에 다르느 프로세스에서는 성공적으로 사용할 수 없다.

커널 오브젝트를 생성하는 함수에서 호출이 실패하면 리턴되는 핸들 값은 보통 0이다.
시스템이 매우 작은 메모리를 가지거나 보안 문제에 직면했을때 이런일이 발생하곤 한다.
그리고 몇몇 함수는 이 경우에 -1(INVALID_HANDLE_VALUE)를 반환한다.
결국, 직접 확인해보고 사용해야 된다.

커널 오브젝트 닫기

BOOL CloseHandle(HANDLE hObj); 

사용 카운트가 0이 되면 해당 커널 오브젝트를 삭제한다.

유효하지 않은 핸들이 인자로 전달되면 CloseHandle?()은 FALSE를 리턴하고 GetLastError?()에서는 -1(ERROR_INVALID_HANDLE)을 리턴한다.

CloseHandle?()을 호출하지 않았다고 한다면 메모리 누수(Memory Leak)가 발생한다.
프로세스 실행중에는 이런 현상이 발생하지만, 프로세스가 종료되면 운영체제는 해당 프로세스가 사용하였던 모든 리소스를 해제한다. 이는 보장되어 있다!!
프로세스가 종료되면 자동적으로 프로세스의 핸들 테이블을 검색한다. 테이블에 아직도 유효한 엔트리가 있다면 시스템은 오브젝트 핸들들을 닫는다.
만약 이러한 오브젝트의 사용 카운트가 0이 되었다면 커널은 해당 커널 오브젝트를 삭제한다.

프로세스 경게를 넘어선 커널 오브젝트 공유

커널의 신뢰성을 위해서 핸들값은 프로세스에서 내부에서만 유효한데 다른 프로세스와의 공유를 시키기 위해서는 복잡해진다.

다른 프로세스와 커널을 공유하기 위한 세가지 메커니즘이 존재한다.

오브젝트 핸들 상속
1. 부모 프로세스가 어떤 커널 오브젝트를 생성할 때, 생성하는 커널 오브젝트의 핸들을 상속가능한 핸들로 만들고 싶다는 사실을 시스템에 알려주어야 한다.
2. 상속 가능한 핸들을 생성하기 위해서는 SECURITY_ATTRIBUTES 구조체에서 bInheritHandle
? 멤버를 TRUE로 만들어 주어야 한다.
3. 핸들을 상속 받기 위한 프로세스를 생성한다.

BOOL CreateProcess( 
 PCTSTR pszApplicationName, 
 PTSTR pszCommandLine, 
 PSECURITY_ATTRIBUTES psaProcess, 
 PSECURITY_ATTRIBUTES psaThread, 
 BOOL bInheriHandles, 
 DWORD dwCreationFlags, 
 PVOID pvEnvironment, 
 PCTSTR pszCurrentDirectory, 
 LPSTARTUPINFO pStartupInfo, 
 PPROCESS_INFORMATION pProcessInformation 
); 

상속된 가능한 핸들을 가지는 자식 프로세스의 생성과정은 다음과 같다.
1. 새로운 프로세스가 생성되고 핸들 테이블은 초기화 된다.
2. 상속 가능한 핸들을 찾아 부모 핸들 테이블과 똑같은 내용은 똑같은 위치에 복사한다. (핸들 값이 같다.)
3. 커널 오브젝트를 참조하는 핸들이 하나 추가되었으므로 사용 카운트를 하나 증가시킨다.

문제는 자식 프로세스가 자신은 어떠한 핸들을 부모로부터 상속받았는지 확인할 방법이 없다는 점이다.
그렇기 때문에 부모가 생성한 어떠한 커널 오브젝트에 대한 접근 권한을 가지는지는 문서화 되어 있어야 한다.

접근 가능한 커널 오브젝트의 핸들값을 알 수 있게끔 하는 방법 중 하나는 부모 프로세스가 자식 프로세스를 생성할 때 해당 오브젝트의 핸들값을 명령행인자로 넘기는 방법이다.
그외에 부모, 자식 프로세스 간에 다른 형태의 통신 방법을 사용할 수도 있다. 또 다른 방법중 하나는 환경 블록에 환경 변수를 추가하는 것이다. 부모 프로세스가 자식 프로세스를 생성할 때 자식 프로세스는 부모 프로세스의 환경 변수를 상속받고 GetEnviromentVariable?()을 호출해 상속된 커널 오브젝트의 핸들 값을 가져오는 것이다.

핸들의 플래그 변경

부모 프로세스가 상속 가능한 커널 오브젝트를 생성하였고 자식 프로세스에 상속할때 어떤 자식 프로세스에는 상속시키고 그외에 다른 자식 프로세스에는 상속시키지 않는다면 몇가지 제어가 필요하다.

BOOL SetHandleInformation( 
 HANDLE hObject, // 유효한 핸들 값. 
 DWORD dwMask, // 대상 마스크 
 DWORD dwFlags // 바뀌는 값. 
); 
 
BOOL GetHandleInformation( 
 HANDLE hObject,  
 PDWORD pdwFlags 
); 

// 마스크 & 플래그. 현재로는 이 두 플래그 밖에 없다. 
#define HANDLE_FLAG_INHERIT 0x00000001 // 상속 가능한 핸들로 지정한다. 
#define HANDLE_FLAG_PROTECT_FROM_CLOSE 0x00000002 // 해당 핸들은 닫혀서는 안된다는 것을 시스템에 알린다. 

// 상속 가능하게 
SetHandleInformation(해당 핸들, HANDLE_FLAG_INHERIT , HANDLE_FLAG_INHERIT); 
// 상속 불가능하게 
SetHandleInformation(해당 핸들, HANDLE_FLAG_INHERIT , 0); 
 
// 이 마스크를 설정하면 해당 핸들을 닫고자 할 때 Exception이 발생한다. 
SetHandleInformation(해당 핸들, HANDLE_FLAG_INHERIT , HANDLE_FLAG_INHERIT); 
CloseHandle(해당 핸들); // Exception is raised!! 

네임드 오브젝트
이름을 가진 커널 오브젝트를 생성할 수 있는 경우가 있다.

뮤텍스, 이벤트, 세마포어, Waitable Timer, 파일 매핑, 작업(job)
이런 커널 오브젝트 생성 함수들을 마지막 파라미터로 PCSTR pszName을 가지고 있다.
이 파라미터에 NULL을 전달하면 이름을 가지지 않는 (익명의) 커널 오브젝트를 생성하고 이름 없는 커널 오브젝트는 다른 커널 오브젝트와 똑같은 방식으로 사용된다.

프로세스 A에서 이름을 가진 커널 오브젝트를 생성하고
다른 프로세스 B에서 똑같은 이름을 가진 커널 오브젝트를 생성한다면
새로운 커널 오브젝트를 생성하지 않고 공유하게 된다. (접근 권한과, 상속 가능 여부는 변경되지 않고 기존의 생성된 커널 오브젝트를 따른다.)
(기존에 존재하는 커널 오브젝트를 오픈했는지 아니면 새로운 커널 오브젝트를 생성했는지 여부는, GetLastError
?()의 반환값이 85(ERROR_ALREADY_EXISTS)인지 여부를 확인하면 된다.)

윈도우즈 에러 코드 http://winapi.co.kr/ApiBoard/content.php?table=tblqa&pk=13490

만약 다른 종류의 커널 오브젝트이거나 접근 권한을 가지지 못한다면 새로운 커널 오브젝트를 생성하지 못하고 실패한다. (NULL을 리턴한다.)

이름을 이용하여 새로운 커널 오브젝트를 생성시키지 않고 공유만을 원한다면 Create- 함수가 아닌 Open- 함수를 호출하면 된다.
Open- 함수들은 pszName에 NULL을 전달해서는 안되며, 명시한 이름의 커널 오브젝트가 존재하지 않는다면 NULL을 리턴하고 GetLastError?()는 2(ERROR_FILE_NOT_FOUND)를 가지게 되다.

오브젝트 핸들의 복사
BOOL DuplicateHandle( 
 HANDLE hSourceProcessHandle, // 원본 프로세스 핸들 
 HANDLE hSourceHandle, // 원본 핸들 
 HANDLE hTargetProcessHandle, // 타겟 프로세스 핸들 
 PHANDLE phTargetHandle, // 복사되어 돌려 받게될 핸들 
 DWORD dwDesiredAccess, // 접근 마스크 
 BOOL bInheritHandle, // 상속 플래그 
 DWORD dwOptions // 옵션 
); 
이 함수는 어떤 프로세스 핸들 테이블의 엔트리를 가져와서 다른 프로세스 핸들 테이블에 복사본을 생성한다. (원본의 핸들 테이블과 내용은 같으나 인덱스는 다르다. 다시말하자면 핸들값은 다르다.)

옵션 설명
DUPLICATE_SAME_ACCESS 타깃 프로세스와 핸들이 소스 프로세스의 핸들과 같은 접근 마스크를 가지게 한다. dwDesiredAccess?는 무시한다.
DUPLICATE_CLOSE_SOURCE 소스 프로세스에서는 핸들을 닫는다. 커널 오브젝트의 사용 카운트를 증가시키지 않는다.

앞의 상속과 마찬가지로 타겟 프로세스에 사용 가능해진 핸들에 대한 통보가 이루어지지 않는다. 이것은 프로세스간의 통신을 통해 해결해야 하는 문제이다.

Part |2| 작업 끝내기

Chapter |4| 프로세스

스케쥴링 대상은 스레드.
라운드-로빈 방식. 각 스레드에 타임 슬라이스(퀀텀(quantum)이라는 단위)를 주어 처리.

윈도우 2000이상에서는 여러 개의 CPU를 가지는 머신을 지원한다.
멀티프로세서 머신의 장점을 살리기 위해서 부가적으로 소스 코드상에서 특별히 해야 할 일은 없다.

첫번째 윈도우즈 애플리케이션 만들기

비주얼 C++에서는 링커 스위치 중에서 /SUBSYSTEM : CONSOLE, /SUBSYSTEM : WINDOWS 로 CUI와 GUI를 구분한다.
CUI기반으로 설정하면 운영체제의 로더는 자동으로 텍스트 콘솔윈도우를 생성시킨다.
일단 애플리케이션이 수행을 시작한 후에는, 운영체제는 애플리케이션의 UI 타입이 무엇인지는 더 이상 관심을 두지 않는다.

윈도우즈 애플리케이션이 시작할 때 호출되는 엔트리 포인트 함수는 4가지 종류가 있다.

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pszCmdLine, int nCmdShow); 
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PWSTR pszCmdLine, int nCmdShow); 
int __cdecl main(int argc, char *argv[], char *envp[]); 
int __cdecl wmain(int argc, wchar_t *argv[], wchar_t *envp[]); 

운영체제는 실제로는 작성한 소스코드상의 엔트리 포인트 함수를 호출하지 않는다.
대신 C/C++ 런타임 Startup 함수를 호출한다.

엔트리 포인트 실행 파일에 들어가는 Startup 함수
WinMain? WinMain?CRTStartup
wWinMain? wWinMain?CRTStartup
main mainCRTStartup
wmain wmainCRTStartup

링커는 실행파일로 링크할 때 적절한 C/C++ 런타임 Startup 함수를 선택한다.
만약 잘못된 Startup 함수가 선택되어 있으면(콘솔 어플리케이션에 윈도우 엔트리 포인트를 요구한다던가) 링커는 "unresolved external symbol" 에러를 리턴한다.

대안은 링커 스위치에 /SUBSYSTEM : XXX 를 삭제하면 알아서 찾는 다고-_-

모든 C/C++ 런타임 Startup 함수는 기본적으로 같은 일을 한다.
차이점은
1. ANSI 혹은 유니코드 중 어떤 문자열을 처리하는지 여부
2. C 런타임 라이브러리를 초기화 한 후 어떠한 엔트리 포인트 함수를 호출하는가?

비주얼 C++ 소스 코드는 C 런타임 라이브러리를 활용해서 실행된다. CRT0.C 파일에서 4가지 Startup 함수들에 대한 코드를 찾아볼 수 있다.
Startup 함수가 하는 일을 요약하면 다음과 같다.
1. 새로운 프로세스의 전체 커맨드 라인을 가리키는 포인터를 가진다.
2. 새로운 프로세스의 환경 변수를 가진다.
3. C/C++ 런타임의 전역 변수를 초기화 한다. Stdlib.h 헤더 파일을 인클루드하면 이러한 전역변수에 접근할 수 있다.
4. C 런타임 메모리 할당 함수와 다른 하위 레벨 입출력 루틴이 사용하는 힙을 초기화 한다.
5. 전역 그리고 정적 C++ 클래스 오브젝트의 생성자 함수를 호출한다.

엔트리 포인트 함수가 수행을 종료하고 리턴할 때, 시작함수는 리턴값 nMainRetVal?을 파라미터로 전달하면서 C 런타임 exit 함수를 호출한다.
exit 함수는 다음과 같은 일을 한다.
1. _onexit 함수를 호출함에 의해 등록된 모든 함수를 호출한다.
2. 모든 전역 그리고 정적 C++ 클래스 오브젝트의 소멸자를 호출한다.
3. 운영체제의 ExitProcess? 함수를 호출하고 nMainRetVal? 을 파라미터로 전달한다. 운영체제는 해당 프로세스를 제거하고, 프로세스의 종료코드를 설정한다.

런타임 전역 변수 타입 설명
_osver unsigned int 빌드 넘버
_winmajor unsigned int 주 버전
_winminor unsigned int 부 버전
_winver unsigned int (_winmajor << 8) + _winminor
__argc unsigned int 명령행 인자의 수
__argv, __wargv char **, wchar_t** 명령행 인자
_environ, _wenviron char **, wchar_t** 환경 문자열
_pgmptr, _wpgmptr char **, wchar_t**

프로세스의 인스턴스 핸들

프로세스의 주소 공간에 로드된 실행 파일 또는 DLL 파일은 고유한 인스턴스 핸들을 가진다. 실행 파일의 인스턴스는 (w)WinMain?()의 첫번째 파라미터인 hInstance를 통해 전달된다. 이 핸들 값은 일반적으로 리소스를 로드하는 함수 호출에 필요하다.

HMODULE 과 HINSTANCE는 완전히 같은 것이다. 16비트 윈도우즈에서 달랐을 뿐이다.

실행 파일 이미지가 로드되는 시작 주소는 링커가 결정한다. 서로 다른 링커는 서로 다른 디폴트 주소를 사용할 수 있다.
비주얼 C++ 링커는 디폴트 시작 주소로 0x00400000을 사용한다. 왜냐하면 윈도우즈98에서 0x00400000는 실행 파일 이미지가 로드될 수 있는 가장 최하위 주소이기 때문이다.
마이크로소프트 링커의 /BASE : address 링커 스위치를 사용해서 애플리케이션이 로드되는 시작주소를 변경할 수 있다.

윈도우즈 98에서 실행 파일을 0x00400000 보다 더 아래쪽의 시작 주소로 로드하고자 하면 윈도우즈98 로더는 실행 파일을 다른 주소로 재배치해야만 한다. 실행은 되지만 로딩 시간이 늘어난다. 그냥 0x00400000 이상으로 잡자.

HMODULE GetModuleHandle(PCTSTR pszModule); 
프로세스 주소 공간에 로드된 실행 파일 또는 DLL 파일의 이름을 명시하는 0으로 끝나는 문자열을 전달한다.
반환값은 DLL 파일 이미지가 로드된 시작 주소를 리턴한다.
인자로 NULL 을 넣어주면 이 함수를 호출한 실행 파일 자체의 시작 주소를 리턴한다.

오직 호출한 프로세스의 주소 공간만을 조사한다. 만약 프로세스가 공통 다이얼로그 박스 관련 함수를 전혀 쓰지 않는 상황에서 "ComDlg32?.dll"을 파라미터로 호출하면 다른 프로세스에서 쓰인다 하더라도 NULL을 리턴한다.

프로세스의 이전 인스턴스 핸들

16비트 이전에 사용했던 것으로 32비트로 넘어오면서 사용되지 않는다.

int WINAPI WinMain( 
 HINSTANCE hInstance, 
 HINSTANCE, // 실수를 미연에 방지하기 위해서 안쓰는 인자를 없애버렸다. 파라미터가 없으므로 "parameter not referenced" 경고를 내지 않는다. Warning Level 4로 수정하면 경고를 다시 확인할 수 있다. 
 PSTR pszCmdLine,  
 int nCmdShow 
); 

프로세스 커맨드라인

PTSTR GetCommandLine(); 
프로세스의 전체 커맨드 라인을 가져온다.

PWSTR CommandLineToArgvW( 
 PWSTR pszCmdLine,  
 int *pNumArgs 
); 
유니코드만 지원. 유니코드 문자열을 토큰으로 분리하는 함수이다.
내부적으로 메로리를 할당한다. 프로세스가 끝날때 운영체제가 해당 메모리를 해제하기를 기대하지만 직접 하고자 한다면
int nNumArgs; 
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgs); 
// use the argument 
 
HeapFree(GetProcessHeap(), 0, ppArgv); 

프로세스 환경 변수

환경 블록은 프로세스 주소 공간 안에 할당된 메모리 블록이다.

VarName1?=VarValue?\0
VarName2?=VarValue?\0
VarName3?=VarValue?\0
...

등호가 이름과 값을 구별하기 때문에 이름에 등호를 사용해서는 안된다. 그리고 공백 또한 의미를 가지기 때문에 공백만으로도 문자들을 구별할 수 있다. ("Windows" " Windows" 는 다르다.)

윈도우즈98에서는 Autoexec.bat 에서 SET VarName?=VarValue? 와 같은 형태로 환경변수를 생성한다. 재부팅하게되면 윈도우즈98상의 어떠한 프로세스에서도 설정한 변수를 사용할 수 있게 된다.

윈도우즈2000에서는 로그인하면, 시스템은 셀 프로세스를 생성하고, 생성된 셀 프로세스와 환경 문자열 집합과 결합한다. 시스템은 레지스트리에서 두 개의 키를 검사해서 환경 문자열 집합을 초기화 한다.
첫번째 키는 시스템에 적용된 모든 환경변수 리스트를 가지고 있다. HKEY_LOCAl_MACHINE\SYSTEM\CurrentControlSet?\Control\Session Manager\Enviroment
두번째 키는 현재 로그인 되어 있는 사용자에 적용되는 모든 환경변수 리스트를 가지고 있다. HKEY_CURRENT_USER\Enviroment
오직 관리자 권한을 가진 사용자만이 시스템 변수 리스트에 있는 변수들을 변경할 수 있다.
또한 애플리케이션은 이 레지스트리의 엔트리를 변경하기 위해 다양한 레지스트리 함수를 사용할 수 있다. 변경한 내용이 모든 애플리케이션에 적용되려면 로그오프 한 뒤 다시 로그인 해야된다. 윈도우 탐색기, 작업 관리자, 제어판 과 같은 몇몇 애플리케이션은 메인 윈도우가 WM_SETTINGCHANGE 메시지를 받을 때 각자의 환경 블록을 새로운 레지스트리 엔트리로 업데이트 할 수 있다.
예를 들어 레지스트리를 업데이트하고 관련된 애플리케이션이 자신의 환경블록을 바로 업데이트 하길 원한다면 다음과 같은 함수를 호출하면 된다.
SendMessage?(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM)TEXT("Enviroment"));

DWORD GetEnvironmentVariable( 
 PCTSTR pszName, // 찾길 원하는 환경 변수 이름 
 PTSTR pszValue, // 버퍼  
 DWORD cchValue // 버퍼의 크기 
); 
리턴값은 버퍼에 담은 문자의 개수. 환경 블록에서 변수 이름을 찾지 못하면 0을 반환

DWORD ExpandEnvironmentStrings( 
 PCSTR pszSrc, // %USERPROFILE%/Administrator 와 같이 % 안에 있는 환경변수 문자열을 확장시키기 위한 API. 환경변수가 포함된 문자열 
 PSTR pszDst, // 대체된 문자열을 담기 위한 버퍼의 크기 
 DWORD nSize // 버퍼의 최대 크기 
); 

BOOL SetEnvironmentVariable( 
 PCTSTR pszName, // 환경 변수 이름 
 PCTSTR pszValue // 설정할 값. NULL을 전달하면 환경 변수 삭제. 
); 

프로세스의 친화력(Affinity)

프로세스의 스레드들이 특정 CPU에서 실행되도록 할 수 있다. 7장에 논의.

프로세스의 에러 모드

프로세스에 심각한 에러가 발생했을 때, 프로세스가 어떻게 반응해야 할 지를 시스템에게 알려주는 플래그의 집합을 설정할 수 있다.
UINT SetErrorMode(UINT fuErrorMode); 
플래그 설명
SEM_FAILCRITICALERRORS 시스템은 치명적인 에러 핸들러 메시지 박스를 디스플레이 하지 않고 단지 호출한 프로세스에게 에러를 리턴한다.
SEM_NOGPFAULTERRORBOX 시스템은 일반적인 오류 메시지 박스를 디스플레이하지 않는다. General Protection Faults를 자체적인 예외 처리기로 처리하는 디버깅 애플리케이션에 의해서만 설정된다.
SEM_NOOPENFILEERRORBOX 시스템은 파일 찾기에 실패할 때 메시지 박스를 디스플레이 하지 않는다.
SEM_NOALIGNMENTFAULTEXCEPTION 시스템은 자동으로 메모리 정렬 에러를 고치고 애플리케이션에게 보이지 않도록 한다. x86에서는 아무 영향이 없다.

각 에러 플래그는 OR 연산을 통해 조합하여 사용할 수 있다.

자식 프로세스는 부모로 부터 에러 모드 플래그를 상속 받는다.

CreateProcess?를 호출할 때, 에러 모드 플래그를 상속 받지 않고 CREATE_DEFAULT_ERROR_MODE 플래그로 기본 플래그 설정할 수 있다.

프로세스의 현재 드라이브와 디렉터리

전체 경로 이름이 제공되지 않는다면 다수의 윈도우즈 함수는 현재 드라이브의 현재 디렉터리에서 해당 파일과 디렉터리를 찾느다.

시스템은 내부적으로 프로세스의 현재 드라이브와 현재 디렉터리 정보를 계속해서 유지한다.
이 정보는 프로세스 단위로 유지되므로 프로세스내 어떤 스레드가 현재 드라이브나 디렉토리를 변경하면, 다른 모든 프로세스내 스레드에 영향을 미친다.

DWORD GetCurrentDirectory( 
 DWORD cchCurDir, 
 PTSTR pszCurDir 
); 
BOOL SetCurrentDirectory(PCTSTR pszCurDir); 

프로세스의 현재 디렉터리
시스템은 프로세스의 현재 드라이브와 현재 디렉터리 정보를 유지하지만, 모든 드라이브에 대한 각각의 현재 디렉터리 정보를 유지하는것은 아니다.
그러나 몇몇 운영체제에서는 여러 개의 드라이브에 대한 각각의 현재 디렉터리를 처리할 수 있도록 지원하고 있다.
이러한 지원은 프로세스의 환경 문자열을 통해서 제공된다.

=C:=C:\Utiliy\Bin 
=D:=D:\Program Files 
이 변수들은 드라이브의 C의 현재 디렉터리와 드라이브 D의 현재 디렉터리를 가리키고 있다.

현재 드라이브가 아닌 다른 드라이브를 지칭하는 드라이브 문자를 전달하여 함수를 호출하면, 시스템은 환경 블록내에서 명시된 드라이브 문자와 관련이 있는 변수를 찾는다.
찾는다면 시스템은 해당 변수의 값을 현재 디렉터리로 사용한다.
못 찾느다면 시스템은 명시된 드라이브 문자 루트 디렉터리를 현재 디렉터리로 간주한다.

* 현재 디렉터리를 변경하기 위해서 SetCurrentDirectory?()가 아닌 C 런타임 함수인 _chdir()을 사용할 수도 있다. _chdir()은 내부적으로 SetCurrentDirectory?()를 호출하지만 더 나아가서 다른 드라이브의 현재 디렉터리가 유지되도록 환경변수를 추가하거나 수정한다.

환경블록을 상속되지 못한다. 그래서 현재 디렉토리에 대한 내용은 상속이 안된다.
자식 프로세스의 현재 디렉터리는 각 드라이브의 루트 디렉터리를 디폴트 기본 값으로 가진다.
자식 프로세스가 부모 현재 디렉터리를 상속받기 위해서는 자식 프로세스를 생성하기 전에 현재 디렉터리의에 대한 드라이브 문자 환경변수를 생성하고, 환경변수에 추가해야 한다.

DWORD GetFullPathName( 
 PCTSTR pszFile, // 드라이브 문자열 ("C:") 
 DWORD cchPath, // 최대 버퍼 크기 
 PTSTR pszPath, // 가져올 버퍼 
 PTSTR *ppszFilePart // 현재 디렉토리 내의 파일이름들을 가져올 버퍼 
); 

시스템 버전

DWORD GetVersion(); 
역사 깊은 함수. MS-DOS 버전 넘버는 하이 워드로 리턴. 윈도우즈 버전 넘버는 로우 워드로 리턴. 각 워드에서 하이 바이트는 메이저 버전, 로우 바이트는 마이너 버전.

실수를 줄이기 위해서 새로운 버전 관련 함수를 추가하였다.

BOOL GetVersionEx(POSVERSIONINFO pVersionInformation); 
 
typedef struct { 
 DWORD dwOSVersionInfoSize; 
 DWORD dwMajorVersion; 
 DWORD dwMinorVersion; 
 DWORD dwBuildNumber; 
 DWORD dwPlatformId; 
 TCHAR szCSDVersion[128]; 
} OSVERSIONINFOEX, *POSVERSIONINFOEX, *LPOSVERSIONINFOEX; 
 
typedef struct { 
 DWORD dwOSVersionInfoSize; 
 DWORD dwMajorVersion; 
 DWORD dwMinorVersion; 
 DWORD dwBuildNumber; 
 DWORD dwPlatformId; 
 TCHAR szCSDVersion[128]; 
 WORD wServicePackMajor; 
 WORD wServicePackMinor; 
 WORD wSuiteMask; 
 BYTE wProductType; 
 BYPTE wReserved; 
} OSVERSIONINFOEX, *POSVERSIONINFOEX; // 윈도우 2000에서 새롭게 등장! 

멤버 설명
dwOSVersionInfoSize? sizeof(OSVERSIONINFO) 또는 sizeof(OSVERSIONINFOEX) 를 설정하고 GetVersionEx?()를 호출해야 한다.
dwMajorVersion? 호스트 시스템의 메이저 버전 넘버
dwMinorVersion? 호스트 시스템의 마이너 버전 넘버
dwBuildNumber? 현재 시스템의 빌드 넘버
dwPlatformId? 현재 시스템이 지원하는 플랫폼을 명시한다. VER_PLATFORM_WIN32s(win32s), VER_PLATFORM_WIN32_WINDOWS(windows95/98), VER_PLATFORM_WIN32_NT (windows nt/2000), VERPLATFORM_WIN32_CEHH(windows ce)
szCSDVersion 인스톨된 운영체제에 관해 좀 더 많은 정보를 제공하는 문자열을 담는다.
wServicePackMajor? 마지막으로 인스톨된 서비스 팩의 메이저 넘버
wServicePackMinor? 마지막으로 인스톨된 서비스 팩의 마이너 넘버
wSuiteMask? 시스템에서 이용 가능한 스위트들을 명시. VER_SUITE_SMALLBUSINESS, VER_SUITE,ENTERPRISE, VER_SUITE_BACKOFFICE, VER_SUITE_COMMUINICATIONS, VER_SUITE_TERMINAL, VER_SUITE_SMALLBUSINESS_RESTRICTED, VER_SUITE_EMBEDDEDNT, VER_SUITE_DATACENTER
wProductType? VER_NT_WORKSTATION, VER_NT_SERVER, VER_NT_DOMAIN_CONTROLLER
wReserved 예약

호스트 시스템의 버전과 애플리케이션이 요구하는 버전의 비교를 더욱 쉽게 하기 위하여 새로운 함수를 제공한다.

BOOL VerifyVersionInfo( 
 POSVERIONINFOEX pVersionInformation, 
 DWORD dwTypeMask,  
 DWORDLONG dwlConditionMask 
); 
dwTypeMask
?에서는 구조체에 어떤 멤버가 사용되었는지 VER_MINORVERSION,VER_MAJORVERSION,VER_BUILDNUMBER, VER_PLATFORMID, VER_SERVICEPACKMINOR, VER_SUITENAME, VER_SERVICEPACKMAJOR, VER_PRODUCT_TYPE 들의 OR 조합으로 사용된다.
dwlConditionMask? 는 함수가 시스템 정보와 애플리케이션이 제시한 정보를 어떻게 비교하는지 제어하기 위한 64비트 값이다.

dwlConditionMask?는 다양한 비트 단위의 조합으로 비교방법을 제시한다.
원하는 비트 조합을 생성하기 위해, 다음의 VER_SET_CONDITION 매크로를 사용할 수 있다. (단, VER_SUITENAME은 비교할 수 없다. 대신 VER_AND나 VER_OR을 사용해 인스톨 여부를 확인할 수 있다.)

VER_SET_CONDITION( 
 DWORDLONG dwlConditionMask, // 비교 방법을 저장하기 위한 변수 
 ULONG dwTypeBitMask, // verifyversioninfo()의 dwTypeMask와 동일 
 ULONG dwConditionMask // VER_EQUAL, VER_GRATER, VER_GRATER_EQUAL, VER_LESS, VER_LESS_EQUAL 중 하나를 사용한다. 
) 

플래그를 다 설정하고 호스트 시스템이 모든 사항을 만족하면 0이 아닌 값을 리턴한다.
0을 반환한다면 GetLastError?()를 호출하여 0을 리턴한 이유를 알아낼 수 있다.
ERROR_OLD_WIN_VERSION은 함수는 정확히 호출하였지만 시스템이 애플리케이션의 요구사항을 충족시키지 못한 경우이다.

호스트 시스템이 정확하게 윈도우즈 2000인지 테스트 하는 방법이다.

OSVERSIONINFOEX osver = {0}; 
osver.dwOSVersionInfoSize = sizeof(osver); 
osver.dwMajorVersion = 5; 
osver.dwMinorVersion = 0; 
osver.dwPlatformId = VER_PLATFORM_WIN32_NT; 
 
DWORDLONG dwlConditionMask = 0; 
VER_SET_CONDITION(dwlConditionMask, VER_MAJORVERSION, VER_EQUAL); 
VER_SET_CONDITION(dwlConditionMask, VER_MINORVERSION, VER_EQUAL); 
VER_SET_CONDITION(dwlConditionMask, VER_PLATFORMID, VER_EQUAL); 
 
if(VerifyVersionInfo(&osver, VER_MAJORVERSION|VER_MINORVERSION|VER_PLATFORMID, dwlConditionMask)) 
{ 
 // 윈도우 2000 
} 
else 
{ 
 // 그외 
} 

CreateProcess? 함수

BOOL CreateProcess( 
 PCTSTR pszApplicationName,  
 PTSTR pszCommandLine,  
 PSECURITY_ATTRIBUTES psaProcess, 
 PSECURITY_ATTRIBUTES psaThread, 
 BOOL bInheritHandles, 
 DWORD fdwCreate, 
 PVOID pvEnvironment, 
 PCTSTR pszCurDir, 
 PSTARTUPINFO psiStartInfo, 
 PPROCESS_INFORMATION ppiProcInfo 
); 

스레드가 CreateProcess?를 호출하면, 시스템은 프로세스 커널 오브젝트를 생성하고 사용 카운트를 1로 설정한다.
생성된 프로세스 커널 오브젝트는 프로세스 그 자체가 아니라, 운영체제가 해당 프로세스를 관리하기 위해 사용하는 자료 구조이다.
이 프로세스 커널 오브젝트를 생성한 다음, 시스템은 프로세스를 위해서 가상주소공간을 생성하고 실행파일의 코드와 데이터, 필요한 DLL들의 코드와 데이터를 주소공간에 로드한다.
다음으로 프로세스는 프라이머리 스레드를 생성하기 위해서 스레드 커널 오브젝트를 생성한다. (사용 카운트는 1) 프로세스 커널 오브젝트와 마찬가지로 스레드 커널 오브젝트는 운영체제가 스레드를 관리하기 위해 사용하는 자료구조이다. 프라이머리 스레드는 C/C++ 런타임 Startup 코드의 실행과 더불어 시작되고, 궁극적으로 WinMain?, wWinMain?, main, wmain 함수중에 하나를 호출한다.
시스템이 성공적으로 새로운 프로세스와 프라이머리 스레드를 생성하다면 TRUE를 리턴한다.

  • CreateProcess?는 자식 프로세스가 완전히 초기화되기 전에 TRUE를 리턴한다. 이것은 운영체제가 필요한 모든 DLL을 로드하지 않은 시점에 리턴한다는 것이다. 만약 필요한 DLL이 로드될 수 없거나 제대로 초기화되지 않다면 자식 프로세스는 종료될 것이다. 그라나. CreateProcess?가 이미 TRUE를 리턴하였다면 부모 프로세스는 자식 프로세스의 초기화 문제를 알수 없다. WaitForInputIdle?()를 사용하여 프로세스가 사용 정상적으로 생성되었는지 확인할 수 있다.

  • pszCommandLine?의 프로토타입은 PTSTR이어야 한다. 기본 문자열은 Visual C++ 6.0 에서 상수로 취급된다. 비주얼 C++의 /Gf 컴파일러 스위치는 실행 파일내의 문자열들을 모두 모아서 관리하고 또 중복되는 동일한 문자열들은 한 카피의 문자열만 가지고 있도록 한다. /GF 스위치는 /Gf와 동일하되 문자열 풀을 읽기전용 메모리에 배치하게 한다. 이는 해당 메모리를 스와핑할 필요가 없게 한다. 대신 시스템은 실행파일이 로드된 위치에서 문자열의 위치를 찾아 연결시킨다. (비주얼 스튜디오의 디버그 모드에서 편집후 계속 디버깅을 할수 있도록 해주는 /ZI 스위치는 /GF 스위치의 동작을 포함한다. 따라서 Project Settings 에서는 컴파일러 스위치에 나타나지 않는다.) 개발자가 할 수 있는 최상의 방법은 /GF 컴파일러 스위치와 임시 버퍼를 사용해 문자열을 복사해서 사용하는 것이다.

pszApplicationName?에는 새로운 프로세스로 생성할 실행 파일의 이름과 확장자를 정확하게 명시한다. 보통은 NULL로 설정하고 밑의 인자를 설정하는게 낫다.
문자열에 결로가 명시되어 있지 않으면 현재 디렉토리를 검사하고 찾지 못하면 바로 실패를 리턴한다.

pszCommandLine?에는 새로운 프로세스로 전달할 실행파일이름을 포함한 전체 커맨드 라인 문자열을 명시한다. pszApplicationName?이 NULL이 아니면 첫번째 인자는 무시된다. 반드시 const 형이 아니어야 한다. 경로를 포함하지 않고 실행 파일만 있을 때 실행파일은 다음과 같은 순서로 찾는다.
1. 호출하는 프로세스의 .exe 파일이 들어있는 디렉터리
2. 호출하는 프로세스의 현재 디렉터리
3. 윈도우즈 시스템 디렉터리
4. 윈도우즈 디렉터리
5. 환경 변수 PATH에 나열된 디렉터리

psaProcess, psaThread 와 bInheritHandles?
새로운 프로세스를 생성하기 위해서는 시스템은 프로세스 커널 오브젝트와 스레드 커널 오브젝트(프로세스의 프라이머리 스레드)를 반드시 생성해야 한다.
psaProcess, psaThread는 각각 프로세스 커널 오브젝트와 스레드 커널 오브젝트에 보안속성을 명시한다. NULL 값을 전달하면, 시스템은 오브젝트에게 디폴트 보안 속성을 적용한다.
bInheritHandles?는 CreateProcess?를 호출하는 현재 프로세스의 상속 가능한 핸들을 받을 것인지 여부를 판단한다.

fdwCreate
새로운 프로세스가 생성되는 방법을 제시하는 플래그를 설정한다. OR 연산을 통하여 여러 개의 플래그를 결합할 수 있다.

플래그 설명
DEBUG_PROCESS 부모 프로세스인 디버거는 현재 생성하는 자식 프로세스뿐만 아니라 자식 프로세스가 미래에 생성할 모든 손자 프로세스까지 디버그 할 것임을 시스템에게 알려주는 플래그이다.
DEBUG_ONLY_THIS_PROCESS 부모 프로세스인 디버거는 현재 생성하는 자식 프로세스에서 발생하는 이벤트만 전달받는다. 나중에 자식 프로세스가 손자 프로세스를 생성한다 하더라도, 디버거는 새로운 손자 프로세스들 상에서 발생하는 이벤트들에 대해서는 전달 받지 못한다.
CREATE_SUSPENDED 새로운 프로세스를 생성하되, 프라이머리 스레드를 서스펜드 시키고자 할 때 사용된다. 부모프로세스가 자식프로세스에 대해 실행코드를 실행하기 전에 추가적인 작업이 필요할 경우 사용된다. 자식 프로세스에 추가적인 작업이 완료되면 ResumeThread?()를 호출하여 프로세스가 다시 시작하도록 한다.
DETACHED_PROCESS 시스템이 생성된 CUI 기반 자식 프로세스가 부모의 콘솔윈도우에 접근하는 것을 막고, 자식 프로세스의 출력을 새로운 콘솔 윈도우에게 보내도록 한다. 디폴트는 자식, 부모 둘다 CUI 기반일 때 부모의 콘솔에 자식의 내용을 출력하게 된다. (커맨드 쉘에서 실행시킬때 하나의 쉘을 공유하는 것으로 쉽게 생각할 수 있다.
CREATE_NEW_CONSOLE 새로운 프로세스를 위해서 새로운 콘솔 윈도우를 생성하도록 한다. DETACHED_PROCESS를 함께 명시하면 에러가 발생한다.
CREATE_NO_WINDOW 콘솔 윈도우를 생성하지 않도록 한다.
CREATE_NEW_PROCESS_GROUP 사용자가 Ctrl+C나 Ctrl+Break를 누를 때, 이러한 사항을 알아야 할 프로세스 목록을 변경한다. 이러한 키 조합이 입력되면, 시스템은 프로세스 그룹 내에 있는 모든 프로세스들에게 사용자가 현재 작업을 멈추기를 원한다는 것을 알려 준다.. 새로운 CUI기반 프로세스를 생성할 때 이 플래그를 명시하면 새로운 프로세스 그룹을 생성한다. 그룹내의 어떤 프로세스가 액티브한 상황에서 사용자가 Ctrl+C 나 Ctrl+Break를 입력하면, 시스템은 사용자의 요청을 이 프로세스가 속한 그룹 내의 프로세스들에게만 알린다.
CREATE_DEFAULT_ERROR_MODE 새롭게 생성되는 자식 프로세스가 부모 프로세스로 부터 에러모드를 상속 받지 않도록 한다.
CREATE_SEPARATE_WOW_VDM 윈도우즈 2000에서 16비트 윈도우즈 애플리케이션을 실행할 때 유용&#54623;. 시스템은 별도의 가상 DOS 머신(VDM)을 생성하고 16비트 애플리케이션을 VDM에서 실행한다. 디폴트로 모든 16비트 애플리케이션은 하나의 VDM을 공유한다. 분리된 VDM의 장점은 각자의 입력큐가 있어 하나의 애플리케이션이 임시적으로 멈추어도, 분리된 VDM에서 실행되는 다른 애플리케이션은 계속 수행이 가능하다.
CREATE_SHARED_WOW_VDM 모든 16비트 애플리케이션은 디폴트로 하나의 VDM에서 실행된다. HKEY_LOCAL_MACHINE\System\CurrentControlSet?\Control\WOW 에 설정된 DefaultSeparate?를 VDM값을 yes/no로 ㅜ저하여 디폴트 동작을 변경할 수 있다.
CREATE_UNICODE_ENVIRONMENT 자식 프로세스 환경 블록이 유니코드 문자를 가지도록 한다. 디폴트는 ANSI 문자열 구성이다.
CREATE_FORCEDOS 시스템이 16비트 OS/2 애플리케이션에 임베드 되어 있는 MS-DOS 애플리케이션을 실행하도록 한다.
CREATE_BREAKEAWAY_FROM_JOB Job 내의 어떤 프로세스가 소속된 Job과 관련이 없는 새로운 프로세스를 생성할 수 있도록 한다. (5장에 자세한 내용)

fdwCreate 파라미터에는 우선 순위도 지정할 수 있다. 마찬가지로 OR 연산으로 결합한다.
우선순위 플래그 식별자
유후 상태 IDLE_PRIORITY_CLASS
보통 미만 BELOW_NORMAL_PRIORITY_CLASS
보통 NORMAL_PRIORITY_CLASS
보통 초과 ABOVE_NORMAL_PRIORITY_CLASS
높음 HIGH_PRIORITY_CLASS
실시간 REALTIME_PRIORITY_CLASS

pvEnviroment
환경문자열을 가지고 있는 메모리 블록을 가리킨다.

PVOID GetEnvironmentStrings(); 
CreateProcess
?()를 호출하는 프로세스가 환경 문자열에 대한 데이터의 블록의 주소를 리턴한다.
pvEnviroment에 NULL을 전달한 경우 GeEnviromentStrings?()의 반환값을 대입하는 것과 완전히 동일하다.

BOOL FreeEnviromentStrings(PTSTR pszEnviromentBlock); 
메모리 블록을 더 이상 사용하지 않는다면 해제시켜 준다.

pszCurDir?
현재 드라이브와 디렉터리를 설정해준다.
NULL이면 새로운 프로세스의 현재 디렉터리는 호출하는 프로세스의 현재 디렉터리를 받는다.

psiStartInfo?
typedef struct _STARTUPINFO { 
 DWORD cb; 
 PSTR lpReserved; 
 PSTR lpDesktopl 
 PSTR lpTitle; 
 DWORD dwX; 
 DWORD dwY; 
 DWORD dwXSize; 
 DWORD dwYSize; 
 DWORD dwXCountChars; 
 DWORD dwYCountChars; 
 DWORD dwFillAttribute; 
 DWORD dwFlags; 
 WORD wShowWindow; 
 WORD cbReserved2; 
 PBYTE lpReserved2; 
 HANDLE hStdInput; 
 HANDLE hStdOutput; 
 HANDLE hStdError; 
} STARTUPINFO, *LPSTARTUPINFO; 

윈도우즈는 새로운 프로세스를 생성할 때 이 구조체의 멤버들을 사용한다. 디폴트로 사용하기 위해서는 최소한 구조체의 모든 멤버를 0으로 설정해야 하고, 멤버 cb에는 이 구조체의 크기를 설정해야 한다.

STARTUPINFO si = {sizeof(si) }; 
... 
CreateProcess(..., &si, ...); 

어떤 멤버는 오버랩드 윈도우를 생성할 때만 의미를 가지고, 어떤 멤버는 CUI 기반으로 입출력을 수행할 때만 의미를 가진다.
멤버 윈도우, 콘솔 또는 둘다 설명
cb 둘다 STARTUPINFO 구조체 자체의 바이트 단위의 크기를 명시한다.
lpReserved 둘다 예약됨 NULL로 초기화 되야함
lpDesktop 둘다 애플리케이션이 시작하는 데스크탑의 이름을 명시한다. 데스크탑이 존재하면, 새로운 프로세스는 명시된 데스크탑과 연결된다. 데스크탑이 존재하지 않거나 NULL인경우 프로세스는 현재의 데스크 탑과 연결된다.
lpTitle 콘솔 콘솔 윈도우의 타이틀을 명시한다. NULL이면 실행파일의 이름을 윈도우 타이틀로 사용한다.
dwX, dwY 둘다 스크린에서 애플리케이션의 윈도우가 위치할 픽셀단위의 x,y 좌표를 명시한다. CreateWindow?의 x,y 파라미터로 CW_USEDEFAULT를 사용하여 처음으로 오버랩드윈도우를 생성할 경우에만 사용된다. 콘솔윈도우에서는 콘솔 윈도우를 생성하는 애플리케이션에서는 콘솔 윈도우의 좌측 상단을 가리킨다.
dwXSize, dwYSize 둘다 애플리케이션 윈도우의 픽셀단위 너비와 높이를 명시한다. CreateWindow?의 nWidth, nHeight 파라미터로 CW_USEDEFAULT를 사용하여 처음으로 오버랩드윈도우를 생성할 경우에만 사용된다. 콘솔윈도우 경우 너비와 높이를 가리킨다.
dwXCountChars?, dwYCountChars? 콘솔 자식 콘솔 윈도우의 너비와 높이(문자단위)를 명시한다.
dwFillAttribute? 콘솔 자식 콘솔 윈도우가 사용할 텍스트와 백그라운드 색깔을 명시한다.
dwFlags 둘다 다음 절과 아래의 테이블에서 기술할 것이다.
wShowWindow? 윈도우 nCmdShow? 파라미터로 SW_SHOWDEFAULT를 전달하면서 처음으로 ShowWindow?를 호출하는 경우 적용된다.
cbReserved2 둘다 예약됨. 0으로 초기화 되어야 한다.
lpReserved2 둘다 예약됨. NULL로 초기화 되어야 한다.
hStdInput?, hStdOutput?, hStdError? 콘솔 콘솔 입출력 핸들들을 명시한다. 디폴트로 input은 키보드 버퍼, output, error는 콘솔 윈도우 버퍼를 가리킨다.

dwFlags 멤버는 자식 프로세스가 생성되는 방법을 변경하는 플래그들의 집합을 명시한다.
단순히 STARTUPINFO 구조체의 어떤 멤버가 적용해야할 정보를 가지고 있고 또 어떤 멤버들이 무시되는지 CreateProcess?에 알려준다.
플래그 설명
STARTF_USESIZE dwXSize, dwYSize 멤버를 사용한다.
STARTF_USESHOWWINDOW wShowWindow? 멤버를 사용한다.
STARTF_USEPOSITION dwX, dwY 멤버를 사용한다.
STARTF_USECOUNTCHARS dwXCountChars?, dwYCountChars? 멤버를 사용한다.
STARTF_USEFILLATTRIBUTE dwFillAttribute? 멤버를 사용한다.
STARTF_USESTDHANDLES hStdInput?, hStdOutput?, hStdError? 멤버를 사용한다.
STARTF_RUN_FULLSCREEN x86 컴퓨터에서 실행되는 애플리케이션이 전체화면모드로 시작하게 한다.
STARTF_FORCEOFFFEEDBACK CreateProcess?()는 커서를 스타트 글래스로 바꾸지 않는다.
STARTF_FORCEONFEEDBACK CreateProcess?()가 새로운 프로세스 초기화를 모니터하고 결과에 따라 커서를 변경하도록 한다. CreateProcess?()가 이 플래그와 함께 호출되면 스타트 글래스로 바뀐다. 만약 2초후에 새로운 프로세스가 GUI를 호출하지 않으면, 다시 화살표로 변경한다. 2초 내에 GUI를 호출하면 이번에는 윈도우를 생성할 때까지 기다린다. 이것은 GUI 호출후 5초이내에 이루어져야 한다. 윈도우가 생성되지 않으면 다시 화살표로 변경한다. 윈도우가 생성되면 다시 5초동안 GetMessage?()호출을 기다린다. 이를 완료하면 즉시 커서를 화살표로 재설정하고, 새로운 프로세스에 대한 모니터링을 중단한다.

부모 프로세스에 의해서 초기화된 STARTUPINFO 구조체의 복사본을 얻을 수 있다.

VOID GetStartupInfo(LPSTARTUPINFO pStartupInfo); 

ppiProcInfo?
typedef struct _PROCESS_INFORMATION { 
 HANDLE hProcess; 
 HANDLE hThread; 
 DWORD dwProcessId; 
 DWORD dwThreadId; 
} PROCESS_INFORMATION; 

CreateProcess?()를 통해 프로세스가 생성되면 그 스스로 사용카운트는 1이된다.
그리고 ppiProcInfo?를 통해 핸들을 반환되기 위해서 프로세스와 스레드 핸들을 오픈하여 반환한다. 결국 사용카운트는 2가된다.
자원의 누수를 막기 위해서는 필요없는 핸들을 바로바로 닫아 주어야 한다.

ProcessID와 ThreadID는 전역적인 풀을 공유한다. 문제는 핸들을 닫고 커널 오브젝트를 삭제한다면 다른 커널 오브젝트가 동일한 ID를 가지게 될 수 있다는 점이다.

때때로 부모 프로세스를 알아내야만 하는 애플리케이션을 개바해야할 경우가 있다. 첫번째로 알아야 할 것은 프로세스간의 부모와 자식관계는 오직 자식이 생성될 때에만 존재한다는 것이다.
초기버전 윈도우즈는 프로세스의 부모 프로세스를 찾을 수 있도록 해주는 함수를 제공하지 않았다. 그러나 지금은 ToolHelp? 함수와 PROCESSENTRY32 구조체를 통해서 부모 프로세스를 알아낼 수 있다. 이 구조체의 내부의 th32ParentProcess?ID를 통하여 부모 프로세스의 ID를 리턴한다.
프로세스나 스레드 ID가 재사용되지 않았다는 것을 보장하는 방법은 프로세스나 스레드의 커널 오브젝트가 소멸되지 않았음을 확신하는 것이다. 만약 새로운 프로세스나 스레드가 생성하는 경우라면, 이 오브젝트의 핸들을 닫지 않았음으로써 간단히 보장할 수 있다. 이때에는, 애플리케이션이 ID의 사용을 마쳤으면, 커널 오브젝트를 해제하기 위해서 CloseHandle? 함수를 호출하고 이젠 더 이상 프로세스 ID를 사용하는 것이 안전하지 않다는 것을 기억하고 있으면된다. 자식프로세스라면, 부모프로세스가 자신의 프로세스나 스레드 오브젝트 핸드을 복사하여 자식 프로세스가 상속받을 수 있도록 해두지 않았다면, 부모의 프로세스 ID와 스레드 ID의 유효성을 보증할 방법이 없다.

프로세스 종료

프로세스는 네가지 방법으로 종료시킬 수 있다.

프라이머리 스레드의 엔트리 포인트 함수 리턴
이 방법만이 모든 프라이머리 스레드의 자원이 안전하게 해제됨을 보장한다.
  • 이 스레드에 의해서 생성되는 모든 C++ 오브젝트들이 그들의 소멸자에 의해 적절하게 소멸될 것이다.
  • 운영체제는 스레드의 스택으로 사용되던 메모리를 적절하게 해제할 것이다.
  • 시스템은 프로세스의 exit 코드(프로세스 커널 오브젝트에서 유지되고 있는)를 엔트리 포인트 함수의 리턴 값으로 설정할 것이다.
  • 시스템은 프로세스 커널 오브젝트의 사용 카운트를 한 단계 감소시킬 것이다.

ExitProcess? 함수
VOID ExitProcess(UINT fuExitCode); 
이 함수는 프로세스를 종료시키고 프로세스의 exit 코드를 fuExitCode?의 값으로 설정한다.
프로세스 자체가 종료하므로 반환값은 없다. ExitProcess?() 다음에 존재하는 어떠한 코드도 실행되지 않는다.

프라이머리 스레드의 엔트리 포인트 함수(WinMain?, main등)가 리턴하면, 결국 이들을 호출한 C/C++ 런타임 Startup코드로 리턴하게 된다.
Startup코드는 프로세스가 사용했던 모든 C 런타임 자원을 적절하게 해제한다. 그리고 나서 C 런타임 Startup 코드는 엔트리 포인터 함수로부터 리턴된 값을 인자로 하여 직업 ExitProcess?()를 호출한다.

하지만 직접 ExitProcess?()를 호출하게 되면 C/C++ 런타임의 적절한 메모리 해제를 보장 받을 수 없게 된다.

  • ExitProcess?(), ExitThread?()를 명시적으로 호출하는 것은 애플리케이션이 스스로 적절한 정리를 할 수 없도록 만든다. ExitThread?()의 경우에는, 프로세스는 계속해서 실행되지만 메모리나 다른 자원들을 낭비할 수 있다.

TerminateProcess? 함수
BOOL TerminateProcess( 
 HANDLE hProcess, 
 UINT fuExitCode 
); 
ExitProcess?()와의 차이점은 어떠한 스레드라도 다른 프로세스 혹은 자신의 프로세스를 종료할 수 있게 만들어 준다.
프로세스를 종료시킬 방법이 없을 때에만 사용해야 한다.
종료될 프로세스는 죽게 될 것이라는 어떠한 통보도 결코 받을 수 없다. 애플리케이션은 제대로 마무리 할 수 없고, 죽는것 자체도 막을 방법이 없다.
이렇게 프로세스가 스스로 정리할 기회를 갖지 못하면, 프로세스가 사용하던 운영체제 자원들이 남아있지 않도록 마무리 정리를 운영체제가 전부 해야 한다.
프로세스가 사용하던 메모리를 해제하고, 열려있는 모든 파일을 닫고, 모든 커널 오브젝트에 대한 사용 카운트를 감소시키고, 모든 User 오브젝트와 GDI 오브젝트를 파괴해야 한다.
어쨋든 프로세스가 일단 종료되면, 시스템은 프로세스에 대한 어떠한 것도 남기지 않음을 보장한다.
프로세스가 실행된 적이 잇는지를 알아낼 방법조차 없다. 하지만 자원의 낭비가 없기를 보장하게 된다.

  • TerminateProcess?()는 비동기적이다. 즉, 어떤 프로세스를 종료하려고 함을 시스템에 알이지만, 해당 프로세스가 이 함수를 리턴하기 전에 종료한다고 보장할 수 없다. WaitForSingleObject?()함수나 이와 유사한 함수를 통해 어떤 프로세스가 종료되었다는 것을 확인할 수 있다. (프로세스 핸들이 종료되면 시그널 상태가 된다.)

프로세스 내의 모든 스레드가 종료될 때
모든 스레드가 ExitThread
?()를 호출했을 경우 또는 TerminateThread?()에 의해 종료될 경우, 운영체제는 프로세스의 주소공간을 더 이상 유지할 이유가 없다고 가정한다. 이렇게 프로세스에 스레드가 없음을 발견한다면 마지막 스레드의 리턴값을 종료코드로 설정한다.

프로세스가 종료될 때
프로세스가 종료될 때 다음과 같은 동작들이 진행된다.
1. 프로세스 내의 모든 스레드가 종료된다.
2. 프로세스가 할당한 모든 User, GDI 오브젝트가 제거되고, 모든 커널 오브젝트가 닫힌다.. (다른 프로세스가 핸들을 오픈하지 않았을 경우에는 파괴된다. 오픈된 경우는 파괴되지 않는다.)
3. 프로세스의 종료 코드가 STILL_ACTIVE로부터 ExitProcess
?()또는 TerminateProcess?()에 의해 전달된 종료 코드로 변경된다.
4. 프로세스 커널 오브젝트의 상태가 신호(signal)상태가 된다. 시스템의 다른 스레드들은 프로세스가 종료될 때까지 스스로 서스펜드상태가 될 수 있다.
5. 프로세스 커널 오브젝트의 사용 카운트가 1 감소한다. ( 프로세스가 실행되고 있다면 일단 사용 카운트는 최소한 1이다.)

프로젝트 커널 오브젝트는 프로세스가 살아 있는 한 항상 살아 있다. 나아가 프로세스보다 프로세스 커널 오브젝트는 더 오래 살아남을 수 도 있다.
프로세스가 종료될때, 시스템은 자동으로 커널 오브젝트의 사용 카운트를 감소시킨다. 이때 0이되면 파괴되지만, 다른 프로세스에서 프로세스 커널 오브젝트의 핸들이 열려진 상태라면 파괴되지 않는다.
이것은 버그가 아니라 특성이다. 프로세스 커널 오브젝트는 프로세스와 관련된 통계적인 정보를 유지하고 있다. 다른 프로세스에서 종료되었건 아니건 간에 프로세스 커널 오브젝트를 통해 종료코드와 같은 정보를 얻을 수 있다.

BOOL GetExitCodeProcess( 
 HANDLE hProcess, 
 PDWORD pdwExitCode 
); 
프로세스가 종료되지 않았다면 STILL_ACTIVE(0x103)을 pdwExitCode?에 넣어줄 것이고 종료되었다면, 실제의 종료 코드 값을 받을수 있다.
다른 프로세스의 통계 데이터에 더 이상 관심이 없을 경우 바로바로 CloseHandle?()을 통해 사용 카운트를 감소시켜야 한다는 점을 염두해 두자.

자식 프로세스

새로운 프로세스를 생성하여 작업을 시키고 결과를 기다려야 한다면, 다음과 유사한 코드를 작성할 것이다.
PROCESS_INFORMATION pi; 
DWORD dwExitCode; 
 
BOOL fSuccess = CreateProcess(..., &pi); 
if(fSuccess) 
{ 
 CloseHandle(pi.hThread); 
 WaitForSingleObject(pi.hProcess, INFINITE); 
 GetExitCodeProcess(pi.hProcess, &dwExitCode); 
 CloseHandle(pi.hProcess); 
} 
 
// 해당 종료 코드에 따른 작업 완료 처리를 해준다. 
... 
 

DWORD WaitForSingleObject(HANDLE hObject, DWORD dwTimeout); 

자식 프로세스를 분리해서 실행하기
탐색기와 같이 부모프로세스와 자식프로세스간의 통신이 필요 없는 경우
PROCESS_INFORMATION pi; 
DWORD dwExitCode; 
 
BOOL fSuccess = CreateProcess(..., &pi); 
if(fSuccess) 
{ 
 CloseHandle(pi.hThread); 
 CloseHandle(pi.hProcess); 
} 

시스템에서 실행되는 프로세스 열거

윈도우즈 API는 원래 실행되고 있는 프로세스를 나열하는 함수가 없었다.
그러나 윈도우 NT는 지속적으로 갱신하는 Performance Data라는 데이터베이스를 가지고 있다. 이 데이터베이스는 아주 많은 정보를 가지고 있으며, HKEY_PERFORMANCE_DATA 루트 키를 명시하여 RegQueryValueEx
?()와 같은 레지스트리 함수를 호출하면 정보를 얻을 수 있었다.
하지만 윈도우 95/98 호환성과 레이아웃이 매우 복잡하기 때문에 사용하기를 꺼려한다.

이 데이터베이스를 보다 쉽게 사용할 수 있도록 하기 위해서, 마이크로소프트는 이 함수들에 대한 Performance Data 도움말(PDH.dll에 되어 있음)을 만들었다.
위에서 설명했듯이 윈도우 95/98에서는 이러한 성능 데이터베이스를 제공하지 않는다. 대시네 ToolHelp? API 라는 프로세스 및 관련 정보를 나열해 주는 자체 함수 집합을 가지고 있다.

재미있게도 윈도우NT 팀은 toolHelp와 같은 함수를 NT에 포함시키지 않았다. 대신 프로세스를 열거하기 위해 자체적으로 Process Status 함수를 제작하였다. (PSAPI.dll에 포함)

매우 복잡하게 되었지만 윈도우 2000부터는 ToolHelp? 함수를 추가하였다.
결국, 이후부터는 ToolHelp? 함수를 통해 공통적인 소스 코드로 툴과 유틸리티를 작성할 수 있다.

프로세스 정보 예제 애플리케이션

생! 략!

Chapter |5| 작업(Jobs)

하나의 목적을 위해 수행되는 여러 개의 프로세스들을 그룹으로 묶어서 하나의 개체처럼 다룰 때 사용한다.
예를 들어, MS Visual Studio 프로젝트 빌드 메뉴를 선택하면 Cl.exe(compiler)를 자식 프로세서로 생성한다. Cl.exe 역시 컴파일의 각 단계를 수행키 위해 필요한 부가적인 자식 프로세스를 생성한다.
그런데 빌드가 완료되기전 수행을 멈추려면, Cl.exe와 부가적인 자식 프로세스를 종료할 수 있어야 한다.
하지만 부모, 자식 관계가 유지되지 않으므로 어렵다-_-

윈도우 2000은 새로운 Job 커널 오브젝트를 제공하여, 프로세스들을 그룹으로 묶어서 프로세스들이 할 수 있는 것들을 제한하는 sandbox를 만든다. (윈도우 98은 지원하지 않는다.)

HANDLE CreateJobObject( 
 PSECURITY_ATTRIBUTES psa, 
 PCTSTR pszName 
); 
첫번째 인자 psa는 새로운 Job 오브젝트에 설정할 보안정보와 시스템 핸들의 상속여부를 알리기 위한 것이다.
두번째 인자 pszName는 이름을 부여하여 OpenJobObject
?()를 통해 다른 프로세스에서도 접근할 수 있게 한다.

HANDLE OpenJobObject( 
 DWORD dwDesiredAccess, 
 BOOL bInheritHandle, 
 PCTSTR pszName 
); 

주의할 점은 Job 핸들을 닫는 것은 Job이 여전히 존재하여도, 모든 프로세스가 더 이상 그 Job을 접근할 수 없도록 만든다는 것이다. 다시 말하자면, CloseHandle?()로 핸들을 닫을 때, Job오브젝트에 실제로 삭제를 위한 표시를 해두었다가(그러므로 다른 프로세스에서의 접근도 막는다.), Job내의 모든 프로세스가 종료되어야만 비로소 자동으로 Job 오브젝트를 소멸시킨다.

Job내의 프로세스에 제한 가하기

  • Basic Limit, Extented Basic Limit는 Job 내의 프로세스들이 시스템의 자원을 독점하지 못하게 한다.
  • Basic UI Limit Restrictions는 Job 내의 프로세스가 사용자 인터페이스를 변경하지 못하게 한다.
  • Security Limit는 Job 내의 프로세스가 보안자원(파일, 레지스트리 키 등)에 접근하는 것을 막는다.

BOOL SetInformationJobObject( 
 HANDLE hJob, // 제한을 가하기 원하는 Job 
 JOBOBJECTINFOCLASS JobObjectInformationClass, // 적용하기를 원하는 제한의 형태 
 PVOID pJobObjectInformation, // 제한 설정을 담고 있는 데이터 구조체 
 DWORD cbJobObjectInformationLength // 데이터 구조체의 크기 
); 

제한 타입 두 번째 파라미터의 값 세 번째 파라미터의 구조
Basic Limit JobObjectBasicLimitInformation? JOBOBJECT_BASIC_LIMIT_INFORMATION
Extended Basic Limit JobObjectExtendedLimitInformation? JOBOBJECT_EXTENDED_LIMIT_INFORMATION
Basic UI Restrictions JobObject?UIRestrictions JOBOBJECT_BASIC_UI_RESTRICTIONS
Basic Security Limit JobObjectSecurityLimitInformation? JOBOBJECT_SECURITY_LIMIT_INFORMATION

JOBOBJECT_BASIC_LIMIT_INFORMATION
typedef struct _JOBOBJECT_BASIC_LIMIT_INFORMATION { 
 LARGE_INTEGER PerProcessUserTimeLimit; 
 LARGE_INTEGER PerJobUserTimeLimit; 
 DWORD LimitFlags; 
 DWORD MinimumWorkingSetSize; 
 DWORD MaximumWorkingSetSize; 
 DWORD ActiveProcessLimit; 
 DWORD Affinity; 
 DWORD PriorityClass; 
 DWORD SchedulingClass; 
}*PJOBOBJECT_BASIC_LIMIT_INFORMATION, JOBOBJECT_BASIC_LIMIT_INFORMATION; 
멤버 설명 주의해야할 점
PerProcessUserTimeLimit? 100ns 간격 안에서 각 프로세스에 할당된 최대 사용자모드 타임을 명기한다. 시스템은 할당된 시간보다 많이 사용하는 프로세스를 자동적으로 중단한다. 이 제한을 설정하려면 LimitFlags? 멤버의 JOB_OBJECT_LIMIT_PROCESS_TIME 플래그를 명기한다.
PerJobUserTimeLimit? 100ns 간격 안에서 얼마나 많은 사용자 모드 타임을 Job의 프로세스들이 사용할 수 있는지 명기한다. 기본으로 시스템은 이 타임 제한에 도달하면 자동적으로 모든 프로세스들을 중단한다. 독자는 job이 실행중일 때 정기적으로 이 값을 변화시킬 수 있다.
LimitFlags? Job에 어떤 제한을 적용할지 가리킨다. 보다 많은 정보를 위해서는 이 테이블 다음에 나오는 절을 참조하라.
MinimumWorkingSetSize?/MaximumWorkingSetSize? 각 프로세스를 위한 최대 / 최소작업 셋 크기를 명시한다. Job에 있는 모든 프로세스들에게 적용되는 것은 아니다. 보통 하나의 프로세스의 작업 셋은 최대보다 높아질 수 있다. MaximumWorkingSetSize?를 셋하면 하드 제한을 한다. 한번 프로세스의 작업 셋이 이 제한에 도달하면 프로세스 페이지는 그에 대항한다. 개인 프로세스가 호출한 SetProcessWorkingSetSize?는 프로세스가 작업 셋을 비우려 하지 않으면 무시된다. 이 제한을 설정하기 위해서는 LimitFlags? 멤버의 JOB_OBJECT_ LIMIT_WORKINGSET플래그를 명기하면 된다.
ActiveProcessLimit? Job에서 동시에 실행될 수 있는 프로세스의 최대수를 명기한다. 이 제한을 넘으려고 하면 새로운 프로세스는 "not enough quota" 에러를 내며 중단된다. 이 제한을 설정하려면 LimitFlags? 멤버의 JOB_OBJECT_ LIMIT_ACTIVE_PROCESS플래그를 명기하면 된다.
Affinity 프로세스들을 실행할 수 있는 CPU들의 서브셋을 명기한다. 개인 프로세스들은 보다 쉽게 제한할 수 있다. 이 제한을 설정 하려면 LinitFlags? 멤버의 JOB_OBJECT_ LIMIT_AFFINITY플래그를 명기하면 된다.
PriorityClass? 모든 프로세스가 사용하는 우선순위 클래스를 명기한다. 프로세스가 SetPriorityClass?를 호출하면 그것이 실제적으로 실패일지라도 성공인 것처럼 리턴한다. 만약 프로세스가 GetPriorityClass?를 호출하면 함수는 프로세스의 실제 우선순위 클래스가 아닐지라도 프로세스가 우선순위 클래스를 설정한 것을 리턴한다. SetThreadPriority?는 일반 우선권을 넘는 스레드를 일으키는데 실패하지만 낮은 스레드의 우선권을 사용하는데 쓰일 수 있다.
SchedulingClass? Job에서 스레드에게 배정된 상대적 시간양을 명기한다. 값은 0부터 9까지 될 수 있다. 5가 기본값이다. 큰 값이 보다 높은 우선순위 이다. 이 제한을 설정하려면 LimitFlags?멤버의 JOB_OBJECT_LIMIT_SCHEDULING_CLASS 플래그를 명기하면 된다.

만약 CPU 사용 시간을 제한하고 싶다면
LimitFlags
?로 JOB_OBJECT_LIMIT_JOB_TIME을 두고 PerProcessUserTimeLimt?에 제한할 시간을 설정하고 SetInformationJobObject?()을 호출하면 된다.
그런데 설정 을 바꾸고 제한했던 동작에 추가로 다른 제한 사항(CPU 친화도 변경)을 하게 된다면
먼저 제한한 시간을 다시 설정함으로써 결국 원하던 CPU 사용 시간보다 더 사용하게 된다.

이럴 경우, JOB_OBJECT_LIMIT_RESERVE_JOB_TIME 플래그를 사용하면 된다.
이 플래그와 JOB_OBJECT_LIMIT_JOB_TIME 플래그는 상호 배타적이다.
JOB_OBJECT_LIMIT_RESERVE_JOB_TIME 플래그는 시간에 대한 제한을 변경하지 않고 나머지 제한을 변경하겠다는 의미이다.

마지막 LimitFlags?인 JOB_OBJET_LIMIT_DIE_ON_UNHANDLED_EXCEPTION 는 특별히 언급할 필요가 있다.
이 플래그는 Jbo과 관련된 각 프로세스들의 "다룰수 없는 예외(unhandled exception)" 다이얼로그 박스를 닫게 만든다. 이는 시스템이 Job 내의 프로세스들에게 SEM_NOGPFAULTERRORBOX 플래그를 전달하는 SetErrorMode?()를 호출함으로써 이루어진다. 이제 다룰수 없는 예외를 발생시키는 Job내의 프로세스는 어떠한 사용자 인터페이스도 보여주지 않고 즉시 종료된다. 이 플래그가 없다면 프로세스는 예외를 발생시킬 수 있고 또 종료되지 않아 시스템 자원을 낭비할 수도 있다.

JOBOBJECT_EXTENDED_LIMIT_INFORMATION
typedef struct _JOBOBJECT_EXTENDED_LIMIT_INFORMATION { 
 JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; 
 IO_COUNTERS IoInfo; 
 SIZE_T ProcessMemoryLimit; 
 SIZE_T JobMemoryLimit; 
 SIZE_T PeakProcessMemoryUsed; 
 SIZE_T PeakJobMemoryUsed; 
} JOBOBJECT_EXTENDED_LIMIT_INFORMATION, *PJOBOBJECT_EXTENDED_LIMIT_INFORMATION; 
이 구조체는 JOBOBJECT_BASIC_LIMIT_INFORMATION 을 포함하고 있다.
IO_COUNTERS는 예약되었다. 어떠한 방식으로든 접근해서는 안된다.
PeakProcessMemoryUsed
?와 PeakJobMemoryUsed?는 읽기전용으로, 각각 Job내의 하나의 프로세스가 사용할 수 있는 committed 기억 용량의 최대값을 프로그래머에게 알려준다.
ProcessMemoryLimit?과 JobMemoryLimit?은 각각 하나의 프로세스가 사용할 수 있는 committed 기억용량과 Job에 속한 프로세스가 사용할 수 있는 committed 기억용량을 제한한다.
이 제한을 사용하기 위해서는 LimitFlags?에 JOB_OBJECT_LIMIT_PROCESS_MEMORY와 JOB_OBJECT_LIMIT_JOB_MEMORY를 설정해야 한다.

JOBOBJECT_BASIC_UI_RESTRICTIONS
typedef struct _JOBOBJECT_BASIC_UI_RESTRICTIONS { 
 DWORD UIRestrictionsClass; 
} JOBOBJECT_BASIC_UI_RESTRICTIONS, *PJOBOBJECT_BASIC_UI_RESTRICTIONS;  

UIRestrictionsClass? 플래그 설명
JOB_OBJECT_UILIMIT_EXITWINDOWS 프로세스가 ExitWingowsEx? 함수를 통해 로그오프, 셧다운, 리부팅 또는 시스템이 꺼지는 것으로부터 방지한다.
JOB_OBJECT_UILIMIT_READCLIPBOARD 프로세스가 클립보드를 읽는 것을 방지한다.
JOB_OBJECT_UILIMIT_WRITECLIPBOARD 프로세스가 클립보드를 지우는 것을 방지한다.
JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS 프로세스가 SystemParametersInfo? 함수를 통해 시스템 파라미터를 변경시키는 것을 방지한다.
JOB_OBJECT_UILIMIT_DISPLAYSETTINGS 프로세스가 ChangeDisplaySetting? 함수를 통해 디스플레이 세팅을 변경시키는 것을 방지한다.
JOB_OBJECT_UILIMIT_GLOBALATOMS Job에게 그 자신의 전역 원자 테이블을 주고 job의 프로세스가 오직 job의 테이블만 접근하도록 제한 한다.
JOB_OBJECT_UILIMIT_DESKTOP 프로세스가 CreateDesktop? 또는 SwitchDesktop? 함수를 사용함으로써 데스크톱을 생성하거나 스위칭하는 것을 방지한다.
JOB_OBJECT_UILIMIT_HANDLES Job의 프로세스가 같은 job 외부 프로세스에 의해 생성된 HWND와 같은 USER 오브젝트를 사용하는 것을 방지 한다.

마지막 플래그인 JOB_OBJECT_UILIMIT_HANDLES는 흥미롭다.
이 제한은 Job 내의 어떠한 프로세스도 Job 오부의 프로세스가 만든 USER 오브젝트에 접근할 수 없도록 만든다.
(반대의 경우인 외부의 프로세스가 Job 내부 프로세스가 만든 USER오브젝트에 접근하는것은 언제든지 (플래그가 설정되었든 아니든) 상관이 없다.)

예는 Spy++을 Job 내부에 속하게 한다음 JOB_OBJECT_UILIMT_HANDLES로 설정하면 Spy++은 다른 윈도우를 찾지 못한다.
반대로 Spy++을 외부에 Job 내부에 메모장을 띄운후 JOB_OBJECT_UILIMIT_HANDLES을 설정하면 Spy++이 메모장을 찾는건 가능하다.

완벽한 제한을 가한 상태로 만드는 것은 쉽지만 보통의 경우 외부의 프로세스와 통신이 필요하다.
가장 쉬운 방법은 윈도우 메시지를 이용하는 것인데 Job 프로세스가 UI 핸들에 접근할 수 없다면, Job 내부의 프로세스는 Job 외부에 윈도우 메시지를 Send하거나 Post할 수 없다. 이 문제는 다음 API로 해결한다.

BOOL UserHandleGrantAccess( 
 HANDLE hUserObj, 
 HANDLE hJob, 
 BOOL fGrant 
); 
hUserObj
?는 유저 오브젝트. 거의 윈도우 핸들..
hJob은 권한을 부여할 Job 오브젝트
fGrant는 권한 부여 여부

JOB_OBJECT_SECURITY_LIMIT_INFORMATION
typedef struct _JOBOBJECT_SECURITY_LIMIT_INFORMATION { 
 DWORD SecurityLimitFlags; 
 HANDLE JobToken; 
 PTOKEN_GROUPS SidsToDisable; 
 PTOKEN_PRIVILEGES PrivilegesToDelete; 
 PTOKEN_GROUPS RestrictedSids; 
} JOBOBJECT_SECURITY_LIMIT_INFORMATION, *PJOBOBJECT_SECURITY_LIMIT_INFORMATION; 

보안 제한은 한번 부여하면 취소할 수 없다.

멤버 설명
SecurityLimitFlags? 관리자 접근을 금하거나 제한되지 않은 토큰 접근을 금하거나 특정한 접근 토큰을 강요하거나 어떤 보안 식별자(SIDs) 와 특권을 비활성화 시킬지를 가리킨다.
JobToken? Job에서 모든 프로세스가 사용하는 토큰에 접근한다.
SidsToDisable? 어떤 SID가 접근 체킹이 비활성화 되었는지 가리킨다.
PrivilegesToDelete? 어떤 특권을 접근 토큰에서 삭제할지를 가리킨다.
RestrictedSids? 접근 토큰에 추가되어야 할 SID의 집합을 가리킨다.

BOOL QueryInformationJobObject( 
 HANDLE hJob, 
 JOBOBJECTINFOCLASS JobObjectInformationClass, 
 PVOID pvJobObjectInformation, 
 DWORD cbJobObjectInformationLength,  
 PDWORD pdwReturnLength 
);  
지금까지 부여된 제한들에 대해 질의 할 경우. 함수를 호출해서 쉽게 질의할 수 있다.

알아 내기 위한 제한 종류(두번째 파라미터) 받아 오기 위한 구조체(세번째 파라미터)
JobObjectBasicAccountingInformation? JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
JobObjectBasicAndIoAccountingInformation? JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION
JobObjectBasicLimitInformation? JOBOBJECT_BASIC_LIMIT_INFORMATION
JobObjectBasicProcessIdList? JOBOBJECT_BASIC_PROCESS_ID_LIST
JobObjectBasic?UIRestrictions JOBOBJECT_BASIC_UI_RESTRICTIONS
JobObjectExtendedLimitInformation? JOBOBJECT_EXTENDED_LIMIT_INFORMATION
JobObjectSecurityLimitInformation? JOBOBJECT_SECURITY_LIMIT_INFORMATION

Job 내에 프로세스 배치하기

프로세스를 생성하고 Job 커널 오브젝트에 소속시키기 위해서는 프로세스를 바로 실행시켜서는 안된다.
CreateProcess
?()를 호출할 때, CREATE_SUSPENDED 플래그를 사용해 잠시 멈추어 둔다.

BOOL AssignProcessToJobObject( 
 HANDLE hJob,  
 HANDLE hProcess 
);  
프로세스를 Job 오브젝트에 소속시킨다.
반드시 다른 Job에 소속된 적이 없어야 되고 한번 소속되면 벗어날 수 없다-_-
소속된 프로세스가 자식 프로세스를 생성하면, 새로운 프로세스는 자동적으로 부모 프로세스가 소속된 Job에 소속된다.

하지만 자식 프로세스를 자동으로 소속되지 않게 하는 방법이 두가지 있다.
첫번째는 JOBOBJECT_BASIC_LIMIT_INFORMATION의 LimitFlags? 멤버에 JOB_OBJECT_BREAKWAY_OK 플래그를 설정하여 새롭게 생성된 프로세스가 Job외부에서 실행되도록 할 수 있다.
전제조건으로 CreateProcess?()를 호출할때 fdwCreate에 CREATE_BREAKWAY_FROM_JOB 플래그를 설정하고 호출해야 한다. 만약 JOB_OBJECT_BREAKWAY_OK 플래그가 설정되지 않은 상태에서 CreateProcess?()에 CREATE_BREAKWAY_FROM_JOB 플래그를 설정하고 호출하면, 프로세스 생성에 실패한다.
두번째는 JOBOBJECT_BASIC_LIMIT_INFORMATION의 LimitFlags? 멤버에 JOB_OBJECT_SILENT_BREAKWAY_OK 플래그를 설정하면 이후 CreateProcess?()를 호출할 때 부가적인 작업 없이 새로운 프로세스는 Job 외부 소속으로 생성된다.
이 방법은 Job 오브젝트가 생기기전에 프로세스들을 위한 방법이다.

프로세스를 Job 오브젝트에 소속시킨후, ResumeThread?()를 호출하여 프로세스를 실행시키면 된다.

Job 내의 모든 프로세스 종료

BOOL TerminateJobObject( 
 HANDLE hJob, 
 UINT uExitCode 
); 
Job에 소속된 프로세스의 종료코드를 uExitCode
?로 설정하면서 TerminateProcess?()를 호출하는 것과 비슷하다.

Job에 대한 통계 질의

기본적인 실행 통계 정보 알아내기
Job에 제한을 알아내기 위해 QueryInformationJobObject
?()를 사용했는데 Job에 대한 기본적인 실행 통계 정보도 같은 함수로 알아 낼 수 있다.
두번째 파라미터에 JobObjectBasicAccountingInformation?
세번째 파라미터로 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION 구조체의 주소를 전달하여 호출하면 된다.

typedef struct _JOBOBJECT_BASIC_ACCOUNTING_INFORMATION { 
   LARGE_INTEGER TotalUserTime; 
   LARGE_INTEGER TotalKernelTime; 
   LARGE_INTEGER ThisPeriodTotalUserTime; 
   LARGE_INTEGER ThisPeriodTotalKernelTime; 
   DWORD TotalPageFaultCount; 
   DWORD TotalProcesses; 
   DWORD ActiveProcesses; 
   DWORD TotalTerminatedProcesses; 
} JOBOBJECT_BASIC_ACCOUNTING_INFORMATION, *PJOBOBJECT_BASIC_ACCOUNTING_INFORMATION; 

멤버 설명
TotalUserTime? 얼마나 많은 사용자모드 CPU타임 프로세스가 job에서 사용되었는지 명기한다.
TotalKernelTime? 얼마나 많은 커널모드 CPU타임 프로세스가 job에서 사용되었는지 명기한다.
ThisPeriodTotalUserTime? 기본 제한 정보를 변경하기 위해 SetInformationJobObject?를 호출할 때와 JOB_OBJECT_LIMIT_PRESERVE_JOB_TIME 제한 플래그가 사용되지 않았을 때 이 값이 0으로 리셋된다는 것을 제외하고는 TotalUserTime?과 같다.
ThisPeriodTotalKernelTime? 이 값이 커널모드 타임을 보여준다는 것을 제외하고는 TimePeriodTotalUserTime?과 같다.
TotalPageFaultCount? Job 안의 프로세스가 발생시킨 페이지 실패의 총 수를 명기한다.
TotalProcesses? Job을 나눈 프로세스의 총 수를 명기한다.
ActiveProcesses? 현재적으로 job의 부분인 프로세스의 총 수를 명기한다.
TotalTerminatedProcesses? 할당된 CPU타임 제한을 넘어서 중단된 프로세스의 총 수를 명기한다.

  • 윈도우2000에는 User모드, Kernel모드 가 있다. Intel CPU는 4가지 특권 모드(privilege mode)를 지원하지만 Compaq ALPHA나 SiliconGraphics? MIPS 칩이 두가지만 지원하기 때문에 윈도우도 이것을 고려하여 만들어졌다.

기본적인 실행 통계 정보 + I/O 관련 정보 알아내기
위의 함수 호출에 대한 확장으로 Job에 대한 기본 실행 통계 정보와 I/O에 관한 실행 정보도 함께 질의할 수 있다.
두번째 파라미터에 JobObjectBasicAndIoAccountingInformation
? (위와 같다.)
세번째 파리미터에 JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION ( IO 가 추가되었다.)
typedef struct JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION { 
 JOBOBJECT_BASIC_ACCOUNTING_INFORMATION BasicInfo; 
 IO_COUNTERS IoInfo; 
} JOBOBJECT_BASIC_AND_IO_ACCOUNTING_INFORMATION; 

typedef struct _IO_COUNTERS { 
 ULONGLONG ReadOperationCount;  
 ULONGLONG WriteOperationCount; 
 ULONGLONG OtherOperationCount; 
 ULONGLONG ReadTransferCount; 
 ULONGLONG WriteTransferCount; 
 ULONGLONG OtherTransferCount; 
} IO_COUNTERS; 
OperationCount?는 연산의 갯수
TransferCount?는 모든 바이트의 수

참고로, Job이 아닌 프로세스의 IO 정보는

BOOL GetProcessIoCounters( 
 HANDLE hProcess,  
 PIO_COUNTERS pIoCounters 
); 
를 사용하여 알아낼 수 있다.(구조체는 같은 것을 쓴다.)

프로세스 ID 집합 알아내기
typedef struct _JOBOBJECT_BASIC_PROCESS_ID_LIST { 
 DWORD NumberOfAssignedProcesses; 
 DWORD NumberOfProcessIdsInList; 
 DWORD ProcessIdList[1]; 
} JOBOBJECT_BASIC_PROCESS_ID_LIST, *PJOBOBJECT_BASIC_PROCESS_ID_LIST;  
Job에 소속된 프로세스의 개수가 얼마인지 모르는 상태이므로 조금 복잡하다.

void EnumProcessIdsInJob(HANDLE hjob)  
{ 
   // I assume that there will never be more  
   // than 10 processes in this job. 
   #define MAX_PROCESS_IDS     10 
 
   // Calculate the number of bytes needed for structure & process IDs. 
   DWORD cb = sizeof(JOBOBJECT_BASIC_PROCESS_ID_LIST) + (MAX_PROCESS_IDS - 1) * sizeof(DWORD); 
   // -1는 구조체에서 하나 잡고 있기 때문.. 결국 최대 10개의 프로세스에 대한 ID를 구하겠다는 의미. 
 
   // Allocate the block of memory. 
   PJOBOBJECT_BASIC_PROCESS_ID_LIST pjobpil = _alloca(cb); // 스택에 할당 
 
   // Tell the function the maximum number of processes  
   // that we allocated space for.  
   pjobpil->NumberOfAssignedProcesses = MAX_PROCESS_IDS; 
 
   // Request the current set of process IDs. 
   QueryInformationJobObject(hjob, JobObjectBasicProcessIdList, pjobpil, cb, &cb); 
 
   // Enumerate the process IDs. 
   for (int x = 0; x < pjobpil->NumberOfProcessIdsInList; x++)  
   { 
      // Use pjobpil->ProcessIdList[x]... 
   } 
 
   // Since _alloca was used to allocate the memory,  
   // we don't need to free it here. 
} 
http://www.debuglab.com/knowledge/stackdynamic.html

이렇게 프로세스 ID를 알아냈으면 프로세스에 대한 더 자세한 통계정보는
Performance Data Helper 함수 라이브러리(PDH.dll)에 존재하는 함수를 통해 알아볼 수 있다.

Job에 대한 정보를 보기 위한 쉬운 방법으로 Microsoft Management Console (MMC) Performance Monitor Snap-In을 사용할 수도 있다.
(XP기준 제어판-관리도구-성능)

Job 통지

프로세스와 스레드 오브젝트가 실행이 끝나면 시그널드 상태가 되듯이 Job도 실행이 끝나면 시그널드 상태가 된다.
이런 방식으로 Job이 실행을 완료한 시점을 쉽게 알아 낼 수 있으리라 생각되지만-_-
MS는 Job에 할당된 시간이 만료될 때에 Job을 시그널드 상태로 만드는 것으로 선택하였다. 왜냐면 에러 상태를 확인할 수 있기 때문이다.

JOBOBJECT_ASSOCIATE_COMPLETION_PORT joacp; 
joacp.CompletionKey  = 1;   // Any value to uniquely identify this job 
joacp.CompletionPort = hIOCP;   // Handle of completion port that  
                                // receives notifications 
SetInformationJobObject(hJob, JobObjectAssociateCompletionPortInformation, 
   &joacp,  sizeof(jaocp)); 
IOCP를 생성 후, SetInformationJobObject
?()를 이용하여 Completion Port를 연결한다.

BOOL GetQueuedCompletionStatus( 
   HANDLE hIOCP,  
   PDWORD pNumBytesTransferred, // 이벤트 (밑의 표 참조) 
   PULONG_PTR pCompletionKey, // 위에서 설정한 Completion Key 
   POVERLAPPED *pOverlapped,  // 이벤트 종류에 따라 프로세스 ID 
   DWORD dwMilliseconds); 

이벤트 설명
JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO Job에서 아무런 프로세스도 실행되지 않고 있을 때 발생
JOB_OBJECT_MSG_END_OF_PROCESS_TIME 프로세스에 할당된 CPU 타임을 초과했을 때 발생. 프로세스는 종료되고 프로세스의 ID가 주어진다.
JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT Job에서 활동적 프로세스의 수를 넘으려고 했을 때 발생
JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT 프로세스의 제한을 넘어 메모리를 지정하려는 시도를 했을 때 발생. 프로세스의 ID가 주어진다.
JOB_OBJECT_MSG_JOB_MEMORY_LIMIT 프로세스가 Jobdml 제한을 넘어 메모리를 지정하려는 시도를 했을 때 발생. 프로세스의 ID가 주어진다.
JOB_OBJECT_MSG_NEW_PROCESS Job에 프로세스가 추가되었을 때 발생. 프로세스의 ID가 주어진다.
JOB_OBJECT_MSG_EXIT_PROCESS 프로세스가 종료되었을 때 발생한다. 프로세스의 ID가 주어진다.
JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS 프로세스가 처리되지 않은 예외상황 때문에 종료되었을 때 발생. 프로세스 ID가 주어진다.
JOB_OBJECT_MSG_END_OF_JOB_TIME Job에게 할당된 CPU타임을 초과했을 때 발생. 프로세스는 종료되지 않는다. 이것이 계속 실행되게 할 수 있고 새로운 타임 설정을 하거나 TerminateJobObject? 함수를 호출할 수도 있다.

디폴트 동작은 Job이 할당된 CPU 시간을 소진하면 Job내의 모든 프로세스들은 자동적으로 종료되고 JOB_OBJECT_MSG_END_OF_JOB_TIME통지는 전달되지 않도록 구성된다.
반대로 Job 오브젝트가 프로세스를 종료하지 못하게 하고 또 시간을 소진했음을 알리게끔 하려면..

JOBOBJECT_END_OF_JOB_TIME_INFORMATION joeojti; 
joeojti.EndOfJobTimeAction = JOB_OBJECT_POST_AT_END_OF_JOB; // Job의 프로세스 종료를 연기한다. 디폴트는 JOB_OBJECT_TERMINATE_AT_END_OF_JOB 이다. 
SetInformationJobObject(hJob, JobObjectEndOfJobTimeInformation, &joeojti, sizeof(joeojti));  

Chapter |6| 스레드의 기본

스레드의 구성
  • 커널 오브젝트 : 운영체제가 스레드를 관리하기 위해 사용. 시스템이 스레드의 통계 정보를 저장, 유지하는 장소
  • 스레드 스택 : 스레드가 코드를 실행할 때 필요한 모든 함수 파라미터들과 지역 변수들을 위한 메모리 공간.

프로세스는 CPU 스케쥴링 대상이 아니다. 단지 스레드를 담기 위한 컨테이너 이다.

프로세스는 스레드보다 더 많은 시스템 자원을 사용한다. 이유는 주소 공간 때문이다. 프로세스를 위한 가상 주소 공간을 생성하는 것은 많은 시스템 자원을 필요로 한다. 많은 레코드들을 시스템 내에서 유지해야 하는데, 이는 많은 메모리를 요구한다. 또한 .exe나 .dll 파일이 주소공간에 로드되어야 하기 때문에, 파일 자원 또한 필요하다.

스레드를 생성할 때

프로세스가 초기화 될때 항상 시스템은 프라이머리 스레드를 생성한다.
이 스레드는 C/C++ 런타임 라이브러리으 Startup 코드의 실행을 시작하고, 그 다음으로 엔트리 포인트 함수(main, wmain, WinMain
?, wWinMain?)을 호출한다. 엔트리 포인트 함수가 리턴하고 C/C++ 런타임 라이브러리 Startup 코드가 ExitProcess?()를 호출할 때 까지 계속 애플리케이션코드를 실행한다.

첫번째 스레드 함수 작성하기!

기본적인 스레드 함수의 형태
DWORD WINAPI ThreadFunc(PVOID pvParam){ 
   DWORD dwResult = 0;. 
    
   // 내용 
 
   return(dwResult); 
} 

스레드 함수는 수행을 마치고 리턴하게 되면, 스레드는 실행을 멈추고, 스택을 위한 메모리를 반환하고, 스레드 커널 오브젝트의 사용 카운트가 감소한다. 사용 카운트가 0이 되면, 스레드 커널 오브젝트는 제거된다. 반드시 스레드와 스레드 커널 오브젝트가 같은 시간에 종료되지 않는 다는 점을 유의해야한다.

스레드 함수 생성

HANDLE CreateThread( 
   PSECURITY_ATTRIBUTES psa, 
   DWORD cbStack, 
   PTHREAD_START_ROUTINE pfnStartAddr, 
   PVOID pvParam, 
   DWORD fdwCreate, 
   PDWORD pdwThreadID 
); 

C/C++ 코드에서는 절대로 CreateThread?()를 호출해서는 안된다. 대신 비주얼 C++ 런타임 라이브러리 함수 _beginthreadex를 사용해야만 한다. 마이크로소프트의 비주얼 C++ 컴파일러를 사용하지 않는다면, 다른 컴파일러 제작자는 그들만의 CreateThread?() 대안을 가지고 잇을 것이다. 이유는 차차!

psa
SECURITY_ATTRIBUTE 구조체이다.

cbStack

스레드가 스택으로 사용할 수 있는 주소 공간의 크기를 명시한다.
CreateProcess
?()가 어떤 프로세스를 시작할 때, 해당 프로세스의 프라이머리 스레드를 초기화하기 위해서 내부적으로 CreateThread?()를 호출한다. 이때 CreateProcess?()는 실행파일의 내부에 저장된 값을 cbStack 파라미터로 전달한다. 이 값은 링커의 /STACK 스위치를 사용해 제어할 수 있다.

/STACK:[reserve][,commit] 
 
reserve의 기본값은 1M 
commit의 기본값은 1페이지 

이 파라미터로 0을 전달하면 링커가 기록한 /STACK 스위치의 값을 사용하고 0이외의 값을 전달하면 지정된 용량만큼의 메모리 공간을 reserve후 commit한다.

pfnStartAdd? & pvParam
새로운 스레드에 대한 스레드 함수의 주소와 스레드 함수 파라미터를 가리킨다.

fdwCreate
스레드 생성을 제어하기 위한 부가적인 플래그이다.
둘중 하나인데,
0이면 스레드가 생성된 후 즉시 스케쥴되고
CREATE_SUSPENDED 이면 스레드를 생성하고 초기화 시켜놓지만 SUSPEND 상태로 시작한다.

pdwThreadID
새로운 시스템에 할당한 ID를 저장한다.

* 윈도우 95,98 에서 NULL 이외의 값을 전달하면 불법. 스레드는 생성되지 않는다.

스레드의 종료

스레드는 4가지 방법으로 종료시킬 수 잇다.
1. 스레드 함수가 리턴한다.
2. 스레드가 ExitThread
?()를 호출해 스스로 죽인다.
3. 같은 프로세스 또는 다른 프로세스 내의 스레드에서 TerminateThread?()를 호출한다.
4. 해당 스레드를 가지고 있는 프로세스를 종료한다.

스레드 함수의 리턴
가장 바람직하게 리소스가 적절히 정리되면서 종료된다.
1. 스레드 함수에서 생성한 모든 C++ 오브젝트는 소멸자에 의해서 제거될 것이다.
2. 운영체제는 스레드의 스택으로 사용된 메모리를 적절하게 해제할 것이다.
3. 시스템은 스레드의 exit 코드를 스레드 함수의 리턴 값으로 설정할 것이다.
4. 시스템은 스레드 커널 오브젝트의 사용 카운터를 감소시킬 것이다.

ExitThread?()
VOID ExitThread(DWORD dwExitCode); 
모든 운영체제 리소스를 정리하게 되지만 C/C++ 리소스를 정리되지 않는다.

앞서 보았던 _beginthreadex()와 같인 C/C++ 코드에서는 _endthreadex() 사용이 권장된다.

TerminateThread?()
정상적인 어플리케이션이라면 이 함수를 사용할 일은 없다!!

BOOL TerminateThread( 
 HANDLE hThread, 
 DWORD dwExitCode 
); 
호출한 스레드를 죽이는 ExitThread?()와는 달리 TerminateThread?()는 어떠한 스레드도 죽일 수 있다.

이 함수는 비동기적으로 동작한다. 해당 스레드가 종료되었음을 보장받기 위해서는 WaitForSingleObject?()와 같은 함수를 통하도록 한다.

리턴 또는 ExitThread?()를 통해 스레드가 종료될 경우, 스레드의 스택은 소멸된다. 그러나 TerminateThread?()로 종료될 경우 해당 프로세스가 종료될 때 까지 스레드의 스택을 소멸시키지 않는다. MS는 TerminateThread?()를 의도적으로 만들었는데 실행중인 스레드가 이미 종료된 스레드의 스택의 값을 참조하게 되면, 다른 스레드는 접근 위반을 일으키게 될 것이다. 그러나 TerminateThread?()를 사용하여 죽은 스레드의 스택을 메모리에 남겨둠으로써, 다른 스레드가 접근 위반 없이 죽은 스레드의 스택을 참조할 수 있게 만들었다.

부가적으로 DLL은 스레드가 종료될 때, 일반적으로 통지를 받는다. 그러나 스레드가 TerminateThread?()에 의해 강제로 죽게 되면, DLL은 이러한 통지를 받지 못한다. 따라서 적절한 정리를 할 수 없다.

프로세스가 종료될 때

ExitProcess?()나 TerminateProcess?()를 사용하여 프로세스를 종료시키면 모든 스레드를 강제 종료시킨다.
내부적으로는 존재하는 스레드들에 TerminateThread?()를 호출하는 것과 같다.
결국, 리소스가 완전히 정리됨을 보장하지 못한다.

스레드가 종료될 때

스레드가 종료될 때 다음과 같은 일이 일어난다.
* 스레드가 가지고 있는 모든 User 오브젝트의 핸들들이 해제된다. 윈도우즈의 대부분의 오브젝트는 해당 오브젝트를 생성한 프로세스가 가지게 된다. 하지만 윈도우와 훅은 스레드가 가진다. 스레드가 죽을 때, 시스템은 자동으로 윈도우객체를 파괴하고, 스레드가 생성하거나 초기화한 모든 훅을 제거한다. 다른 오브젝트들은 프로세스가 종료될때 제거된다.
* 스레드의 exit 코드를 STILL_ACTIVE에서 ExitThread
?()나 TerminateThread?()에 전달된 exit 코드로 변경한다.
* 스레드 커널 오브젝트의 상태가 시그널(signal) 상태로 바뀐다.
* 해당 스레드가 프로세스에서 마지막 액티브 스레드이면, 시스템은 해당 프로세스 역시 종료 시킨다.
* 스레드 커널 오브젝트의 사용 카운트를 하나 감소시킨다.

BOOL GetExitCodeThread( 
 HANDLE hThread, 
 PDWORD pdwExitCode 
); 
pdwExitCode?에슨 스레드의 exit 코드가 들어온다. 스레드가 아직 종료되지 않았다면 STILL_ACTIVE(0x103)을 리턴한다.

Some Thread Internals

스레드는 스레드 컨텍스트라 불리는, 독립적인 CPU 레지스터들의 집합을 가지고 있다. 컨텍스트는 스레드가 제일 최근에 실행될 때의 CPU 레지스터 값을 가지고 있다.
CONTEXT 구조체는 WinNT.h 헤더파일에 정의되어 있다. CONTEXT 구조체 자체는 스레드 커널 오브젝트 안에 위치한다.

인스트럭션 포인터(IP)와 스택 포인터 레지스터(SP)는 스레드 컨텍스트에서 두 개의 가장 중요한 레지스터이다. 이 두 개의 주소는 해당 스레드를 소유하고 있는 프로세스의 주소 공간의 메모리를 지칭한다. CONTEXT 내의 SP는 pfnStartAddr?가 기록되어 있는 스레드 스택의 주소로 설정된다. IP는 BaseThreadStart?()라는 문서화되지 않은 함수의 주소로 설정된다. 이 함수는 Kernel32.dll 모듈 내에 포함되어 있다.(CreateThread?()가 구현되어 있는 곳이기도 하다.)

VOID BaseThreadStart(PTHREAD_START_ROUTINE pfnStartAddr, PVOID pvParam) 
{ 
 __try  
 { 
  ExitThread((pfnStartAddr)(pvParam)); 
 } 
 __except(UnhandledExceptionFilter(GetExceptionInformation())) 
 { 
  ExitProcess(GetExceptionCode()); // 스레드가 처리못한 예외는 메세지박스로 보여주고 프로세스를 종료시킴.  
 } 
 // 여기는 결코 도달하지 않는다. ExitThread()나 ExitProcess()를 통해 함수 내에서 죽는다.반환값이 없는 이유는 스레드 스택자체가 사라지므로 반환값을 저장할 수 없기 때문이다. 
} 

스레드가 완전히 초기화된 후, 시스템은 CREATE_SUSPENDED 플래그가 전달되었는지 검사하고, 전달되지 않았다면 서스펜드 카운트를 0으로 감소시킨다.

새로운 스레드는 인자를 2개를 가지는데 일반적인 스레드 함수 형식은 아니다. 이것은 운영체제가 직접적으로 스레드 스택에 접근하여 값을 기록하였기 때문이다.
다만, 몇몇 CPU 아키텍쳐는 스택 대신 CPU 레지스터를 사용해서 파라미터를 전달한다는 점에 주의해야한다.

프로세스의 프라이머리 스레드가 초기화될 때 프라이머리 스레드의 IP는 BaseProcessStart?()라는 또 다른 문서화되지 않은 함수로 설정된다.

VOID BaseProcessStart(PPROCESS_START_ROUTINE pfnStartAddr) // 유일한 차이점은 pvParam이 없다. 
{ 
 __try  
 { 
  ExitThread((pfnStartAddr)()); // C/C++ 런타임 라이브러리 StartUp 코드를 호출한다. main,wmain,WinMain,wWinMain중의 하나가 된다. 
 } 
 __except(UnhandledExceptionFilter(GetExceptionInformation())) 
 { 
  ExitProcess(GetExceptionCode());   
 } 
} 

C/C++ 런타임 라이브러리 살펴보기

라이브러리 이름 설명
LibC.lib 싱글스레드 애플리케이션의 정적 링크 라이브러리(새로운 프로젝트를 생성할 때, 기본 라이브러리이다.)
LibCD.lib 싱글스레드 애플리케이션의 정적 링크 디버그 버전
LibCMt.lib 멀티스레드 애플리케이션의 정적 링크 릴리지 버전
LibCMt.lib 멀티스레드 애플리케이션의 정적 링크 디버그 버전
MSVCRt.lib MSVCRt.dll 라이브러리의 동적 링크 릴리즈 버전을 더함. 이 라이브러리는 싱글스레드와 멀티스레드 애플리케이션 둘 다 지원한다.
MSVCRtD.lib MSVCRt.dll 라이브러리의 동적 링크 디버그 버전을 더함. 이 라이브러리는 싱글스레드와 멀티스레드 애플리케이션 둘 다 지원한다.

싱글스레드 애플리케이션용 라이브러리가 필요한 이유는 처음 라이브러리가 만들어진 70년대에는 멀티스레드에 대한 개념이 없었기 때문이다.
표준 C/C++ 런타임 라이브러리가 멀티스레드 애플리케이션을 고려해서 애초에 설계되지 않아서 문제가 생기는 변수와 함수는
errno, _doserrno, strtok, _wcstok, strerror, _strerror, tmpnam, tmpfile, asctime, _wasctime, gmtime, _ecvt, fcvt 등이 있다.

결국 싱글스레드용 C/C++ 런타임은 자신만의 스레드의 데이터 블록안에서 작업을 수행하도록 해야 한다.
그래서 이런 것들이 고려된 스레드 생성 함수가 만들어졌다.

unsigned long _beginthreadex( 
 void *security,  
 unsigned stack_size,  
 unsigned (*start_address)(void *),  
 void *arglist,  
 unsigned initflag,  
 unsigned *thrdaddr 
); 
파라미터가 CreateThread
?()의 파라미터와 정확히 같지 않다. 그래서 매크로를 사용해서 편리하게 작업할 수 있다.
typedef unsigned (__stdcall *PTHREAD_START) (void *); 
 
#define chBEGINTHREADEX(psa, cbStack, pfnStartAddr, pvParam, fdwCreate, pdwThreadID)   \ 
      ((HANDLE) _beginthreadex(                     \ 
         (void *) (psa),                            \ 
         (unsigned) (cbStack),                      \ 
         (PTHREAD_START) (pfnStartAddr),            \ 
         (void *) (pvParam),                        \ 
         (unsigned) (fdwCreate),                    \ 
         (unsigned *) (pdwThreadID)))  
주의점은 _beginthreadex()는 오직 C/C++ 런타임 멀티스레드용 라이브러리에서만 존재한다는 점이다.

_beginthreadex()의 의사코드

unsigned long _ _cdecl _beginthreadex ( 
   void *psa, 
   unsigned cbStack, 
   unsigned (_ _stdcall * pfnStartAddr) (void *), 
   void * pvParam, 
   unsigned fdwCreate, 
   unsigned *pdwThreadID) { 
 
   _ptiddata ptd;         // Pointer to thread's data block 
   unsigned long thdl;    // Thread's handle 
 
   // Allocate data block for the new thread. 
   if ((ptd = _calloc_crt(1, sizeof(struct tiddata))) == NULL) 
      goto error_return; 
 
   // Initialize the data block. 
   initptd(ptd); 
 
   // Save the desired thread function and the parameter 
   // we want it to get in the data block. 
   ptd->_initaddr = (void *) pfnStartAddr; 
   ptd->_initarg = pvParam; 
 
   // Create the new thread. 
   thdl = (unsigned long) CreateThread(psa, cbStack, 
      _threadstartex, (PVOID) ptd, fdwCreate, pdwThreadID); 
   if (thdl == NULL) { 
      // Thread couldn't be created, cleanup and return failure. 
      goto error_return; 
   } 
 
   // Create created OK, return the handle. 
   return(thdl); 
 
error_return: 
   // Error: data block or thread couldn't be created. 
 
   _free_crt(ptd); 
   return((unsigned long)0L); 
} 
  • 각 스레드는 C/C++ 런타임 라이브러리의 힙에 할당한 각자의 tiddata 메모리 블록을 갖는다. (tiddata 구조체는 Mtdll.h 파일 내의 비주얼C++ 소스코드에 있다.)
  • tiddata 메모리 블록에는 스레드 함수 주소와 스레드 함수 파라미터 또한 저장된다.
  • CreateThread?()로 스레드를 생성할때 pfnStartAddr?이 호출되는 게 아니라 _threadstartex()가 실행되며 파라미터로 tiddata가 전달된다.
  • 하나라도 실패하면 NULL이 리턴. 성공하면 스레드 핸들이 리턴

struct _tiddata { 
   unsigned long   _tid;       /* thread ID */ 
 
   unsigned long   _thandle;   /* thread handle */ 
 
   int     _terrno;            /* errno value */ 
   unsigned long   _tdoserrno; /* _doserrno value */ 
   unsigned int    _fpds;      /* Floating Point data segment */ 
   unsigned long   _holdrand;  /* rand() seed value */ 
   char *      _token;         /* ptr to strtok() token */ 
#ifdef _WIN32 
   wchar_t *   _wtoken;        /* ptr to wcstok() token */ 
#endif  /* _WIN32 */ 
   unsigned char * _mtoken;    /* ptr to _mbstok() token */ 
 
   /* following pointers get malloc'd at runtime */ 
   char *      _errmsg;        /* ptr to strerror()/_strerror() buff */ 
   char *      _namebuf0;      /* ptr to tmpnam() buffer */ 
#ifdef _WIN32 
   wchar_t *   _wnamebuf0;     /* ptr to _wtmpnam() buffer */ 
#endif  /* _WIN32 */ 
   char *      _namebuf1;      /* ptr to tmpfile() buffer */ 
#ifdef _WIN32 
   wchar_t *   _wnamebuf1;     /* ptr to _wtmpfile() buffer */ 
#endif  /* _WIN32 */ 
   char *      _asctimebuf;    /* ptr to asctime() buffer */ 
#ifdef _WIN32 
   wchar_t *   _wasctimebuf;   /* ptr to _wasctime() buffer */ 
#endif  /* _WIN32 */ 
   void *      _gmtimebuf;     /* ptr to gmtime() structure */ 
   char *      _cvtbuf;        /* ptr to ecvt()/fcvt buffer */ 
 
   /* following fields are needed by _beginthread code */ 
   void *      _initaddr;      /* initial user thread address */ 
   void *      _initarg;       /* initial user thread argument */ 
 
   /* following three fields are needed to support signal handling and 
   * runtime errors */ 
   void *      _pxcptacttab;   /* ptr to exception-action table */ 
   void *      _tpxcptinfoptrs; /* ptr to exception info pointers */ 
   int         _tfpecode;      /* float point exception code */ 
 
   /* following field is needed by NLG routines */ 
   unsigned long   _NLG_dwCode; 
 
   /* 
   * Per-Thread data needed by C++ Exception Handling 
   */ 
   void *      _terminate;     /* terminate() routine */ 
   void *      _unexpected;    /* unexpected() routine */ 
   void *      _translator;    /* S.E. translator */ 
   void *      _curexception;  /* current exception */ 
   void *      _curcontext;    /* current exception context */ 
#if defined (_M_MRX000) 
   void *      _pFrameInfoChain; 
   void *      _pUnwindContext; 
   void *      _pExitContext; 
   int         _MipsPtdDelta; 
   int         _MipsPtdEpsilon; 
#elif defined (_M_PPC) 
   void *      _pExitContext; 
   void *      _pUnwindContext; 
   void *      _pFrameInfoChain; 
   int         _FrameInfo[6]; 
#endif  /* defined (_M_PPC) */ 
}; 
 
typedef struct _tiddata * _ptiddata; 

static unsigned long WINAPI threadstartex (void* ptd) { 
   // Note: ptd is the address of this thread's tiddata block. 
 
   // Associate the tiddata block with this thread. 
   TlsSetValue(_ _tlsindex, ptd); 
 
   // Save this thread ID in the tiddata block. 
   ((_ptiddata) ptd)->_tid = GetCurrentThreadId(); 
 
   // Initialize floating-point support (code not shown).  
 
   // Wrap desired thread function in SEH frame to  
   // handle run-time errors and signal support. 
   _ _try { 
      // Call desired thread function, passing it the desired parameter. 
      // Pass thread's exit code value to _endthreadex. 
      _endthreadex( 
         ( (unsigned (WINAPI *)(void *))(((_ptiddata)ptd)->_initaddr) ) 
            ( ((_ptiddata)ptd)->_initarg ) ) ; 
   } 
   _ _except(_XcptFilter(GetExceptionCode(), GetExceptionInformation())){ 
      // The C run-time's exception handler deals with run-time errors 
      // and signal support; we should never get it here. 
      _exit(GetExceptionCode()); 
   } 
 
   // We never get here; the thread dies in this function. 
   return(0L); 
} 

void _ _cdecl _endthreadex (unsigned retcode) { 
   _ptiddata ptd;         // Pointer to thread's data block 
 
   // Clean up floating-point support (code not shown). 
 
   // Get the address of this thread's tiddata block. 
   ptd = _getptd(); 
 
   // Free the tiddata block. 
   _freeptd(ptd); 
 
   // Terminate the thread. 
   ExitThread(retcode); 
} 

Gaining a Sense of One's Own Identity

자신의 스레드와 프로세스 에 접근하는 방법이다.
HANDLE GetCurrentProcess(); // -1 

HANDLE GetCurrentThread(); // -2 

이 함수는 호출하는 스레드의, 프로세스 혹은 스레드 커널 오브젝트에 대한 의사핸들을 리턴한다.
중요한 점은 사용카운트를 증가시키지 않는다는 점이다. 그러므로 CloseHandle
?()을 통해 핸들을 닫아줄 필요는 없다. 혹시나 CloseHandle?()을 호출하면 호출을 무시하고 FALSE를 리턴한다.

DWORD GetCurrentProcessId(); 
DWORD GetCurrentThreadId(); 
각각 프로세스 ID와 스레드 ID를 반환한다.

의사 핸들을 실제 핸들로 변환

DuplicateHandle?()을 사용한다.
HANDLE hThreadParent; 
 
DuplicateHandle( 
      GetCurrentProcess(),     // Handle of process that thread  
                               // pseudo-handle is relative to 
      GetCurrentThread(),      // Parent thread's pseudo-handle 
      GetCurrentProcess(),     // Handle of process that the new, real, 
                               // thread handle is relative to 
      &hThreadParent,          // Will receive the new, real, handle 
                               // identifying the parent thread 
      0,                       // Ignored due to DUPLICATE_SAME_ACCESS 
      FALSE,                   // New thread handle is not inheritable 
      DUPLICATE_SAME_ACCESS);  // New thread handle has same  
                               // access as pseudo-handle 

Chapter |7| 스레드 스케줄링, 우선순위, 친화도

윈도우는 선점형 멀티스레드 운영체제이다. 그래서 스레드 간의 스케쥴링에 대한 알고리즘이 필요하다.

컨텍스트 스위치(Context Switch)
스케쥴 할 수 있는 스레드 커널 오브젝트들 중에서 하나를 선택하고, 해당 스레드의 컨텍스트에 마지막으로 저장된 값을 CPU 레지스터에 로드한다.

퀀텀(Quantum)
윈도우의 스레드간의 컨텍스트 스위치가 일어날 시간 간격을 정할때 퀀텀이라는 단위를 사용한다.
CPU의 타이머 인터럽트는 각 CPU 마다 약간씩의 차이가 있다. (약 10~15ms)
윈2000프로는 6으로 윈2000서버는 36으로 설정된 퀀텀값을 가지고 있는데 CPU 타이머 인터럽트가 호출될때 마다 해당 스레드의 퀀텀값을 줄여가면서 스레드의 내용을 수행한다.
값이 0으로 떨어지면 다음 스레드를 스케쥴하게 된다.

결국, 윈도우는 어떤 이벤트가 발생한 후 일정 시간 주기 안에, 특정 스레드의 실행이 된다는 보장을 할 수 없다. (작은 시간이긴 하지만..)

스레드의 중지(suspending)와 재시작(resuming)

스레드에는 서스펜드 카운트가 있다. 만약 CreateThread?()나 CreateProcess?()에 CREATE_SUSPENDED 플래그를 인자로 전달한다면 서스펜드 카운트가 증가하고...
서스펜드 카운트가 0이 되지 않은 스레드는 스케쥴대상에서 제외된다.
서스펜드 카운트가 0일 경우, 스레드가 키보드 입력과 같은 특정 이벤트를 기다리지 않는 이상, 스레드는 스케쥴될 수 있는 상태가 된다.

DWORD ResumeThread(HANDLE hThread); 
성공적으로 수행되면, 줄어들기 이전의 서스펜드 카운트를 리턴한다. 실패를 하면 0xFFFFFFFF를 리턴한다.

DWORD SusependThread(HANDLE hThread); 
다른 스레드를 서스펜드 시킬수 있다. 마찬가지로 이전의 스레드 서스펜드 카운트를 리턴한다.
최대 MAXIMUM_SUSPEND_COUNT(WinNT.h 에서 127로 정의) 횟수만큼 서스펜드될 수 있다.

#include "Windows.h" 
 
DWORD WINAPI ThreadFunc(PVOID pvParam) 
{  
        DWORD dwResult = 0; 
 
        for(int i=0;i<20;i++) 
        { 
                printf("%d loop\n", i); 
                Sleep(10); 
        } 
 
        return(dwResult);  
}  
 
 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
         
        HANDLE hThread = CreateThread(NULL, 0, ThreadFunc, NULL, CREATE_SUSPENDED, NULL); 
 
 
        DWORD threadCount = ResumeThread(hThread); 
 
        printf("thread Count = %d\n", threadCount); 
 
        WaitForSingleObject(hThread,50000); 
        return 0; 
} 

프로세스의 서스펜드와 재시작

프로세스에 대한 서스펜드와 재시작의 개념은 존재하지 않는다.
윈도우에서는 프로세스가 스케쥴링대상이 아니기 때문이다.

어쩔수 없이 불완전한 Jeffrey의 SuspendProcess?()

VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) { 
 
   // Get the list of threads in the system.  
   HANDLE hSnapshot = CreateToolhelp32Snapshot( 
      TH32CS_SNAPTHREAD, dwProcessID); 
 
   if (hSnapshot != INVALID_HANDLE_VALUE) { 
 
      // Walk the list of threads. 
      THREADENTRY32 te = { sizeof(te) }; 
      BOOL fOk = Thread32First(hSnapshot, &te); 
      for (; fOk; fOk = Thread32Next(hSnapshot, &te)) { 
 
         // Is this thread in the desired process? 
         if (te.th32OwnerProcessID == dwProcessID) { 
 
            // Attempt to convert the thread ID into a handle. 
            HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, 
               FALSE, te.th32ThreadID); 
 
            if (hThread != NULL) { 
 
               // Suspend or resume the thread. 
               if (fSuspend) 
                  SuspendThread(hThread); 
               else 
                  ResumeThread(hThread); 
            } 
            CloseHandle(hThread); 
         } 
      } 
      CloseHandle(hSnapshot); 
   } 
} 

슬립

일정 시간 동안에 스케줄을 원하지 않음을 시스템에게 알리는 방법이다.
VOID Sleep(DWORD dwMillisecond); 

Sleep()을 호출한 스레드는 현재 남아있는 타임 슬라이스를 포기하고 명시된 밀리초만큼 스케줄할수 없는 상태로 만든다.
시간이 지난후 스케줄 가능한 상태로 바뀌지만 정확히 그 시간에 다시 스케줄된다는 보장이 없으므로 정확한 시간만큼 멈추는 것은 불가능하다.

Sleep(INFINITE);
무한대 슬립... 절대로 이 스레드가 스케줄될 수 없도록 한다.

Sleep(0);
현재 스레드의 남아잇는 타임슬라이스를 포기하고, 시스템이 다른 스레드를 스케줄 하도록한다. 하지만 우선순위가 높다면 다시 그 스레드를 스케줄될 수도 있다.

다른 스레드로 스위칭

BOOL SwitchToThread(); 
이 함수가 호출되면 시스템은 CPU 시간을 할당 받지 못하고 있는 스레드가 존재하는지를 검사한다. 이런 스레드가 없으면 SwitchToThread
?()는 즉시 리턴한다. 이런 스레드가 있다면 starving 스레드는 한 개의 퀀텀 시간동안 실행하게 되고, 이 후 시스템 스케줄러는 평소대로 동작한다.

SwitchToThread?()를 호출하였을 때 어떠한 스레드도 실행할 수 없으면, 이 함수는 FALSE를 리턴한다. 그렇지 않으면 0이 아닌 값을 리턴한다.

Sleep()와 매우 유사한데 다른 점은 SwitchToThread?()는 낮은 우선순위의 스레드를 실행하는 반면, Sleep()은 높은 우선순위의 스레드가 없다면, 낮은 우선순위의 스레드가 starving 상황에서도, 호출한 스레드를 즉시 다시 스케줄할 수 있다는 점이다.

스레드의 실행 시간

// Get the current time (start time). 
DWORD dwStartTime = GetTickCount(); 
 
// Perform complex algorithm here. 
 
// Subtract start time from current time to get duration. 
DWORD dwElapsedTime = GetTickCount() - dwStartTime; 
고전적인 방법은 최초 시간과 알고리즘 실행후 시간을 비교하여 수행시간을 판단한다.
하지만 윈도우는 실시간 운영체제가 아니므로 언제든지 컨텍스트 스위치가 일어날 수 있다는 점이다.
그렇기 때문에 정확한 시간을 판단하는 새로운 방법은 스레드를 생성해 스레드의 실행시간을 판단하는 방법이다.

BOOL GetThreadTimes( 
   HANDLE hThread, 
   PFILETIME pftCreationTime,  
   PFILETIME pftExitTime, 
   PFILETIME pftKernelTime,    
   PFILETIME pftUserTime 
); 

시간 값 의미
Creation time 영국 그리니치 시간 기준으로 1601년 1월 1일 자정부터 스레드가 생성된 시간까지의 시간을 100 나노초(nanosecond)로 표현한 절대값
Exit time 영국 그리니치 시간 기준으로 1601년 1월 1일 자정부터 스레드가 끝나는 시간까지의 시간을 100나노초로 표현한 절대값. 스레드가 계속 실행되면 Exit time은 정의되지 않는다.
Kernel time 스레드가 운영체제 코드를 실행하면서 얼마나 많은 CPU 시간을 사용하는지를 100 나노초로 나타낸 상대값.
User time 스레드가 애플리케이션 코드를 실행하면서 얼마나 많은 CPU 시간을 사용하는지를 100나노초로 나타낸 상대값.

_ _int64 FileTimeToQuadWord (PFILETIME pft) { 
   return(Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime); 
} 
 
 
void PerformLongOperation () { 
 
   FILETIME ftKernelTimeStart, ftKernelTimeEnd; 
   FILETIME ftUserTimeStart,   ftUserTimeEnd; 
   FILETIME ftDummy; 
   _ _int64 qwKernelTimeElapsed, qwUserTimeElapsed,  
      qwTotalTimeElapsed; 
 
   // Get starting times. 
   GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, 
      &ftKernelTimeStart, &ftUserTimeStart); 
 
 
   // Perform complex algorithm here. 
 
 
   // Get ending times. 
   GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy, 
      &ftKernelTimeEnd, &ftUserTimeEnd); 
 
   // Get the elapsed kernel and user times by converting the start  
   // and end times from FILETIMEs to quad words, and then subtract  
   // the start times from the end times. 
   qwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) -  
      FileTimeToQuadWord(&ftKernelTimeStart); 
 
   qwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) -  
      FileTimeToQuadWord(&ftUserTimeStart); 
 
   // Get total time duration by adding the kernel and user times. 
   qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed; 
 
   // qwTotalTimeElapsed이 최종 스레드 실행 시간 
} 

BOOL GetProcessTimes( 
   HANDLE hProcess, 
   PFILETIME pftCreationTime,  
   PFILETIME pftExitTime, 
   PFILETIME pftKernelTime,    
   PFILETIME pftUserTime 
); 
프로세스 시간을 가져온다. 프로세스에 존재하는 모든 스레드 시간의 합이다.

  • 윈도우98에서는 신뢰성있는 스레드 사용 시간을 판단할 수 있는 방법이 없다.

CONTEXT 다루기

CONTEXT 구조체는 프로세서마다 고유한 레지스터 데이터를 가지고 있다. 시스템은 다양한 내부 연산을 수행하기 위해 이러한 CONTEXT 구조체를 사용한다. 현재로는, Intel, MIPS, Alpha, PowerPC 프로세서들에 대한 CONTEXT 구조체들이 정으되어 있다. 이 구조체는 WinNT.h 헤더 파일에서 참조할 수 잇다.

윈도우에서 유일하게 프로세서마다 다르게 정의된 구조체이다.
x86 프로세서는 Eax, Ebx, Ecx, Edx 등이 있으며 Alpha 프로세서의 경우 IntV0
?, IntT0?, IntT1?, IntS0?, IntRa?, IntZero? 등이 있다.

typedef struct _CONTEXT { 
 
    // 
    // The flags values within this flag control the contents of 
    // a CONTEXT record. 
    // 
    // If the context record is used as an input parameter, then 
    // for each portion of the context record controlled by a flag 
    // whose value is set, it is assumed that that portion of the 
    // context record contains valid context. If the context record 
    // is being used to modify a threads context, then only that 
    // portion of the threads context will be modified. 
    // 
    // If the context record is used as an IN OUT parameter to capture 
    // the context of a thread, then only those portions of the thread's 
    // context corresponding to set flags will be returned. 
    // 
    // The context record is never used as an OUT only parameter. 
    // 
 
    DWORD ContextFlags; 
 
    // 
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is 
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT 
    // included in CONTEXT_FULL. 
    // 
 
    DWORD   Dr0; 
    DWORD   Dr1; 
    DWORD   Dr2; 
    DWORD   Dr3; 
    DWORD   Dr6; 
    DWORD   Dr7; 
 
    // 
    // This section is specified/returned if the 
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT. 
    // 
 
    FLOATING_SAVE_AREA FloatSave; 
 
    // 
    // This section is specified/returned if the 
    // ContextFlags word contians the flag CONTEXT_SEGMENTS. 
    // 
 
    DWORD   SegGs; 
    DWORD   SegFs; 
    DWORD   SegEs; 
    DWORD   SegDs; 
 
    // 
    // This section is specified/returned if the 
    // ContextFlags word contians the flag CONTEXT_INTEGER. 
    // 
 
    DWORD   Edi; 
    DWORD   Esi; 
    DWORD   Ebx; 
    DWORD   Edx; 
    DWORD   Ecx; 
    DWORD   Eax; 
 
    // 
    // This section is specified/returned if the 
    // ContextFlags word contians the flag CONTEXT_CONTROL. 
    // 
 
    DWORD   Ebp; 
    DWORD   Eip; 
    DWORD   SegCs;              // MUST BE SANITIZED 
    DWORD   EFlags;             // MUST BE SANITIZED 
    DWORD   Esp; 
    DWORD   SegSs; 
 
    // 
    // This section is specified/returned if the ContextFlags word 
    // contains the flag CONTEXT_EXTENDED_REGISTERS. 
    // The format and contexts are processor specific 
    // 
 
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; 
 
} CONTEXT; 

CONTEXT_CONTROL 은 명령 포인터(Instruction Pointer), 스택 포인터(Stack Pointer), 플래그, 함수 리턴 주소들과 같은, CPU 제어 레지스터를 포함한다.
(함수 호출시 x86 CPU의 경우 함수 리턴주소를 스택에 푸시하지만, Alpha CPU의 경우 리턴주소를 특정 레지스터에 넣는다.)

CONTEXT_INTERGER의 경우 CPU의 정수 레지스터를 담고 있다.

CONTEXT_FLOATING_POINT는 CPU의 부동 소수점 레지스터를 담고 있다.

CONTEXT_SEGMENTS는 CPU의 세그먼트 레지스터를 담고 있다. (오직 x86에서만)

CONTEXT_DEBUG_REGISTERS는 CPU의 디버그 레지스터를 담고 있다.

CONTEXT_EXTENDED_REGISTERS는 CPU의 확장 레지스터를 담고 있다.(오직 x86에서만)

윈도우즈에서는 스레드 커널 오브젝트의 내부에 있는, 현재의 CPU 레지스터 집합을 GetThreadContext?()를 통해 얻을 수 있다.

BOOL GetThreadContext( 
   HANDLE hThread,  
   PCONTEXT pContext 
); 
이 함수를 호출하기 위해서는 CONTEXT 구조체에 ContextFlags? 멤버에 얻어내고자 하는 레지스터 섹션을 명시하면, 함수는 해당 레지스터들의 값을 구조체의 해당 멤버에 채워준다.
사실 스레드 컨텍스트는 유저모드와 커널모드의 두 개의 CONTEXT를 가진다. GetThreadContext?()는 오직 유저모드 CONTEXT를 리턴할 수 있다.
함수 호출전 SuspendThread?()를 호출해야 하는데 이유는 GetThreadContext?()를 통해 CONTEXT를 가져오는 순간에도 다른 스레드로 컨텍스트 스위치가 일어날 수 있고, 또한 커널모드로 실행 중이라면 스레드를 아직 중지 시키지 못하더라도 유저 모드 CONTEXT는 안정화 되어 원하는 결과를 얻을 수 있다.

CONTEXT 구조체에 ContextFlags?에 넣을 수 있는 값
CONTEXT_CONTROL, CONTEXT_INTEGER, CONTEXT_FLOATING_POINT, CONTEXT_SEGMENTS, CONTEXT_FULL(x86의 경우 ~CONTROL|~INTEGER|~SEGMENTS, Alpha의 경우 ~CONTROL|~FLOATING~|~INTEGER)

CPU 타입에 따른 명령어포인터(IP)와 스택 포인터(SP)의 멤버들이다.
x86 CONTEXT.Eip CONTEXT.Esp
Alpha CONTEXT.Fir CONTEXT.IntSp?

BOOL SetThreadContext( 
   HANDLE hThread,  
   CONST CONTEXT *pContext 
); 
스레드 커널 오브젝트 내에 새로운 레지스터 값을 넣을 수 있다.

만약, 원격 스레드에 CONTEXT를 바꾸고자 한다면. 다룰수 없는 예외 메시지 박스가 나타나면 프로세스가 종료할 것이다.

자세한 내용은 24장에서 다룬다.

스레드 우선순위

윈도우의 스레드 0부터 31까지의 숫자로 우선순위를 받는다.

우선순위 31의 스레드가 실행할 수 있는 상태이면 우선순위가 그보다 낮은 스레드는 실행되지 못한다.
이렇게 디자인된 시스템에서, 낮은 우선순위 스레드는 실행될 기회가 없을 수도 있지만
대부분의 스레드는 어느 한 순간 스케줄 될 수 없는 상태가 된다. 예를 들어, 프로세스의 프라이머리 스레드가 GetMessage
?()를 호출하였을때, 시스템이 프라이머리
스레드의 메시지 큐에 기다리는 메시지가 없다는 것을 알게되면, 스레드를 서스펜드 시키고, 남아있는 타임 슬라이스를 포기하고, 즉시 CPU를 기다리는 다른 스레드에 할당한다.

시스템을 부팅시킬때 시스템은 제로 페이지 스레드(zero page thread)라 불리는 특별한 스레드를 생성한다.
이 스레드는 우선순위 0이 할당되고, 전체 시스템에서 유일하게 우선순위 0을 가지는 스레드이다.
제로 페이지 스레드는 다른 모든 스레드가 수행을 원치 않을 때, 시스템의 램에 존재하는 모든 프리 페이지를 0으로 초기화 하는 임무를 수행한다.

우선순위의 추상적 고찰

스케줄러의 동작은 완전히 문서화되어 있지 않다.
스케줄러의 특서으로 비롯되는 모든 장점을 애플리케이션이 취할 수 없도록 하였다.
스케줄러의 알고리즘이 수정되기 마련이기 때문에, 개발자들에게 방어적 코드를 작성하도록 하였다.

프로세스 우선순위 클래스 Idle Below Normal Normal Above Normal High Real-time
상대 스레드 우선순위
Time-critical 15 15 15 15 15 31
Highest 6 8 10 12 15 26
Above Normal 5 7 9 11 14 25
Normal 4 6 8 10 13 24
Below Normal 3 5 7 9 12 23
Lowest 2 4 6 8 11 22
Idle 1 1 1 1 1 16

우선순위 레벨0은 제로 페이지 스레드에 예약되어 있다.
우선순위 레벨17, 18, 19, 20, 21, 27, 28, 29, 30은 디바이스 드라이브 프로그램을 위해 예약되어 있다. 즉 유저 모드 애플리케이션에서는 사용할 수 없다.
또, Real-time은 우선순위 레벨16이하로 내려갈 수 없고 그외 우선순위 클래스는 레벨15이상으로 올라갈수 없다.

프로그래밍 우선순위

CreateProcess?()를 호출할 때, fdwCreate 파라미터에 원하는 우선순위 클래스를 전달하면 된다.

우선순위 클래스 기호 식별자
실시간 REALTIME_PRIORITY_CLASS
높음 HIGH_PRIORITY_CLASS
보통 초과 ABOVE_NORMAL_PRIORITY_CLASS
보통 NORMAL_PRIORITY_CLASS
보통 미만 BELOW_NORMAL_PRIORITY_CLASS
유휴 IDLE_PRIORITY_CLASS

이미 실행된 프로세스의 우선순위를 바꾸기 위해서는 SetPriorityClass?()를 호출해야 한다.

BOOL SetPriorityClass(HANDLE hProcess, DWORD fdwPriority); 

DWORD GetPriorityClass(HANDLE hProcess); 

커맨드 쉘에서 애플리케이션을 실행할때, 시작 우선순위를 변경할 수 있다.

C:\>START /LOW CALC.EXE 
옵션으로 /BELOWNORMAL, /NORMAL, /ABOVENORMAL, /HIGH, /REALTIME
98에는 없다.

윈도우 2000 이상은 작업관리자를 이용해서도 우선순위를 변경할 수 있다.

스레드의 우선순위를 변경하는 방법에서 CreateThread?()를 사용하는 방법은 존재하지 않는다.
이상하게도..

BOOL SetThreadPriority(HANDLE hThread, int nPriority); 

int GetThreadPriority(HANDLE hThread); 

상대 스레드 우선순위 기호 상수
시간 우선 THREAD_PRIORITY_TIME_CRITICAL
아주 높음 THREAD_PRIORITY_HIGHEST
보통 초과 THREAD_PRIORITY_ABOVE_NORMAL
보통 THREAD_PRIORITY_NORMAL
보통 미만 THREAD_PRIORITY_BELOW_NORMAL
아주 낮음 THREAD_PRIORITY_LOWEST
유휴 THREAD_PRIORITY_IDLE

스레드 우선순위 레벨의 동적 상승(boosting)

스레드의 우선순위는 동적으로 변경될 수 있다.
시스템은 스레드의 우선순위 레벨을 가끔 상승시킨다. 일반적으로 윈도우메시지, 디스크읽기, 몇몇 I/O 이벤트에 반응해야 할 때 우선순위 레벨을 높인다.

예를들어 High 우선순위 클래스 프로세스 내의 Normal 스레드 우선순위를 가지는 스레드는, 베이스 우선순위는 13이다.
사용자가 키보드 키를 누르면, 시스템은 WM_KEYDOWN 메시지를 스레드의 큐에 넣는다. 메시지가 스레드의 큐에 나타났기 때문에, 스레드는 이제 스케줄가능하다.
부가적으로 키보드 디바이스 드라이버는 시스템에게 임시로 스레드의 레벨을 올려주도록 요구한다. 따라서 스레드의 우선순위는 2가 늘어날 것이며,
실행시에는 우선순위 레벨은 15가 된다.
우선순위 15에서 하나의 타임슬라이스동안 스케줄된다. 할당된 타임 슬라이스를 모두 소모하면 다음 타임슬라이스동안 하나 낮춰서 14가 되도록 한다.
스레드의 세번째 타임 슬라이스는 우선순위 13이 된다. 스레드가 계속 추가적은 타임 슬라이스가 필요로하면, 애초의 베이스 우선순위인 13에서 실행된다.

스레드의 현재 우선순위 레벨은, 언제나 스레드의 베이스 우선순위 레벨 아래로는 절대로 내려가지 않는다.

시스템의 동적 상승을 허용하지 않기 위한 함수가 존재한다.

BOOL SetProcessPriorityBoost( 
   HANDLE hProcess,  
   BOOL DisablePriorityBoost 
); 
 
BOOL GetProcessPriorityBoost( 
   HANDLE hProcess,  
   PBOOL pDisablePriorityBoost 
); 
BOOL SetThreadPriorityBoost( 
   HANDLE hThread, 
   BOOL DisablePriorityBoost 
); 
 
BOOL GetThreadPriorityBoost( 
   HANDLE hThread,  
   PBOOL pDisablePriorityBoost 
); 

윈도우 98은 지원하지 않는다. 언제나 FALSE를 리턴한다.

Foreground 프로세스를 위한 스케줄러 조정(tweaking)

현재 활성화된 윈도우의 반응성을 향상 시키기 위해서 우선순위를 조정한다.
윈도우 2000에서는 foreground 프로세스 스레드에게, 일반적으로 받는 양보다 더 많은 시간 퀀텀을 할당한다.

선호도(Affinity)

디폴트로 스레드를 프로세서에 할당할때 윈도우 2000은 soft affinity를 사용한다. 이것은 모든 다른 인자가 같을 경우에는, 마지막으로 실행했던 프로세서 상에서 스레드가 실행하려고 시도함을 뜻한다.

시스템은 부팅할 때에 얼마나 많은 CPU 를 사용할 수 있는지를 파악한다. GetSystemInfo?()를 호출하여, 머신상의 CPU 개수를 질의할 수 있다.

한 프로세스 내의 스레드들이, 사용 가능한 CPU 들의 어떤 부분 집합 내에서 실행되도록 제한하려면 다음의 API를 사용한다.

BOOL SetProcessAffinityMask( 
 HANDLE hProcess, 
 DWORD_PTR dwProcessAffinityMask 
); 
 
BOOL GetProcessAffinityMask( 
 HANDLE hProcess, 
 PDWORD_PTR pdwProcessAffinityMask, // 이 값은 언제나 pdwSystemAffinityMask의 부분집합이다. 
 PDWORD_PTR pdwSystemAffinityMask 
); 

  • 윈도우즈98은 언제나 1개의 CPU 만을 사용한다.

특정 스레드를 대상으로 CPU의 집합을 제한하는 방법이다. Get~~류의 API는 없다.
DWORD_PTR SetThreadAffinityMask( 
 HANDLE hThread, 
 DWORD_PTR dwThreadAffinityMask 
); 

조금 더 유연한 방법으로
만약 CPU 1,2,3는 유휴상태이고 CPU 0이 특정 스레드로 인해 점유되었을 때
특정 CPU에서 실행시키고 싶지만, 다른 CPU를 사용할 수 있다면, 스레드를 해당 CPU로 이동할 수 있도록 해달라고 하는 방법이다.

DWORD SetThreadIdealProcessor( 
 HANDLE hThread, 
 DWORD dwIdealProcessor 
); 
dwIdealProcessor?는 지금까지 함수들과 다르게 비트 마스크가 아닌, 0~31까지의 정수이다.
WinNT.h에 정의된 MAXIMUM_PROCESSORS의 값을 전달해서, 스레드가 선호하는 CPU가 없음을 나타낼 수 있다.
리턴값은 이전의 이상적인 CPU를 리턴하거나, 이상적인 CPU를 가지고 있지 않으면 MAXIMUM_PROCESSORS를 리턴한다.

실행파일의 헤더 부분에 프로세스 Affinity를 설정할 수 있다. 하지만 이를 위한 링커 스위치가 없다.
그러나 다음과 같은 코드를 사용할 수 있다.

// Load the EXE into memory. 
PLOADED_IMAGE pLoadedImage = ImageLoad(szExeName, NULL); 
 
// Get the current load configuration information for the EXE. 
IMAGE_LOAD_CONFIG_DIRECTORY ilcd; 
GetImageConfigInformation(pLoadedImage, &ilcd); 
 
// Change the processor affinity mask. 
ilcd.ProcessAffinityMask = 0x00000003; // I desire CPUs 0 and 1 
 
// Save the new load configuration information. 
SetImageConfigInformation(pLoadedImage, &ilcd); 
 
// Unload the EXE from memory. 
ImageUnload(pLoadedImage); 

ImageCfg?.exe라는 유틸리티를 사용해서, 실행 가능한 모듈의 헤더에서 플래그를 변경할 수 있다.
http://robpol86.com/pages/imagecfg.php

"ImageCfg?.exe -a ..." 옵션을 사용하면 될듯 하다.

usage: IMAGECFG [switches] image-names... 
              [-?] display this message 
              [-a Process Affinity mask value in hex] 
              [-b BuildNumber] 
              [-c Win32 GetVersionEx Service Pack return value in hex] 
              [-d decommit thresholds] 
              [-g bitsToClear bitsToSet] 
              [-h 1|0 (Enable/Disable Terminal Server Compatible bit) 
              [-k StackReserve[.StackCommit]     
              [-l enable large (>2GB) adresses 
              [-m maximum allocation size] 
              [-n bind no longer allowed on this image 
              [-o default critical section timeout 
              [-p process heap flags] 
              [-q only print config info if changed 
              [-r run with restricted working set] 
              [-s path to symbol files] 
              [-t VirtualAlloc threshold] 
              [-u Marks image as uniprocesor only] 
              [-v MajorVersion.MinorVersion] 
              [-w Win32 GetVersion return value in hex] 
              [-x Mark image as Net - Run From Swapfile] 
              [-y Mark image as Removable - Run From Swapfile] 

시스템을 부팅할 때, boot.ini 파일을 이용해 시스템이 사용할 CPU의 개수를 제한할 수 있다.

[boot loader] 
timeout=2 
default=multi(0)disk(0)rdisk(0)partition(1)\WINNT 
[operating systems] 
multi(0)disk(0)rdisk(0)partition(1)\WINNT=”Windows 2000 Server” 
   /fastdetect 
multi(0)disk(0)rdisk(0)partition(1)\WINNT=”Windows 2000 Server” 
   /fastdetect /NumProcs=1 
/NumProcs?=1 스위치가 시스템은 머신의 프로세서중 하나만 사용해야 한다는 것을 지시한다.
멀티스레드 애플리케이션 디버깅시 유용하다.


댓글 달기 (로그인이 필요합니다)
제목
내용

http://codesarang.com. mail to cpueblocpueblo.com