1. 라이브러리와 printf!

: 헤더파일에는 함수에 대한 선언만 들어있지 실행가능한 바이너리 코드가 들어있지는 않다.

: 해답은 라이브러리(Library) 안에 있다. 라이브러리란 여러 프로그램에서 자주 사용하는 함수와 데이터들을 실행이 가능한 바이너리 형태로 묶어놓은 파일을 의미한다.

: printf의 경우 C 런타임 라이브러리(C Run-Time Library)에 있다.

* 라이브러리 작성에 대한 동기

: 사용될 소지가 높은 함수들을 라이브러리로 구성하면 편리할 것이다.

* 라이브러리 작성

: 정적라이브러리로 프로젝트를 만들자

- 라이브러리 함수의 입력

: .h(헤더파일)과 .cpp 파일을 만들자

: .lib 파일이 만들어질 것이다.

Tip

실행파일이 생성되는 3가지 단계(전처리->컴파일->링크)에 대한 이해, 특히 링크 과정에서 하는 일이 무엇인지 확실히 이해한다.

헤더 파일 include가 지니는 의미와 헤더파일 안에 들어가는 일반적인 선언들이 무엇인지를 이해한다.

헤더파일을 정의하는 이유와 정의했을 때 얻게 되는 이점을 이해한다.


* Static Library

: 우리가 만든 라이브러리는 링커에 의해 실행파일안에 포함되게 된다.(이 과정을 링크라고 한다.) 

: 처음 만들어질 때부터 하나로 묶이게 되고 이러한 형태의 라이브러리를 가리켜 정적 라이브러리라고 한다.

* 동적라이브러리 DLL(Dynamic Linking Library) 

: 링커는 링크 과정을 통해서 실행 파일을 생성해낸다. 확장자로 .exe를 갖는 실행파일을 만들어 내기도 하지만, .dll을 지니는 라이브러리를 만들어 내기도 한다. 이것이 DLL이라고 불리는 라이브러리다.

* DLL 과 정적 라이브러리의 차이점

: 정적라이브러리와 DLL은 모두 라이브러리라는 점에서는 동일하다. 하지만, 정적 라이브러리는 정적인 특성을 가지고 DLL은 동적인 특성을 가진다. 

: 실행가능한 프로그램에서 라이브러리를 가져다 쓰는 방법에 따른 차이점이다.

- 정적 라이브러리의 특성

: 라이브러리 코드를 완전히 포함해서 .exe파일을 생성하는 형태의 정적 링크를 통하기 때문에 "실행의 독립성"을 가진다.

: 실행파일만 있으면 어디서든지 실행가능하다.

: 메모리 공간을 많이 차지하기 때문에 비효율적이다.


- DLL의 특성

: 프로세스를 여러개 실행시켰을 때 그 차이는 두드러 지게 된다.

: 메인메모리에서 페이지 단위로 DLL이 공유되기 때문에 컨텍스트 스위칭 부분에 있어서 효율이 극대화 된다.

: DLL에서 lib 파일은 링크할 떄 필요하고, dll 파일은 실행할 때 필요하다.

* DLL 제작 : 암묵적 연결

-DLL과 extern 선언

: C언어는 상관 없지만, C++은 컴파일 과정에서 Name Mangling(이름을 엉망으로 만든다는 의미) 작업을 하기 때문에 함수의 이름이 규칙에 따라 이상하게 변경되버린다.

: 이러한 작업을 막기 위해서는 .cpp, .h파일에 extern 명령어를 추가하여야 한다.

* DLL 제작 : 명시적 연결

: 소스코드 내에 DLL 연결 코드가 명시적으로 존재하는 방법이다.

: lib 파일이 필요 없다.

장점 1. : DLL이 필요한 시점에서 로딩하고, 불필요해지면 반환하기 때문에 메모리가 절약된다.

장점 2. : 프로그램 실행 중에 DLL 교체 및 선택이 가능하다.

장점 3. : 프로그램 실행 전에 로드하지 않기 때문에 실행시간을 단축시킬 수 있고, DLL 로딩에 걸리는 시간을 분산시킬 수 있다.

* 한번이상 로드될 수 있는 DLL

: 가상메모리 주소가 서로 다른 프로세스일 경우 두번 로딩되게 된다.

3. 도대체 헤더파일을 몇개나 만들 작정이야!

: 암묵적, 명시적, C 컴파일러, C++컴파일러용 으로 헤더를 만들려면 최소 3개의 헤더가 필요하다.

* 하나의 헤더파일로 모두 지원하기

: #ifdef 명령어


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 가상 메모리 (Virtual Memory) 컨트롤

* Reserve, Commit 그리고 Free

: 페이지의 상태의 Reserve는 예약, Commit은 할당, Free는 할당되지 않았음을 의미한다.

: <페이지의 개수 = 가상 메모리의 크기 / 페이지 하나당 크기> 이므로, 페이지 개수는 가상 메모리의 크기에 비례하며, 모든 페이지는 Reserved, Commit, Free 이 세가지 상태 중 하나를 지닌다.

: 여기서 물리 메모리란, 램과 하드디스크를 모두 포함하는 말이다. 즉, 해당 페이지가 물리 메모리에 할당된 상태를 가리켜 Commit 이라고 한다. 

: 할당되지 않은 상태를 Free라고 한다. 

: Windows는 Reserve라는 상태를 하나 더 두어 메모리 사용의 효율성을 높일 기회를 제공한다.

: 아주 큰 배열을 선언했을 때, 모두 Commit으로 둔다면 해당 페이지는 모두 물리 메모리에 할당되어져 버린다. 이 떄 Reserve를 통해 메모리 사용되어져 메모리의 효율성이 높아지게 된다.

: 일부 페이지를 Reserve상태로 둠으로써 다른 메모리 할당 함수에 의해 해당 번지가 할당되지 못하도록 선언할 수 있다. 동시에 메모리 소비는 발생하지 않는다.

* 메모리 할당의 시작점과 단위 확인하기

: 메모리를 할당하기 전에 기본적으로 생각해야 하는 것은 두가지이다. 메모리 할당의 시작 주소, 할당할 메모리의 크기

: 가상 메모리 시스템은 페이지 단위로 관리되기 때문에, 페이지의 중간 위치에서부터 할당을 시작할 수 없으며, 페이지 크기의 배수 단위로 할당을 해야만 한다.

: Windows 는 메모리가 지나치게 조각나는 것을 막기 위해서, 또 관리의 효율성을 위해 조금 더 넓은 범위의 값을 할당의 경계로 정의하고 있다.

: 메모리 할당의 시작 주소가 될 수 있는 기본 단위를 가리켜 Allocation Granularity Boundary 라고 한다. 

: Allocation Granularity Boundary와 페이지의 크기는 GetSystemInfo 를 통해 알 수 있다. 

* VirtualAlloc & VirtualFree 함수

: VirtualAlloc 함수는 페이지 상태를 Reserve와 Commit으로 만드는 역할을 한다. 

: VirtualFree 함수는 VirtualAlloc 함수가 정해놓은 상태를 되돌린다.

* Dynamic Array Design

: 배열의 크기만큼 물리 메모리가 할당되는 것이 아니라, 사용량의 증가에 따라 물리 메모리에 할당되는 동적 배열 디자인이다.

-시나리오

1. 페이즈 사이즈와 Allocation Granularity Boundary 를 얻어온다.

2. 예상되는 최대 크기로 메모리를 예약한다

3. 필요한 만큼 Commit한다. 이 후 필요에 따라 점진적으로 할당한다.

4. 할당했던 메모리를 반환한다.

2. 힙(Heap) 컨트롤

: 힙을 컨트롤하는 것은 가상 메모리를 컨트롤하는 것보다 최소 몇배 이상 유용ㅎ안 일이다.

* 힙(Heap) 컨트롤에 대한 필자의 기억

: 리스트 자료구조의 특성상 리스트 정보는 동적할당되어 힙에 저장된다.

: 이를 반환하려면 두가지 문제가 등장한다.

1. 메모리 유출(삭제되지 않는 위험), 메모리 누수가 누적되면 프로그램이 종료된다

2. 성능, 일일이 돌아다니면서 삭제하기위해서는 리스트의 길이에 따라 성능 저하 요인이 된다.

: Windows에서는 이 둘의 문제를 해결할 수 있다..

* 디폴트 힙(Default Heap) & Windows 시스템에서의 힙

: C언어의 경우 malloc 함수와 free 함수를, C++을 사용할 경우 new와 delete 연산자를 사용해서 힙 영역에 메모리를 할당한다.

: 이 경우 1M바이트 크기의 디폴트 힙 영역에 메모리를 할당하게 된다.

: 디폴트 힙은 프로세스에 기본적으로 할당되는 힙이라 하여 프로세스 힙(Process Heap)이라고도 부른다.    

: Windows 함수를 이용하면 2번째 사진과 같은 힙 메모리 형태를 만들 수 있다.

: 홍길동이 탈퇴한다면, 해당 힙 메모리를 제거함으로써 메모리 유출과 해제에 대한 성능 저하 문제를 해결할 수 있다.

* 디폴트 힙 컨트롤

: 디폴트 힙의 기본 크기는 1M바이트이지만, 링커(Linker) 옵션을 통해서 변경이 가능하다.

: 하지만, 그 이상의 크기를 사용할 때에 Windows가 알아서 그 크기를 늘려주므로 따로 재정의해줄 필요는 없다.

: 물론, 디폴트 힙 크기를 정해줌으로써 프로세스가 실행중에 새로운 메모리를 할당해서 시간이 소요되는 것을 방지할 수 있다.

* 힙(Dynamic Heap) 생성이 가져다 주는 또 다른 이점

1. 메모리 단편화의 최소화에 따른 성능 향상

: 힙을 미리 A,B,C 처럼 할당하여 둔다면, 연속성이 무너지지 않을 것이다.

: 하지만, 디폴트 힙에서 처리한다면, 단편화가 되어 프로그램의 로컬리티의 특성이 낮아질 우려가 크다. 

: 이는 성능에 많은 영향을 끼치므로 힙 생성을 적절하게 사용한다면 성능향상을 기대할 수 있다.

: 페이지 단위로 힙을 불러오기 때문에, 로컬리티 특성이 매우 좋아지며, 섞여 있을 경우 여러 페이지를 모두 불러와야 하기 때문에 로컬리티 특성이 매우 낮아진다.

2. 동기화 문제에서 자유로워짐으로 인한 성능 향상

: 쓰레드 마다 다른 힙을 사용할 경우 동기화를 하지 않아도 되기 때문에 이로 인한 성능향상을 기대할 수 있다.

: 힙자체를 반환해 버릴 수 있다는 장점도 존재한다.

* 힙의 생성과 소멸 그리고 할당

: 리스트 자료구조에 동적 힙의 개념을 도입하면 메모리 유출의 방지와 프로그램 구현의 용이성이라는 두가지 장점을 얻게 된다.

3. MMF(Memory Mapped File)

* MMF의 이해

: MMF는 File을 Memory에 Mapping(연결) 시킨다는 의미를 지니고 있다. 파일의 일부 영역을 가상 메모리 일부에 연결시키는 매커니즘을 가리켜 MMF라고 한다. 

: 위의 그림을 보면, 가상메모리와 파일의 데이터가 연결되어있다. 이는 실제로도 메모리에만 데이터가 저장되는 것이 아니라, 메모리에 연결된 파일에 실제 데이터가 저장된다.

장점 1. 프로그래밍하기 편하다

: 파일안에 저장되어 있는 데이터를 조작하려면 일단 메모리로 읽어 들여야 하기 때문에, 너무 번거롭다. MMF를 사용하면 메모리 상에 저장된 데이터를 조작하는 방식으로 파일 내 데이터를 조작할 수 있어 편리하다.

장점 2. 성능이 향상된다

: 성능이 저하될 것 같지만, 일반적으로는 향상된다.

: 메모리는 중간의 캐쉬 역할만 한다고 생각하여 로컬리티 특성만 충족시킬 경우 아주 높은 성능 향상이 이루어진다. 하지만, 메모리 상의 데이터가 변경되었을 때 바로 반영한다면, 이는 구현의 용이성만 얻게되고 성능 향상은 어렵다.

* MMF의 구현과정

1 단계 : 파일개방

: 반드시 CreateFile 함수 호출을 통해서 파일을 열고 해당 파일의 핸들을 얻어야 한다.

: 핸들은 파일을 읽고 쓰는데 필요하다.

2 단계 : 파일 연결 오브젝트 생성

: 메모리에 연결할 파일 정보를 담고 있는 커널 오브젝트를 생성하는 것이다.

: CreateFileMapping 함수 호출을 통해 만들어진다.

: 파일 연결 오브젝트는 메모리에 매핑시키는데 필요하다.

3 단계 : 가상 메모리에 파일 연결

: 가상메모리에 파일을 연결한다.

: MapViewOfFile 함수를 통해 완성되며, 반환되는 포인터를 가지고 메모리에 접근하여 데이터를 변경하면, 파일에 반영된다.

* Copy-On-Write(COW)

: 데이터를 쓸 때 복사하라.

: 이는 MMF처럼 시스템 함수 수준에서 제공되거나, OS와 같은 고급 소프트웨어 를 구현할 떄 내부적으로 적용하는 최적화(Optimization) 기술이다.

: 일반적으로 쓰레드는 항상 테이블을 복제해서 사용하였다.

: 하지만, 쓰레드가 생성될 때마다 기본 테이블을 참조하며, 특정 테이블을 변경했을 때에만 테이블을 복사한다면 더 효율적일것이다. 이것이 COW이다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


 "엔지니어는 기술로 인정 받을 수는 있지만 존중되는 이유는 다른 데에 있다."

1. 비동기(Asynchronous)  I/O

* 비동기 I/O의 이해
: 프로그램의 CPU 활용도가 심한 기복이 있다는 것은 활용 방식에 문제가 있다는 것이며 성능저하로 이어진다.
: 데이터 수신과 플레이가 반복되는 동영상 플레이어의 경우, 중간중간 플레이 할때 끊길 수 밖에 없다.
: 한번 호출되면 완료될 떄까지 블로킹 되는 함수들을 가리켜 블로킹(Blocking)함수라 하고, 이 함수들을 활용한 입,출력 연산을 가리켜 동기(Synchronous) I/O 라 표현한다.
: 데이터 수신과 플레이를 동시에 병행한다면 위의 사례보다 비교도 안되는 성능향상을 이룰수 있다.
: 데이터 수신은 CPU할당을 크게 요구하지 않는 작업이기 때문이다.
: 이러한 구조의 I/O를 가리켜 비동기(Asynchronous) I/O라 한다.
* 중첩(Overlapped) I/O
: Windows에서 제공하는 비동기 I/O 방식 중에서 가장 대표적인 것이 중첩 I/O 이다.
: 데이터도 읽고 플레이도 동시에 가능하려면 Non-Blocking 함수이어야 한다.
: Non-Blocking함수는 작업의 완료에 상관없이 바로 반환해 버리는 특성을 지닌다. 물론, 반환후에도 작업은 계속된다.
: 이 경우 함수를 호출한 다음 다른 함수를 호출하여 동시에 여러 작업을 진행할수 있게 된다 이것이 중첩 I/O이다.
: 비동기 I/O는 파일보다 네트워크 통신에서 더 큰 의미를 지닌다.
* 중첩(Overlapped) I/O 예제
: 이벤트를 통해 항상 I/O가 완료 되었는지 확인해야 한다.
: 동기 방식 I/O연산에서는 데이터가 클라이언트에 전송되어야 완료되는것이 아니라, 전송을 위해 할당된 내부 메모리 버퍼에 복사가 완료되면 함수는 반환된다.
* 완료루틴 기반 확장 I/O
: 확장 I/O 제공 기능 = 중첩 I/O 제공기능 + a(루틴 컨트롤을 자동으로 해준다는 것)
: 따라서 중첩 I/O와는 달리 이벤트 오브젝트를 생성하지 않는다. I/O가 완료되면 완료루틴(함수)가 자동으로 호출되기 때문이다.
* 알림가능 상태(Alertable State)
: Windows는 완료루틴 실행 타이밍을 우리들이 결정할수있도록 한다.
: SleepEx 함수를 통해 미룰 수 있다.
* OVERLAPPED 구조체의 파일 위치 정보
: 하나의 파일에 중복 I/O로써 접근할 때 커널 오브젝트에 존재하는 파일의 위치정보는 아무 의미가 없다.
: 즉, 파일에 문자가 같은 위치에 덮여져 쓰일 오류가 있다는 것이다.
: 그러므로, OVERLAPPED구조체 맴버 중 데이터 I/O의 시작 위치를 지정하는 Offset, OffsetHigh 을 활용하여 각 함수의 데이터 I/O 시작 위치를 개발자가 직접 계산해야만한다.
* 타이머에서의 완료루틴
: 14장의 타이머와 SleepEx함수를 통해 완료 루틴을 만들수 있다.

* 지금까지의 내용 정리
-키워드 : 비동기 I/O, 동기 I/O, 블로킹 함수, 넌 블로킹 함수, 중첩(Overlapped), 완료루틴
: Windows는 기보넞ㄱ으로 두가지 방식의 비동기 I/O를 지원한다. 중첩 I/O방식과 완료루틴 확장I/O방식이다.
: 블로킹 함수는 호출된 함수가 일을 다 끝낸 다음에 반환하는 함수이다
: 넌 블로킹 함수는 호출되자마자 바로 반환을 하기 때문에 일이 완료되는 시점과 반환하는 시점이 서로 다르다. 그러므로 비동기 I/O는 넌블로킹 방식을 사용한다.
: 완료루틴의 경우 I/O가 완료되었을 때 지정된 함수가 호출된다는 특징을 가진다.
: 하지만 중첩 I/O의 경우 그러한 장치가 없기 때문에 중간에 I/O가 완료되었음을 확인하는 과정이 필요하다.

* APC(Asynchronous Procedure Call)
: 비동기 함수 호출 매커니즘을 의미한다.
* APC의 구조
: 두가지 종류로 나뉜다. User-mode APC 와 Kernel-mode APC 이다.
: Kernel-mode APC는 Normal Kernel-mode APC 와 Special Kernel-mode APC 로 나뉜다.
: 모든 쓰레드는 자신만의 APC Queue 라는 것을 가지고 있다. 

: APC Queue가 쓰레드마다 독립적이라는 것을 생각하면, 완료루틴이 쓰레드 마다 독립적인 메커니즘이라는 것을 알 수 있다.

* APC Queue의 접근
: APC Queue에 함수 정보를 전달 할 수 있는 방법은 소개한 내용 기준으로 세개 이다. WriteFileEX, ReadFileEx, SetWaitableTimer 함수이다.
: QueueUserAPC 함수를 이용하면 직접 전달할 수 있다.
: 나중에 완료루틴이 실행되었을 때 APC Queue에 있는 함수들이 순차적으로 실행되는 것이다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기





18장의 경우 함수 중심의 설명이기 때문에, 생략합니다.


1. 기본적인 파일 처리 함수들

* 파일 열기 닫기

: CreateFile, CloseHandle

* 파일 읽기 & 쓰기와 포인터

: ReadFile & WriteFile

* 파일의 시간 정보 얻어오기

: GetFileTime

* 파일 사이즈 얻어오기

: GetFileSize

* 파일 특성 정보 얻어오기

: GetFileAttributes, SetFileAttributes

* 파일의 특성 정보 핸들로부터 얻어오기 + a

: GetFileInformationByHandle

* 파일의 경로(path) 정보 얻어오기

: GetFullPathName

* 파일 포인터의 이동

: SetFilePointer

2. 디렉터리 관련 함수 및 그 밖의 함수들

* 디렉터리의 생성과 소멸

: CreateDirectory, RemoveDirectory

* 현재 디렉터리, 시스템 디렉터리 그리고 Windows 디렉터리

- 현재 디렉터리

: 초기에는 프로그램이 로드된 디렉터리로 설정되며 그 이후 변경 가능하다.

: GetCurrentDirectory

- 시스템 디렉터리(System Directory) & Windows 디렉터리

: 변경하면 안되는 디렉터리 이기 때문에 변경이 제한된다. 중요 라이브러리들이 저장되어 있다.

: GetSystemDirectory

* 디렉터리에서 파일 찾기

: SearchPath


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. SEH(Stuctured Exception Handling)

* 예외처리의 필요성

: 프로그램이 동작하는 부분과 흐름에 대한 예외처리 하는 부분을 구분하여 볼 수 있기 때문에 편리하다.

* 예외와 에러의 차이점

: 프로그램 실행 시 발생하는 문제점 대부분을 예외라고 인식해야 한다

: 예외 처리 가능하도록 프로그램을 구현해야 한다.

* 하드웨어 예외와 소프트웨어 예외

: 하드웨어 예외란 하드웨어에서 인식하고 알려주는 예외
    ex) 10 / 0 을 연산할 때 CPU가 운영체제에게 예외가 발생핬다는 것을 알린다.

: 소프트웨어 예외는 하드웨어에서 감지하는 예외이다.

: 하드웨어의 예외는 늘릴 수 없지만, 소프트웨어의 예외사항은 늘릴 수 있다.

2. 종료 핸들러(Termination Handler)

: SEH(Structured Exception Handling) 구조적 예외처리 메커니즘은 성능을 약간 저하 시키기 때문에, 서버프로그래밍에서는 사용하지 않는다.

: 종료 핸들러(Termination Handler)와 예외 핸들러(Exception Handler)로 나뉜다.

* 종료 핸들러의 기본 구성과 동작 원리

: _try 블록을 한줄이라도 실행하게 되면, 반드시 _finally 블록을 실행해야 한다.

: _try에서 return을 하더라도, 프로그램이 종료되기 전에 _finally 블록은 실행된다.

: _try 구문을 빠져나오게 하는 대표적 상황이다. {return, continue, break, goto, "예외"}

: 프로세스나 쓰레드의 강제종료일 떄에는 실행되지 않는다.

* 종료 핸들러 활용사례 연구 1

: 파일을 개방했을 때 무조건 닫아줘야 손실을 막을 수 있다.

: 메모리 동적할당시 해제 해주어야 한다.

* 종료 핸들러 활용 사레 연구 2

: 뮤텍스를 반환해야 할때.

3. 예외 핸들러(Exception Handler)

: 예외상황 발생 시 선별적으로 실행한다

* 예외 핸들러와 필터 (Exception Handler & Filters)

: _try 블록에서 예외가 발생했을 때, _except(예외필터) 블록에서 이 상황을 처리하게 된다. 

: 이 때 예외 필터를 통해 예외처리 메커니즘을 어떻게 동작시킬지 결정할 수 있다. 

: 예외상황 이후의 라인을 실행 시키지 않을 수도있고 실행 시킬 수도있다. (EXCEPTION_EXECUTE_HANDLER)

: 예외 핸들러를 사용하면 프로그램이 강제종료 되지 않는다.

* 예외 핸들러의 활용 사례 연구

: 예외 처리 이후의 실행위치를 결정할 수 있다.

* 처리되지 않은 예외의 이동

: 예외 처리가 되지 않은 블록일 경우 상위 블록에 예외처리가 있는지 스택 구조상 아래로 이동하고 해당 스택 프레임은 반환된다.

: main 함수내에서도 예외 처리할 수 없다면 프로그램이 종료된다.

* 핸들러의 중복

: 예외 핸들러는 예외 핸들러끼리 중복이 가능하고 종료 핸들러와도 중복이 가능하다.

* 정의되어 있는 예외의 종류와 예외를 구분하는 방법

: _except 블록 안에서 GetExceptionCode함수를 통해 예외의 종류를 확인할 수 있다.

* EXCEPTION_CONTINUE_EXECUTION & EXCEPTION_CONTINUE_SEARCH

: EXCEPTION_CONTINUE_EXECUTION 은 예외 발생시 다시 그 곳으로 가서 실행을 이어나가는 방식이다.

: EXCEPTION_CONTINUE_SEARCH 은 예외 발생시 다른 곳의 예외처리를 따르게 하는 방식이다.

4. 소프트웨어 기반의 개발자 정의 예외

: 개발자는 소트프웨어 예외에 해당하는 예외상황을 정의 및 추가할 수 있다.

* 소프트웨어 예외(Software Exceptions) 발생

: RaiseException 함수를 통해 예외알림을 추가할 수 있다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 메모리 계층(Memory Hierarchy)

: 컴퓨터를 구입할 때 CPU가 가장 중요시 되고 성능의 기준처럼 느껴지곤한다.

: 하지만, 개발자의 관점에서는 메모리가 가장 중요한 요소이다.

: 제한된 환경에서(CPU속도는 이미 결정되었다고 가정한다) 가장 높은 성능을 낼 수 있도록 하기 위해서는 메모리의 특성을 잘 파악하고 있어야 하기 때문이다.

* 메모리의 범위와 종류

: 컴퓨터를 구성하는 요소 중 임시적이든, 영구적이든 조금이라도 저장기능을 가지고 있으면 메모리 범위에 포함된다.

- 메인 메모리

: RAM 이다. 정확히는 D-RAM 계열의 메모리이다. 메인메모리가 반드시 램이어야할 이유는 없다. 보통 RAM을 사용하기 때문에 일반적인 의미에서 메인메모리가 RAM을 뜻한다

- 레지스터(Register)

: CPU안에 내장되어 연산을 위한 저장소를 제공한다.

- 캐쉬(Cache)

: 캐쉬는 D-RAM 보다 빠른 S-RAM으로 구성하는데, RAM과 구별하기 위해 보통 Cashe라고 표현한다. 

: CPU와 RAM사이에서 중간 저장소 역할을 하는 메모리이다.

: 요즘 캐쉬가 CPU에 내장되어있다고 표현하기도 하는데, 캐쉬 메모리는 원래 CPU의 일부로 존재하는 메모리 개념이 아니다. CPU에 근접해 있는 메모리 개념이다.

: CPU와 같이 올려져 디자인되는 것 뿐이다.

- 하드디스크(Hard Disk)와 이외의 저장 장치들

: 하드 디스크와 SD카드, CD-ROM과 같은 I/O 장치들도 메모리에 해당한다.

: 프로그래머가 개발하는 데 있어서 Register, Cashe, main memory, hard disk 뿐만아니라 I/O장치들과의 입출력 타이밍 및 대기시간 들을 가장 중요한 요소로 생각하고 항상 고민해야 한다.

* 메모리 계층 구조(Memory Hierarchy)

: 프로그램이 실행되는 동안 메모리가 하는 역할은 데이터의 입력 및 출력이다. 기본적인 역할은 모든 메모리가 동일하다.

: 가장 큰 차이점은 CPU기준으로 얼마나 떨어져 있느냐이다. 레지스터는 CPU와 가깝다 못해 안에 있는 메모리고, 그다음으로 캐쉬, 메인메모리, 하드디스크 순서가 된다.

: CPU와 가까이 있는 것이 가장 빠르고 멀리 있을 수록 속도가 느리다. Register에는 별다른 절차 없이 접근할 수 있지만, 메인 메모리에 접근하기 위해서는 복잡한 과정을 거쳐야 한다. Bus Interface Control 을 지나는 등의 과정이다.

: 메모리의 계층이 나뉘는 것은 기술과 비용때문이다. 

: 결국 모든 메모리의 역할은 자신 아래 계층에 대한 캐쉬(자주 사용되는 메모리의 일부를 저장해서 속도를 향상시키는 것)의 의미에서 존재하는 것으로 이해해야 한다.

: 이것이 비 효율적이게 보이겠지만, 실제로는 L1캐쉬와 L2 캐쉬에 연산에 필요한 데이터가 존재할 확률이 90% 이상 된다고 한다. 캐쉬가 속도를 아주 많이 향상시키는 셈이다.


Tip Level 1 캐쉬와 Level2 캐쉬

: 시스템의 성능을 좌우하는 클럭속도는 항상 느린쪽에 맞춰지게 된다. CPU가 아무리 좋아져도 Main Memory의 속도는 이를 따라가지 못하였고, CPU의 연산처리 속도가 두배 빨라졌다고 해도 주변장치의 속도가 그대로라면, 기대하는 만큼 속도 향상을 이룰 수 없다.

: CPU가 연산을 하기 위해서는 Main Memory 에서 피 연산자에 해당하는 데이터를 가져와야 하는데, 그 연산 결과를 메모리에 저장해야 다음 작업을 할 수 있다. I/O에 굉장한 시간이 소모되게 되고, 메모리가 중요하다는 이유가 여기 있다.

: 따라서 CPU가 Main Memory 의 병목현상 때문에 쉬고 있는 것을 줄이기 위해, L1 캐쉬를 두게 되었고 L1캐쉬의 용량적 한계때문에 하나 더 두어 L2 캐쉬가 있게 되었다. 


2. 캐쉬(cache)와 캐쉬 알고리즘

* 컴퓨터 프로그램의 일반적인 특성

: Temporal Locality(템퍼럴 로컬리티)란, 프로그램 실행 시 한번 접근이 이뤄진 주소의 메모리 영역은 자주 접근하게 된다는 프로그램 특성을 표현할 때 사용한다.

: Spatial Locality(스페이셜 로컬리티)란, 프로그램 실행 시 접근하는 메모리 영역은 이미 접근이 이루어진 영역이 근처일 확률이 높다는 프로그램 성격을 표현할 때 사용한다.

: 파워 개발자는 이런 식으로 캐쉬의 도움을 받을 수 있도록 프로그램을 구현하며 이를 캐쉬 프랜들리 코드(Cache Friendly Code)라고 한다.

* 캐쉬 알고리즘

: ALU 연산 과정에서 필요한 데이터가 있다면 Register로 가져와야하는데 이때 L1캐쉬에 데이터가 존재할 경우 이를 가리켜 캐쉬 힛(Cache Hit)라고 하며, 존재하지 않을 경우 캐쉬 미스(Cache Miss)라고 한다. 

: 캐쉬 미스가 발생했을 경우 L2 캐쉬 혹은 메인 메모리에서 데이터를 가져오게 되는데, 이때 해당하는 데이터 뿐만아니라, 데이터가 속해있는 하나의 블록을 모두 가져온다. 이는 블록 단위로 전송을 해서 스페이셜 로컬리티(Spatial Locality)의 특성을 성능 향상에 십분 활용하는 것이다.

: 아래 단계에 있는 메모리 일 수록, 블록 단위를 크게 잡아서 접근 횟수를 줄여 느린 속도에 대한 성능 향상을 도모한다.

: 캐쉬 미스가 발생했을 경우 데이터를 가져오게 되는데, 보통 캐쉬는 메모리를 꽉 채워 놓아 데이터 소유 확률을 높인다. 그렇기 때문에 캐쉬는 블록 교체 알고리즘에 대해 캐쉬 교체 정책(Cache's Replacement Policy)에 따르게 된다. 대학의 운영체제 수업에서 언급되는게 LRU(Least-Recently Used)알고리즘이며 가장 오래전에 참조된 블록을 밀어내는 알고리즘인데 실제로는 캐쉬 정책에 따라 조금씩 차이를 보인다.

* 캐쉬 프렌들리 코드(Cache Friendly Code) 작성 기법

: 가능한 템퍼럴 로컬리티와 스페이셜 로컬리티의 특성을 만족시키는 방향으로 가야 한다.

3. 가상 메모리 (Virtual Memory)

* 물리 주소(Physical Address)

: 실제 물리적인 메인 메모리의 주소 범위에 주소를 할당하는 것을 물리적 주소 지정(Physical Addressing)이라고 하고, 메인 메모리 크기에 따라서 지정 가능한 주소의 범위가 결정된다. 

: 주소범위에 제한이 생긴다는 것은 프로그램 개발에 있어서 엄청난 제약사항이다.

* 가상 주소 (Virtual Address) 시스템 1

: 실제 물리적인 공간은 할당할 수 있는 메모리 크기에 비해 턱없이 부족하다. 따라서 실제 존재하지 않는 가상의 주소를 지정하는 것을 가상 주소 지정(Virtual Addressing)이라 하며, 이러한 메모리 공간을 가상 메모리 공간(Virtual Address Space)라고 한다.

: 하드디스크가 메인 메모리의 역할을 못하라는 법은 없다. 더군다나 조금 느리지만, 용량이 많은 상황에서 메모리 공간을 할당해 준다고해서 문제될 것은 없다.

: 이렇게 되면 둘이상의 프로세스들에게도 4G이상의 메모리 공간 할당이 가능하다.

: 두가지 문제점만 고려하자

1. 선 할당으로 인한 부담

: 실제로 사용할지도 안할지도 모르는 프로세스에게 미리 많은 메모리를 할당한다는 것은 메모리와 시간 낭비이다.

2. 느린 속도의 개선 필요성

: 하드디스크는 너무 느리다

: 대부부분의 시스템에서 Paging(페이징) 이라는 기법을 사용한다.

: 아래 상황은 16bit 시스템에 16K 메인메모리, 프로세스별 64K메모리 할당을 가정한다.

: MMU(Memory Management Unit)은 16K밖에 존재하지 않는 메모리를 64K바이트가 존재하는 것처럼 CPU가 느끼도록 컨트롤 한다.

: 실제로 CPU와 함께 하나로 패키징 되는 장치이고 CPU는 메모리에 직접 접근하지 않고, MMU를 통해 요청한다.

: CPU가 20바이트 할당만 요청해도 MMU는 메모리 할당 단위에 따라 예를들어 4K단위로 할당하여 연산을 줄이고 속도를 빠르게 한다.

: 이러한 메모리 할당 단위, 즉 블럭을 페이지 프레임(Page Frame) 혹은 페이지(Page) 라고 하고 스페이셜 로컬리티의 특성을 위해 이용한다.

: 가상의 메모리에서 물리 메모리로 한 페이지씩 가져온다고 생각한다.

: 페이지 크기를 4K 바이트로 정하는 경우 64K 바이트 메모리 공간에서 얻는 페이지는 16개이다. 페이지 테이블을 통해 해당 페이지의 물리 주소를 찾아낼 수 있다. 예를 들어 상위 4비트는 몇번째 테이블인지, 하위 12비트는 해당 테이블의 어느 주소인지를 나타낸다.

* 가상주소(Virtual Address) 시스템 2

: 물리 메모리의 한계를 하드디스크를 통해 해결할 수 있다.

: 스왑 파일 (Swap File)이라는 개념을 도입해서 램(RAM)에 해당하는 메인 메모리를 하드디스크로까지 확장 한다.

: 가상 메모리에서 메모리의 할당과 주소 변환에 대한 내용들만 MMU가 처리한다.



뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 쓰레드 풀에 대한 이해

: 쓰레드의 생성과 소멸은 컴퓨터의 성능을 저하 시킨다.

: 쓰레드 풀이 기본 원리는 쓰레드의 재활용이다. 

: 쓰레드 풀에 존재하는 쓰레드 보다 일이 많으면 일이 순서대로 처리하게 할 수 도 있고 쓰레드 갯수를 유동적으로 변화시킬 수 있다.

2. 쓰레드 풀의 구현

* 쓰레드 풀 구현의 모듈별 해석

- 쓰레드 풀 자료구조

-쓰레드 풀의 함수 관계

3. 명령 프롬프트 프로젝트 기능 추가

*입력과 출력을 연결하는 파이프

: 서로 다른 프로세스의 입력과 출력을 연결하는 경우, 파이프를 구성한다고 표현한다

ex) type Random.txt | sort


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


1. 실행순서에 있어서의 동기화

: 메모리에 접근하는 쓰레드의 실행순서를 동기화 한다.

* 생산자/소비자 모델

: 입력하는 쓰레드와 출력하는 쓰레드를 독립시켜야 한다.

* 이벤트(Event) 기반 동기화

: 이벤트 오브젝트를 사용한다. 이벤트 오브젝트는 두가지 모드가 있다.

: 이벤트 오브젝트는 프로그래머 요청에 의해서 Signaled 상태가 된다.

: 함수가 Blocked 상태를 빠져나올 때 자동 리셋 모드 이벤트 오브젝트라면, Non-Signaled 상태로의 변화가 자동으로 이루어진다.

* 수동 리셋(Manual-Reset)모드 이벤트(Event)의 활용 예

: 수동 리셋 모드 이벤트는 원하는 타이밍에 둘 이상의 쓰레드를 동시에 깨워서 실행해야 할 때 아주 좋은 도구가 될 수 있다.

: 하지만 둘 이상의 쓰레드의 순서는 뮤텍스를 함께 사용해야 해결 할 수 있다.

2. 이벤트(Event) 더하기 뮤텍스(Mutex)

* 이벤트와 뮤텍스 오브젝트 적용 예제

3. 타이머(TImer) 기반 동기화

: Windows에서 Signaled 상태라는 개념이 중요하다. 어떤 커널 오브젝트는 자동으로 되기도 하고 함수호출을 통해 변하기도한다.

: 타이머 오브젝트는 정해진 시간이 지나면 자동으로 Signaled 상태가 되는 특성을 지닌다.

: 여기서 말하는 동기화는 쓰레드의 실행시간 및 실행주기를 결정하겠다는 의미이다.

: Non-Signal 로 생성된다.

- 수동리셋 타이머

: 가장 일반적인 타이머, 설정한 시간에 이벤트가 발생한다.

- 주기적 타이머

: 설정한 시간에 주기적으로 이벤트가 발생하거나, 설정한 시간 간격 주기로 이벤트가 발생한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기


: 동기화는 컴파일의 문제가 아니라 런타임에서 발생하는 오류다. 이러한 오류는 눈에 띄지 않고 디버깅하기 힘든 오류이다.

: 쓰레드에 관련된 런타임 오류를 미리 예측하고 막는것이 더 쉽다.


1. 쓰레드 동기화란 무엇인가?

* 두가지 관점에서의 쓰레드 동기화

: 순서에 있어서 질서가 지켜지고 있음을 의미하는 동기화 이다.

- 실행순서의 동기화

: 쓰레드 간의 실행 순서를 정의하고 이 순서에 따르도록 하는 것이 쓰레드의 동기화이다.

- 메모리 접근에 대한 동기화

: 메모리 접근에 있어서 동시 접근을 막는 것 또한 쓰레드의 동기화이다.

* 쓰레드 동기화에 있어서의 두가지 방법

- 유저 모드 동기화

: 동기화 시 커널의 힘을 빌리지 않는다. 커널모드로 전환되지 않기 때문에 성능상 이점이 있지만, 기능상 제한도 있다.

- 커널 모드 동기화

: 커널에서 제공하는 동기화 기능을 활용하는 방법이다.

2. 임계영역(Critical Section) 접근 동기화

* 임계영역(Critical Section)에 대한 이해

: 쓰레드가 동시에 접근하여 문제의 원인이 될 수 있는 코드의 블록을 가리켜 임계 영역이라고 한다.

: 임계 영역이란 배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 공유 리소스(전역변수와 같은)에 접근하는 코드 블록을 의미한다.

- 유저모드 동기화

1. 크리티컬 섹션 (Critical Section) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

2. 인터락 함수 (Interlocked Family Of Function) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

- 커널모드 동기화

3. 뮤텍스(Mutex) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

4. 세마포어(Semaphore) 기반의 동기화 : 메모리 접근 동기화에 사용할 예정

5. 이름있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화 : 프로세스 간 동기화에 사용할 예정

6. 이벤트(Event) 기반의 동기화 : 실행순서 동기화에 사용할 예정

3. 유저모드의 동기화 (Synchronization In User Mode)

: 유저모드는 커널 모드에 비해 성능상 이점이 있고 활용 방법도 단순하다.

* 크리티컬 섹션(Critical Section) 기반의 동기화

: 크리티컬 섹션 오브젝트를 만든다.

: 초기화 한다. -> 쓰레드가 크리티컬 섹션 오브젝트를 가져간다 -> 그 이외 쓰레드는 Blocked 된다.

: 쓰레드가 크리티컬 섹션 오브젝트를 반환한다. -> blocked된 쓰레드 중 하나가 가져간다. -> 반복 후 -> 오브젝트를 제거한다.

* 인터락 함수 ( Interlocked Family Of Function ) 기반의 동기화

: 변수 하나에 대한 동기화를 할 때 유용하다.

: volatile 을 사용하면, 컴파일러가 최적화를 하지 않고 캐쉬되지 않고 메모리로 바로 저장한다.

4. 커널 모드 동기화 (Synchronization In Kernel Mode)

: 유저모드 보다 느리지만, 다양한 기능이 사용 가능하다.

* 뮤텍스(Mutex) 기반의 동기화

: 크리티컬 섹션과 비슷

: 함수를 통해 생성되는 뮤텍스는 커널 오브젝트이다.

: 뮤텍스는 누군가에 의해 획득이 가능할 때 Signaled 상태에 놓인다.

: 사용 중일 때에는 Non-Signaled 상태이다.

* 세마포어(Semaphore) 기반의 동기화

: 뮤텍스와 비슷하지만, 임계영역에 접근가능한 쓰레드 개수를 조절하는 기능이 있다.

: 키를 반환할 때, 카운트가 하나씩 줄어들어 0이 되었을 경우 더이상 임계영역에 접근이 불가능하다.

* 이름있는 뮤텍스(Named Mutex) 기반의 프로세스 동기화

: 뮤텍스가 커널 오브젝트 기반이라면, 서로 다른 프로세스에 있는 쓰레드 끼리의 동기화도 가능할 것이다.

: 하지만, 핸들 테이블의 유효성 때문에 한계에 부딫힌다.

: 이를 해결하기 위해 뮤텍스에게 이름을 붙여준 것이다.

* 뮤텍스의 소유와 WAIT_ABANDONED

: 뮤텍스의 소유 쓰레드가 예상치 못하게 종료되었을 때 커널은 이를 반환해주고 다른 쓰레드에게 WAIT_ABANDONED를 반환한다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기




1. Windows에서의 쓰레드 생성과 소멸

* 쓰레드의 생성

: 쓰레드를 생성할 수 있는 최대 개수는 메모리가 허용하는 만큼이다.

: 쓰레드의 흐름은 예측할 수 없다. 누가 먼저 실행될 것인지 예측하는 것은 의미가 없다.

: Sleep 함수 -> 자신에게 현재 할당된 타임슬라이스를 포기하고 해당 시간동안 우선순위가 같은 다른 쓰레드에게 실행의 기회를 양보한다.

* 쓰레드의 소멸

: 쓰레드 함수 내에서 return으로 소멸시키는 것이 가장 이상적이다.

case 1 : 쓰레드 종료시 return을 이용하면 좋은 경우(거의 대부분의 경우)

: 1~10까지 더하는 상황을 가정했을 때 (I/O 작업이 발생한다고 가정)

: 이를 세개의 쓰레드에서 나눠서 진행한다면, 정해진 시간 동안에 CPU에게 보다 많은 일 시킬 수 있고, Blocked 상태에 놓이는 경우도 나눠서 감당하기 때문에 속도가 높아질 확률이 높다.

: 구현은 쓰레드에 나누어 실행 시킨뒤에 쓰레드의 커널 오브젝트를 관찰하여 종료되었을 때 메인 쓰레드에서 감지하고 종료한다.

case 2 : 쓰레드 종료 시 ExitThread 함수 호출이 유용한 경우(특정 위치에서 쓰레드의 실행을 종료시키고자 하는 경우)

: 언제 어디서나 쓰레드를 종료시킬 수 있지만, 메모리 누수현상이 일어날 수 있다.

: 하지만, 특정 함수가 호출되었을 때 종료시키고 싶다면, 유용하다.

case 3 : 쓰레드 종료 시 TerminateThread 함수 호출이 유용한 경우(외부에서 쓰레드를 종료시키고자 하는 경우)

: 강제 종료이기 때문에 사용하면 좋지 않다.

2. 쓰레드의 성격과 특성

* 힙, 데이터 영역, 그리고 코드 영역의 공유에 대한 검증

: 쓰레드 끼리 공유하고 있는 total 이라는 전역변수에 직접 결과값을 더함으로써 처리해야하는 코드 양이 줄었다,

* 동시접근에 있어서의 문제점

:  하지만 실제로는 total 값을 불러와서 레지스터에 저장한 뒤, 연산하고 다시 total에 저장하려고 할때 블럭킹되어 다른 쓰레드가 실행된 뒤 total에 값을 저장하게 되어, 연산 값이 제대로 반영되지 않는 경우가 다반사 이다.

: 실제로 Context Switching은 빈번하게 나타나며, 메모리 영역을 동시에 참조하는 것은 문제를 일으킬 가능성이 매우 높다.

* 프로세스로부터의 쓰레드 분리

: 쓰레드도 프로세스와 마찬가지로 생성될 때 Usage Count가 2가 된다. 따라서 CloseHandle함수를 곧바로 호출 함으로써 프로세스로부터 쓰레드를 분리할 수 있다.

: 그래야 쓰레드의 종료시점이 쓰레드의 소멸시점이 된다.

* ANSI 표준 C라이브러리와 쓰레드

: 마이크로소프트에서는 멀티 스레드에 안전한 ANSI 표준 라이브러리를 제공하고 있다. 

: CreateThread라는 함수 대신에 _beginthreadex 함수를 사용한다. 독립적인 메모리 블록을 할당한다는 차이점이 있다.

: 라이브러리에서 제공하는 함수를 사용하는 것이 안전하다.

3. 쓰레드의 상태 컨트롤

: 쓰레드의 상태는 운영체제가 관리하나, 필요에 따라서 프로그래머가 변경하는 경우도 발생한다.

* 쓰레드의 상태 변화

: 프로세스와 동일하게 이해하면된다.

: Running, Ready, Blocked

* Suspend & Resume

: SuspendThread는 Blocked 상태에 두는 함수이고 ResumeThread는 Ready 상태에 두기 위한 함수이다.

: SuspendThread 함수가 호출되면 해당 쓰레드의 커널 오브젝트에 SuspendCount(디폴트는 0)이 증가하고 Blocked상태가 된다. 반면 ResumeThread는 SuspendCount를 감소시키는 함수인데, 만약 SuspendThread 함수가 두번 호출되었다면, ResumeThread가 두번 호출되어 SuspendCount를 0으로 만들어야 쓰레드가 Ready 상태가 된다.

: 쓰레드 생성시 SuspendCount를 1로 두어 Blocked상태로 생성할 수 있다.

4. 쓰레드 우선순위 컨트롤    

: 사실 우선순위는 프로세스가 가지는 것이아니라, 쓰레드가 가진다.

: 프로세스는 기준 우선순위를 가지며, 쓰레드가 가지는 상대 우선순위와 결합하여 쓰레드의 우선순위가 결정된다.


뇌를 자극하는 윈도우즈 시스템 프로그래밍
국내도서
저자 : 윤성우
출판 : 한빛미디어 2007.03.30
상세보기



+ Recent posts