Windows via C/C++

via.bmp 

목차


  1. 에러 핸들링
  2. 문자와 문자열로 작업하기
    1. 문자 인코딩
    2. ANSI 문자와 유니코드 문자 그리고 문자열 자료형
    3. 윈도우 내의 유니코드 함수와 ANSI 함수
    4. C 런타임 라이브러리 내의 유니코드 함수와 ANSI 함수
    5. C 런타임 라이브러내의 안전 문자열 함수
      1. 새로운 안전 문자열 함수에 대한 소개
    6. 문자와 문자열 작업에 대한 권고사항
    7. 유니코드 문자열과 ANSI 문자열 사이의 변경
      1. 멀티바이트-문자 문자열을 와이드-문자 문자열로 변경하기 위해서는 MultiByteToWideChar 윈도우 함수를 사용한다.
      2. 와이드-문자 문자열을 멀티바이트-문자 문자열로 변경하기 위해서는 WideCharToMultiByte 함수를 사용하면 된다.
  3. 커널 오브젝트
    1. 커널 오브젝트란 무엇인가?
      1. 사용 카운트
      2. 보안
    2. 프로세스의 커널 오브젝트 핸들 테이블
      1. 커널 오브젝트 생성하기
      2. 커널 오브젝트 삭제하기
      3. 프로세스간 커널 오브젝트의 공유
        1. 오브젝트 핸들의 상속을 이용하는 방법
        2. 명명된 오브젝트를 사용하는 방법
        3. 오브젝트 핸들의 복사를 이용하는 방법
  4. 프로세스
    1. 첫 번째 윈도우 애플리케이션 작성
    2. CreateProcess 함수
    3. 프로세스의 종료
    4. ExitProcess 함수
    5. TerminateProcess 함수
    6. 프로세스 내의 모든 스레드가 종료되면
    7. 프로세스가 종료되면
    8. 자식 프로세스
  5. 스레드의 기본
    1. 스레드를 생성해야 하는 경우
    2. 스레드를 생성하지 말아야 하는 경우
    3. 처음으로 작성하는 스레드 함수
    4. CreateThread 함수
    5. 스레드의 종료
      1. 스레드 함수 반환
      2. ExitThread 함수
      3. TerminateThread 함수
      4. 프로세스가 종료되면
      5. 스레드가 종료되면
    6. 스레드의 내부
    7. 자신의 구분자 얻기
  6. 스레드 스케줄링, 우선순위, 그리고 선호도
    1. 스레드의 정지와 계속 수행
    2. 슬리핑
    3. 다른 스레드로의 전환
    4. 스레드의 수행 시간
    5. 컨텍스트 내의 CONTEXT 구조체
    6. 스레드 우선순위
    7. 우선순위의 추상적인 의미
    8. 우선순위 프로그래밍
    9. 동적인 우선순위 레벨 상승
    10. 포그라운드 프로세스를 위한 스케줄러 변경
    11. 선호도
  7. 유저 모드에서의 스레드 동기화
    1. 원자적 접근 : Interlocked 함수들
    2. 캐시라인
    3. 고급 스레드 동기화 기법
    4. 크리티컬 섹션
      1. 크리티컬 섹션과 스핀락
      2. 크리티컬 섹션과 에러 처리
    5. 슬림 리더 - 라이터 락
    6. 조건변수
    7. 유용한 팁과 테크닉
  8. 커널 오브젝트를 이용한 스레드 동기화
    1. 대기 함수들
    2. 성공적인 대기의 부가적인 영향
    3. 이벤트 커널 오브젝트
    4. 대기 타이머 커널 오브젝트
    5. 세마포어 커널 오브젝트
    6. 뮤텍스 커널 오브젝트
    7. 그 외의 스레드 동기화 함수들
  9. 동기 및 비동기 장치 I/O
    1. 장치 열기와 닫기
    2. 파일 장치 이용
      1. 파일 크기 얻기
      2. 파일 포인터 위치 지정
      3. 파일의 끝 설정
    3. 동기 장치 I/O 수행
    4. 비동기 장치 I/O의 기본
      1. OVERLAPPED 구조체
      2. 비동기 장치 I/O 사용시 주의점
      3. 요청된 장치 I/O의 취소
    5. I/O 요청에 대한 완료 통지의 수신
      1. 디바이스 커널 오브젝트의 시그널링
      2. 이벤트 커널 오브젝트의 시그널링
      3. 얼러터블 I/O
      4. I/O 컴플리션 포트
  10. 윈도우 스레드 풀
    1. 비동기 함수 호출
    2. 시간 간격을 두고 함수 호출
    3. 커널 오브젝트가 시그널되면 함수 호출
    4. 비동기 I/O 요청이 완료되면 함수 호출
    5. 콜백 종료 동작
  11. 파이버
  12. 윈도우 메모리 구조
    1. 프로세스의 가상 주소 공간
    2. 가상 주소 공간의 분할
    3. 주소 공간 내의 영역
    4. 물리적 저장소를 영역으로 커밋하기
    5. 물리적 저장소와 페이징 파일
    6. 보호특성
    7. 모두 함께 모아
    8. 데이터 정렬의 중요성
  13. 가상 메모리 살펴보기
    1. 시스템 정보
    2. 가상 메모리 상태
    3. NUMA 머신에서의 메모리 관리
    4. 주소 공간의 상태 확인하기
  14. 애플리케이션에서 가상 메모리 사용 방법
    1. 주소 공간 내에 영역 예약하기
    2. 예약 영역에 저장소 커밋하기
    3. 영역에 대한 예약과 저장소 커밋을 동시에 수행하는 방법
    4. 언제 물리적 저장소를 커밋하는가
    5. 물리적 저장소의 디커밋과 영역 해제하기
    6. 보호 특성 변경하기
    7. 물리적 저장소의 내용 리셋하기
    8. 주소 윈도우 확장
  15. 스레드 스택
    1. C/C++ 런타임 라리브러리의 스택 확인 함수
  16. 메모리 맵 파일
    1. 실행 파일과 DLL 파일에 대한 메모리 맵
    2. 메모리 맵 데이터 파일
    3. 메모리 맵 파일 사용하기
    4. 메모리 맵 파일을 이용하여 큰 파일 처리하기
    5. 메모리 맵 파일과 일관성
    6. 메모리 맵 파일의 시작 주소 지정하기
    7. 메모리 맵 파일의 세부 구현사항
    8. 프로세스간 데이터 공유를 위해 메모리 맵 파일 사용하기
    9. 페이징 파일을 이용하는 메모리 맵 파일
    10. 스파스 메모리 맵 파일
    1. 프로세스 기본 힙
    2. 추가적으로 힙을 생성하는 이유
    3. 추가적으로 힙을 생성하는 방법
    4. 기타 힙 관련 함수들
  17. DLL의 기본
    1. DLL과 프로세스 주소 공간
    2. 전반적인 모습
  18. DLL의 고급 기법
    1. 명시적인 DLL 모듈 로딩과 심벌 링킹
    2. DLL의 진입점 함수
    3. DLL의 지연 로딩
    4. 함수 전달자
    5. 알려진 DLL
    6. DLL 리다이렉션
    7. 모듈의 시작 위치 변경
    8. 모듈 바인딩
  19. 스레드 지역 저장소(TLS)
    1. 동적 TLS
    2. 정적 TLS
  20. DLL 인젝션과 API 후킹
    1. DLL 인젝션
    2. 레지스트리를 이용하여 DLL 인젝션하기
    3. 윈도우 훅을 이용하여 DLL 인젝션하기
    4. 원격 스레드를 이용하여 DLL 인젝션하기
    5. 트로얀 DLL을 이용하여 DLL 인젝션하기
    6. 디비거를 이용하여 DLL 인젝션하기
    7. CreateProcess를 이용하여 코드 인젝션하기
    8. API 후킹
  21. 종료 처리기
  22. 예외 처리기와 소프트웨어 예외
    1. EXCEPTION_EXECUTE_HANDLER
    2. EXCEPTION_CONTINUE_EXECUTION
    3. EXCEPTION_CONTINUE_SEARCH
    4. GetExceptionCode
    5. GetExceptionInformation
    6. 소프트웨어 예외
  23. 처리되지 않은 예외, 벡터화된 예외 처리, 그리고 C++ 예외
    1. UnhandledExceptionFilter 함수의 내부
    2. 저스트-인-타임(JIT) 디버깅
    3. 벡터화된 예외와 컨티뉴 처리기
    4. C++ 예외와 구조적 예외
    5. 예외와 디버거
  24. 에러 보고와 애플리케이션 복구
    1. 윈도우 에러 보고 콘솔
    2. 프로그램적으로 윈도우 에러 보고하기
    3. 프로세스 내에서 사용자 정의 문제 보고서 생성하기
    4. 사용자 정의 문제 보고서 생성과 변경
    5. 자동 애플리케이션 재시작과 복구
  25. 빌드환경
    1. CmmHdr.h 헤더 파일
  26. 메시지 크래커, 차일드 컨트롤 매크로, 그리고 API 매크로
    1. 메시지 크래커
    2. 차일드 컨트롤 매크로
    3. API 매크로

 

 

  1. 에러 핸들링#

    윈도우 함수의 대표적인 반환 자료형 

    자료형 실패했을 때의 값
     VOID  이 함수는 절대 실패하지 않는다.
     BOOL  함수가 실패하면 0을 반환한다. 성공시에는 0이 아닌 값을 반환한다. 반환값을 TRUE와 비교해서는 안된다. 함수의 성공 여부를 확인하기 위해 FALSE인지를 비교하는 것이 가장 좋은 방법이다.
     HANDLE  함수가 실패하면 반환 값은 대개 NULL이다. 성공 시에는 유효한 오브젝트 핸들을 반환한다. 몇몇 함수들은 -1로 정의된 INVALID_HANDLE_VALUE를 반환하는 경우가 있기 때문에 주의가 필요하다.
     PVOID  함수가 실패하면 NULL을 반환한다. 성공 시에는 PVOID가 데이터를 저장하고 있는 메모리 주소를 가리킨다.
     LONG / DWORD  이러한 종류의 함수는 대개 LONG이나 DWORD형으로 개수를 반환한다. 어떤 이유로 인해 개수를 반환하지 못하게 되면 0이나 -1을 반환한다.(어떤 값을 반환하느냐는 함수별로 각기 다르다)

     

호출한 함수가 실패한 것으로 판단되면 어떤 에러가 발생했는지 확인하기 위해 GetLastError 함수를 사용할 수 있다.

  1. DWORD GetLastError();

이 함수는 단순히 가장 최근에 호출된 함수의 에러 코드를 스레드 지역 저장소로부터 가져온다. WinError.h 헤더 파일은 MS가 정의한 모든 에러 코드의 리스트를 가지고 있다. 함수 호출이 실패하면 관련 에러 코드를 확인하기 위해 지체 없이 GetLastError를 호출해야 한다. 만일 이 함수를 호출하기 전에 다른 함수를 호출하게 되면 다른 함수의 수행 결과가 겹쳐 써지게 된다. 함수 호출이 성공하면 ERROR_SUCCESS를 에러 코드로 기록한다.

디버깅을 수행하는 동안에는 스레드 지역 저장소에 기록된 에러 코드를 지속적으로 확인할 수 있으면 편리한데, MS의 Visual Studio 내에 포함된 디버거는 Watch 창을 통해 현재 수행 중인 스레드의 마지막 에러 코드와 메시지 텍스트를 확인할 수 있는 기능을 제공하고 있다. 이를 위해 Watch 창에 특정 행을 선택하고 $err, hr을 입력하면 된다.

 

 

  1. 문자와 문자열로 작업하기#

    1. 문자 인코딩#

      윈도우 비스타는 유니코드 문자를 UTF-16으로 인코딩한다. UTF-16은 각 문자를 2바이트로 구성한다. 닷넷 프레임워크의 경우 모든 문자와 문자열을 UTF-16으로 인코딩한다.

      UTF-8은 하나의 문자를 나타내기 위해 1,2,3,4 바이트로 인코딩을 수행한다. 매우 일반적인 인코딩 방식이지만 0x0800 이상의 문자를 많이 사용할 경우 비효율적이다.

      UTF-32는 모든 문자를 4바이트로 인코딩한다. 문자 변환 알고리즘을 간단히 구성하려 할 때나 가변 길이의 인코딩 방식을 사용하고 싶지 않은 경우에 유용하나 메모리 사용에 있어 매우 비효율적이다.

    2. ANSI 문자와 유니코드 문자 그리고 문자열 자료형#

       최근의 MS의 C/C++ 컴파일러는 16비트 유니코드를 표현하기 위한 wchar_t 자료형을 내장 자료형으로 처리할 수 있는 기능이 추가되었다. 유니코드 문자와 유니코드 문자열은 다음과 같이 선언한다.

      1. wchar_t c= L'A';
      2. wchar_t szBuffer[100] = L"A String";

      문자열 앞의 대문자 L은 컴파일러가 문자열을 유니코드로 다루도록 한다. 컴파일러가 문자열을 데이터 섹션에 삽입할 때 각 문자들은 UTF-16으로 인코딩된다. 위 예제의 경우 문자열이 모두 ASCII 문자이므로 각 문자 사이에 0이 삽입된다.

      MS의 윈도우 팀은 C 언어의 자료형으로부터 윈도우 자신의 자료형을 구분 짓기 위해 WinNT.h 헤더 파일에 다음과 같이 자료형을 정의하고 있다.

      1. typedef char CHAR;   // 8비트 문자
      2. typedef CHAR *PCHAR;
      3. typedef CHAR *PSTR;
      4. typedef CONST CHAR *PCSTR;
      5. typedef wchar_t WCHAR;   // 16비트 문자
      6. typedef WCHAR *PWCHAR;
      7. typedef WCHAR *PWSTR;
      8. typedef CONST WCHAR *PCWSTR;
    3. 윈도우 내의 유니코드 함수와 ANSI 함수#

      윈도우 NT 이후의 모든 윈도우 버전은 유니코드를 바탕으로 작성되었다. 따라서 창을 생성하고, 텍스트를 출력하고, 문자열을 다루는 것과 같은 핵심 함수들은 모두 유니코드 문자열을 요구한다. 만일 윈도우 함수에 ANSI문자열을 전달하면 호출된 함수는 먼저 전달된 문자열을 유니코드로 변경하고 변경된 문자열을 운영체제에 전달한다. 만일 ANSI 문자열이 반환되기를 기대하는 함수가 있다면 유니코드 문자열을 ANSI 문자열로 변경한 후 반환한다. 문자열 변경을 수행하기 위한 시간과 메모리 낭비를 피할 수 없다.

      애플리케이션을 좀 더 효율적으로 동작하게 하기 위해서는 처음부터 유니코드를 사용하도록 애플리케이션을 개발하는 것이 좋다.

    4. C 런타임 라이브러리 내의 유니코드 함수와 ANSI 함수#

      윈도우 함수와 마찬가지로 C 런타임 라이브러리도 ANSI 문자(열)를 다루는 함수와 유니코드 문자(열)를 다루는 함수를 세트로 제공하고 있다.

      C 런타임 라이브러리의 대표적인 함수로 ANSI 문자열의 길이를 반환하는 strlen 함수와 유니코드 문자열에 대해 동일한 기능을 수행하는 wcslen함수가 있다. 이 함수들의 원형은 String.h에 정의되어 있으나, ANSI와 유니코드 환경에서 모두 컴파일될 수 있는 코드를 작성하려면 다음과 같은 매크로가 정의되어 있는 TChar.h 헤더 파일도 참조해야 한다.

      1. #ifdef _UNICODE
      2. #define _tcslen      wcslen
      3. #else
      4. #define _tcslen      strlen
      5. #endif

      여러분의 코드에서는 가능한 한 _tcslen을 사용하는 것이 좋다. _tcslen을 사용하면 _UNICODE가 정의되어 있는 경우 wcslen으로 변경되고, 그렇지 않으면 strlen으로 변경된다. C 언어는 모든 구분자에 항상 언더스코어를 붙인다. 그런데 이것이 C++의 표준안은 아니기 때문에 윈도우 개발팀은 언더스코어를 UNICODE 구분자에 포함시키지 않았다. 이런 이유로 우리는 항상 UNICODE와 _UNICODE를 함께 정의하거나 둘 다 정의하지 말아야 한다.

    5. C 런타임 라이브러내의 안전 문자열 함수#

      1. 새로운 안전 문자열 함수에 대한 소개#

        StrSafe.h 헤더 파일을 포함하면 String.h 헤더 파일도 같이 포함된다. StrSafe.h 헤더 파일은 C 런타임 라이브러리에 포함되어 있는 _tcscpy 매크로와 같은 기존의 문자열 처리 함수를 사용할 경우 더 이상 사용되지 않는 함수라는 경고를 나타낼수 있도록 설정되어 있다. StrSafe.h에 대한 include구문은 다른 include 구문보다 반드시 뒤쪽에 위치되어야 한다. 컴파일시 경고가 나타나면 경고를 유발한 이전 함수를 안전 문자열 함수로 명시적으로 변경할 것을 권한다. 컴파일시 관련 경고를 유발한 구문들은 잠재적으로 버퍼 오버플로를 발생시킬 가능성이 있으며, 이는 복구가 불가능하다.

        _tcscpy나 _tcscat 같은 기존 함수에는 동일한 이름에 _s(secure를 의미함)가 붙은 안전 문자열 함수가 제공된다.

        1. PTSTR _tcscpy( PTSTR strDestination, PCTSTR strSource );
        2. errno_t _tcscpy_s( PTSTR strDestination, size_t numberOfCharacters, PCTSTR strSource );
        3. PTSTR _tcscat( PTSTR strDestination, PCTSTR strSource );
        4. errno_t _tcscat_s( PTSTR strDestination, size_t numberOfCharacters, PCTSTR strSource );

        이러한 함수들은 쓰여질 버퍼와 함께 버퍼의 크기도 인자로 전달하도록 정의되어 있는데, 이 값으로는 문자의 개수를 전달해야 한다. 문자의 개수는 버퍼에 대해 _countof 매크로를 사용하면 쉽게 계산될 수 있다.

        안전 문자열 함수는 내부적으로 가장 먼저 인자의 유효성을 검사한다. 만일 테스트가 실패하면 함수는 C 런타임 라이브러리에서 유지하고 있는 스레드 지역 저장소 변수인 errno에 에러 코드를 설정하고, 성공 실패 여부를 나타내는 errno_t형 값을 반환한다. 디버그 빌드의 경우 함수를 반환하기에 앞서 사용자에게 어설션 다이얼로그 박스를 표시하고 애플리케이션을 종료한다. 릴리즈 빌드의 경우 이러한 단계없이 바로 애플리케이션을 종료한다.

        C 런타임 라이브러리는 인자의 유효성 검증이 실패하였을 경우 사용자가 정의한 함수를 통해 에러 내용을 전달할 수 있는 기능을 제공하고 있다. 이러한 함수를 이용하면 에러를 기록하거나 디버거를 기동하는 등의 사용자 작업을 수행할 수 있다. 이를 위해서는 먼저 다음과 같은 원형의 함수를 작성해야 한다.

        1. void InvalidParameterHandler( PCTSTR expression, PCTSTR function, PCTSTR file, unsigned int line, uintptr_t /*pReserved*/);

        expression 매개변수는 (L"Buffer is too small" && 0 )와 같이 C 런타임 함수 내에서 발생한 테스트 실패에 대해 설명하는 문자열이 전달되며, 뒤따라오는 function, file, line 매개변수를 통해 각각 함수이름, 소스 파일명, 에러가 발생한 소스 코드의 행 번호와 같은 정보가 전달된다.

        다음 단계로, _set_invalid_parameter_handler를 호출하여 앞서 작성한 함수를 등록해야 한다. 하지만 이러한 절차를 수행하더라도 여전히 어설션 다이얼로그 박스는 나타날 것이기 때문에 _CrtSetReportMode( _CRT_ASSERT, 0 ) 을 애플리케이션 시작 시점에 호출하여 C 런타임이 어설션 다이얼로그 박스를 띄우지 않도록 하여야 한다.

        이제 String.h에 정의된 기존 문자열 함수를 대체하는 안전 문자열 함수들을 사용하면 된다. 호출한 함수가 정상 수행되었는지의 여부를 확인하려면 반환되는 errno_t 값을 확인하면 된다. S_OK가 반환되면 함수가 성공한 것이다. 그 외에 다른 값은 errno.h에 정의 되어 있다.

    6. 문자와 문자열 작업에 대한 권고사항#

      1. 문자열을 char 타입이나 byte의 배열로 생각하지 말고, 문자의 배열로 생각하라.
      2. 문자나 문자열을 나타낼 때 중립 자료형( TCHAR/PTSTR과 같은 )을 사용하라.
      3. 바이트나 바이트를 가리키는 포인터, 데이터 버퍼 등을 표현하기 위해서는 명시적인 자료형( BYTE나 PBYTE )을 사용하라.
      4. 문자나 문자열 상수 값을 표현할 때에는 TEXT나 _T 매크로를 사용하라. 일관성과 가독성을 유지하기 위해 두 개의 매크로를 혼용해서는 안 된다.
      5. 문자나 문자열과 관련된 자료형을 애플리케이션 전반에 걸쳐 변경하라.( 예를 들어 PSTR을 PTSTR로 변경하라 )
      6. 문자열에 대한 산술적인 계산 부분을 수정하라. 예를 들어 보통의 함수들은 버퍼의 크기를 전달해야 할 때 바이트 단위가 아닌 문자 단위로 값을 전달한다. 그렇기 때문에 sizeof(szBuffer)를 사용하는 대신 _countof(szBuffer)를 사용해야 한다. 또한 문자열을 저장하기 위한 메모리 블록을 할당해야 하고, 문자열을 구성하는 문자의 개수를 알고 있는 경우 메모리 할당은 바이트 단위로 수행해야 함을 잊어서는 안된다. 즉, malloc(nCharacters)를 써서는 안되고, malloc( nCharacters * sizeof(TCHAR) )를 써야 한다.
      7. printf 류의 함수를 사용하는 것을 피라하. 특히 ANSI 문자열을 유니코드 문자열로 변경하거나 그 반대로의 변경을 수행하기 위해 %s나 %S 등을 사용하는 것은 좋지 않다.
      8. UNICODE와 _UNICODE 심벌은 항상 동시에 정의하거나 해제하라.
    7. 유니코드 문자열과 ANSI 문자열 사이의 변경#

      1. 멀티바이트-문자 문자열을 와이드-문자 문자열로 변경하기 위해서는 MultiByteToWideChar 윈도우 함수를 사용한다.#
        1. int MultiByteToWideChar( UINT uCodePage, DWORD dwFlags, PCSTR pMultiByteStr,
        2. int cbMultiByte, PWSTR pWideCharStr, int cchWideChar );

        uCodePage 매개변수는 멀티바이트 문자열과 관련된 코드 페이지를 지정한다. dwFlags 매개변수에는 악센트 기호와 같은 발음을 위한 특수 기호에 대한 추가적인 제어를 수행하기 위한 플래그를 전달한다. 일반적으로 0을 전달하면 된다.pMultiByteStr 매개변수에는 변경할 문자열을 전달하고, cbMultiByte 매개변수에는 변경할 문장려의 길이를 (바이트 단위로) 전달한다. 만일 cbMultiByte 매개변수로 -1을 전달하면 변경할 문자열의 길이를 자동으로 계산한다.

        pWideCharStr 매개변수에는 유니코드로 변경된 문자열을 저장하기 위한 메모리 버퍼의 주소를 전달한다. cchWideChar 매개변수로는 버퍼의 최대 크기를 (문자 단위로) 전달한다. 만일 cchWideChar 매개변수에 0을 전달하면 이 함수는 변경을 수행하는 대신 변경에 필요한 버퍼의 크기( 종결문자 '0'을 포함한 크기 )를 문자 단위로 반환해 준다.

        1. pWideCharStr 매개변수에 NULL, cchWideChar 매개변수에 0, cbMultiBYte 매개변수에 -1을 주어 MultiByteToWideChar함수를 호출한다.
        2. 유니코드 문자열로의 변경에 필요한 충분한 메모리 공간을 할당한다. 이 크기는 앞서 호출한 MultiByteToWideChar 함수의 반환 값에 sizeof(wchar_t)를 곱한 값을 근간으로 계산될 수 있다.
        3. MultiByteToWideChar 함수를 재호출한다. 이번에는 pWideCharStr에 할당된 버퍼의 주소를 전달하고, cchWideChar에 앞서 호출한 MultiByteToWideChar 함수의 반환 값을 전달한다.
        4. 변경된 유니코드 문자열을 사용한다.
        5. 유니코드 문자열에 의해 점유도힌 메모리 공간을 해제한다.
      2. 와이드-문자 문자열을 멀티바이트-문자 문자열로 변경하기 위해서는 WideCharToMultiByte 함수를 사용하면 된다.#
        1. int WideCharToMultiByte( UINT uCodePage, DWORD dwFlags, PCWSTR pWideCharStr, int cchWideChar,
        2. PSTR pMultiByteStr, int cbMultiByte, PCSTR pDefaultChar, PBOOL pfUsedDefaultChar );

        uCodePage 매개변수로는 새롭게 변경될 문자열과 관련된 코드 페이지를 전달한다. dwFlags 매개변수를 지정하면 문자열 변경 작업 이외에 추가적인 작업을 수행할 수 있는데, 발음을 위한 특수 기호와 시스템이 변경하지 못하는 문자에 대해 특수 동작을 지정한다. 일반적으로 0을 전달하면 된다. pWideCharStr 매개변수에는 변경할 문자열을 담고 있는 메모리 주소를 전달하고, cchWideChar 매개변수에는 문자열의 길이( 문자 단위 )를 전달한다. cchWideChar 매개변수로 -1을 전달하면 변경할 문자열의 길이를 자동으로 결정해준다. 멀티바이트-문자 문자열이 저장될 pMultiByteStr 매개변수로는 문자열을 저장할 수 있는 충분한 크기의 버퍼를 전달해야 하고, cbMultiByte 매개변수로는 버퍼의 최대 크기( 바이트 단위 )를 전달해야 한다.

        WideCharToMultiByte 함수는 MultiByteToWideChar 함수에 비해 추가적으로 2개의 매개변수를 더 필요로 한다는 점에 주의해야 한다. 이러한 매개변수들은 변경할 와이드 문자가 uCodePage에 의해 지정된 코드 페이지 내에 적절한 문자가 존재하지 않을 경우에 사용된다. 와이드 문자가 적절히 변경될 수 없는 경우 pDefaultChar 매개변수에 의해 지정된 문자로 대체된다. 이 매개변수를 NULL로 지정하면 시스템 기본 문자인 물음표로 대체된다. pfUsedDefaultChar 매개변수에는 BOOL 값을 가리키는 포인터가 전달되며, 변경할 와이드-문자 문자열 중 한 자라도 멀티바이트-문자 문자열로 변경하는 것이 실패하는 경우 TRUE가 전달된다. 반면, 모든 문자열에 대해 변경이 성공적이면 FALSE를 반환한다.

 

 

  1. 커널 오브젝트#

    1. 커널 오브젝트란 무엇인가?#

      각 커널 오브젝트는 커널에 의해 할당된 간단한 메모리 블록이다. 이 메모리 블록은 커널에 의해서만 접근이 가능한 구조체로 구성되어 있으며, 커널 오브젝트에 대한 세부 정보들을 저장하고 있다. 커널 오브젝트의 데이터 구조체는 커널에 의해서만 접근이 가능하기 때문에 애플리케이션에서 데이터 구조체가 저장되어 있는 메모리 위치를 직접 접근하여 그 내용을 변경하는 것은 불가능하다. MS는 정제된 방법을 통해 구조체의 내용에 접근할 수 있도록 일련의 함수 집합을 제공하고 있어서 이를 통해 커널 오브젝트의 내부적인 값에 접근할 수 있다. 커널 오브젝트를 생성하는 함수를 호출하면 함수는 각 커널 오브젝트를 구분하기 위한 핸들 값을 반환해 준다. 핸들 값은 프로세스 내의 모든 스레드에 의해 사용 가능한 값이지만 특별한 의미를 가지고 있지는 않다. 핸들은 32비트 윈도우 프로세스에서는 32비트 값이고, 64비트 윈도우 프로세스에서는 64비트 값이다. 이러한 핸들은 다양한 윈도우 함수들의 매개변수로 전달될 수 있는데, 운영체제는 매개변수로 전달된 핸들 값을 통해 어떤 커널 오브젝트를 조작하고자 하는지 구분할 수 있다. 운영체제를 견고하게 하기 위해 이러한 핸들 값들은 프로세스별로 독립적으로 유지된다.

      1. 사용 카운트#

        커널 오브젝트는 프로세스가 아니라 커널에 의해 소유된다. 다시 말해, 만일 프로세스가 특정 함수를 통해 커널 오브젝트를 생성한 후 종료된다 하더라도 반드시 생성된 커널 오브젝트가 프로세스와 함께 삭제되는 것은 아니라는 의미이다. 각 커널 오브젝트는 내부적으로 사용 카운트 값을 유지하고 있기 때문에 커널은 이 값을 통해 얼마나 많은 프로세스들이 커널 오브젝트를 사용하고 있는지 알 수 있다.

      2. 보안#

        커널 오브젝트는 보안 디스크립터를 통해 보호될 수 있다. 보안 디스크립터는 누가 커널 오브젝트를 소유하고 있으며, 어떤 그룹과 사용자들에 의해 접근되거나 사용될 수 있는지, 혹은 어떤 그룹과 사용자들에 대해 접근이 제한되어 있는지에 대한 정보를 가지고 있다. 커널 오브젝트를 생성하는 거의 대부분의 함수들은 SECURITY_ATTRIBUTES 구조체에 대한 포인터를 인자로 받아들인다. 하지만 구조체 내의 lpSecurityDescriptor 멤버만이 보안과 관련되어 있다. 대부분의 애플리케이션에서는 현재 프로세스의 보안 토큰을 근간으로 하는 기본 보안 디스크립터를 사용하기 때문에 오브젝트 생성시 단순히 NULL 값을 전달하면 된다.

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

      프로세스가 초기화되면 운영체제는 프로세스를 위해 커널 오브젝트 핸들 테이블을 할당한다. 이러한 핸들 테이블은 사용자 오브젝트나 GDI 오브젝트에 의해서는 사용되지 않고 유일하게 커널 오브젝트에 의해서만 사용된다.

      프로세스 핸들 테이블 구조

       인덱스 커널 오브젝트의 메모리 블록을 가리키는 포인터 액세스 마스크( 각 비트별 플래그 값을 가지는 DWORD ) 플래그
       1  0x????????  0x????????  0x????????
       2  0x????????  0x????????  0x????????
       ... ... ... ...

      위의 표와 같이 단순한 데이터 구조체 배열로 이루어져 있으며, 각 데이터 구조체는 커널 오브젝트에 대한 포인터, 액세스 마스크, 플래그로 구성된다.

      1. 커널 오브젝트 생성하기#

        프로세스가 최초로 초기화되면 프로세스의 핸들 테이블은 비어 있다. 프로세스 내의 스레드가 CreateFileMapping과 같은 함수를 호출하면 커널은 커널 오브젝트를 위한 메모리 블록을 할당하고 초기화한다. 이후 커널은 프로세스의 핸들 테이블을 조사하여 비어 있는 공간을 찾아낸다. 핸들 테이블은 완전히 비어있기 때문에 커널은 인덱스가 1인 위치를 찾아내고 초기화를 수행한다. 포인터 멤버는 커널 오브젝트의 자료 구조를 가리키는 내부적인 메모리 주소로 할당되며, 액세스 마스크는 "풀 액세스"로, 플래그는 "설정" 상태로 초기화된다. 커널 오브젝트를 생성하는 모든 함수는 프로세스별로 고유한 핸들 값을 반환하며, 이 값은 프로세스내의 모든 스레드들에 의해 사용될 수 있다. 핸들 값은 실제로 프로세스 핸들 테이블의 인덱스 값으로 활용될 수 있기 때문에 프로세스별로 고유한 값이며, 다른 프로세스에 의해 사용될 수 없는 값이다. 만일 다른 프로세스와 공유를 시도하면 다른 프로세스의 프로세스 핸들 테이블로부터 동일한 인덱스 값을 가진 완전히 다른 커널 오브젝트를 참조하게 될 것이며, 이 오브젝트가 무엇인지에 대해서는 알 방법이 없다.

        커널 오브젝트를 생성하는 함수가 실패하면 반환되는 핸들 값은 보통 0(NULL)이 된다. 시스템의 가용 메모리가 매우 작거나 보안 문제로 인해 함수가 실패하는 경우 몇몇 함수들은 불행히도 -1(INVALID_HANDLE_VALUE, WinBase.h에서 정의된)을 반환하는 경우가 있다. 따라서 커널 오브젝트를 생성하는 함수의 반환 값을 확인할 때에는 상당한 주의가 필요하다.

      2. 커널 오브젝트 삭제하기#

        커널 오브젝트를 어떻게 생성했는지와 상관없이 CloseHandle 함수를 호출하여 더 이상 커널 오브젝트를 사용하지 않을 것임을 시스템에게 알려줄 수 있다.

        1. BOOL CloseHandle ( HANDLE hobject );

        내부적으로 이 함수는 프로세스의 핸들 테이블을 검사하여 전달받은 핸들 값을 통해 실제 커널 오브젝트에 접근 가능한지를 확인한다. 핸들이 유효한 값이고 시스템이 커널 오브젝트의 자료 구조를 획득하게 되면, 구조체 내의 사용 카운트 멤버를 감소시킨다. 만일 이 값이 0이 되면 커널 오브젝트를 파괴하고 메모리로부터 제거한다.

      3. 프로세스간 커널 오브젝트의 공유#

        MS가 핸들을 프로세스별로 고유한 값으로 설계한 가장 중요한 이유는 안정성이다. 프로세스별로 고유한 핸들이 좀더 보안에 강하기 때문이다.

        1. 오브젝트 핸들의 상속을 이용하는 방법#

          오브젝트 핸들의 상속은 오브젝트를 공유하고자 하는 프로세스들이 부모-자식 관계를 가질 때에만 사용될 수 있다. 즉, 하나 혹은 다수의 커널 오브젝트 핸들이 부모 프로세스에 의해 사용되고 있고, 부모 프로세스가 새로운 자식 프로세스를 생성하기로 결정하였을 때 자식 프로세스가 부모 프로세스가 사용하고 있는 커널 오브젝트에 접근할 수 있도록 해 주는 방법이다.

          상속 가능한 핸들을 만들기 위해서는 부모 프로세스가 SECURITY_ATTRIBUTES 구조체를 초기화하고 이렇게 초기화된 값을 Create 함수에 전달해야 한다. 다음은 뮤텍스 오브젝트를 생성하고 상속 가능한 핸들을 얻어내는 코드다.

          1. SECURITY_ATTRIBUTES sa;
          2. sa.nLength = sizeof( sa );
          3. sa.lpSecurityDescriptor = NULL;
          4. sa.bInheritHandle = TRUE;      // 상속 가능한 핸들을 만든다.
          5. HANDLE hMutex = CreateMutex( &sa, FALSE, NULL );

          위 코드는 기본 보안 디스크립터를 사용하고 상속 가능한 핸들을 반환하도록 SECURITY_ATTRIBUTES를 초기화한다.

          상속 가능한 핸들을 사용하기 위한 다음 단계는 부모 프로세스가 자식 프로세스를 생성하는 것이다. 이러한 작업은 CreateProcess 함수를 이용하면 된다.

          1. BOOL CreateProcess ( PCTSTR pszApplicationName,
          2. PTSTR pszCommandLine,

          3. PSECURITY_ATTRIBUTES psaProcess,

          4. PSECURITY_ATTRIBUTES psaThread,

          5. BOOL bInheritHandles,

          6. DWORD dwCreationFlags,

          7. PVOID pvEnvironment,

          8. PCTSTR pszCurrentDirectory,

          9. LPSTARTUPINFO pStartupInfo,

          10. PPROCESS_INFORMATION pProcessInformaion );

          보통 프로세스를 생성할 때 bInheritHandles 매개변수에 FALSE 값을 전달한다. 이 매개변수는 시스템에게 자식 프로세스가 부모 프로세스 핸들 테이블에 있는 상속 가능한 핸들을 상속하기를 원하지 않는다는 것을 시스템에게 알려주는 역할을 한다. bInheritHandles 매개변수로 TRUE를 전달하면 자식 프로세스는 부모 프로세스의 상속 가능한 핸들 값들을 상속하게 된다. 시스템은 상속 가능한 핸들을 찾아내어 자식 프로세스 핸들 테이블 내에 복사한다. 이때 자식 프로세스 핸들 테이블 내의 복사 위치는 부모 프로세스 핸들 테이블에서의 위치와 정확히 일치한다. 이것은 매우 중요한데, 이렇게 함으로써 특정 커널 오브젝트를 구분하는 핸들 값이 부모 프로세스와 자식 프로세스에 걸쳐 동일한 값을 이용할 수 있기 때문이다.

        2. 명명된 오브젝트를 사용하는 방법#

          모두는 아니지만 대부분의 커널 오브젝트는 이름을 가질 수 있다.

          1. HANDLE CreateMutex ( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName );

          이름을 이용하여 커널 오브젝트를 공유하려면 당연히 커널 오브젝트에 이름을 지정해야 한다. pszName에 NULL을 전달하는 대신 '\0'으로 끝나는 문자열을 가리키는 주소를 전달하여 오브젝트의 이름을 지정할 수 있다. 이러한 이름은 최대 MAX_PATH(260) 길이가 될 수 있다.

          A 프로세스가 수행되어 다음과 같이 함수를 호출했다고 하지.

          1. HANDLE hMutexProcessA = CreateMutex( NULL, FALSE, TEXT("JeffMutex") );

          이 함수 호출은 새로운 뮤텍스 커널 오브젝트를 생성하여 "JeffMutex"라고 명명한다. 이제 새로운 B 프로세스가 수행되어 다음과 같은 코드를 수행한다.

          1. HANDLE hMutexProcessB = CreateMutex( NULL, FALSE, TEXT("JeffMutex") );

          B 프로세스가 CreateMutex를 호출하게 되면 운영체제는 먼저 JeffMutex라는 이름의 커널 오브젝트가 존재하는지 확인한다. 만일 동일 이름의 오브젝트가 존재한다면, 다음으로 오브젝트의 타입을 확인해야 한다. 이후 운영체제는 B 프로세스가 오브젝트에 대한 최대 접근 권한을 가지고 있는지 확인한다. 만일 그렇다면 운영체제는 B 프로세스의 핸들 테이블 상에 비어 있는 항목을 추가하고 이미 존재하고 있던 커널 오브젝트를 가리키도록 설정한다. 만일 오브젝트의 타입이 일치하지 않거나 접근 권한이 없는 경우 CreateMutex는 실패하고 NULL을 반환한다. B 프로세스가 CreateMutex를 호출할 때 보안 특성 정보와 두 번째 인자 값을 전달하였더라도 이미 존재하는 커널 오브젝트의 정보와 일치하지 않을 경우 이 값들은 무시된다.

          명명된 오브젝트가 이미 생성되어 있는 경우에 한해서만 활용할 수 있는 함수들도 있는데, Open*류의 함수가 이러한 함수들이다.

          1. HANDLE OpenMutex ( DWORD dwDesireAccess, BOOL bInheritHandle, PCTSTR pszName );

          마지막 매개변수인 pszName은 커널 오브젝트의 이름을 지정하는데 사용된다. 이 값으로 NULL을 사용해서는 안 되며, 반드시 '\0'으로 끝나는 문자열을 지정해야 한다. 이러한 함수들은 커널 오브젝트를 위한 단일의 네임스페이스 내에서 검색을 시도한다. Create*류의 함수와 Open*류의 함수 사이의 주요 차이점은 커널 오브젝트가 존재하지 않는 경우에 Create*류의 함수는 새로운 오브젝트를 생성하지만 Open*류의 함수는 실패한다는 것이다.

        3. 오브젝트 핸들의 복사를 이용하는 방법#

          프로세스 간에 커널 오브젝트를 공유하는 마지막 방법은 DuplicateHandle 함수를 사용하는 것이다.

          1. BOOL DuplicateHandle ( HANDLE hSourceProcessHandle,
          2. HANDLE hSourceHandle,

          3. HANDLE hTargetProcessHandle,

          4. PHANDLE phTargetHandle,

          5. DWORD dwDesiredAccess,

          6. BOOL bInheritHandle,

          7. DWORD dwOptions );

          DuplicateHandle 함수는 특정 프로세스 핸들 테이블 내의 항목을 다른 프로세스 핸들 테이블로 복사하는 함수다. 이 함수를 사용하려면 첫 번째와 세 번째 매개변수에 프로세스 커널 오브젝트의 핸들을 넘겨주어야 한다. 이러한 핸들은 이 함수를 호출하는 프로세스와 연관되어 있는 프로세스들일 것이다. 추가로, 이 두 매개변수에는 반드시 프로세스 커널 오브젝트에 대한 핸들을 전달해야 한다. 만일 다른 타입의 커널 오브젝트를 전달하면 이 함수는 실패하게 된다. 시스템에서 새로운 프로세스가 생길 때마다 새로운 프로세스 커널 오브젝트가 생긴다.

          두 번째 매개변수인 hSourceHandle 로는 어떤 타입의 커널 오브젝트라도 전달할 수 있으며, DuplicateHandle 함수를 호출한 프로세스와 아무런 연관성을 가지지 않는다. 대신 hSourceProcessHandle 매개변수로 지정된 핸들 값이 가리키는 프로세스에서만 의미를 가지는 프로세스 고유의 값이다. 네 번째 매개변수인 phTargetHandle로는 HANDLE 변수의 주소 값을 전달하게 되며, 함수 호출 이후에 hTargetProcessHandle 값이 가리키는 프로세스에서만 사용될 수 있는 고유의 핸들 값을 전달받게 된다. 물론 이 값은 소스 핸들의 복사본이다.

          DuplicateHandle의 마지막 3개의 매개변수에는 타깃 프로세스 고유의 커널 오브젝트 핸들이 가진 속성 정보인 액세스 마스크와 속성 플래그의 값을 지정하게 된다. dwOptions 매개변수는 0 혹은 DUPLICATE_SAME_ACCESS와 DUPLICATE_CLOSE_SOURCE의 조합으로 지정 될 수 있다.

          DUPLICATE_SAME_ACCESS를 지정하면 타깃 핸들이 소스 프로세스의 핸들과 동일한 액세스 마스크를 가지기를 원한다는 사실을 DuplicateHandle에게 알려주게 된다. 이 플래그를 사용하면 dwDesireAccess 매개변수는 무시된다.

          DUPLICATE_CLOSE_SOURCE를 지정하면 소스 프로세스의 핸들을 삭제한다. 이 플래그를 사용하면 하나의 프로세스에서 다른 프로세스로 쉽게 커널 오브젝트를 이동시킬 수 있으며, 커널 오브젝트의 사용 카운트에는 영향을 주지 않는다.

 

 

  1. 프로세스#

    프로세스는 일반적으로 수행 중인 프로그램의 인스턴스라고 정의하며, 두 개의 컴포넌트로 구성된다.

    프로세스를 관리하기 위한 목적으로 운영체제가 사용한는 커널 오브젝트. 시스템은 프로세스에 대한 각종 통계정보를 프로세스 커널 오브젝트에 저장하기도 한다.

    실행 모듈이나 DLL의 코드와 데이터를 수용하는 주소 공간. 이러한 주소 공간은 스레드 스택이나 힙 할당과 같은 동적 메모리 할당에 사용되는 공간도 포함한다.

    프로세스는 자력으로 수행될 수 없다. 프로세스가 무언가를 수행하기 위해서는 반드시 프로세스의 컨텍스트내에서 수행되는 스레드가 있어야 한다. 각 스레드들은 자신만의 CPU 레지스터 집합과 스택을 가진다. 각 프로세스는 프로세스 주소 공간 내의 코드를 수행하기 위해 적어도 한 개의 스레드를 가지고 있다. 프로세스가 생성되면 시스템은 자동적으로 첫 번째 스레드를 생성해 주는데 이를 주 스레드라고 부른다. 각 스레드들은 라운드 로빈방식으로 주어지는 (퀀텀) 단위 시간만큼만 수행될 수 있다. 윈도우 운영체제에서는 스레드에 대한 모든 관리와 스케줄링을 윈도우 커널이 담당한다.

    1. 첫 번째 윈도우 애플리케이션 작성#

      윈도우 애플리케이션은 애플리케이션이 수행을 시작할 진입점 함수를 반드시 가져야 한다. C/C++ 개발자는 두 가지 형태의 진입점 함수를 사용할 수 있다.

      1. int WINAPI _tWinMain( HINSTANCE hInstanceExe, HINSTANCE,
      2. PTSTR pszCmdLine, int nCmdShow );

      3. int _tmain( int argc, TCHAR *argv[], TCHAR *envp[] );

      어떤 진입점 함수를 사용할지는 유니코드 문자열의 사용 여부에 달려 있다. 사실 운영체제는 우리가 작성한 진입점 함수를 직접 호출하지는 않으며, C/C++ 런타임에 의해 구현된 C/C++ 런타임 시작함수를 호출한다. 이러한 함수는 링크 시 -entry:명령행 옵션을 통해 설정된다. C/C++ 런타임 시작 함수는 malloc 이나 free와 같은 함수가 호출될 수 있도록 C/C++ 런타임 라이브러리에 대한 초기화를 수행한다. 또한 개발자가 코드 상에서 선언한 각종 전역 오브젝트나 static으로 선언된 C++ 오브젝트들을 코드가 수행되기 전에 적절히 생성하는 역할을 수행한다. 아래에 시작 함수가 수행하는 작업들을 간단히 요약해 보았다.

      1. 새로운 프로세스의 전체 명령행을 가리키는 포인터를 획득한다.
      2. 새로운 프로세스의 환경변수를 가리키는 포인터를 획득한다.
      3. C/C++ 런타임 라이브러리의 전역변수를 초기화한다. 사용자 코드가 StdLib.h 파일을 인클루드하면 이 변수에 접근할 수 있다.
      4. C/C++ 런타임 라이브러리의 메모리 할당 함수(malloc과 calloc)와 저수준 입출력 루틴이 사용하는 힙을 초기화한다.
      5. 모든 전역 오브젝트와 static C++ 클래스 오브젝트의 생성자를 호출한다.
      6. 이러한 초기화 작업이 모두 완료되고 나서야 C/C++ 시작 함수는 비로소 애플리케이션의 진입점 함수를 호출한다. 모든 실행 파일과 DLL 파일은 프로세스의 메모리 공간 상에 로드될 때 고유의 인스턴스 핸들을 할당 받는다. 이러한 인스턴스 핸들은 (w)WinMain의 첫 번째 매개변수인 hInstanceExe를 통해 전달된다. hInstanceExe 매개변수의 실제 값은 시스템이 프로세스의 메모리 주소 공간 상에 실행 파일을 로드할 시작 메모리 주소다. 실제로 HMODULE과 HINSTANCE는 완전히 동일하다. 어떤 함수가 HMODULE을 요구한다면 HINSTANCE를 넘겨줘도 무방하며, 그 반대도 마찬가지다. 16비트 윈도우에서는 완전히 구분되는 자료형으로 존재했었지만 지금은 혼용하고 있다.
    2. CreateProcess 함수#

      CreateProcess 함수를 이용하면 새로운 프로세스를 생성할 수 있다.

      1. BOOL CreateProcess( PCTSTR pszApplicationName,
      2. PTSTR pszCommandLine,

      3. PSECURITY_ATTRIBUTES psaProcess,

      4. PSECURITY_ATTRIBUTES psaThread,

      5. BOOL bInheritHandles,

      6. DWORD fdwCreate,

      7. PVOID pvEnvironment,

      8. PCTSTR pszCurDir,

      9. PSTARTUPINFO psiStartInfo,

      10. PROCESS_INFORMATION ppiProcInfo );

      스레드가 CreateProcess를 호출하면 시스템은 사용 카운트가 1인 프로세스 커널 오브젝트를 생성한다. 프로세스 커널 오브젝트는 프로세스 자체를 의미하는 것은 아니며, 운영체제가 프로세스를 관리하기 위한 목적으로 생성한 조그마한 데이터 구조체다. 프로세스 커널 오브젝트가 생성되고 나면 시스템은 새로운 프로세스를 위한 가상 주소 공간을 생성하고, 실행 파일의 코드와 데이터 및 수행에 필요한 추가적인 DLL 파일들을 프로세스의 주소 공간 상에 로드한다.

      다음 단계로, 시스템은 새로 생성된 프로세스의 주 스레드를 위한 스레드 커널 오브젝트를 생성한다. 주 스레드는 링커에 의해 진입점으로 지정된 C/C++ 런타임 시작 코드를 실행한다. 이러한 시작 코드는 종국에는 사용자가 작성한 WinMain, wWinMain, main, 또는 wmain 함수를 호출하게 된다. 만일 시스템이 성공적으로 프로세스를 생성하고 주 스레드를 생성하였다면 CreateProcess는 TRUE를 반환한다.

      pszApplicationNamepszCommandLine

      각각 프로세스를 생성할 실행 파일명과 새로운 프로세스에 전달할 명령행 문자열을 지정하게 된다. pszCommandLine 매개변수의 자료형이 PTSTR인 점에 주목할 필요가 있다. 이것은 함수 내에서 변경될 수 있는 형태로 전달되어야 함을 의미한다. CreateProcess는 내부적으로 우리가 전달하는 명령행 문자열에 변경 작업을 수행한다. 하지만 반환 직전에 그 내용을 원래의 값으로 돌려놓는다.

      pszCommandLine을 이용하면 CreateProcess가 새로운 프로세스를 생성하기 위해 필요한 추가 정보를 제공할 수 있다. pszCommandLine을 통해 전달되는 문자열의 첫 번째 토큰은 실행하고자 하는 프로그램의 파일명으로 간주되며, 확장자가 전달되지 않으면 .exe로 가정한다. CreateProcess는 실행 파일을 찾기 위해 다음과 같이 순차적으로 검색을 진행한다.

      1. 생성할 프로세스의 실행 파일명에 포함된 디엑터리
      2. 생성할 프로세스의 현재 디렉터리
      3. 윈도우 시스템 디렉터리. 즉, GetSystemDirectory가 반환하는 System32 서브폴더
      4. 윈도우 디렉터리
      5. PATH 환경변수에 포함된 디렉터리들

        물론 생성할 프로세스의 파일명이 전체 경로를 포함하고 있는 경우라면 이러한 전체 경로만을 이용하여 실행 파일을 찾게 되고 나머지 디렉터리에서는 검색을 수행하지 않는다.

        pszApplicationName 매개변수를 NULL로 지정하는 한 이와 같이 작업이 수행된다. 하지만 pszApplicationName 매개변수로 실행 파일명을 담고 있는 문자열의 주소를 전달할 수도 있다. 이 경우 파일명의 확장자를 .exe로 가정하지 않기 때문에 반드시 확장자를 포함하도록 파일명을 지정해야 한다.

psaProcess, psaThread, bInheritHandles

새로운 프로세스를 생성하기 위해 시스템은 새로운 프로세스 커널 오브젝트와 스레드 커널 오브젝트를 생성해야 한다. 2개의 오브젝트들은 모두 커널 오브젝트이므로 부모 프로세스는 각각에 대해 보안 특성을 지정할 수 있어야 한다. CreateProcess 함수에서는 psaProcess와 psaThread 매개변수를 통해 프로세스 커널 오브젝트와 스레드 커널 오브젝트 각각에 대해 원하는 보안 특성을 지정할 수 있다. 기본 보안 디스크립터를 사용하길 원한다면 각 매개변수를 NULL로 지정하면 된다. 그렇지 않은 경우라면 SECURITY_ATTRIBUTES 구조체를 생성하여 프로세스 오브젝트와 스레드 오브젝트 각각에 대해 적절한 보안 권한을 설정하면 된다. psaProcess와 psaThread 매개변수로 SECURITY_ATTRIBUTES를 사용하는 또 다른 이유는 두 개의 커널 오브젝트 핸들을 상속 가능하도록 생성하여 추후 부모 프로세스가 새로운 자식 프로세스를 생성할 때 이 커널 오브젝트들을 사용할 수 있도록 하기 위함이다.

fdwCreate

새로운 프로세스를 어떻게 생성할지를 결정하게 된다. 이 매개변수로는 여러 개의 플래그 값을 비트 OR 연산으로 결합하여 전달할 수 있다.

DEBUG_PROCESS, CREATE_SUSPENDED, CREATE_NO_WINDOW, 등등...

pvEnvironment

새로운 프로세스가 사용할 환경변수 문자열을 포함하고 있는 메모리 블록을 가리키는 포인터를 지정한다. 대부분의 경우 자식 프로세스가 부모 프로세스의 환경블록을 상속받아 사용할 수 있도록 하기 위해 이 매개변수로 NULL을 지정한다.

pszCurDir

부모 프로세스가 자식 프로세스의 현재 드라이브와 디렉터리를 설정할 수 있도록 한다. 이 매개변수로 NULL을 전달하면 새로 생성되는 프로세스의 현재 디렉터리는 새로운 프로세스를 생성하는 애플리케이션의 현재 디렉터리와 동일하게 설정된다. 경로명에는 반드시 드라이브 문자를 포함시켜야 함에 주의하기 바란다.

psiStartInfo

STARTUPINFO나 STARTUPINFOEX 구조체를 가리키는 포인터를 지정해야 한다. 윈도우는 새로운 프로세스를 생성할 때 이 구조체의 멤버들을 사용한다. 대부분의 애플리케이션들은 기본 값을 사용하여 프로세스를 생성하는데, 이 경우에도 최소한 구조체의 모든 멤버를 0으로 초기화하고 구조체 내의 멤버 cb를 구조체의 크기로 설정하는 작업을 수행해야 한다.

ppiProcInfo

PROCESS_INFORMATION 구조체를 가리키는 포인터로 지정되며, 함수 호출에 앞서 반드시 메모리를 할당해야 한다. CreateProcess 함수는 반환되기 직전에 이 구조체의 멤버를 초기화해준다. 이 구조체의 멤버는 다음과 같다.

  1. typedef struct _PROCESS_INFORMATION {

  2. HANDLE hProcess;

  3. HANDLE hThread;

  4. DWORD dwProcessId;

  5. DWORD dwThreadId;

  6. } PROCESS_INFORMATION;

새로운 프로세스를 만들면 시스템은 새로운 프로세스 커널 오브젝트와 스레드 커널 오브젝트를 생성한다. 커널 오브젝트가 새롭게 생성되면 사용 카운트 값은 1로 초기화된다. CreateProcess의 경우 함수가 반환되기 직전에 프로세스 커널 오브젝트와 스레드 커널 오브젝트를 최대 접근 권한으로 한 번 더 열게 되고, PROCESS_INFORMATION 구조체 내의 hProcess와 hThread 멤버에 이 함수를 호출한 프로세스에서 사용할 수 있는 커널 오브젝트 핸들 값을 할당하게 된다. 이처럼 CreatePorcess 내부에서 오브젝트를 다시 한 번 열기 때문에 사용 카운트는 2가 된다. 따라서 프로세스 커널 오브젝트가 파괴되려면 프로세스가 종료되고(사용 카운트가 1만큼 줄어든다) 부모 프로세스가 CloseHandle을 호출해야만 한다(사용 카운트가 또다시 1만큼 줄어들어 0이 된다). 마찬가지로, 스레드 커널 오브젝트를 파괴하려면 스레드가 종료되고 부모 프로세스가 스레드 커널 오브젝트 핸들을 삭제해야만 한다. 자식 프로세스의 핸들과 자식 프로세스의 주 스레드 핸들은 반드시 삭제해야만 애플리케이션이 수행되는 동안에 리소스 누수를 피할 수 있다.

프로세스 커널 오브젝트가 생성되면 시스템은 이 오브젝트에 고유의 ID를 부여한다. 시스템 내의 다른 프로세스 커널 오브젝트는 동일한 ID를 가지지 못한다. 이러한 동작 방식은 스레드 커널 오브젝트에 대해서도 동일하다. 즉, 스레드 커널 오브젝트가 생성되면 시스템 전체에 걸쳐 고유의 ID를 부여받게 된다. 프로세스 ID와 스레드 ID는 동일한 숫자 풀을 공유하기 때문에 프로세스 오브젝트와 스레드 오브젝트가 동일한 ID값을 가지는 경우는 없다. 또한 ID 값은 0이 될 수 없다. 프로세스와 스레드의 ID 값은 즉각적으로 재사용된다. 프로세스나 스레드 ID가 재사용 되는 것을 막는 유일한 방법은 프로세스나 스레드 커널 오브젝트가 파괴되지 않도록 하는 것이다.

  1. 프로세스의 종료#

    1. 주 스레드 진입점 함수의 반환

      프로세스가 종료되어야 할 때에는 항상 주 스레드의 진입점 함수가 반환하도록 애플리케이션을 설계하는 것이 좋다. 이 방법만이 유일하게 주 스레드의 리소스들이 적절히 해제되는 것을 보장할 수 있다. 주 진입점 함수가 반환되면 다음과 같은 작업을 수행한다.

    2. 주 스레드에 의해 생성된 C++ 오브젝트들이 파괴자를 이용하여 적절하게 파괴한다.

    3. 운영체제는 스레드 스택의 용도로 할당한 메모리 공간을 적절히 해제한다.

    4. 시스템은 진입점 함수의 반환값으로 프로세스의 종료 코드(프로세스 커널 오브젝트 내에 포함되어 있는)를 실행한다.

    5. 시스템은 프로세스 커널 오브젝트의 사용 카운트를 감소시킨다.

  2. ExitProcess 함수#

    프로세스 내의 스레드가 ExitProcess를 호출하면 프로세스는 종료된다.

    1. VOID ExitProcess( UINT fuExitCode );

    ExitProcess를 호출하는 코드 뒤쪽에 있는 코드는 절대 수행되지 않는다. ExitProcess나 ExitThread를 호출하면 프로세스나 스레드가 함수 내에서 종료되어 버린다. 운영체제가 이에 대해 적절한 처리를 하기 때문에 이러한 동작은 유효하며, 프로세스와 스레드의 모든 운영체제 리소스는 완벽하게 제거될 것이다. 하지만 C/C++ 애플리케이션은 이 함수를 가능한 한 호출하지 말아야 한다. 왜냐하면 C/C++ 런타임이 관리하는 리소스에 대한 정리 작업은 수행되지 않기 때문이다. ExitProcess는 이 함수의 호출 시점에 프로세스를 종료하기 때문에 C/C++ 런타임이 오브젝트를 정리할 수 있는 기회를 받지 못하고 따라서 오브젝트의 파괴자를 호출하지 못한다.

  3. TerminateProcess 함수#

    1. BOOL TerminateProcess( HANDLE hProcess, UINT fuExitCode );

    Terminateprocess는 자신의 프로세스뿐만 아니라 다른 프로세스까지도 종료시킬 수 있다. hProcess 매개변수로 종료시키고자 하는 프로세스의 핸들을 전달하면 된다. 다른 방법으로는 프로세스를 종료시킬 수 없을 경우에 한해서만 TerminateProcess를 호출하도록 해야 한다. 이 함수를 이용하여 프로세스를 종료하면 종료와 관련된 어떠한 통지도 받지 못하기 때문에 애플리케이션은 적절한 정리 작업을 할 수도 없고, 종료를 회피할 수도 없다. 비록 프로세스가 자신을 정리할 만한 기회는 갖지 못하겠지만, 프로세스가 사용하던 모든 리소스는 완벽하게 정리된다. 프로세스가 사용하던 모든 메모리는 해제되고, 열려 있던 파일은 닫히고, 모든 커널 오브젝트의 사용 카운트는 감소되며, 사용자 오브젝트와 GDI 오브젝트는 제거된다. TerminateProcess는 비동기 함수다. 이 함수를 호출하여 종료를 요청한다 하더라도 함수가 반환되는 시점에 맞추어 프로세스가 항상 종료될 것이라는 것은 보장하지 못한다는 것이다.

  4. 프로세스 내의 모든 스레드가 종료되면#

    프로세스 내의 모든 스레드가 종료되면 운영체제는 더 이상 프로세스의 주소 공간을 유지할 이유가 없다고 판단한다.

  5. 프로세스가 종료되면#

    프로세스가 종료되면 다음과 같은 작업이 이루어진다.

    1. 프로세스 내에 남아 있는 스레드가 종료된다.

    2. 프로세스에 의해 할당되었던 모든 사용자 오브젝트와 GDI 오브젝트가 삭제되며, 모든 커널 오브젝트는 파괴된다.

    3. 프로세스의 종료 코드는 STILL_ACTIVE에서 ExitProcess나 TerminateProcess 호출 시 설정한 종료 코드로 변경된다.

    4. 프로세스 커널 오브젝트의 상태가 시그널 상태로 변경된다. 이것은 시스템에서 수행되는 다른 스레드가 프로세스 종료 시까지 대기할 수 있도록 하기 위함이다.

    5. 프로세스 커널 오브젝트의 사용 카운트가 1만큼 감소한다.

      프로세스 커널 오브젝트는 적어도 프로세스 자체보다는 오랫동안 유지된다.

  1. 자식 프로세스#

    애플리케이션을 설계할 때 다른 코드 블록을 수행해야 하는 상황에 직면할 수 있다. 이 같은 작업은 새로운 자식 프로세스를 생성하는 방법이 있다. 주소 공간을 보호하는 방법 중 하나는 처리해야 하는 작업을 새로운 프로세스가 수행하도록 하는 것이다. 새로 생성되는 프로세스는 자신의 주소 공간에서 수행되므로, 부모 주소 공간 상에 존재하는 자료 중 작업을 수행하는 데 필요한 자료에 대해서만 새로 생성되는 프로세스가 접근할 수 있도록 해야 할 것이다. 이렇게 하면 작업과 관련 없는 다른 자료는 새로 생성되는 프로세스로부터 보호될 수 있다. 윈도우는 서로 다른 프로세스 간에 자료를 전송할 수 있도록 DDE, DLE, 파이프, 메일슬롯 등과 같은 다양한 방법을 제공하고 있다. 가장 편리한 방법은 메모리 맵 파일을 이용하여 자료를 공유하는 것이다.

 

 

 

  1. #

    마이크로소프트 윈도우는 잡 커널 오브젝트를 이용하여 프로세스들을 하나의 그룹으로 묶어서 다루거나 샌드박스를 만들어서 프로세스들이 수행하는 작업에 제한을 가할 수 있다. 잡 오브젝트를 프로세스의 컨테이너와 같은 역할을 수행한다고 생각하면 좀 더 이해하기 쉽다.

 

 

 

  1. 스레드의 기본#

    스레드는 두 개의 요소로 구성되어 있다.

    운영체제가 스레드를 다루기 위해 사용하는 스레드 커널 오브젝트. 스레드 커널 오브젝트는 시스템이 스레드에 대한 통계 정보를 저장하는 공간이기도 하다.

    스레드가 코드를 수행할 때 함수의 매개변수와 지역변수를 저장하기 위한 스레드 스택.

    스레드는 항상 프로세스의 컨텍스트 내에 생성되며, 프로세스 안에만 살아 있을 수 있다.

    1. 스레드를 생성해야 하는 경우#

      스레드는 프로세스 내의 수행 흐름을 의미한다. 프로세스의 초기화가 진행되는 동안에 시스템은 주 스레드를 생성한다. 멀티스레딩을 사용하면 사용자 인터페이스를 좀 더 단순화시킬 수 있다. 또한 멀티스레딩을 이용하면 애플리케이션을 좀 더 확장성이 좋은 구조로 설계할 수 있다.

    2. 스레드를 생성하지 말아야 하는 경우#

      스레드를 잘못 사용하는 예로는 애플리케이션의 사용자 인터페이스를 개발할 때 자주 나타난다. 거의 모든 애플리케이션에서 사용자 인터페이스를 위한 컴포넌트들은 반드시 동일한 스레드를 사용해야만 한다. 하나의 스레드를 이용하여 모든 윈도우의 차일드 윈도우를 생성하는 것이 가장 좋다. 간혹 서로 다른 스레드를 이용하여 각기 윈도우를 만드는 것이 유용할 때도 있지만, 이러한 경우는 정말로 드문 경우다.

    3. 처음으로 작성하는 스레드 함수#

      모든 스레드는 수행을 시작할 진입점 함수를 반드시 가져야 한다. 프로세스 내에 두 번째 스레드를 만들려면 새로 생성되는 스레드는 아래와 같은 형태의 진입점 함수를 반드시 가져야 한다.

      1. DWORD WINAPI ThreadFunc( PVOID pvParam ) {
      2. DWORD dwResult = 0;

      3. ...

      4. return (dwResult);

      5. }

      스레드 함수가 반환되는 시점에 스레드는 수행을 멈추고 스레드가 사용하던 스택도 반환된다. 또한 스레드 커널 오브젝트의 사용 카운트도 감소한다.

    4. CreateThread 함수#

      두 번째 스레드를 생성하고 싶다면 이미 수행 중인 스레드 내에서 CreateThread를 호출하면 된다.

      1. HANDLE CreateThread( PSECURITY_ATTRIBUTES psa,
      2. DWORD cbStackSize,

      3. PTHREAD_START_ROUTINE pfnStartAddr,

      4. PVOID pvParam,

      5. DWORD dwCreateFlags,

      6. PDWORD pdwThreadID );

      CreateThread가 호출되면 시스템은 스레드 커널 오브젝트를 생성한다. 이 스레드 커널 오브젝트는 스레드 자체는 아니며, 운영체제가 스레드를 다루기 위한 조그만 데이터 구조체에 불과하다.

      C/C++로 코드를 작성하는 경우에는 CreateThread를 사용해서는 안되고, 마이크로소프트 C/C++ 런타임 라이브러리에서 제공하는 _beginthreadex 함수를 사용해야 한다. 다른 컴파일러에서도 CreateThread 함수를 대체할 만한 함수를 제공할 것이며, 반드시 컴파일러에 의해 제공되는 다른 함수를 사용해야 한다.

      다음으로, 시스템은 스레드가 사용할 스택을 확보한다. 새로운 스레드는 스레드를 생성한 프로세스와 동일한 컨텍스트 내에서 수행된다. 따라서 새로운 스레드는 프로세스의 모든 커널 오브젝트 핸들뿐만 아니라 프로세스에 있는 모든 메모리, 그리고 같은 프로세스에 있는 다른 모든 스레드의 스택에조차 접근이 가능하다. 따라서 동일 프로세스 내의 스레드들은 손쉽게 상호 통신을 할 수 있다.

      psa 매개변수는 SECURITY_ATTRIBUTES 구조체를 가리키는 포인터다. 스레드 커널 오브젝트에 대해 기본 보안 특성을 사용할 것이라면 이 매개변수로 NULL을 전달하면 된다.

      cbStackSize 매개변수로는 스레드가 자신의 스택을 위해 얼마만큼의 주소 공간을 사용할지를 지정하게 된다. 모든 스레드는 자신만의 고유한 스택을 가지고 있다. CreateProcess를 호출하여 프로세스가 시작되면 내부적으로 CreateThread 함수를 호출하여 프로세스의 주 스레드를 초기화하는데, 이때 CreateProcess는 실행 파일 내부에 저장되어 있는 값을 이용하여 cbStackSize 매개변수의 값을 결정한다. 실행 파일 내에 저장되는 스택의 크기를 변경하기 위해서는 링커의 /STACK 스위치를 사용하면 된다.

      /STACK: [reserve] [,commit]

      reserve 인자는 시스템이 스레드 스택을 위해 지정된 크기만큼의  주소 공간을 예약하게 된다. 기본 값은 1MB다. commit 인자는 스택으로 예약된 주소 공간에 커밋된 물리적 저장소의 초기 크기를 나타낸다. 기본 값은 한 페이지 크기다. 스레드가 사용하는 스택은 필요 시 동적으로 커지게 된다. 스택에서 사용할 예약된 영역은 /STACK 링커 스위치와 cbStackSize에 지정된 값 중 큰 값을 이용한다. 예약된 영역의 크기는 스택으로 사용할 수 있는 공간의 최대 크기를 지정하는 것이다. 스택 크기를 제한함으로써 애플리케이션이 물리적 저장소의 대부분을 사용하는 것을 제한할 수 있으며, 프로그램 내에 버그가 있음을 더 빨리 확인할 수 있을 것이다.

      pfnStartAddr 매개변수는 새로이 생성되는 스레드가 호출할 스레드 함수의 주소를 가리킨다. 이 스레드 함수의 pvParam 매개변수로는 CreateThread 함수의 pvParam 매개변수로 전달한 값이 그대로 전달된다.

      dwCreateFlags 매개변수는 스레드를 생성할 때 세부적인 제어를 수행하기 위한 추가적인 플래그를 지정하는 데 사용된다. 만일 이 값으로 0을 전달하면 스레드는 생성되는 즉시 CPU에 의해 스케줄 가능하게 된다. 만일 CREATE_SUSPENDED를 전달하면 시스템은 스레드를 생성하고 초기화를 완료한 이후 CPU에 의해 바로 스케줄되지 않도록 일시 정지 상태를 유지하게 된다.

      pdwThreadID 에는 새로운 스레드에 할당되는 스레드 ID 값을 저장할 DWORD 변수를 가리키는 주소를 지정하면 된다.

    5. 스레드의 종료#

      1. 스레드 함수 반환#

        스레드 함수가 반환되면 다음과 같은 작업이 수행된다.

        스레드 함수 내에서 생성한 모든 C++ 오브젝트들은 파괴자를 통해 적절히 제거된다.

        운영체제는 스레드 스택으로 사용하였던 메모리를 반환한다.

        시스템은 스레드의 종료 코드를 스레드 함수의 반호나 값으로 설정한다.

        시스템은 스레드 커널 오브젝의 사용 카운트를 감소시킨다.

      2. ExitThread 함수#

        스레드를 강제로 종료할 수 있다.

        VOID ExitTHread( DWORD dwExitCode );

        이 함수는 스레드를 강제로 종료하고 운영체제가 스레드에서 사용했던 모든 운영체제 리소스를 정리하도록 한다. 하지만 C/C++ 리소스는 정리되지 않는다. 만일 C/C++로 코드를 작성하는 경우라면 ExitThread를 사용하는 대신 C/C++ 런타임 라이브러리 함수인 _endthreadex 함수를 사용하는 것이 좋다.

      3. TerminateThread 함수#

        BOOL TerminateThread( HANDLE hThread, DWORD dwExitCode );

        스레드 반환을 통해 혹은 ExitThread 함수를 호출해서 종료되는 경우라면 스레드가 사용하던 스택이 정상적으로 정리된다. 하지만 TerminateThread 함수를 사용하면 시스템은 종료된 스레드를 소유하고 있던 프로세스가 살아 있는 동안 그 스레드가 사용하였던 스레드 스택을 정리하지 않는다.

      4. 프로세스가 종료되면#

        프로세스를 강제 종료하면 프로세스 내에 남아 있는 스레드들에 대해 각각 TerminateThread 함수가 호출된다. 이와 같이 프로세스를 종료하게 되면 C++ 파괴자가 호출되지도 못하고 디스크로 자료를 저장하는 등의 적절한 정리 작업도 수행되지 못한다.

      5. 스레드가 종료되면#

        스레드가 종료되면 다음과 같은 작업들이 수행된다.

        스레드가 소유하고 있던 모든 유저 오브젝트 핸들이 삭제된다. 윈도우와 윈도우 훅 두 개의 사용자 오브젝트는 스레드에 의해 소유되고 다른 형태의 오브젝트들은 모두 프로세스에 의해 소유된다.

        스레드의 종료 코드는, STILL_ACTIVE에서 ExitThread나 TerminateThread에서 지정한 종료코드로 변경된다.

        스레드 커널 오브젝트의 상태가 시그널 상태로 변경된다.

        종료되는 스레드가 프로세스 내의 마지막 활성 스레드라면 시스템은 프로세스도 같이 종료되어야 하는 것으로 간주한다.

        스레드 커널 오브젝트의 사용 카운트가 1만큼 감소한다.

    6. 스레드의 내부#

      InitThread.bmp  

      CreateThread 함수가 호출되면 시스템은 스레드 커널 오브젝트를 생성한다. 이 오브젝트는 초기 사용 카운트로 2를 가진다. 스레드 커널 오브젝트의 다른 속성들도 초기화된다. 정지 카운트는 1로, 종료 코드는 STILL_ACTIVE(0x103)로, 오브젝트의 상태는 논시그널로 각각 초기화된다. 스레드 커널 오브젝트가 생성되면 시스템은 스레드 스택으로 활용할 메모리 공간을 할당한다. 스레드는 자신만의 가상 메모리 공간을 가지지 않으므로 스택으로 활용할 메모리는 프로세스의 주소 공간으로부터 할당된다. 이후 시스템은 새로 생성된 스레드 스택의 가장 상위에 두 개의 값을 기록한다( 스레드 스택은 항상 상위 메모리로부터 하위 메모리 순으로 사용된다.) 스택에 쓰여진 첫 번째 값은 CreateThread 함수 호출 시 전달된 pvParam 매개변수 값이며, 두 번째 값은 마찬가지로 CreateThread 함수 호출 시 전달된 pfnStartAddr 매개변수의 값이다.

      각 스레드는 자신만의 CPU 레지스터 세트를 가지는데 이를 스레드 컨텍스트라고 부른다. 이러한 컨텍스트는 스레드가 마지막으로 수행되었을 당시의 스레드 CPU 레지스터 값을 가지고 있다.

      인스트럭션 포인터(IP) 레지스터와 스택 포인터(SP) 레지스터는 스레드 컨텍스트에 저장되는 값 중에서 가장 중요한 레지스터다. 스레드는 항상 프로세스의 컨텍스트 내부에서 수행된다는 사실을 기억하기 바란다. 이런 이유로 두 레지스터의 값은 프로세스 메모리 공간 상의 특정 위치를 가리키고 있다. 스레드 커널 오브젝트가 초기화되면 CONTEXT 구조체 내의 스택 포인터 레지스터는 pfnStartAddr를 저장하고 있는 스레드 스택의 주소로 설정된다. 인스트럭션 포인터 레지스터는 NTDLL.dll 모듈이 익스포트하고 있는 RtlUserThreadStart라는 문서화되지 않은 함수의 주소를 가리키도록 설정된다. RtlUserThreadStart 함수는 스레드가 실질적으로 수행하는 최초 위치가 된다. 새로운 스레드가 RtlUserThreadStart 함수를 호출하면 다음과 같은 작업이 수행된다.

      스레드 함수 내에서 예외가 발생했을 경우 시스템이 제공하는 기본적인 예외 처리 코드를 수행할 수 있도록 구조적 예외 처리(SEH) 프레임이 설정된다.

      시스템은 CreateThread 함수 호출 시에 전달한 pvParam 매개변수로 스레드 함수를 호출한다.

      스레드 함수가 반환되면 RtlUserThreadStart 함수는 스레드 함수가 반환한 값을 인자로 ExitThread 함수를 호출한다. 스레드 커널 오브젝트의 사용 카운트는 감소되고, 스레드는 수행을 종료한다.

      만일 스레드가 예외를 유발하고 이러한 예외가 처리되지 않으면 RtlUserThreadStart 함수가 설정한 SEH 프레임이 예외를 처리하게 된다. 이때 사용자에게 메시지 박스를 출력하는데, 사용자가 프로그램 닫기를 선택하면 RtlUserThreadStart는 ExitProcess를 호출하여 예외를 유발한 스레드뿐만 아니라 전체 프로세스를 종료시켜 버린다.

    7. 자신의 구분자 얻기#

      윈도우 운영체제는 스레드가 자신을 소유하는 프로세스의 커널 오브젝트나 자신을 나타내는 스레드 커널 오브젝트를 손쉽게 얻을 수 있는 함수를 제공하고 있다.

      1. HANDLE GetCurrentProcess();
      2. HANDLE GetCurrentThread();

      이 두개의 함수는 해당 함수를 호출한 스레드를 소유하고 있는 프로세스나 스레드 자신을 나타내는 스레드 커널 오브젝트의 허위 핸들을 반환한다. 이러한 함수는 프로세스의 커널 오브젝트 핸들 테이블에 어떠한 새로운 핸들도 생성하지 않기 때문에, 프로세스나 스레드 커널 오브젝트의 사용 카운트에도 영향을 미치지 않는다.

      때때로 허위 핸들 대신 실제 핸들 값을 얻어 와야 할 때도 있다. 여기서 "실제" 핸들이란 특정 스레드를 대표할 수 있는 고유의 핸들 값을 의미한다. 스레드의 허위 핸들은 항시 현재 스레드의 핸들이다. 허위 핸들을 실제 핸들로 변경하기 위해서는 DuplicateHandle 함수를 이용하면 된다.

      1. BOOL DuplicateHandle( HANDLE hSourceProcess,   // 현재 스레드를 소유하고 있는 프로세스의 허위 핸들
      2. HANDLE hSource,               // 변경할 스레드, 프로세스의 허위 핸들

      3. HANDLE hTargetProcess,      // 새로운 스레드, 프로세스 핸들을 생성할 프로세스 핸들

      4. PHANDLE phTarget,            // 실제 핸들값이 반환 됨

      5. DWORD dwDesiredAccess,

      6. BOOL bInheritHandle,

      7. DWORD dwOptions );

      DuplicateHandle은 지정된 커널 오브젝트의 사용 카운트를 증가시키기 때문에 복사된 오브젝트 핸들이 더 이상 사용할 필요가 없게 되면 오브젝트의 사용 카운트를 감소시키기 위해 반드시 대상 핸들을 전달하여 CloseHandle 함수를 호출해 주어야 한다.

 

 

  1. 스레드 스케줄링, 우선순위, 그리고 선호도#

    윈도우는 매 20밀리초 정도마다 모든 스레드 커널 오브젝트 중 스케줄 가능 상태에 있는 스레드 커널 오브젝트를 검색하고, 이 중 하나를 선택하여 스레드 컨텍스트 구조체 내에 저장된 레지스터 값을 CPU 레지스터로 로드한다. 이러한 작업을 컨텍스트 전환이라고 한다. 이와 같이 컨텍스트 전환이 일어나면 CPU 시간을 할당받은 스레드는 프로세스의 주소 공간 내에 위치한 코드를 수행하고 데이터를 사용하게 된다. 다시 20밀리초 정도가 지나면 윈도우는 CPU 레지스터 정보를 스레드의 컨텍스트로 저장하게 되고 스레드는 수행이 정지된다. 시스템은 남아 있는 스레드 중 스케줄 가능 상태에 있는 스레드 커널 오브젝트를 확인하여 이 중 하나를 다시 선택하고, 스레드 커널 오브젝트 내에 저장되어 있는 레지스터 값을 CPU 레지스터로 로드한다. 윈도우는 언제라도 특정 스레드를 정지하고 다른 스레드를 수행할 수 있기 때문에 선점형 멀티스레드 기반 운영체제라고 불린다. 우리가 생성한 스레드가 항상 수행되고 있다고 가정해서는 안된다는 것을 반드시 기억하기 바란다.   

    1. 스레드의 정지와 계속 수행#

      스레드 커널 오브젝트 내에는 정지 카운트라는 값이 저장되어 있다. CreateProcess나 CreateThread를 호출하면 스레드 커널 오브젝트가 생성되고 정지 카운트가 1로 초기화된다. 이렇게 되면 스레드는 스케줄 불가능 상태가 된다. 스레드가 초기화되면 CreateProcess나 CreateThread 함수는 인자 값으로 CREATE_SUSPENDED 플래그가 전달되었는지 확인한다. 만일 이러한 플래그가 전달되었다면 호출된 함수는 반환되고 새로 생성된 스레드는 정지 상태를 유지한다. 만일 이러한 플래그가 전달되지 않았다면 함수는 스레드의 정지 카운트를 0으로 감소시킨다. 스레드의 정지 카운트가 0이 되면 이 스레드는 어떤 일이 발생하기를 기다리는 상태가 아니라면 스케줄 가능한 상태가 된다.

      스레드를 정지 상태로 생성하면 반드시 스레드를 스케줄 가능 상태로 변경해 주어야만 스레드가 수행될 수 있다. 이렇게 하기 위해서는 CreateThread 함수가 반환하는 스레드 핸들 값을 인자로 ResumeThread 함수를 호출하면 된다.

      1. DWORD ResumeThread( HANDLE hThread );

      ResumeThread가 성공하면 스레드의 이전 정지 카운트 값이 반환되며, 실패하면 0xFFFFFFFF이 반환된다.

      스레드는 여러 번 정지될 수 있다. 3번 정지가 수행된 스레드는 3번 수행 명령을 내려야만 CPU시간을 할당받을 수 있는 자격이 생긴다. CREATE_SUSPENDED 플래그를 이용하여 정지된 스레드를 생성하는 것 외에도 SuspendThread 함수를 호출하여 스레드를 정지할 수도 있다.

      1. DWORD SuspendThread( HANDLE hThread );

      이 함수는 수행 중인 스레드가 어떤 작업을 수행하던 중에 정지될 수 알 수 없기 때문에 주의가 필요하다.

    2. 슬리핑#

      스레드는 Sleep 함수를 호출하여 일정 시간 동안 자신을 스케줄하지 않도록 운영체제에게 명령을 내릴 수 있다.

      1. VOID Sleep( DWORD dwMilliseconds );

      이 함수를 호출하면 dwMilliseconds 매개변수로 주어진 시간만큼 스레드를 일시 정지시키게 된다. Sleep을 호출하면 스레드는 자발적으로 남은 타임 슬라이스를 포기한다.

    3. 다른 스레드로의 전환#

      윈도우는 스케줄 가능 상태에 있는 다른 스레드를 수행하기 위한 SwitchToThread 함수를 제공한다.

      1. BOOL SwitchToThread();

      이 함수를 호출하면 시스템은 일정 시간 동안 CPU 시간을 받지 못하여 수행되지 못하고 있던 스레드가 있는지 확인한다. 만일 그러한 스레드가 없다면 이 함수는 바로 반환하지만, 그러한 스레드가 존재한다면 SwitchToThread는 해당 스레드를 스케줄한다. 이 함수를 이용하면 리소스를 소유하고 있는 낮은 우선순위의 스레드가 해당 리소스를 빨리 사용하고 반환할 수 있도록 해 준다.

    4. 스레드의 수행 시간#

      때로는 특정 작업을 완료하기 위해 스레드가 얼마만큼의 시간을 사용했는지를 알아야 할 필요가 있다. 필요한 함수는 스레드가 부여받은 CPU 시간이 얼마나 되는지를 알아내는 함수일 것이다.

      1. BOOL GetThreadTimes( HANDLE hThread,
      2. PFILETIME pftCreationTime,   // 스레드가 생성된 시점

      3. PFILETIME pftExitTime,         // 스레드가 종료된 시점

      4. PFILETIME pftKernelTime,      // 커널 모드에서의 시간

      5. PFILETIME pftUserTime );      // 애플리케이션 코들 수행하는데 소요된 시간을 100나노초 단위로 반환

      이 함수를 다음과 같이 사용하면 복잡한 알고리즘을 완료하는데까지 어느 정도의 시간이 소요되었는지 정확히 알 수 있다.

      1. __int64 FileTimeToQuadWord( PFILETIME pft ) {
      2. return ( Int64ShllMod32 ( pft->dwHighDateTime, 32 ) | pft->dwLowDateTime );

      3. }

      4.  

      5. void PerformLongOperation () {
      6. FILETIME ftKernelTimeStart, ftKernelTimeEnd;

      7. FILETIME ftUserTimeStart, ftUserTimeEnd;

      8. FILETIME ftDummy;

      9. __int64 qwKernelTimeElapsed, wqUserTImeElapsed, qwTotalTimeElapsed;

      10.  

      11. // 시작  시간을 획득
      12. GetThreadTimes ( GetCurrentThread(), &ftDummy, &ftDummy, &ftKernelTimeStart, &ftUserTimeStart );
      13.  
      14. // 복잡한 알고리즘을 수행
      15.  
      16. // 종료 시간을 획득
      17. GetThreadTimes ( GetCurrentThread(), &ftDummy, &ftDummy, &ktKernelTimeEnd, &ftUserTImeEnd );
      18.  
      19. // 커널 시간과 유저 시간을 FILETIME에서 쿼드 워드 형태로 변경하여 획득 이후에 종료 시간에서 시작 시간을 뺀다
      20. qwKernelTimeElapsed = FileTimeToQuadWord( &ftKernelTimeEnd ) - FileTimeToQuadWord( &ftKernelTimeStart );
      21. qwUserTimeElapsed = FileTimeToQuadWord( &ftUserTimeEnd ) - FileTimeToQueadWord( &ftUserTimeStart );
      22.  
      23. // 커널 시간과 유저 시간을 합하여 전체 소요시간을 계산
      24. qwTotalTimeElapsed = qwKernelTimeElapsed + qwUserTimeElapsed;
      25. // 전체 소요 시간을 qwTotalTImeElapsed에 저장
      26. }

      GetProcessTimes 함수는 GetThreadTimes와 유사하며 프로세스 내의 모든 스레드들이 소요한 시간 정보를 얻어온다.

      정밀한 시간 측정이 필요한 경우라면 GetThreadTimes 함수가 충분하지 않을 수도 있다. 윈도우는 정밀한 시간 측정을 위해 다음과 같은 함수들을 가지고 있다.

      1. BOOL QueryPerformanceFrequency( LARGE_INTEGER* pliFrequency );
      2. BOOL QueryPerformanceCounter( LARGE_INTEGER* pliCount );

      이 함수들은 윈도우 스케줄러가 해당 스레드를 선점하지 않을 경우에만 정확하게 시간을 측정할 수 있다.

      1. class CStopwatch {
      2. public:
      3. CStopwatch() {

      4. QueryPerformanceFrequency( &m_liPerfFreq );

      5. Start();

      6. }

      7. void Start() {

      8. QueryPerformanceCounter( *m_liPerfStart );

      9. }

      10. // Start 함수 호출 이후 경과된 시간을 밀리초 단위로 반환

      11. __int64 Now() const {

      12. LARGE_INTEGER liPerfNow;

      13. QueryPerformanceCounter( &liPerfNow );

      14. return( ( ( liPerfNow.QuadPart - m_liPerfstart.QuadPart ) * 1000 ) / m_liPerfFreq.QuadPart );

      15. }

      16. // Start 함수 호출 이후 경과된 시간을 마이크로초 단위로 반환

      17. __int64 NowInMicro() const {

      18. LARGE_INTEGER liPerfNow;

      19. QueryPerformanceCounter( &liPerfNow );

      20. return( ( ( liPerfNow.QuadPart - m_liPerfstart.QuadPart ) * 1000000 ) / m_liPerfFreq.QuadPart );

      21. }

      22. private:
      23. LARGE_INTEGER m_liPerfFreq;   // 초당 카운트 수

      24. LARGE_INTEGER m_liPerfStart;   // 시작 카운트

      25. };
      26. 이 클래스를 다음과 같이 사용할 수 있다.
      27.  
      28. // 시간 측정 타이머를 생성한다.( 기본적으로 현재 시간이 획득된다 )
      29. CStopwatch stopwatch;
      30.  
      31. // 소요 시간을 측정하고자 하는 코드가 나타날 위치
      32.  

      33. // 얼마만큼의 시간이 경과되었는지를 측정한다.
      34. __int64 qwElapsedTime = stopwatch.Now();
      35. // qwElapsedTime은 소요된 시간을 밀리초 단위로 측정한 값을 가지고 있다.

       

    5. 컨텍스트 내의 CONTEXT 구조체#

      CONTEXT 구조체는 시스템이 저장하는 스레드의 상태 정보로, 다음번에 CPU가 스레드를 수행할 때 어디서부터 수행을 시작해야 할지를 알려주는 역할을 담당한다. 윈도우는 스레드 커널 오브젝트의 내부에 저장되어 있는 컨텍스트 정보를 확인하고, 이 값들을 가져올 수 있도록 GetThreadContext 함수를 제공하고 있다.

      1. BOOL GetThreadContext ( HANDLE hThread, PCONTEXT pContext );

      이 함수를 호출하기 위해서는 먼저 CONTEXT 구조체를 할당하고, 어떤 레지스터 값을 가져올지를 설정하기 위해 플래그를 구성한 후( 구조체 내의 ContextFlags 멤버를 이용하여 ) GetThreadContext에 구조체의 주소를 전달하면 된다. 이 함수를 호출하기 위해서는 반드시 SuspendThread 함수를 먼저 호출해 주어야 한다. CONTEXT 구조체 내의 멤버 값을 변경한 후 SetThreadContext 함수를 호출하여 스레드 커널 오브젝트 내의 레지스터 값을 지정된 값으로 변경할 수도 있다.

      1. BOOL SetThreadContext  ( HANDLE hThread, CONST CONTEXT *pContext );

      스레드 컨텍스트의 내용을 변경시키기고자 하는 스레드는 컨텍스트를 변경하기 전에 반드시 정지되어 있어야 한다.

    6. 스레드 우선순위#

      모든 스레드들은 0(가장 낮은)부터 31(가장 높은) 범위 내의 우선순위를 가진다. 31번 우선순위를 가진 스레드가 스케줄 가능한 상태에 있는 동안에는 0부터 30번 우선순위를 가진 스레드들은 절대 CPU 시간을 할당받지 못한다. 이러한 상태를 기아 상태라고 한다. 기아 상태는 높은 우선순위의 스레드들이 너무 많은 CPU 시간을 사용해서 낮은 우선순위의 스레드들이 수행되지 못하는 상황을 말한다. 낮은 우선순위의 스레드가 어떤 작업을 수행하든지 높은 우선순위의 스레드는 항상 낮은 우선순위의 스레드보다 선행하게 된다. 우선순위가 5인 스레드가 수행 중이고 그보다 더 높은 우선순위의 스레드가 스케줄 가능 상태가 되면 지체 없이 낮은 우선순위의 스레드를 정지하고 높은 우선순위의 스레드에 타임 슬라이스 크기만큼 CPU 시간을 할당하게 된다.

    7. 우선순위의 추상적인 의미#

      윈도우는 6개의 우선순위 클래스를 제공한다. 보통 우선순위가 가장 일반적인 우선순위 클래스이며, 99 퍼센트의 애플리케이션이 보통 우선순위로 수행된다.

       

       우선순위 클래스 설명
       실시간  즉각적인 응답이 필요한 이벤트를 처리하는 프로세스의 스레드에서 사용된다. 이러한 프로세스의 스레드는 운영체제가 운용하는 프로세스의 스레드보다 우선적으로 수행된다.
       높음  즉각적인 응답이 필요한 이벤트를 처리하는 프로세스의 스레드에서 사용된다. 작업 관리자의 경우 사용자가 수행 중인 프로세스를 종료할 수 있도록 하기 위해 이 우선순위 클래스로 수행된다.
       보통이상  보통과 높음 사이의 우선순위
       보통  특별한 스케줄링 규칙이 필요하지 않은 프로세스의 스레드에 의해 사용된다.
       보통 이하  보통과 유휴 상태 사이의 우선순위
       유휴 상태  시스템이 수행할 다른 스레드가 없는 경우에 수행할 프로세스의 스레드에서 사용된다. 주로 화면 보호기나 백그라운드로 수행되는 유틸리티, 통계/정보수집 프로그램 등에서 사용된다.

       

      윈도우는 7가지의 상대 스레드 우선순위를 제공한다.

       

      상대 스레드 우선순위 설명
       타임 크리티컬  실시간 우선순위 클래스에서는 31, 다른 우선순위 클래스에서는 15로 동작한다.
       가장 높음  보통보다 두 단계 높음
       보통 이상  보통보다 한 단계 높음
       보통  프로세스의 우선순위 클래스에 대해 보통으로 수행됨
       보통 이하  보통보다 한 단계 낮음
       가장 낮음  보통보다 두 단계 낮음
       유휴 상태  실시간 우선순위 클래스에서는 16, 다른 우선순위 클래스에서는 1로 동작한다.

       

      정리하면, 프로세스에는 프로세스 우선순위 클래스가 할당되고, 스레드에는 프로세스 우선순위에 상대적인 스레드 우선순위가 할당된다. 애플리케이션 개발자는 우선순위 레벨을 절대 직접 사용할 수 없다. 대신 시스템은 프로세스 우선순위 클래스와 상대 스레드 우선순위를 이용하여 우선순위 레벨로 매핑을 수행한다. 이러한 매핑방식에 대해 마이크로소프트는 가능한 언급하지 않으려 하며, 사실 이러한 매핑정보는 각 시스템의 버전별로 상이하다.

      일반적으로, 높은 우선순위 레벨의 스레드는 오랫동안 스케줄 가능 상태로 남아 있지 않도록 해야 한다. 반면, 우선순위 레벨이 낮은 스레드는 가능한 오랫동안 스케줄 가능한 상태를 유지하는 것이 좋다.

    8. 우선순위 프로그래밍#

      CreateProcess를 호출할 때 fdwCreate 매개변수로 우선순위 클래스를 전달해서 설정할 수 있다.

      프로세스가 수행된 이후에 SetPriorityClass 함수를 호출하여 자신의 우선순위 클래스를 변경할 수 있다.

      1. BOOL SetPriorityClass ( HANDLE hProcess, DWORD fdwPriority );

      프로세스의 우선순위 클래스를 획득하기 위해 사용할 수 있는 함수도 있다.

      1. DWORD GetPriorityClass ( HANDLE hProcess );

      프로세스가 최초로 생성되면 상대 스레드 우선순위는 항상 보통으로 설정된다. CreateThread 함수를 이용하여 새로운 스레드를 생성할 때, 스레드의 상대 우선순위를 설정할 수 있는 방법을 제공하지 않는다는 것은 조금 이상해 보인다. 어쨌든 스레드의 상대 우선순위를 설정하기 위해 다음 함수를 사용할 수 있다.

      1. BOOL SetThreadPriority ( HANDLE hThread, int nPriority );

      hThread 매개변수는 우선순위를 변경하고자 하는 스레드를 나타내는 핸들이며, nPriority 매개변수는 밑의 표에 나열된 7개의 구분자 중 하나를 전달해야 한다.

       

      상대 스레드 우선순위 구분자
       타임 크리티컬  THREAD_PRIORITY_TIME_CRITICAL
       가장 높음  THREAD_PRIORITY_HIGHEST
       보통 이상  THREAD_PRIORITY_ABOVE_NORMAL
       보통  THREAD_PRIORITY_NORMAL
       보통 이하  THREAD_PRIORITY_BELOW_NORMAL
       가장 낮음  THREAD_PRIORITY_LOWEST
       유휴 상태  THREAD_PRIORITY_IDLE

       

      스레드의 상대 우선순위를 획득하기 위해 사용할 수 있는 함수도 있다.

      1. int GetThreadPriority ( HANDLE hThread );
    9. 동적인 우선순위 레벨 상승#

      시스템은 스레드의 우선순위 레벨을 스레드의 상대 우선순위와 스레드가 속한 프로세스의 우선순위 클래스를 결합하여 산출한다. 때로는 이 값을 스레드의 기본 우선순위 레벨이라고 한다. 시스템은 /O 이벤트에 대해 응답하거나 윈도우 메시지나 디스크를 읽기 위해 스레드의 우선순위 레벨을 상승시키기도 한다. 시스템은 스레드의 기본 우선순위 레벨이 1부터 15사이인 스레드에 대해서만 우선순위 레벨 상승을 시도한다. 그래서 이 범위의 우선순위 레벨을 동적 우선순위 범위라고 한다. 또한 시스템은 실시간 우선순위 범위(15를 초과하는)로 레벨 상승을 수행하지 않는다. 실시간 우선순위 범위 내의 스레드들은 대부분 운영체제의 기능을 수행하기 때문이다.

      1. BOOL SetProcessPriorityBoost ( HANDLE hProcess, BOOL bDisablePriorityBoost );
      2. BOOL SetThreadPriorityBoost ( HANDLE hThread, BOOL bDisablePriorityBoost );

      SetProcessPriorityBoost 를 이용하면 해당 프로세스 내의 모든 스레드에 대해 동적인 우선순위 레벨 상승을 가능하거나 불가능하도록 설정할 수 있다. SetThreadPriorityBoost는 개별 스레드에 대해 적용된다. 시스템은 CPU 시간을 오랫동안 할당받지 못한 스레드에 대해 동적으로 우선순위를 상승시켜 완전히 수행되지 않는 스레드가 없게 한다.

    10. 포그라운드 프로세스를 위한 스케줄러 변경#

      어떤 프로세스가 윈도우를 가지고 있고 사용자가 그 윈도우를 이용하여 작업을 수행한다면 이러한 프로세스를 포그라운드 프로세스라고 하고, 그 외의 다른 프로세스를 백그라운드 프로세스라고 한다. 포그라운드 프로세스의 응답성을 개선하기 위해 윈도우는 포그라운드 프로세스에 대한 스레드 스케줄링 알고리즘을 약간 변경하여 포그라운드 프로세스에게 일반적으로 사용하는 퀀텀 시간에 비해 좀 더 긴 시간의 퀀텀 시간을 제공할 수 있도록 하고 있다. 이러한 개선사항은 포그라운드 프로세스가 보통 우선순위 클래스에서 수행될 때에만 적용되며, 다른 우선순위 클래스가 지정된 프로세스에는 적용되지 않는다.

    11. 선호도#

      기본적으로 윈도우 비스타는 스레드를 프로세서에 할당할 때 소프트 선호도를 사용한다. 이것은 다른 조건이 모두 동일하다면 마지막으로 수행했던 프로세서가 동일 스레드를 다시 수행하도록 하는 것을 말한다. 이렇게 하는 이유는 동일한 프로세서에서 스레드가 수행될 경우 프로세서의 메모리 캐시에 있는 데이터를 재사용할 가능성이 있기 때문이다.

      윈도우 비스타는 시스템 구조를 고려하여 프로세스와 스레드의 선호도를 지정할 수 있는 방법을 제공하고 있다. 즉, 어느 스레드를 어떤 CPU에서 수행할지를 제어하는 방법을 제공하는 것이다. 이를 하드선호도라고 한다.

      기본적으로 스레드는 어떤 CPU에서도 수행될 수 있다. 특정 프로세스 내의 스레드들을 전체 CPU 중 일부 CPU에서만 수행되도록 하려면 SetProcessAffinityMask 함수를 이용하면 된다.

      1. BOOL SetProcessAffinityMask ( HANDLE hProcess, DWORD_PTR dwProcessAffinityMask );

      첫 번째 매개변수인 hProcess는 프로세스를 가리키는 핸들 값이다. 두 번째 매개변수인 dwProcessAffinityMask는 어떤 CPU에서 스레드를 수행할지 여부를 나타내는 비트마스크 값이다. 이 값으로 0x00000005가 전달되면 CPU 0과 CPU 2번에서만 해당 프로세스 내의 스레드를 수행하겠다는 의미가 된다.

 

 

 

  1. 유저 모드에서의 스레드 동기화#

    다음 두 가지의 기본적인 상황에서 스레드는 상호 통신을 수행해야 한다.

    다수의 스레드가 공유 리소스에 접근해야 하며, 리소스가 손상되지 않도록 해야 하는 경우

    어떤 스레드가 하나 혹은 다수의 다른 스레드에게 작업이 완료되었음을 알려야 하는 경우

    1. 원자적 접근 : Interlocked 함수들#

      스레드 동기화를 수행하기 위해서는 리소스에 원자적으로 접근해야 한다. 원자적 접근이란 어떤 스레드가 특정 리소스를 접근할 때 다른 스레드는 동일 시간에 동일 리소스에 접근할 수 없는 것을 말한다. 인터락 계열 함수들은 모두 원자적으로 값을 다룬다. 또 이 함수들은 매우 빠르게 동작한다. 인터락 함수는 보통 수행을 완료하는 데 단 몇 CPU 사이클만을 필요로 하며 유저 모드와 커널 모드 간의 전환도 발생시키지 않는다.

    2. 캐시라인#

      CPU가 메모리로부터 값을 가져올 때는 바이트 단위로 값을 가져오는 것이 아니라 캐시 라인을 가득 채울 만큼 충분한 양을 한 번에 가져온다. 캐시 라인은 32바이트, 64바이트, 128바이트 크기로 구성되며( CPU에 따라 다르다 ), 각기 32바이트, 64바이트, 128바이트 경계로 정렬되어 있다. 캐시 라인은 성능 향상을 위해 존재하는 것이다. 보통의 경우 애플리케이션들은 인접한 바이트들을 자주 사용하는 경향이 있다. 만일 인접한 바이트들이 캐시 라인에 이미 존재한다면, 비교적 많은 시간을 소비하는 메모리 버스에 대해 CPU가 추가적으로 접근할 필요가 없게 된다.

      애플리케이션이 사용하는 데이터는 캐시 라인의 크기와 그 경계 단위로 묶어서 다루는 것이 좋다. 이렇게 함으로써 적어도 하나 이상의 캐시 라인 경계로 분리된 서로 다름 메모리 블록에 각각의 CPU가 독립적으로 접근하는 것을 보장할 수 있게 된다. 또한 읽기 전용의 데이터와 읽고 쓰는 데이터를 분리하는 것이 좋다. 그리고 동일한 시간에 접근하는 데이터들을 묶어서 구성하는 것이 좋다.

    3. 고급 스레드 동기화 기법#

      인터락 계열의 함수들은 하나의 값에 원자적으로 접근해야 하는 경우 훌륭하게 동작한다. 하지만 대부분의 애플리케이션들은 훨씬 더 복잡한 자료 구조를 다루는 것이 보통이다.

      1. volatile BOOL g_fFinishedCalculation = FALSE;

      volatile은 타입 한정자로서 컴파일러에게 이 변수가 운영체제나 하드웨어 혹은 동시에 수행중인 다른 스레드와 같이 외부에서 그 내용이 변경될 수 있음을 알려주는 역할을 한다. volatile 타입 한정자가 지정되면 컴파일러는 이 변수에 대해 어떠한 최적화도 수행하지 않으며, 변수의 값이 참조될 때 항상 메모리로부터 값을 다시 가지고 오도록 코드를 생성한다. 부울변수 선언 시 volatile을 지정하지 않으면 컴파일러는 코드를 최적화하고 그 결과로 BOOL 변수는 CPU 레지스터 상에 단 한 번만 로드되게 되고, CPU는 로드된 레지스터 값을 반복적으로 확인하게 된다. 분명 이렇게 하는 것이 매번 메모리로부터 값을 읽어와서 값을 확인하는 절차를 반복하는 것보다 더 좋은 성능을 보일 것이기 때문에 컴파일러의 최적화 기능은 이와 같이 코드를 생성한다. 인터락 함수들은 변수 값 자체를 인자로 취하지 않고 변수 값이 저장되어 있는 주소를 인자로 받아들이기 때문에 항시 메모리로부터 값을 얻어오게 되며, 최적화 기능은 이에 영향을 주지 않게 된다.

    4. 크리티컬 섹션#

      크리티컬 섹션이란 공유 리소스에 대해 배타적으로 접근해야 하는 작은 코드의 집합을 의미한다. 크리티컬 섹션은 공유 리소스를 다루는 여러 줄의 코드를 "원자적"으로 수행하기 위한 방법이다. 크리티컬 섹션의 우수한 점은 사용하기도 쉽고 내부적으로 인터락 함수를 사용하고 있기 때문에 매우 빠르게 동작한다는 것이다. 그러나 이러한 장점에도 불구하고 서로 다른 프로세스에 존재하는 스레드 사이의 동기화에는 사용할 수 없다는 치명적인 단점이 있다. 크리티컬 섹션을 사용하여 대기 상태가 된 스레드는 절대 기아 상태로 빠지지 않는다. EnterCriticalSection은 지정된 시간이 만료되는 경우 예외를 발생시키도록 작성되어 있다. 기본 값은 약 30일이다.

      1. 크리티컬 섹션과 스핀락#

        다른 스레드가 이미 진입한 크리티컬 섹션에 특정 스레드가 재진입을 시도하면, 스레드는 바로 대기 상태로 변경된다. 이것은 스레드가 유저모드에서 커널모드로 전환되어야 함을 의미하며, 이러한 과정은 매우 값비싼 동작에 해당한다. 성능을 개선하기 위해 마이크로소프트는 크리티컬 섹션에 스핀락 메커니즘을 도입하였다. 즉, EnterCriticalSection이 호출되면 일정 횟수 동안 스핀락을 사용하여 리소스 획득을 시도하는 루프를 수행하도록 하였다. 스핀락을 수행하는 동안 공유 리소스에 대한 획득에 실패한 경우에만 스레드를 대기 상태로 전환하기 위해 커널 모드로의 전환을 시도하도록 변경하였다.

        크리티컬 섹션에 스핀락을 사용하려면 크리티컬 섹션 초기화 시 다음 함수를 사용해야 한다.

        1. BOOL InitializeCriticalSectionAndSpinCount ( PCRITICAL_SECTION pcs, DWORD dwSpinCount );

        dwSpinCount는 스레드를 대기 상태로 변경하기 전에 리소스 획득을 위해 얼마만큼 스핀락 루프를 반복할지의 횟수를 전달하면 된다. 만일 이 함수를 단일 프로세서를 가진 머신에서 호출하게 되면 dwSpinCount 매개변수로 전달한 값은 무시되며 0으로 설정된다.

      2. 크리티컬 섹션과 에러 처리#

        아주 드문 경우이긴 하지만 InitializeCriticalSection 함수도 실패할 수 있다. 마이크로소프트는 이 함수를 설계할 때 이러한 에러 발생 가능성에 대해 전혀 생각하지 않았기 때문에 이 함수의 반환 값을 VOID로 선언하였다. 이 함수는 내부적으로 디버깅 정보를 저장하기 위한 메모리 블록을 할당하기 때문에 실패할 수 있다. 메모리 할당에 실패하면 STATUS_NO_MEMORY 예외가 발생하게 되는데, 구조적 예외 처리를 사용하면 이 에러를 확인할 수 있다.

    5. 슬림 리더 - 라이터 락#

      SRWLock ( Slim Reader - Writer Lock )은 단순 크리티컬 섹션과 유사하게 다수의 스레드로부터 단일의 리소스를 보호할 목적으로 사용된다. 차이점은, SRWLock의 경우 리소스의 값을 읽기만 하는 스레드(리더)들과 그 값을 수정하려는 스레드(라이터)들이 완전히 구분되어 있을 경우에만 사용할 수 있다는 것이다. 공유 리소스의 값을 읽기만 하는 리더들은 동시에 리소스에 접근한다 하더라도 공유 리소스의 값을 손상시키지 않기 때문에 동시에 수행되어도 무방하다. 동기화는 라이터 스레드가 리소스의 내용을 수정하려고 시도하는 경우에만 필요하며, 이 경우 리소스에 대한 배타적인 접근이 이루어져야 한다.

    6. 조건변수#

      조건변수를 사용하면 스레드가 리소스에 대한 락을 해제하고 SleepConditionVariableCS나 SleepConditionVariableSRW 함수에서 지정한 상태가 될 때까지 스레드를 블로킹해 준다.

      1. BOOL SleepConditionVariableCS ( PCONDITION_VARIABLE pConditionVariable,
      2. PCRITICAL_SECTION pCriticalSection,

      3. DWORD dwMilliseconds );

      4. BOOL SleepConditionVariableSRW ( PCONDITION_VARIABLE pConditionVariable,
      5. PSRWLOCK pSRWLock,

      6. DWORD dwMilliseconds,

      7. ULONG Flags );

      pConditionVariable 매개변수로는 스레드가 대기할 수 있도록 초기화된 조건변수를 가리키는 포인터를 전달하고, 두 번째 매개변수로는 공유 리소스에 대한 동기화를 위해 사용한 크리티컬 섹션이나 SRWLock 변수를 가리키는 포인터 값을 전달하면 된다. dwMilliseconds 매개변수로는 스레드가 조건변수가 시그널 상태가 될 때까지 얼마만큼 오랫동안 기다릴지를 결정하는 값을 전달해야 한다. Flags 매개변수로는 조건변수가 시그널 상태가 되었을 때 어떻게 락을 수행할지를 알려주는 값을 전달하면 된다. 두 개의 함수는 조건변수가 시그널 상태가 되기 전에 타임아웃이 되면 FALSE를 반환하고, 그렇지 않으면 TRUE를 반환한다. FALSE가 반환되는 경우에는 락을 수행하지 않으며, 크리티컬 섹션을 획득하지도 못한다는 점에 주의하기 바란다.

      특정 스레드가 SleepConditionVariable* 함수를 호출하여 블로킹 되어 있는 상태에서 리더 스레드가 읽어올 자료가 생겼다거나 라이터 스레드가 자료를 저장할 공간이 생긴 경우와 같이 적절한 상황이 되어 블로킹된 스레드를 깨워야 할 필요가 있다면 밑의 두 함수를 호출하면 된다.

      1. VOID WakeConditionVariable ( PCONDITION_VARIABLE ConditionVariable );
      2. VOID WakeAllConditionVariable ( PCONDITION_VARIABLE ConditionVariable );

      조건 변수는 항상 크리티컬 섹션이나 SRWLock과 함께 사용되어야 한다.

    7. 유용한 팁과 테크닉#

      크리티컬 섹션이나 리더-라이터 락과 같은 락을 사용할 때에는 반드시 사용해야 하거나 절대 사용하지 말아야 할 것이 있다.

      1. 원자적으로 관리되어야 하는 오브젝트 집합당 하나의 락만을 사용하라.

        예를 들어 컬렉션에 특정 항목을 추가하는 경우 항목 추가와 함께 컬렉션에 포함되어 있는 항목의 개수도 동시에 갱신되어야 할 것이다. 이와 같이 논리적으로 단일의 리소스처럼 사용되어야 하는 리소스들을 읽거나 쓸 경우에는 하나의 락만을 유지해야 한다.

      2. 다수의 논리적 리소스들에 동시에 접근하는 방법

        때때로 둘 이상의 논리적 리소스에 동시에 접근해야 할 때가 있다. 다수의 리소스에 락을 설정하는 순서를 항상 동일하게 유지하도록 코드를 작성해야 한다. LeaveCriticalSection의 호출 순서는 문제가 되지 않는데, 이는 이 함수가 스레드를 대기 상태로 변경하지는 않기 때문이다.

      3. 락을 장시간 점유하지 마라

        락을 너무 오랜 시간 점유하고 있게 되면 다른 스레드들이 계속 대기 상태에 머물러 있게 되기 때문에 애플리케이션의 성능에 나쁜 영향을 미칠 수 있다.

 

 

  1. 커널 오브젝트를 이용한 스레드 동기화#

    커널 오브젝트는 유저 모드 동기화 메커니즘에 비해 폭넓게 활용될 수 있다. 유일한 단점으로는 성능이 그다지 좋지 않다는 것이다. 유저 모드에서 커널 모드로의 전환을 필요로 한다. 모든 커널 오브젝트는 시그널 상태와 논시그널 상태가 될 수 있다. 스레드는 특정 오브젝트가 시그널 상태가 될 때까지 자신을 대기 상태로 만들 수 있다. 시그널 상태와 논시그널 상태에 대한 규칙은 커널 오브젝트의 타입별로 서로 상이하다.

    1. 대기 함수들#

      대기 함수를 호출하면 인자로 전달한 커널 오브젝트가 시그널 상태가 될 때까지 이 함수를 호출한 스레드를 대기 상태로 유지한다. 만일 대기 함수가 호출된 시점에 커널 오브젝트가 이미 시그널 상태였다면 대기 상태로 전환되지 않는다. 대기 함수 중 가장 많이 쓰이는 함수는 WaitForSingleObject 다.

      1. DWORD WaitForSingleObject ( HANDLE hObject, DWORD dwMilliseconds );

      이 함수를 이용하려면 첫 번째 매개변수인 hObject로 시그널과 논시그널 상태가 될 수 있는 커널 오브젝트의 핸들을 전달하고, 두 번째 매개변수인 dwMilliseconds로는 커널 오브젝트가 시그널 상태가 될 때까지 얼마나 오랫동안 기다려 볼 것인지를 나타내는 시간 값을 지정하면 된다. 이 함수의 반환 값은 함수를 호출한 스레드가 어떻게 다시 스케줄 가능하게 되었는지를 알려준다. 만일 스레드가 대기하던 오브젝트가 시그널되었다면 반환 값은 WAIT_OBJECT_0 가 되며, 타임아웃이 발생하였다면 반환 값은 WAIT_TIMEOUT이 된다.

      다음으로 살펴볼 함수는 여러 개의 커널 오브젝트들에 대해 시그널 상태를 동시에 검사할 수 있다.

      1. DWORD WaitForMultipleObjects ( DWORD dwCount, CONST HANDLE * phObjects,
      2. BOOL bWaitAll, DWORD dwMilliseconds );

      dwCount 매개변수로는 이 함수가 검사해야 하는 커널 오브젝트들의 개수를 지정한다. 이 값으로는 1부터 MAXIMUM_WAIT_OBJECTS( WinNT.h 헤더 파일에 64로 정의되어 있다) 범위 내의 값을 지정할 수 있다. phObjects 매개변수로는 커널 오브젝트 핸들의 배열을 가리키는 포인터를 지정하면 된다.

      WaitForMultipleObjects는 서로 다른 두 가지 방법으로 사용될 수 있는데, 첫 번째 방법은 매개변수로 전달한 커널 오브젝트들 전체가 시그널 상태가 될 때까지 스레드를 대기 상태로 두는 것이고, 두 번째 방법은 이 중 하나만이라도 시그널 상태가 되면 대기 상태에서 빠져나오도록 하는 방법이다. bWaitAll 매개변수를 통해 이 함수가 어떠한 방식으로 동작할지를 결정하게 되는데, 이 매개변수로 TRUE를 전달하면 모든 오브젝트들이 시그널 상태가 될 때까지 스레드를 대기 상태로 두게 된다. 만일 bWaitAll로 TRUE 값을 지정하였으며 모든 오브젝트들이 시그널 상태가 된 경우, 함수의 반환 값은 WAIT_OBJECT_0이 된다. 만일 bWaitAll로 FALSE를 지정하였다면, 지정한 오브젝트들 중 하나라도 시그널 상태가 되면 함수가 반환될 것이다. 이 경우 과연 어떤 오브젝트가 시그널 상태가 되었는지를 알 수 있어야 할 것이다. 이 경우 반환 값은 WAIT_OBJECT_0과 ( WAIT_OBJECT_0 + dwCount - 1 ) 사이의 값이 된다. 다시 말하면 반환 값에서 WAIT_OBJECT_0을 뺀 값이 WaitForMulitpleObjects의 두 번째 매개변수로 전달하였던 커널 오브젝트 핸들 배열 중 시그널된 오브젝트가 저장된 배열의 인덱스가 된다.

      예제 코드

      1. HANDLE h[3];
      2. h[0] = hProcess1;
      3. h[1] = hProcess2;
      4. h[2] = hProcess3;
      5. DWORD dw = WaitForMultipleObjects( 3, h, FALSE, 5000 );
      6. switch( dw ) {
      7. case WAIT_FAILED:

      8. // 함수를 잘못 호출하였다.

      9. break;

      10. case WAIT_TIMEOUT:

      11. // 5000밀리초 이내에 어떠한 오브젝트도 시그널 상태가 되지 못했다.

      12. break;

      13. case WAIT_OBJECT_ 0+ 0:

      14. // h[0] 이 가리키는 프로세스가 종료되었다.

      15. break;

      16. case WAIT_OBJECT_0 + 1:

      17. // h[1] 이 가리키는 프로세스가 종료되었다.

      18. break;

      19. case WAIT_OBJECT_0 + 2:

      20. // h[2] 이 가리키는 프로세스가 종료되었다.

      21. break;

      22. }

      bWaitAll 매개변수로 FALSE를 전달한 경우 WaitForMultipleObjects는 인자로 전달한 배열을 0번째 배열 항목으로부터 오름차순으로 오브젝트의 시그널 상태를 확인한다. 이러한 동작 방식은 예기치 않은 문제를 유발할 가능성이 있다. 예를 들어 스레드가 3개의 자식 프로세스가 종료되기를 기다린다고 가정하고 각 프로세스를 나타내는 핸들 값을 배열로 구성하여 WaitForMulitpleObjects 함수를 호출하였다고 하자. 이 경우 배열 인덱스가 0인 프로세스가 종료되면 이 함수는 반환될 것이다. 필요한 작업을 수행하고 나머지 프로세스의 종료를 기다리기 위해 다시 한 번 동일한 인자 값으로 이 함수를 호출했다고 하자. 이 경우 WaitForMultipleObjects는 즉각 WAIT_OBJECT_0 값을 반환하게 될 것이다. 배열로부터 이미 시그널 상태가 된 항목을 제거하지 않는 이상 이러한 코드는 정상적으로 수행되지 않을 것이다.

    2. 성공적인 대기의 부가적인 영향#

      일부 커널 오브젝트들은 WaitForSingleObject나 WaitForMulitpleObjects가 성공적으로 호출되는 경우 오브젝트의 상태가 변경되는 경우가 있다. '성공적인 호출'이란 함수 호출 시 인자로 전달한 커널 오브젝트가 시그널 상태가 되어 WAIT_OBJECT_0을 반환하는 경우를 말한다. 성공적인 호출을 통해 오브젝트의 상태가 변경되는 것을 일컬어 '성공적인 대기의 부가적인 형향'이라고 한다. 예를 들어 어떤 스레드가 자동 리셋 이벤트 커널 오브젝트 핸들을 인자로 대기 함수를 호출하는 경우, 이 오브젝트가 시그널 상태가 되면 WAIT_OBJECT_0을 반환할 것이다. 또한 함수가 반환되기 직전에 '성공적인 대기의 부가적인 영향'으로 인해 이벤트 커널 오브젝트는 논시그널 상태로 변경될 것이다.

      스레드가 WaitForMultipleObjects를 호출하면 모든 오브젝트들의 상태를 확인하고 성공적인 대기로 인한 오브젝트의 상태 변경까지도 원자적으로 수행해 준다. 이 함수가 내부적으로 커널 오브젝트들의 상태를 확인하는 시점에는 다른 어떤 스레드도 오브젝트들의 상태를 변경하지 못한다. 이렇게 함으로써 데드락이 발생하는 것을 미연에 방지할 수 있다. 만일 다수의 스레드가 하나의 커널 오브젝트를 대기 하고 있는 경우라면 커널 오브젝트가 시그널 상태가 되었을 때 어떤 스레드를 깨어나게 할 것인가? 마이크로소프트의 공식적인 답변은 "알고리즘은 공평하다"이다. 이는 내부적인 알고리즘이 스레드 우선순위에 영향을 받지 않음을 의미한다.

    3. 이벤트 커널 오브젝트#

      이벤트는 어떤 작업이 완료되었음을 알리기 위해 주로 사용되며, 수동 리셋 이벤트와 자동 리셋 이벤트의 서로 다른 두 가지 형태가 있다. 수동 리셋 이벤트가 시그널 상태가 되면 이 이벤트를 기다리고 있던 모든 스레드들은 동시에 스케줄 가능 상태가 된다. 자동 리셋 이벤트의 경우에는 대기 중인 스레드들 중 하나의 스레드만이 스케줄 가능 상태가 된다.

      1. HANDLE CreateEvent ( PSECURITY_ATTRIBUTES psa,
      2. BOOL bManualReset,

      3. BOOL bInitialState,

      4. PCTSTR pszName );

      bManualRest 매개변수는 부울 값으로 시스템에게 수동 리셋 이벤트(TRUE)를 생성할 것인지, 자동 리셋 이벤트(FALSE)를 생성할 것인지의 여부를 전달하면 된다. bInitialState 매개변수로는 이벤트의 초기 상태를 시그널 상태(TRUE)로 만들 것인지, 논시그널 상태(FALSE)로 만들 것인지를 결정하는 값을 전달하게 된다.

    4. 대기 타이머 커널 오브젝트#

      대기 타이머는 특정 시간에 혹은 일정한 간격을 두고 자신을 시그널 상태로 만드는 커널 오브젝트로서, 주로 특정 시간에 맞추어 어떤 작업을 수행해야 할 경우에 사용된다.

      대기 타이머를 생성하려면 단순히 CreateWaitableTimer를 호출하면 된다.

      1. HANDLE CreateWaitableTimer ( PSECURITY_ATTRIBUTS psa,
      2. BOOL bManualReset,

      3. PCTSTR pszName );

      다른 프로세스에서는 OpenWaitableTimer 함수를 이용하여 이미 생성된 대기 타이머를 가리키는 프로세스 고유의 핸들 값을 얻을 수 있다.

      1. HANDLE OpenWaitableTimer ( DWORD dwDesireAccess, BOOL bInheritHandle, PCTSTR pszName );

      bManualRest 매개변수는 수동 리셋 타이머를 생성할 것인지 아니면 자동 리셋 타이머인지를 생성할 것인지를 결정하는 값을 전달한다. 자동 리셋 타이머가 시그널 상태가 되면 이 타이머를 대기 중인 스레드들 중 유일하게 한 개의 스레드만이 스케줄 가능 상태가 된다.

      대기 타이머는 항상 논시그널 상태로 생성되며, 언제 시그널 상태가 될 것인지를 지정하기 위해 SetWaitableTimer 함수를 사용한다.

      1. BOOL SetWaitableTimer ( HANDLE hTimer,
      2. const LARGE_INTEGER * pDueTime,

      3. LONG lPeriod,

      4. PTIMERAPCROUTINE pfnCompletionRoutine,

      5. PVOID pvArgToCompletionRoutine,

      6. BOOL bResume );

      hTimer 매개변수는 설정하고자 하는 대기 타이머를 나타내는 핸들 값이다. 두 번째 매개변수인 pDueTime과 lPeriod는 항상 같이 사용되는데, pDueTime은 시그널 상태가 되는 최초 시간을, lPeriod는 그 후 얼마의 주기로 시그널 상태를 반복할 것인지를 지정한다.

      타이머가 시그널 상태가 되어야 하는 절대 시간을 설정하지 않고 상대적인 시간을 이용하여 SetWaitableTimer를 호출할 수도 있다. 이 경우에는 pDueTime 매개변수로 음수 값을 전달하면 된다. 또한 100 나노초 단위로 시간을 지정해야 한다. 단 한 번만 시그널 상태가 되고 주기적으로 시그널 상태를 반복할 필요가 없는 타이머를 생성하려면 lPeriod 매개변수로 0을 전달하면 되고, 이후에 타이머를 삭제하기 위해서는 CloseHandle을 호출하면 된다. 시간을 재설정하기 위해서는 SetWaitableTimer를 다시 호출하면 되고, 이 함수 호출 이후에는 새로운 기준을 따르게 된다.

    5. 세마포어 커널 오브젝트#

      세마포어 커널 오브젝트는 리소싀 개수를 고려해야 하는 상황에서 주로 사용된다. 이 커널 오브젝트는 모든 커널 오브젝트와 마찬가지로 사용 카운트를 가지고 있으며, 이 외에도 2개의 32비트 값을 가지고 있어서 최대 리소스 카운트와 현재 리소스 카운트를 저장하고 있다. 최대 리소스 카운트는 세마포어가 제어할 수 있는 리소스의 최대 개수를 나타내는 데 사용되고, 현재 리소스 카운트는 사용 가능한 리소스의 개수를 나타내는 데 사용된다.

      세마포어는 다음과 같은 규칙에 따라 동작한다.

      현재 리소스 카운트가 0보다 크면 세마포어는 시그널 상태가 된다.

      현재 리소스 카운트가 0이면 세마포어는 논시그널 상태가 된다.

      시스템은 현재 리소스 카운트를 음수로 만들 수 없다.

      현재 리소스 카운트는 최대 리소스 카운트보다 커질 수 없다.

      세마포어 커널 오브젝트를 생성하려면 CreateSemaphore 함수를 사용하면 된다.

      1. HANDLE CreateSemaphore ( PSECURITY_ATTRIBUTE psa, LONG lInitialCount,
      2. LONG lMaximumCount, PCTSTR pszName );

      lMaximumCount 매개변수로는 애플리케이션에서 사용할 수 있는 리소스의 최대 개수를 지정하면 된다. lInitialCount 매개변수로는 현재 사용 가능한 리소스의 개수를 지정하면 된다.

      스레드가 리소스에 대한 접근을 요청하기 위해 대기 함수를 호출할 때에는 세마포어의 핸들을 전달하면 된다. 대기 함수는 내부적으로 세마포어의 현재 리소스 카운트 값을 확인하여 이 값이 0보다 크면 값을 1만큼 감소시키고 대기 함수를 호출한 스레드를 스케줄 가능 상태로 만든다. 세마포어의 현재 리소스 카운트를 증가 시키기 위해서는 ReleaseSemaphore 함수를 호출하면 된다.

      1. BOOL ReleaseSemaphore ( HANDLE hSemaphore, LONG lReleaseCount, PLONG plPreviousCount );

      이 함수는 단순히 lReleaseCount에 지정된 값만큼 세마포어의 현재 리소스 카운트를 증가시키는 역할을 수행한다. 보통의 경우 lReleaseCount 매개변수로 1을 전달하지만 항상 그렇게 사용해야 하는 것은 아니다. 실제로 2나 그 이상의 수를 사용하기도 한다. *plPreviousCount로 증가되기 이전의 현재 리소스 카운트 값을 반환해 준다.

    6. 뮤텍스 커널 오브젝트#

      뮤텍스 커널 오브젝트는 스레드가 단일의 리소스에 대해 배타적으로 접근할 수 있도록 해 준다. 이 커널 오브젝트는 사용 카운트, 스레드 ID, 반복 카운터를 저장할 수 있는 공간을 가지고 있다. 뮤텍스의 동작 방식은 크리티컬 섹션과 동일하다. 하지만 크리티컬 섹션이 유저 모드 동기화 오브젝트인 데 반해 뮤텍스는 커널 오브젝트라는 차이점이 있다. 이러한 차이점 때문에 뮤텍스는 크리티컬 섹션에 비해 느리지만, 서로 다른 프로세스에서 동일 뮤텍스에 대해 접근이 가능하며, 리소스에 대한 접근 권한을 획득할 때 시간 제한을 지정할 수 있다는 장점이 있다. 스레드 ID는 시스템 내의 어떤 스레드가 뮤텍스를 소유하고 있는지를 나타내는 값이다. 반복 카운터는 뮤텍스를 소유하고 있는 스레드가 몇 회나 반복적으로 뮤텍스를 소유하고자 했는지에 대한 횟수를 나타내는 값이다. 뮤텍스는 다음의 규칙에 따라 동작한다.

      스레드 ID가 0 ( 유효하지 않은 스레드 ID )이면 뮤텍스는 어떠한 스레드에 의해서도 소유되지 않은 것이며, 이때 뮤텍스는 시그널 상태가 된다.

      스레드 ID가 0이 아니면 뮤텍스는 특정 스레드에 의해 소유된 것이며, 이때 논시그널 상태가 된다.

      다른 커널 오브젝트와는 다르게 뮤텍스는 특수한 코드를 포함하고 있어서 일반적인 규칙을 위반하는 경우도 있다.

      뮤텍스를 사용하려면 CreateMutex 함수를 호출해서 뮤텍스를 생성해야 한다.

      1. HANDLE CreateMutex ( PSECURITY_ATTRIBUTES psa, BOOL bInitialOwner, PCTSTR pszName );

      bInitialOwner 매개변수는 뮤텍스의 초기 상태를 제어하는 용도로 사용된다. 이 값을 FALSE(보통의 경우)로 설정하면 뮤텍스의 스레드 ID의 반복 카운터는 0으로 설정된다. 이것은 뮤텍스가 어떠한 스레드에 의해서도 소유되지 않았으며, 시그널 상태임을 나타내게 된다. 만일 bInitialOwner 값을 TRUE로 설정하게 되면 뮤텍스의 스레드 ID는 함수를 호출한 스레드의 ID로 설정되며, 반복 카운터는 1로 설정된다. 스레드 ID가 0이 아니므로 뮤텍스는 논시그널 상태가 된다.

      뮤텍스의 경우 내부적인 스레드 ID 값이 대기 함수를 호출한 스레드의 ID 값과 동일한 경우 뮤텍스 오브젝트가 논시그널 상태임에도 불구하고 이 스레드를 스케줄 가능 상태로 만든다. 반복 카운터가 1을 초과하는 값으로 변경되는 유일한 경우는 동일 뮤텍스 오브젝트에 대해 동일 스레드가 여러 번 대기 함수를 호출한 경우에만 예외적으로 일어날 수 있다. 리소스에 대한 접근 권한을 획득한 스레드가 더 이상 리소스를 사용할 필요가 없어지면 반드시 ReleaseMutex 함수를 호출하여 뮤텍스의 소유권을 해제해 주어야 한다.

      1. BOOL ReleaseMutex ( HANDLE hMutex );

      이 함수는 오브젝트의 반복 카운터 값을 1만큼 감소시킨다. 만일 동일 뮤텍스에 대해 동일 스레드가 여러 번에 걸쳐 성공적인 대기를 수행한 경우라면 스레드는 동일 횟수만큼 ReleaseMutex 를 호출해야만 뮤텍스의 반복 카운터 값을 0으로 만들 수 있다.

      1. 버림 문제

        뮤텍스는 다른 모든 커널 오브젝트와는 다르게 "스레드 소유권"의 개념을 가지고 있다. 다른 어떤 커널 오브젝트도 어떤 스레드가 성공적인 대기를 수행하였는지를 기록해 두지 않는 데 반해, 유일하게 뮤텍스만이 어떤 스레드가 성공적인 대기를 수행하였는지를 기록해 둔다. 이러한 뮤텍스의 스레드 소유권이라는 개념 때문에 뮤텍스가 논시그널 상태임에도 불구하고 스레드가 뮤텍스를 다시 소유할 수 있는 예외적인 규칙을 가지게 된 것이다.

        이러한 예외상황은 뮤텍스에 대한 소유권을 해제하는 과정에서도 나타난다. 스레드가 ReleaseMutex를 호출하면 함수 내부적으로 뮤텍스 오브젝트가 가지고 있는 스레드의 ID가 함수를 호출한 스레드의 ID와 동일한지 여부를 확인한다. 만일 두 값이 동일하면 앞서 말한 것처럼 반복 카운트가 감소된다. 만일 두 값이 동일하지 않으면 ReleaseMutex는 아무런 작업도 하지 않고 FALSE 값을 반환한다.

        뮤텍스 버림이란 뮤텍스를 소유한 스레드가 종료되어 소유권을 해제하지 못하는 경우를 말한다. 뮤텍스의 버림이 발생하면 버려진 뮤텍스의 스레드 ID와 반복 카운터를 0으로 변경한다.

    7. 그 외의 스레드 동기화 함수들#

      1. WaitForInputIdle

        1. DWORD WaitForInputIdle ( HANDLE hProcess, DWORD dwMilliseconds );

        이 함수를 호출하면 hProcess가 가리키는 프로세스의 첫 번째 윈도우를 생성한 스레드가 대기 상태가 될 때까지 WaitForInputIdle 함수를 호출한 스레드를 대기 상태로 유지한다.

      2. MsgWaitForMultipleObjects(Ex)

        1. DWORD MsgWaitForMultipleObjects ( DWORD dwCount,
        2. PHANDLE phObjects,

        3. BOOL bWaitAll,

        4. DWORD dwMilliseconds,

        5. DWORD dwWakeMask );

        이 함수는 WaitForMulitpleObjects 함수와 매우 유사하다. 차이점이라면 이 함수들은 커널 오브젝트가 시그널될 때 외에도 이 함수를 호출한 스레드가 생성한 윈도우에 메시지가 전달되었을 경우에도 스케줄 가능 상태가 된다는 것이다.

      3. WaitForDebugEvent

        디버거가 수행되고 디버기가 연결되면 디버거는 운영체제가 디버기와 관련된 디버그 이벤트를 줄 때까지 유휴 상태로 대기하게 된다. 디버거가 디버그 이벤트를 기다리기 위해서는 WaitForDebugEvent 함수를 호출하면 된다.

        1. BOOL WaitForDebugEvent ( PDEBUG_EVENT pde, DWORD dwMilliseconds );

        디버가가 이 함수를 호출하면 디버거의 스레드는 대기 상태가 된다. 시스템은 디버그 이벤트가 발생한 경우 WaitForDebugEvent가 반환되도록 하여 디버그 이벤트가 발생하였음을 알려준다. pde 매개변수가 가리키는 구조체는 디버거의 스레드가 깨어나기 전에 시스템에 의해 채워지며, 어떤 디버그 이벤트가 발생했는지에 대한 정보를 포함하고 있다.

      4. SignalObjectAndWait

        특정 커널 오브젝트를 시그널 상태로 만들어주고, 이와는 또 다른 커널 오브젝트가 시그널 상태가 되기를 대기하는 기능을 원자적으로 수행한다.

        1. DWORD SignalObjectAndWait ( HANDLE hObjectToSignal,
        2. HANDLE hObjectToWaitOn,

        3. DWORD dwMilliseconds,

        4. BOOL bAlertable );

        이 함수를 호출할 때에는 hObjectToSignal 매개변수로 뮤텍스, 세마포어, 또는 이벤트가 전달되어야 하며, 이 함수는 전달되는 핸들의 오브젝트 타입에 맞추어 내부적으로 ReleaseMutex, ReleaseSemaphore(1을 인자로), 또는 SetEvent 함수를 각각 호출해 준다.

        이 함수는 두 가지 이유로 인해 윈도우에 추가되었다. 첫째로, 개발자들은 특정 오브젝트를 시그널 상태로 만들어 준 후 다른 오브젝트를 대기하는 식의 코드를 자주 작성하게 되는데, 단일 함수로 이와 같은 작업을 수행하게 되면 수행 시간을 절약해 주는 효과가 있다. 둘째로, SignalObjectAndWait 함수를 이용하면 이 함수를 호출한 스레드가 대기 상태에 있음을 보장할 수 있기 때문에 PulseEvent와 같은 함수를 사용할 때 유용하게 활용될 수 있다.

 

 

  1. 동기 및 비동기 장치 I/O#

    1. 장치 열기와 닫기#

      어떤 형태의 I/O를 수행하든지 장치에 대해 가장 먼저 수행해야 할 작업은 열기 작업이다. 열기 작업을 수행하여 핸들 값을 획득하는 방법은 사용하고자 하는 장치가 무엇이냐에 따라 서로 상이하다. 열기 함수들은 각각의 장치를 구분할 수 있는 고유의 핸들 값을 반환한다. 장치와 통신을 수행하는 함수들을 사용하려면 이 핸들 값을 인자로 전달해야 한다. 장치를 모두 사용하였다면 반드시 닫기를 수행해야 한다. 대부분의 장치에 대해 핸들을 닫기 위해서는 CloseHandle 함수를 사용하면 된다. 하지만 사용하던 장치가 소켓이라면 closesocket 함수를 호출해야 한다. 또한 장치에 대한 핸들을 알고 있다면, 이 핸들 값을 이용하여 장치의 타입을 알아내기 위해 GetFileType 함수를 이용할 수 있다.

      1. DWORD GetFileType ( HANDLE hDevice );

      CreateFile 함수를 이용하면 디스크에 새로운 파일을 생성하거나 기존 파일에 대한 열기를 수행할 수 있을 뿐만 아니라 파일이 아닌 다른 장치에 대해서도 열기 작업을 수행할 수 있다.

      1. HANDLE CreateFile ( PCTSTR pszName, DWORD dwDesiredAccess,
      2. DWORD dwShareMode, PSECURITY_ATTRIBUTES psa,

      3. DWORD dwCreateionDisposition, DWORD dwFlagsAndAttributes,

      4. HANDLE hFileTemplate );

      pszName 매개변수로는 특정 장치의 인스턴스를 나타내는 값을 전달할 수 있을 뿐만 아니라 장치의 타입을 구분할 수 있는 값을 전달할 수도 있다. dwDesiredAccess 매개변수는 장치와 데이터를 어떻게 주고받기를 원하는지 결정하기 위해 사용된다. dwShareMode 매개변수는 장치의 공유 특성을 지정하는 데 사용된다. psa 매개변수는 보안 정보를 설정하거나 CreateFile이 반환하는 핸들을 상속 가능하도록 구성할 것인지의 여부를 결정하는 SECURITY_ATTRIBUTES 구조체를 가리키는 포인터로 설정된다. dwCreationDispotion 매개변수는 CreateFile 함수를 파일 장치에 대해 사용할 때 가장 큰 의미를 가진다. dwFlagsAndAttributes 매개변수는 두 가지 목적으로 사용된다. 하나는 데이터를 송수신할 때 세부적인 통신 플래그를 설정하기 위한 용도로 사용되며, 다른 하나는 파일의 특성을 설정하기 위한 용도로 사용된다.

      CreateFile 캐시 플래그

      FILE_FLAG_NO_BUFFERING. 이 플래그를 사용하면 파일에 접근할 때 어떠한 버퍼링도 수행하지 않는다. 특수한 목적이 있는 경우가 아니라면 이 플래그는 가능한 사용하지 않는 것이 좋다. 이 플래그를 사용하지 않으면 캐시 매니저는 가장 최근에 접근한 파일시스템의 영역을 메모리에 유지한다. 이렇게 하면 파일로부터 몇 바이트 정도를 읽어온 후 추가적으로 몇 바이트를 더 읽어오려 하는 경우 그 내용이 이미 메모리에 로드되어 있을 가능성이 높기 때문에 디스크에 다시 접근하지 않아도 되고, 이는 상당한 개선 효과를 가져온다.

      FILE_FLAG_SEQUENTIAL_SCANFILE_FLAG_RANDOM_ACCESS. 이 플래그들은 시스템이 파일 데이터에 대한 버퍼링을 수행하는 경우에만 유용하다. 만일 FILE_FLAG_NO_BUFFERING 플래그와 함께 사용되면 이 플래그들은 모두 무시된다.

      FILE_FLAG_SEQUENTIAL_SCAN 플래그를 사용하면 시스템은 사용자가 파일을 순차적으로 접근할 것으로 생각한다. 따라서 파일 읽기를 시도하는 경우 실제로 필요한 크기보다 더 많은 데이터를 메모리로 읽어 들인다. 이러한 방법으로 하드 디스크에 대한 직접적인 접근 횟수를 줄여 애플리케이션의 성능을 향상시킨다. 하지만 파일에 대한 접근 위치를 자주 이동하는 경우라면 FILE_FLAG_RANDOM_ACCESS 플래그를 지정하는 편이 낫다. 이 플래그를 사용하면 시스템이 파일 데이터를 미리 읽지 않도록 한다.

      FILE_FLAG_WRITE_THROUGH. 이 플래그를 사용하면 파일에 데이터를 쓸 때 데이터 손실의 가능성을 줄이기 위한 중간 캐싱 기능을 사용하지 않도록 한다. 이 플래그를 지정하면 시스템은 파일에 대한 변경사항을 디스크에 직접 쓰게 된다.

      기타 CreateFile 플래그

      FILE_FLAG_DELETE_ON_CLOSE. 이 플래그를 사용 시 파일과 관련된 모든 핸들이 닫히면 파일이 삭제된다.

      FILE_FLAG_OVERLAPPED. 이 플래그는 시스템에게 장치에 대해 비동기적으로 접근하길 원한다고 알려준다. 이 플래그를 사용하지 않고 장치에 대해 열기를 수행하면 기본적으로 동기 I/O를 수행하게 된다.

      핸들을 반환하는 대부분의 윈도우 함수들은 함수가 실패했을 때 NULL을 반환한다. 하지만 CreateFile은 이와 다르게 INVALID_HANDLE_VALUE(-1)를 반환한다.

    2. 파일 장치 이용#

      윈도우에서 파일 작업을 수행할 때 가장 먼저 알아두어야 할 것은 윈도우 운영체제가 매우 큰 파일을 다룰 수 있다는 사실이다. 윈도우는 최초 설계 시부터 파일의 크기를 나타내기 위해 32비트 값이 아닌 64비트 값을 이용하였다. 따라서 이론적으로 파일의 크기는 최대 16EB(엑사바이트)가 될 수 있다.

      1. 파일 크기 얻기#

        파일의 크기를 얻어오는 가장 쉬운 방법은 GetFileSizeEx 함수를 이용하는 것이다.

        1. BOOL GetFileSizeEx ( HANDLE hFile, PLARGE_INTEGER pliFileSize );

        hFile은 파일의 핸들이며, pliFileSize 매개변수는 LARGE_INTEGER로 선언된 구조체를 가리키는 포인터다. 파일의 크기를 얻어오는 또 다른 함수로는 GetCompressedFileSize가 있다.

        1. DWORD GetCompressedFileSize ( PCTSTR pszFileName, PDWORD pdwFileSizeHigh );

        GetFileSizeEx가 파일의 논리적인 크기를 반환하는 데 반해, 이 함수는 파일의 물리적인 크기를 반환한다. 예를 들어 100KB 파일이 있고, 이 파일이 85KB로 압축되었다고 하자. 이 경우 GetFileSizeEx 함수를 호출하면 논리적인 파일의 크기인 100KB가 반환되지만, GetCompressedFileSize는 실제로 디스크를 점유하고 있는 크기인 85KB를 반환한다.

      2. 파일 포인터 위치 지정#

        CreateFile을 호출하면 시스템은 파일에 대한 작업을 관리하기 위한 파일 커널 오브젝트를 생성한다. 이 커널 오브젝트는 내부적으로 파일 포인터를 가지고 있다. 파일 포인터란 64비트 오프셋 값으로 동기적인 I/O를 수행할 위치 정보를 가지고 있다. 파일 열기를 수행하면 파일 포인터가 0으로 초기화된다. 각각의 파일 커널 오브젝트는 자신만의 파일 포인터를 가지고 있기 때문에 동일 파일을 여러 번 여는 경우 각각은 서로 독립적으로 수행된다.

        파일의 임의 위치에 접근하려 하는 경우 SetFilePointerEx 함수를 호출하여 파일 커널 오브젝트 내의 파일 포인터 값을 변경하면 된다.

        1. BOOL SetFilePointerEx ( HANDLE hFile, LARGE_INTEGER liDistanceToMove,
        2. PLARGE_INTEGER pliNewFilePointer, DWORD dwMoveMethod );

        hFile 매개변수로는 변경하고자 하는 파일 포인터를 가지고 있는 파일 커널 오브젝트를 참조하는 핸들을 전달하면 된다. liDistanceToMove 매개변수로는 파일 포인터를 얼마만큼 인동하고자 하는지를 바이트 단위로 전달하면 된다. 이 값은 파일 포인터에 더해지기 때문에 음수를 지정하여 역방향으로 이동할 수도 있다. SetFilePointerEx의 마지막 매개변수인 dwMoveMethod로는 시스템이 liDistanceToMove 매개변수를 어떻게 해석할 것인지 알려주어야 한다( FILE_BEGIN, FILE_CURRENT, FILE_END ).

      3. 파일의 끝 설정#
        1. BOOL SetEndOfFile ( HANDLE hFile );

        파일 커널 오브젝트의 파일 포인터가 가리키는 현재 위치를 파일의 끝으로 설정함으로써 기존 파일의 크기를 더 확장하거나 더 작게 줄일 수 있다.

    3. 동기 장치 I/O 수행#

      가장 쉽고도 일반적인 함수는 장치로부터 읽고 쓰는 것이며, 이는 ReadFile과 WriteFile 함수를 통해 수행 할 수 있다.

      1. BOOL ReadFile ( HANDLE hFile, PVOID pvBuffer, DWORD nNumbytesToRead,
      2. PDWORD pdwNumBytes, OVERLAPPED *pOverlapped );

      3. BOOL WriteFile ( HANDLE hFile, CONST VOID *pvBuffer, DWORD nNumBytesToWrite,
      4. PDWORD pdwNumBytes, OVERLAPPED *pOverlapped );

      hFile 매개변수로는 읽고 쓰기를 수행할 장치를 가리키는 핸들 값을 전달하면 된다. 장치를 열 때 FILE_FLAG_OVERLAPPED 플래그를 사용해서는 안 된다. 이 플래그를 사용하여 장치를 열면 시스템은 사용자가 비동기 I/O를 수행할 것으로 기대한다. pvBuffer 매개변수로는 장치로부터 읽어온 데이터를 저장할 버퍼를 가리키는 포인터를 지정하거나, 장치로 쓸 데이터를 저장하고 있는 버퍼를 가리키는 포인터를 지정하면 된다. nNumBytesToRead와 nNumBytesToWrite 매개변수로는 ReadFile과 WriteFile을 이용하여 장치로부터 몇 바이트를 읽거나 쓰기를 원하는지 전달하면 된다. pdwNumBytes 매개변수로는 DWORD 값을 저장할 수 있는 변수의 주소를 전달하면 되는데, 이 값으로는 장치와 성공적으로 송수신한 데이터의 크기를 받아오게 된다. 마지막 매개변수인 pOverlapped는 동기 I/O를 수행하려는 경우에는 NULL로 지정하면 된다.

      ReadFile은 GENERIC_READ 플래그를 포함하여 장치가 열렸을 경우에만 성공적으로 수행될 수 있으며, WriteFile은 GENERIC_WRITE 플래그를 포함하여 장치가 열렸을 경우에만 성공적으로 수행될 수 있다.

    4. 비동기 장치 I/O의 기본#

      스레드가 비동기 I/O 요청은 실제로 I/O 작업을 수행할 장치의 디바이스 드라이버로 전달된다. 디바이스 드라이버가 장치로부터의 응답을 대기해 주기 때문에 애플리케이션의 스레드는 I/O 요청이 완료될 때까지 대기할 필요 없이 다른 작업을 계속 수행할 수 있다.

      장치에 비동기적으로 접근하려면 CreateFile을 호출하여 장치에 대한 열기 작업을 수행할 때 dwFlagsAndAttributes 매개변수로 FILE_FLAG_OVERLAPPED 플래그를 전달해 주어야 한다.

      1. OVERLAPPED 구조체#

        비동기 장치 I/O를 수행하려면 pOverlapped 매개변수를 통해 초기화된 OVERLAPPED 구조체를 가리키는 주소를 전달해 주어야 한다.

        1. typedef struct _OVERLAPPED {
        2. DWORD Internal;         // [out] 에러 코드

        3. DWORD InternalHigh;   // [out] 전송된 바이트 수

        4. DWORD Offset;            // [in] 32비트 하위 파일 오프셋

        5. DWORD OffsetHigh;      // [in] 32비트 상위 파일 오프셋

        6. HANDLE hEvent;         // [in] 이벤트 핸들이나 데이터

        7. } OVERLAPPED, *LPOVERLAPPED;

        이 중 3개의 멤버들 - Offset, OffsetHigh, hEvent - 은 ReadFile이나 WriteFile을 호출하기 전에 초기화되어야 한다. 나머지 두 개의 멤버는 디바이스 드라이버에 의해 설정되며, I/O 작업이 완료되었는지 여부를 확인하기 위해 사용될 수 있다.

        OffsetOffsetHigh. 파일에 대한 I/O를 수행하는 경우 이 멤버들을 통해 파일 내에서 I/O 작업을 시작할 위치를 64비트 오프셋 값으로 지정할 수 있다. 파일이 아닌 다른 장치를 다루는 경우에도 무시되지 않기 때문에 주의할 필요가 있다. 만일 이 두 값을 0으로 설정하지 않고 I/O 요청을 시도하면, 요청은 실패하게 되고 GetLastError는 ERROR_INVALID_PARAMETER 값을 반환하게 된다.

        hEvent. 이 멤버는 I/O 완료 통지를 수신하는 네 가지 방법 중 하나의 방법에서 사용된다. 얼러터블 /O 통지 방법을 사용하는 경우에는 사용자 임의로 이 멤버를 사용할 수도 있다.

        Internal. 이 멤버는 처리된 I/O의 에러코드를 담는 데 사용된다. 비동기 I/O 요청을 시도하면 디바이스 드라이버는 Internal 멤버 값을 STATUS_PENDING으로 설정하여 아직 작업이 완료되지 않았으며 어떠한 에러도 발생하지 않았음을 나타낸다.

        InternalHigh. 비동기 I/O 작업이 완료되면 이 멤버는 실제로 송수신된 바이트 수를 저장하게 된다.

        비동기 I/O 요청이 완료되면 I/O 요청 시 사용했던 OVERLAPPED 구조체의 주소를 돌려주게 된다. 따라서 OVERLAPPED 구조체에 추가적인 컨텍스트 정보를 포함시키면 매우 유용하게 사용될 수 있다.

      2. 비동기 장치 I/O 사용시 주의점#

        첫째로, 디바이스 드라이버는 비동기 I/O 요청을 항상 선입선출 방식으로만 처리하지는 않는다는 것이다. 디바이스 드라이버는 일반적으로 수행 성능을 개선하기 위해 I/O 요청을 순서에 따라 수행하지는 않는다. 파일시스템 드라이버의 경우 디스크의 헤드를 움직이는 시간과 검색 시간을 줄이기 위해 I/O 요청 목록을 검토하여 하드 디스크의 현재 위치로부터 물리적으로 가장 가까운 위치에 대한 I/O 요청을 가장 먼저 수행할 수 있다.

        둘째로, 에러 확인을 수행하는 적당한 방법에 대해 알고 있어야 한다. ReadFile과 WriteFile은 I/O 요청이 동기적으로 수행되는 경우 0이 아닌 값을 반환한다. 하지만 I/O 요청이 비동기적으로 수행되는 경우나 에러가 발생하게 되면 FALSE를 반환하게 된다. 따라서 FALSE가 반환되면 반드시 GetLastError를 호출하여 그 결과 값의 의미를 다시 한 번 확인해야 한다. 만일 GetLastError가 ERROR_IO_PENDING를 반환한다면 I/O 요청은 성공적으로 전달된 것이며, 언젠가는 완료될 것이다.

        셋째로, 비동기 I/O 요청을 수행할 때 사용되는 데이터 버퍼와 OVERLAPPED 구조체는 I/O 요청이 완료될 때까지 옮겨지거나 삭제되지 않아야 한다. 디바이스 드라이버로 I/O 요청이 전달될 때에는 데이터 버퍼의 주소와 OVERLAPPED 구조체의 주소가 전달되는 것이지, 실제 블록이 전달되는 것이 아니라는 점에 주의해야 한다. 이렇게 주소만을 전달하는 이유는 메모리 복사 과정이 매우 비용이 많이 드는 작업이므로 CPU 시간을 낭비할 수 있기 때문이다.

      3. 요청된 장치 I/O의 취소#

        CancelIo 함수를 호출하여 이 함수를 호출한 스레드가 삽입한 모든 I/O 요청을 취소할 수 있다( 핸들이 I/O 컴플리션 포트와 연계되어 있지 않다면).

        1. BOOL CancelIo ( HANDLE hFile );

        어떤 스레드가 I/O  요청을 삽입하였는지를 고려하지 않고 삽입된 모든 I/O 요청을 완전히 취소하고 싶다면 장치에 대한 핸들을 닫으면 된다.

        핸들이 I/O 컴플리션 포트와 연계되어 있는 경우를 제외하면, 스레드가 종료될 때 종료된 스레드가 삽입하였던 모든 I/O 요청이 취소된다.

        만일 특정 장치에 대해 하나의 I/O 요청만을 취소하고 싶다면 CancelIoEx 함수를 사용하면 된다.

        1. BOOL CancelIoEx ( HANDLE hFile, LPOVERLAPPED pOverlapped );

        이 함수는 hFile이 가리키는 장치에 대해 대기 중인 I/O 요청 중 pOverlapped가 가리키는 요청을 취소된 것으로 표시한다.

    5. I/O 요청에 대한 완료 통지의 수신#

      완료 통지 수신 방법

       

      방법 요약
       디바이스 커널 오브젝트의 시그널링 단일의 장치에 대해 다수의 I/O 요청을 수행하는 경우에는 적합하지 않다. 특정 스레드가 I/O 요청을 삽입하고 다른 스레드가 완료 통지를 수신할 수 있다.
       이벤트 커널 오브젝트의 시그널링  단일의 장치에 대해 다수의 I/O 요청을 수행할 수 있다. 특정 스레드가 I/O 요청을 삽힙하고 다른 스레드가 완료 통지를 수신할 수 있다.
       얼러터블 I/O 사용  단일의 장치에 대해 다수의 I/O 요청을 수행할 수 있다. 항상 I/O 요청을 삽입한 스레드가 완료 통지를 수신한다.
       I/O 컴플리션 포트 사용  단일의 장치에 대해 다수의 I/O 요청을 수행할 수 있다. 특정 스레드가 I/O 요청을 삽입하고 다른 스레드가 완료 통지를 수신할 수 있다. 이 방법이 가장 확장성이 뛰어나고 유연성이 있다.

        

      1. 디바이스 커널 오브젝트의 시그널링#

        윈도우에서는 디바이스 커널 오브젝트도 여타 다른 커널 오브젝트와 마찬가지로 시그널과 논시그널 상태를 가지기 때문에 스레드의 동기화에 사용될 수 있다. ReadFile과 WriteFile 함수를 통해 I/O 요청을 시도하면 요청이 삽입되기 전에 해당 디바이스 커널 오브젝트는 논시그널 상태가 된다. 다비이스 드라이버가 I/O 요청에 대한 처리를 마치면 디바이스 커널 오브젝트는 시그널 상태로 변경된다.

      2. 이벤트 커널 오브젝트의 시그널링#

        OVERLAPPED 구조체의 마지막 멤버인 hEvent는 이벤트 커널 오브젝트에 대한 핸들을 저장할 수 있다. 여기에 저장할 핸들은 CreateEvent 함수를 호출하여 얻은 값이어야 한다. 비동기 I/O 요청이 완료되면 디바이스 드라이버는 가장 먼저 OVERLAPPED 구조체의 hEvent 멤버가 NULL인지 여부를 확인한다. 만일 hEvent가 NULL이 아니면 이 값을 인자로 SetEvent 함수를 호출해 준다. 만일 여러 번의 비동기 I/O 요청을 동시에 수행하기를 원한다면 각 요청마다 서로 다른 이벤트 커널 오브젝트를 생성해야 한다.

      3. 얼러터블 I/O#

        스레드가 생성되면 시스템은 각 스레드별로 비동기 프로시저 콜(APC)큐라고 불리는 큐를 하나씩 생성한다. 비동기 I/O 요청을 전달하는 함수를 호출할 때 디바이스 드라이버에게 I/O 작업 완료 통지를 스레드의 APC 큐에 삽입해 줄 것을 요청할 수 있다. 이를 위해서는 ReadFileEx나 WriteFileEx 함수를 사용하면 된다.

        1. BOOL ReadFileEx ( HANDLE hFile, PVOID pvBuffer,
        2. DWORD nNumBytesToRead, OVERLAPPED *pOverlapped,

        3. LPOVERLAPPED_COMPLETION_ROUTINE pfnCompletionRoutine );

        4. BOOL WriteFileEx ( HANDLE hFile, CONST VOID *pvBuffer,
        5. DWORD nNumBytesToWrite, OVERLAPPED *pOverlapped,

        6. LPOVERAPEED_COMPLETION_ROUTINE pfnCompletionRoutine );

        *Ex 함수들은 I/O 작업이 수행된 바이트 수를 돌려받기 위한 DWORD 포인터를 전달하지 않는다. 이러한 정보는 콜백함수를 통해 획득할 수 있다. 또 *Ex 함수들은 컴플리션 루틴이라고 불리는 콜백함수의 주소를 필요로 한다. 컴플리션 루틴은 반드시 다음과 같은 형태로 구현되어야 한다.

        1. VOID WINAPI CompletionRoutine ( DWORD dwError, DWORD dwNumBytes, OVERLAPPED *po );

        ReadFileEx와 WriteFileEx 함수를 이용하여 비동기 I/O를 수행하면, 이 함수들은 디바이스 드라이버에게 컴플리션 루틴의 주소 값을 전달해 준다. 디바이스 드라이버가 I/O요청을 마치면 스레드의 APC 큐에 완료 통지를 나타내는 항목을 추가하는데, 이 항목에는 컴플리션 루틴의 주소와 최초 I/O 요청시 사용되었던 OVERLAPPED 구조체의 주소가 포함되어 있다. 스레드가 얼러터블 상태가 되면 시스템은 APC 큐의 내용을 확인하여 큐에 삽입된 모든 항목에 대해 컴플리션 루틴을 호출해 준다. 이때 I/O 에러 코드, 송수신된 바이트 수, OVERLAPPED 구조체의 주소가 전달된다. APC 큐는 시스템에 의해 내부적으로 관리되기 때문에 I/O 작업이 요청한 순서대로 완료될 수도 있지만 그 역순으로 완료될 수도 있다. APC 큐에 항목이 삽입되었다 하더라도 컴플리션 루틴이 바로 호출되지는 않는다. APC 큐의 항목을 처리하려면 스레드가 자신을 얼러터블 상태로 변경해야 하며, 이를 통해 스레드가 인터럽트 가능한 상태가 되었음을 알려주어야 한다. 윈도우는 스레드를 얼러터블 상태로 변경할 수 있는 6개의 함수를 제공한다. SleepEx, WaitForSingleObjectEx, WaitForMultipleObjectsEx, SignalObjectAndWait, GetQueuedCompletionStatusEx, MsgWaitForMultipleObjectsEx. 앞에서부터 5개의 함수는 마지막 인자로 스레드를 얼러터블 상태로 변경할 것인지의 여부를 나타내는 부울 값을 전달받는다. MsgWaitForMultipleObjectsEx의 경우에는 MWMO_ALERTABLE 플래그를 이용하여 스레드를 얼러터블 상태로 변경할 수 있다.

        위에서 나열한 6개의 함수 중 하나를 사용하면 스레드를 얼러터블 상태로 변경할 수 있으며, 이때 비로소 시스템은 스레드의 APC 큐에 항목이 존재하는지를 확인한다. 만일 큐에 하나 이상의 항목이 존재하면 스레드를 대기 상태로 전환하지 않고 APC 큐에 존재하는 항목을 하나씩 빼내어 지정된 콜백루틴을 호출한다. APC 큐가 완전히 비워지면 커널 오브젝트가 시그널 상태가 될 때까지 기다리지 않고, 앞서 얼러터블 상태로 변경하기 위해 호출하였던 함수가 반환된다. 6개의 함수들은 반환 값을 통해 이 함수가 왜 반환되었는지를 알려준다. WAIT_IO_COMPLETION이 반환되었다면, 적어도 하나 이상의 항목이 APC 큐에 존재하였으며 이데 대한 처리를 수행하였음을 의미한다.

      4. I/O 컴플리션 포트#

        I/O 컴플리션 포트를 구현한 이론적 배경은 동시에 수행할 수 있는 스레드 개수의 상한을 설정할 수 있어야 한다는 것이다. 수행 가능한 스레드의 개수를 CPU 개수보다 많이 유지하게 되면, 시스템은 스레드 컨텍스트 전환을 위해 CPU 시간을 필요로 하게 되고, 이는 결국 중요한 CPU 자원을 낭비하게 된다. I/O 컴플리션 포트는 스레드 풀을 이용하도록 설계되었다. I/O 컴플리션 포트를 생성하기 위해서는 CreateIoCompletionPort 함수를 사용하면 된다.

        1. HANDLE CreateIoCompletionPort ( HANDLE hFile, HANDLE hExistingCompletionPort,
        2. ULONG_PTR CompletionKey, DWORD dwNumberOfConcurrentThreads );

        이 함수는 서로 다른 두 가지 작업을 수행한다. I/O 컴플리션 포트를 생성하기도 하지만 장치와 I/O 컴플리션 포트를 연계하는 작업도 수행한다.

        CreateIoCompletionPort를 호출할 때 앞의 세 개의 매개변수는 특정 장치를 I/O 컴플리션 포트와 연계할 때에만 필요한 매개변수들이다. 따라서 I/O 컴플리션 포트를 생성하는 시점에는 각각 INVALID_HANDLE_VALUE, NULL, 0 값을 전달하기만 하면 된다.

        dwNumberOfConcurrentThreads 매개변수는 I/O 컴플리션 포트에게 동일 시간에 동시에 수행할 수 있는 스레드의 최대 개수를 알려주는 역할을 한다. 만일 dwNumberOfConcurrentThreads 매개변수로 0을 전달하면 I/O 컴플리션 포트는 머신에 설치된 CPU의 개수를 동시에 수행 가능한 스레드의 최대 개수로 설정한다. CreateIoCompletionPort 함수는 윈도우에서 제공하는 다양한 커널 오브젝트 생성 함수 중 유일하게 SECURITY_ATTRIBUTES 구조체의 포인터를 인자로 전달할 필요가 없는 함수다. 이는 I/O 컴플리션 포트가 단일 프로세스 내에서만 수행될 수 있도록 하기 위함이다.

        I/O  컴플리션 포트를 생성하면 윈도우 커널은 내부적으로 밑의 그림과 같이 5개의 서로 다른 데이터 구조를 생성한다.

        IO1.bmp 

        IO2.bmp 

        첫 번째 데이터 구조는 I/O 컴플리션 포트와 연계된 단일 혹은 다수의 장치를 관리하기 위한 리스트다. 장치를 I/O 컴플리션 포트와 연계하기 위해서는 앞서와 마찬가지로 CreateIoCompletionPort를 사용한다. 이 함수를 호출할 때는 앞서 생성해 둔 I/O 컴플리션 포트의 핸들과 장치에 대한 핸들( 파일, 소켓, 메일슬롯, 파이프 등을 나타내는 핸들이 될 수 있다) 그리고 컴플리션 키(이 값의 의미는 사용자가 임의로 결정할 수 있다. 운영체제는 이 값을 단순히 전달하기만 할 뿐이다)를 전달하면 된다. 새로운 장치를 I/O 컴플리션 포트와 연계시킬 때마다 시스템은 I/O 컴플리션 포트의 내부적인 데이터 구조인 장치 리스테 새로운 항목을 추가한다.

        I/O 컴플리션 포트를 구성하는 두 번째 데이터 구조는 I/O 컴플리션 큐이다. 장치에 대한 비동기 I/O 요청이 완료되면 시스템은 장치와 연계된 I/O 컴플리션 포트가 있는지 확인한다. 만일 연계된 I/O 컴플리션 포트가 있으면 I/O 컴플리션 큐에 I/O 요청의 완료 통지를 나타내는 새로운 항목을 삽입한다. 각각의 항목에는 송수신된 바이트 수, 장치와 I/O 컴플리션 포트를 연계할 때 지정한 컴플리션 키 값, 비동기 I/O 작업을 요청할 때 사용하였던 OVERLAPPED 구조체를 가리키는 포인터, 그리고 에러 코드를 가지고 있다.

        I/O 컴플리션 포트를 이용한 아키텍처 설계

        서비스 애플리케이션이 초기화를 진행하는 동안 CreateIoCompletionPort 같은 함수를 호출하여 I/O 컴플리션 포트를 생성하고 클라이언트의 요청을 처리하는 스레드 풀을 생성해야 한다. 보통의 경우라면 서비스 애플리케이션을 운영할 머신의 CPU 개수에 2를 곱한 수준에서 스레드를 생성하는 것이 가장 일반적이다.

        풀 내의 모든 스레드들은 동일한 스레드 함수를 수행하도록 구성하는 것이 좋다. 보통 이러한 스레드들은 초기화 작업을 거친 후 루프로 진입하며, 서비스 애플리케이션이 종료될 때 루프를 탈출하도록 구성된다. 루프 내에서는 비동기 장치 I/O 작업이 완료되어 I/O 컴플리션 포트를 통해 완료 통지가 전달될 때 이를 곧바로 처리할 수 있도록 스레드를 대기 상태로 유지해야 하는데, 이를 위해 GetQueuedCompletionStatus 함수를 사용하면 된다.

        1. BOOL GetQueuedCompletionStatus ( HANDLE hCompletionPort,
        2. PDWORD pdwNumberOfBytesTransferred,

        3. PULONG_PTR pCompletionKey,

        4. OVERLAPPED **ppOverlapped,

        5. DWORD dwMilliseconds );

        첫 번째 매개변수인 hCompletionPort로는 어떤 I/O 컴플리션 포트에 대한 대기를 수행할 것인지를 결정하는 핸들 값을 전달하면 된다. 대다수의 서비스 애플리케이션은 단지 하나의 I/O 컴플리션 포트만을 사용하고, 이를 통해 비동기 I/O 요청에 대한 완료 통지를 처리한다. 기본적으로 GetQueuedCompletionStatus는 이 함수를 호출한 스레드를 I/O 컴플리션 포트 내의 컴플리션 큐에 새로운 항목이 삽입될 때까지 대기 상태로 유지하며, 적절한 타임아웃 값을 지정할 수도 있다.

        I/O 컴플리션 포트를 구성하는 세 번째 데이터 구조는 대기 스레드 큐이다. 스레드 풀 내의 여러 개의 스레드들이 각기 GetQueuedCompletionStatus 함수를 호출하면 이 함수를 호출한 스레드의 ID 값이 대기 스레드 큐에 삽입되며, 이를 통해 I/O 컴플리션 포트 커널 오브젝트는 어떤 스레드들이 비동기 I/O 요청에 대한 완료 통지를 처리할 것인지를 알 수 있다. I/O 컴플리션 큐에 항목이 추가되면 I/O 컴플리션 포트는 대기 스레드 큐에 있는 스레드 중 하나를 깨우게 되고, 이 스레드는 컴플리션 큐에 삽입된 항목으로부터 송수신된 바이트 수, 컴플리션 키, OVERLAPPED 구조체의 주소를 가져오게 된다. 이러한 정보는 GetQueuedCompletionStatus 함수의 pdwNumberOfBytesTransferred, pCompletionKey, ppOverlapped 매개변수를 통해 전달된다.

        GetQueuedCompletionStatus 함수가 왜 반환되었는지 그 원인을 파악하는 것은 일면 까다로운 부분이 있다. 아래에 함수가 반환된 원인을 확인하는 알맞은 방법을 나타냈다.

        1. DWORD dwNumBytes;
        2. ULONG_PTR CompletionKey;
        3. OVERLAPPED *pOverlapped;
        4. // hIOCP는 프로그램의 다른 부분에서 이미 초기화되었다.
        5. BOOL bOk = GetQueuedCompletionStatus ( hIOCP, &dwNumBytes, &CompletionKey, &pOverlapped, 1000 );
        6. DWORD dwError = GetLastError();
        7. if ( bOk ) {
        8. // 성공적으로 수행된 I/O 완료 통지에 대한 처리

        9. } else {
        10. if ( pOverlapped != NULL ) {

        11. // 실패한 I/O 완료 통지에 대한 처리

        12. // dwError 변수는 실패에 이유를 담고 있다.

        13. } else {

        14. if ( dwError == WAIT_TIMEOUT ) {

        15. // I/O 컴플리션 큐 대기 중에 대기 시간 만료가 발생

        16. } else {

        17. // GetQueuedCompletionStatus를 잘못 호출하였다.

        18. // dwError는 잘못된 호출의 이유를 나타내는 값을 담고 있다.

        19. }

        20. }

        21. }

        I/ O 컴플리션 큐는 예측대로 선입선출(FIFO) 방식으로 항목을 삽입하고 제거한다. 하지만 GetQueuedCompletionStatus를 호출하는 스레드는 예상 외로 후입선출(LIFO)방식으로 깨어난다. 이 또한 성능 향상을 위한 동작 방식이다. 완료 통지가 매우 느리게 도달하게 되면 단일의 스레드가 모든 완료 통지를 처리할 수도 있을 것이다. 시스템은 가능한 한 앞서 작업을 수행했던 스레드를 다시 깨워서 작업을 처리하려 하며 다른 스레드들은 대기 상태를 유지하도록 한다. 후입선출 알고리즘을 이용하면 스케줄되지 않는 스레드들이 사용하는 메모리를 디스크로 내보낼 수 있으며, 프로세서의 캐시를 비울 수도 있다.

        I/O 컴플리션 포트의 스레드 풀 관리 방법

        완료 통지가 삽입되면 I/O 컴플리션 포트는 대기 중인 스레드를 깨우게 되는데, 이 값으로 설정된 개수 이상을 초과할 수 없다. 수행을 재개한 스레드는 각각의 I/O 완료 통지를 처리하고 나서 다시 GetQueuedCompletionStatus를 호출할 것이고, 시스템은 I/O 컴플리션 큐에 삽입된 항목이 남아 있는 경우 나머지 항목을 처리하기 위해 이 스레드를 다시 깨울 것이다.

        I/O 컴플리션 포트가 지정된 개수만큼의 스레드만을 동시에 수행시킬 수 있다면 왜 이보다 많은 스레드를 스레드풀로 관리해야 하는 것일까? I/O 컴플리션 포트가 특정 스레드의 수행을 재개시키는 경우 I/O 컴플리션 포트 내부적으로 관리되는 네 번째 자료 구조인 릴리즈 스레드 리스트에 깨어난 스레드의 ID를 기록해 둔다. 이렇게 함으로써 I/O 컴플리션 포트는 어떤 스레드가 깨어났는지를 알 수 있으며, 이 스레드의 수행 상황을 지속적으로 확인할 수 있게 된다. 만일 릴리즈 스레드 리스트에 삽입된 스레드 중 하나가 어떤 함수를 호출하였더니 대기 상태로 진입하게 되었다고 하자. 이 경우 I/O 컴플리션 포트는 대기 상태로 진입하였음을 감지하게 되고 릴리즈 스레드 리스트로부터 이 스레드의 ID 값을 빼내어 일시 정지 스레드 리스트로 항목을 옮긴다.

        I/O 컴플리션 포트는 항상 자신을 생성할 때 지정한 동시 수행 가능 스레드 개수만큼 릴리즈 스레드 리스트의 항목 수를 유지하려 한다. 만일 릴리즈 스레드 리스트에 있던 스레드가 어떤 이유로 인해 대기 상태로 전환되면, 릴리즈 스레드 리스트의 항목 개수가 줄게 되므로 I/O 컴플리션 포트는 대기 상태에 있는 스레드 중 하나를 릴리즈 스레드 리스트로 옮겨온다. 또한 대기 상태로 전환되어 일시 정지 스레드 리스트에 있던 스레드가 다시 수행을 재개하는 경우에도 일시 정지 스레드로부터 릴리즈 스레드 리스트로 그 항목을 옮겨오게 된다. 이렇게 되면 릴리즈 스레드 리스트는 I/O 컴플리션 포트에서 설정한 동시에 수행 가능한 스레드의 개수를 일시적으로 초과하는 개수의 항목을 가지게 된다.

        I/O 컴플리션 포트에 할당된 스레드는 다음의 3가지 경우에 할당 해제될 수 있다.

        스레드가 종료되는 경우

        다른 I/O 컴플리션 포트의 핸들을 인자로 GetQueuedCompletionStatus를 호출한 경우

        스레드가 할당된 I/O 컴플리션 포트가 종료되는 경우

        I/O 완료 통지 흉내 내기

        I/O 컴플리션 포트 커널 오브젝트는 스레드간 통신을 수행할 때 상당히 유용하게 사용될 수 있다. PostQueuedCompletionStatus 함수를 사용하면 된다.

        1. BOOL PostQueuedCompletionStatus ( HANDLE hCompletionPort,
        2. DWORD dwNumBytes,

        3. ULONG_PTR CompletionKey,

        4. OVERLAPPED *pOverlapped );

        이 함수를 호출하면 완료 통지를 I/O 컴플리션 큐에 삽입해 준다. 함수의 첫 번째 매개변수인 hCompletionPort로는 완료 통지를 삽입할 I/O 컴플리션 포트의 핸들을 전달하면 된다. 나머지 3개의 매개변수인 dwNumBytes, CompletionKey, pOverlapped로는 GetQueuedCompletionStatus를 호출한 스레드에게 전달할 값을 넘겨주면 된다. PostQueuedCompletionStatus 함수는 스레드 풀에 존재하는 스레드들과 통신을 수행해야 할 경우 유용하게 사용될 수 있다.

 

 

 

  1. 윈도우 스레드 풀#

    스레드를 생성하고 파괴하는 방법은 사용자별로 서로 다르게 구현할 수 있는 사항이다. 윈도우는 개발자들이 좀 더 쉽게 개발을 수행할 수 있도록 자체적인 스레드 풀 매커니즘을 제공하고 있으며, 이를 이용하면 스레드의 생성, 파괴, 관리 작업을 좀 더 쉽게 구현할 수 있다. 새로운 스레드 풀 함수들을 이용하면 다음과 같은 작업을 수행할 수 있다.

    비동기 함수 호출, 시간 간격을 두고 함수 호출, 커널 오브젝트가 시그널되면 함수 호출, 비동기 I/O 요청이 완료되면 함수 호출

    1. 비동기 함수 호출#

      ...

    2. 시간 간격을 두고 함수 호출#

      ...

    3. 커널 오브젝트가 시그널되면 함수 호출#

      ...

    4. 비동기 I/O 요청이 완료되면 함수 호출#

      ...

    5. 콜백 종료 동작#

      ...

 

 

  1. 파이버#

    마이크로소프트는 UNIX 서버 애플리케이션들을 윈도우로 쉽게 포팅할 수 있도록 윈도우에 파이버를 추가하였다.

 

 

  1. 윈도우 메모리 구조#

    1. 프로세스의 가상 주소 공간#

      ...

    2. 가상 주소 공간의 분할#

      ...

    3. 주소 공간 내의 영역#

      ...

    4. 물리적 저장소를 영역으로 커밋하기#

      ...

    5. 물리적 저장소와 페이징 파일#

      ...

    6. 보호특성#

      ...

    7. 모두 함께 모아#

      ...

    8. 데이터 정렬의 중요성#

      ...

 

 

  1. 가상 메모리 살펴보기#

    ...

    1. 시스템 정보#

      ...

    2. 가상 메모리 상태#

      ...

    3. NUMA 머신에서의 메모리 관리#

      ...

    4. 주소 공간의 상태 확인하기#

      ...

 

 

 

  1. 애플리케이션에서 가상 메모리 사용 방법#

    ...

    1. 주소 공간 내에 영역 예약하기#

      ...

    2. 예약 영역에 저장소 커밋하기#

      ...

    3. 영역에 대한 예약과 저장소 커밋을 동시에 수행하는 방법#

      ...

    4. 언제 물리적 저장소를 커밋하는가#

      ...

    5. 물리적 저장소의 디커밋과 영역 해제하기#

      ...

    6. 보호 특성 변경하기#

      ...

    7. 물리적 저장소의 내용 리셋하기#

      ...

    8. 주소 윈도우 확장#

      ...

 

 

 

  1. 스레드 스택#

    ...

    1. C/C++ 런타임 라리브러리의 스택 확인 함수#

      ...

 

 

 

  1. 메모리 맵 파일#

    ...

    1. 실행 파일과 DLL 파일에 대한 메모리 맵#

      ...

    2. 메모리 맵 데이터 파일#

      ...

    3. 메모리 맵 파일 사용하기#

      ...

    4. 메모리 맵 파일을 이용하여 큰 파일 처리하기#

      ...

    5. 메모리 맵 파일과 일관성#

      ...

    6. 메모리 맵 파일의 시작 주소 지정하기#

      ...

    7. 메모리 맵 파일의 세부 구현사항#

      ...

    8. 프로세스간 데이터 공유를 위해 메모리 맵 파일 사용하기#

      ...

    9. 페이징 파일을 이용하는 메모리 맵 파일#

      ...

    10. 스파스 메모리 맵 파일#

      ...

 

 

 

  1. #

    ...

    1. 프로세스 기본 힙#

      ...

    2. 추가적으로 힙을 생성하는 이유#

      ...

    3. 추가적으로 힙을 생성하는 방법#

      ...

    4. 기타 힙 관련 함수들#

      ...

 

 

 

  1. DLL의 기본#

    ...

    1. DLL과 프로세스 주소 공간#

      ...

    2. 전반적인 모습#

      ...

 

 

 

  1. DLL의 고급 기법#

    ...

    1. 명시적인 DLL 모듈 로딩과 심벌 링킹#

      ...

    2. DLL의 진입점 함수#

      ...

    3. DLL의 지연 로딩#

      ...

    4. 함수 전달자#

      ..

    5. 알려진 DLL#

      ...

    6. DLL 리다이렉션#

      ...

    7. 모듈의 시작 위치 변경#

      ...

    8. 모듈 바인딩#

      ...

 

 

 

  1. 스레드 지역 저장소(TLS)#

    ...

    1. 동적 TLS#

      ...

    2. 정적 TLS#

      ...

 

 

 

  1. DLL 인젝션과 API 후킹#

    ...

    1. DLL 인젝션#

      ...

    2. 레지스트리를 이용하여 DLL 인젝션하기#

      ...

    3. 윈도우 훅을 이용하여 DLL 인젝션하기#

      ...

    4. 원격 스레드를 이용하여 DLL 인젝션하기#

      ...

    5. 트로얀 DLL을 이용하여 DLL 인젝션하기#

      ...

    6. 디비거를 이용하여 DLL 인젝션하기#

      ...

    7. CreateProcess를 이용하여 코드 인젝션하기#

      ...

    8. API 후킹#

      ...

 

 

 

  1. 종료 처리기#

    ...

 

 

 

  1. 예외 처리기와 소프트웨어 예외#

    ...

    1. EXCEPTION_EXECUTE_HANDLER#

      ...

    2. EXCEPTION_CONTINUE_EXECUTION#

      ...

    3. EXCEPTION_CONTINUE_SEARCH#

      ...

    4. GetExceptionCode#

      ...

    5. GetExceptionInformation#

      ...

    6. 소프트웨어 예외#

      ...

 

 

 

  1. 처리되지 않은 예외, 벡터화된 예외 처리, 그리고 C++ 예외#

    ...

    1. UnhandledExceptionFilter 함수의 내부#

      ...

    2. 저스트-인-타임(JIT) 디버깅#

      ...

    3. 벡터화된 예외와 컨티뉴 처리기#

      ...

    4. C++ 예외와 구조적 예외#

      ...

    5. 예외와 디버거#

      ...

 

 

 

  1. 에러 보고와 애플리케이션 복구#

    ...

    1. 윈도우 에러 보고 콘솔#

      ...

    2. 프로그램적으로 윈도우 에러 보고하기#

      ...

    3. 프로세스 내에서 사용자 정의 문제 보고서 생성하기#

      ...

    4. 사용자 정의 문제 보고서 생성과 변경#

      ...

    5. 자동 애플리케이션 재시작과 복구#

      ...

 

 

 

  1. 빌드환경#

    ...

    1. CmmHdr.h 헤더 파일#

      ...

 

 

 

  1. 메시지 크래커, 차일드 컨트롤 매크로, 그리고 API 매크로#

    ...

    1. 메시지 크래커#

      ...

    2. 차일드 컨트롤 매크로#

      ...

    3. API 매크로#

      ...