반응형

 

 

게임, 전화, 디스플레이, 네트워크 데이터 등등 각각의 프로세스별로 레디큐를 두어 처리 하는 방식이다

(게임 ,전화 등등이 우선순위가 하나의 기준으로 우선순위가 처리되지 않음으로)

각 우선순위를 위한 분리된 큐

 

 

 

위에 있는 것일 수록 우선순위가 높음

각각 레디큐가 별도고 가장 높은 우선순위의 큐가 비게 되면 그다음 system process 의 큐를 실행하고

system process 도 다 끝나면 그다음 interactive process (예를 들어 ui같은) 를 그다음 순위로 처리하는 방식이다

 

그런데 이때 문제점이 있는데

가장 상단의 우선순위가 높기 때문에 아래 있는 것일 수록 실행 되지 않는 기아상태가 발생할 수도 있기 때문에

 

그래서 우선순위가 가장 높은 큐는 퀀텀을 8 을 줘서 짧게 실행하게 하고 나가고 그다음 우순위에선 퀀텀 시간을 16을 줘서 좀 더 실행하게 하고 그다음 후순위는 FCFS 로 아래로 갈 수록 더 많은 CPU Burst time 을 할당 받기 때문에 아래 있는 것도 우선순위가 낮아 대기는 하지만 한번 실행될때 대기 한 만큼 좀 더 실행이 되도록 하여 어느정도 균형을 좀 맞출수 있는 형태가 된다

 

 

 

그런데 현대적 컴퓨터에선 프로세스 스케줄링을 하지 않고 스레드 스케줄링을 한다

그런데 지금까지 본것은 프로세스 스케줄링인데 스레드 스케줄링은 프로세스 스케줄링에 견주어 생각해보면 된다

그리고 프로세스에는 커널스레드와 유저스레드가 있는데 이 중에서도 커널 스레드만 스케줄링 하면 되는데

유저 스레드는 스레드 라이브러리에 의해서 관리 되는 소프트웨어적인 측면이기 때문에 커널 유저스레드에 대해 모른다

즉 커널 스레드가 스케룰이 되면 커널스레드는 유저스레드를 서비스만 해주면 된다

 

즉 지금까지 본것은 커널스레드를 스케줄링한다고 생각하면 된다

 

 

 

반응형
반응형

기본적으로 SJF 인데 비교 대상이 Burst Time 이 아니가 Priority 를 두어 이것을 기반으로

먼저 처리 할것을 선정하는 방식이다

 

이때 발생하는 문제가 실행시간이 짧은 프로세스가 있는데 우선순위가 먼저 처리되야 하는게 계속 들어오게 되면 실행되지 못하고 게속 대기해야 하는 기아(starvation) 이 발생하게 되는 문제가 있다

 

 

 

그래서 이 문제를 해결 하기 위해서 하는 방법은 프로세스에 aging 숫자를 도입해 오래 된 프로세스일 수록 우선순위를 높여주는 처리를 하여 이 문제를 막을 수 있다

 

 

 

 

좀더 나은 대안으로 RR 과 우선순위 기반의 스케줄링 두개를 섞어서 사용하는 방식도 있다

즉 우선순위를 먼저 처리하고 RR 로 돌아가면서 실행되도록

 

먼저 우선순위 높은걸 처리한다음 동일한 우선순위의 프로세스에 대해선 RR 로 처리한다

 

처음 P4 우선순위가 높기 때문에 P4 가 7초 동안 실행된 다음 P2, P3 우선순위가 같기 때문에 time quantum 즉 시간을 분할 하여 2초 마다 P2, 와 P3  환원큐 방식으로 돌아가면서 처리 되도록 하는 형태인걸 알수 있다

 

 

반응형
반응형

 

선행글 : https://3dmpengines.tistory.com/2340

 

이렇게 남아 있는 시간중에 더 짧은 시간인것이 먼저 처리 되게 하는 방식을

SRTF : 즉 Preemptive SJF 스케줄링 이라 한다 , R 은 Remaining

 

0초에 P1 이 도착

1 초에 P2 가 도착

2 초에 P3 가 도착

3 초에 P4 가 도착

 

위 처럼 뭉처 있게 되는데

 

실행순서를 보면 처음 P1 은 레디큐에 아무것도 없기 때문에 바로 실행된다

 

이때 1초가 지났을때 즉 절대 시간 1초에서 보면 

 

P1 의 Burst Time 은 8초에서 이제 7초가 남았고 

P2 가 이제 들어오게 되는데 P2 의 Burst Time 이 4 라서 P1 에 남아 있는 시간

7초보다 작으니 P2가 선점되어 실행되게 된다

 

그다음 절대 시간 3초에서 보면 P2의 남은 시간은 3초고 P3가 이제 도착 했는데 Burst Time 이 9초 임으로

P2 가 계속 실행된다 

 

절대시간 4초에서 보면 P2의 남아 있는 시간은 2초고 P4의 Burst Time 이 5초 임으로 마찬가지로 P2 가 계속 실행된다

 

절대시간 5초에서 보면 P2 는 끝났고 이제 남아 있는 P1의 시간과 나머지 P2, P4 와 마찬가지로 시간 경합을 하여 실행하게 된다

 

최종적으로 실행된 결과는 다음 처럼 된다

사실 위에 있는 이미지와 동일한데 한번더 쓴것

 

 

 

이것은 SJF 하고 비교하면 SRTF 가 평균대기 시간이 좀더 짧을 수 있다

실제 계산해보면 SJF 는 평균 대기 시간이 7.75가 된다

 

 

반응형
반응형

SRTF의 경우 짧은 시간을 먼저 실행하도록 계속 변경하다보면

만약  긴 CPU Burst time 동안 실행 되는게 있을때 계속 바뀌면 유저가 끊기는 불편을 느낄 수도 있다

게임이 끊겨서 버벅 거리는 것 과 같이 

 

 

 

RR 은 FCFS 를 돌면서 처리 하는 것인데 

FCFS 에서 시간을 쪼개서( time Quantum 또는time slice )실행하게 한다음 빠져나가게 처리 하는 방식 

그런데 이 쪼개는 시간 단위를 아주 작은 시간단위로 준다 10 ~ 100 밀리 세컨드의 길이로 분할한다

 

이렇게 되면 Ready Queue 에서는 환원큐로 구현하고

 

P1, P2, P3, P4 그다음 다시 P1 앞의 것이 실행 되도록 시간을 분할하여 돌아가며 실행되는 형태이다

 

 

 

그런데 만약 time quantum 시간 보다 프로세스가 처리 되는 시간이 짧다면?

레디큐에서 다음 실행할 프로세스를 바로 실행시키고

1 time quantum 시간 보다 길다면 OS 에서 시간이 됐을때 인터럽트를 발생시켜서 컨텍스트스위치가 일어나게 되고 다음 프로세스 실행시킨다

RR 은 평균 대기 시간이 조금 더 길수는 있다

RR은 time quantum 을 얼마는 주는지에 따라 성능이 확확 달라진다

quantum 시간을 짧게 주면 줄수록 즉 잘게 짜를 수록 컨텍스트 스위칭이 자주 발생하게 되어

전환시간 또한 높아지게 된다(dispatch latency)

quantum 을 최대로 길게하면 FCFS 가 된다

 

 

 

반응형

'운영체제 & 병렬처리 > 시스템프로그래밍' 카테고리의 다른 글

스케줄링 시간 : Priority-Base 과 RR  (0) 2023.02.26
스케줄링 시간 : SRTF  (0) 2023.02.25
스케줄링 시간 : SJF  (0) 2023.02.23
스케줄링 시간 : FCFS  (0) 2023.02.22
스케줄링  (0) 2023.02.21
반응형

SJF(Shortest-Job-First)

수행시간이 적은 것을 먼저 처리 하는 방식

만약 다음 처리 시간이 같다면 FCFS 로처리

 

 

 

Process 가 Ready Queue 에 있고 Burst Time 이 주어질때

 

Burst Time 이 가장 작은것 먼저 실행 시키면 위 처럼 간트차트가 된다

 

총 대기시간 28초

평균대기시간은 7초

 

턴어라운드 타임은 프로세스당 앞에 기다린 시간과 해당 프로세스가 처리 되는 시간까지 임으로

 

토탈 턴어라운드 타임은 52

평균 턴어라운드 타임은 13이 된다

 

 

최적이 증명가능하다

평균 대기 시간이 왜 최소로 갖느냐면 짧은게 앞에 있으니 대기시간이 짤방질 수 밖에 없다

 

그런데 이건 구현이 불가능한데, 다음 CPU Burst 타임을 알수가 없기 때문이다

대신 근사적으로 구하는건 예측을 통해서 구현할수 있는데 

예측은 이 프로세스가 과거에 얼마만큼 시간을 썼는지를 보고 판단하게 된다

 

시간은 지수적 시간을 적용하는데 그 이유는 최근에 많이 썼을 수록 가중치를 더 줘서 CPU burst 시간을 조정한다

 

10*1/2 + 6*1/2 = 8

타우식은 CPU Burst 타임이 타우 그래프에 따라 파란색 처럼 이동 평균선 역할을 해준다

 

그런데 이 전 사용시간에 대해서도 저장처리를 해야 하기 때문에 이론적으론 옵티멀한데 실제 사용하긴 어렵다

 

 

SJF 는 Preemtpvie 할 수도 있고 Non-Preemptive 할 수도 있다

앞서서 5초로 CPU burst 이 계산된게 있는데 새로 계산된 프로세스의 시간이 1 이라면 

이때 1초가 먼저 실행 되면 선점형이고 기다렸다 처리되면 비선점형이 된다

그런데 이렇게 할려면 해당  프로세스의 남은 CUP burst Time 을 알아야 하기 때문에 이것 또한 쉽지 않다

 

 

 

 

 

반응형
반응형

 

선행글 : https://3dmpengines.tistory.com/2338

 

 

 

시간 0 에서 위 프로세스들이 도착 했다고 가정 했을때 CPU 사용 시간이 24, 3, 3 이라 가정했을때

FCFS 는 다음과 같을 것이다 (아래 그림을 간츠차트라 한다)

FCFS : non-preemptive 로 비선점형이다

 

도착은 P1, P2, P3 순으로 도착하게 되고 P1 은 24 초에 끝나고 P2는 27초에 끝나게 된다

 

대기 시간을 보면

 

P1은 대기 시간이 0 

P2는 24

P3는 27이 된다

 

그래서 총 대기 시간은 0 + 24 + 27 = 51 초가 된다

평균은 51/3 = 17초가 된다

 

이때 턴어라운드 타임은 = p1=24, p2=27, p3=30  이 되고 

총 턴어라운드 타입은 = 24+27+30 = 81

평균 턴어라운드 타임은 = 81/3 = 27 이 된다

 

 

 

이때 평균 대기 시간을 줄일수 있는가? 에 대한 질문이 나오게 된다

여기서 선수를 바꾸면

이렇게 되고

 

그래서 총 대기 시간은 9 초가 된다

평균은 3 초가 되어 줄어들게 된다

 

이때 턴어라운드 타임은 = p1=3, p2=6, p3=30  이 되고

총 턴어라운드 타입은 = 3+6+30 = 39

평균 턴어라운드 타임은 = 39/3 = 13 이 된다

 

결론 : FCFS 같은 경우 프로세스의 처리 시간과 순서에 따라 평균 대기 시간이 확달라진다

FCFS 로는 좋은 효율을 얻기에는 어렵다

 

 

반응형
반응형

CPU 에 현재 프로세스가 끝난다음 이후 프로세스를 어떤거롤 선택할 것인가?

 

FIFO 는 큐형태를 말하는 것

우선순위큐는 우선순위가 높은것이 먼저 돌아간다

 

선점형 비선점형으로 우선순위가 갈리는데

 

Preemptive : 쫓아내는 것 , 돌아가고 있는 프로세스 쫓아내는 것

Non-preemptive : 못 쫓아내는 것 , 이것은 프로세스가 cpu 를 선점하고 나면 그 프로세스가 릴리즈 끝날때까지 기다린다는 것

 

 

 

 

1,4번의 경우를 보면 자발적으로 wait 으로 가거나 terminate 상태로 간다 즉 non-preemptive 하기 때문에 non-preemptive  나 preemptive를 고민할 필요가 없다

 

2,3 번의 경우는 2번은 돌다가 레디로 가니 쫓아내어지게 되는 것이고 3번은 대기하고 있는것이 준비가 갔을때 준비로간 프로세스 우선순위가 높다면 바로 실행으로 가기 때문에 쫓아내는 형태가 될수 있다

 

 

디스패처(Dispatcher) 란? : 컨텍스트 스위치를 해주는 모듈을 디스패처라고 한다

디스패처는 당연히 빨라야 한다

 

P0, P1 이 컨텍스트 스위칭 할때

 

PCB 란 운영체제가  프로세스를 제어하기 위해 정보를 저장해 놓은 곳으로 프로세스의 상태 정보를 저장하는 구조체이다

Context Switching 할대 필요하다

PCB 는 프로세스 생성 시 만들어지며 주기억장치에 유지된다

 

PCB0 번 블럭과 PCB1 번 블럭이 교체 되면서 컨텍스트 스위칭이 일어난다

그리고 이렇게 교체 되는 딜레이 시간을 dispatch latency 라고 한다 그리고 이 시간은 가금적 짧아야 한다

 


-------PCB에서 유지되는 정보----------
● PID : 프로세스의 고유 번호
● 상태 : 준비, 대기, 실행 등의 상태
● 포인터 : 다음 실행될 프로세스의 포인터
● Register save area : 레지스터 관련 정보
● Priority : 스케줄링 및 프로세스 우선순위
● 할당된 자원 정보
● Account : CPU 사용시간, 실제 사용된 시간
● 입출력 상태 정보
-------------------------------------------




CPU의 레지스터들의 값이 다른 것로 바뀌기 전에 어딘가에 저장을 하고 나가야해요.

이때 PCB에 CPU에서 수행되던 레지스터 값들이 저장이 됩니다. 내가 수행하던 프로세스가
어디까지 수행됐는지(프로그램 카운터), stack pointer의 위치가 어디인지, 그 외 register들의
집합 정보들을 잠시 저장한다는거죠. 어디에? PCB에!

그림을 보면 executing되다가 다른 프로세스 P1)을 수행시키기 위해 PCB0에다가
P0프로세스 정보를 저장하네요. 그리고 레지스터에 PCB1에 저장되어있던 process 1 정보를
가져와 P1을 수행시킵니다.


이런 저장 공간을 PCB라고 하고 사실 이렇게 수행중인 프로세스를 변경할 때 레지스터에
프로세스의 정보가 바뀌는 것을 Context Switching 문맥교환이라고 합니다.

 

 

위 그림은 컨텍스트 스위치가 얼마나 자주일어나는지 알수 있는 내용이다

vmstat 1 3 은 1초에 3번 확인한것

1초에 72번 1초에 73번 1초에 81번은 CW 가 일어났다는 것

 

 

스케줄링 기준

CPU utilization : cup 가 놀지 않고 최대한 CPU 를 돌리는 방식을 말한다

Throughput : 이 수치를 높이는 것인데 단위시간 내에 프로세스의 완결되는 숫자를 높이는 것

Turnaround time : 프로세스가 실행하면서 종료 될때까지 즉 실행에서 종료되는 시간까지의 시간을 최소화 시키는 것

Waiting time : 어떤 프로세스가 Ready Queue 에서 대기 시간을 최소화 시켜주는 것으로 Ready queue  에 있는 대기 시간의 합을 최소화 시켜주는 방식

 

Response time : 응답 시간을 최소화 하는 것 (UI 같이 반응이 즉각 일어 나는 것들의 반응 시간을 최소화 하는것)

 

 

스케줄링 프로블럼 : Ready Queue 에 있는 프로세스 중에서 어떤걸 CPU 에 올릴것인지에 대한 문제 

 

 

해결방법

 

FCFS : 처음 들어온걸 먼저 올려준다

SJF : SRTF  : 의 방식은 짧은 잡을 먼저 올린다

이렇게 둘은 전체의 해당 프로세스의 시간이 있을때 그 시간 전체를 모두다 할당해 주는 방식이다

 

RR :Round-Robin : 시분할로 시간을 분할하여  정해진 시간 만큼 실행되고 해당 시간이 종료되면 다른것이 올라와 실행 되는 방식

 

Priority-based : RR 을 쓰는데 우선순위를 부여하여 선점하는 방식

MLQ : Multi-Level Queue :  경우에 따라서 위의 방식들 중에서 스케줄링 방식을 선택하는 것

MLFQ :  MLQ를 하는데 피드백을 줘서 상황에 따라 더 좋은 걸 선택하려는 방식

 

 

 

 

 

반응형
반응형

모바일 AP에서 쓰로틀링(Throttling)이란?



모바일 AP 쪽에 관심이 있으신 분들은 ‘쓰로틀링(Throttling)’이라는 표현을 보셨을 겁니다. ‘목을 조르다’ 는 뜻의 영어 단어 ‘throttle’에서 온 단어로 모바일 AP 쪽에서는 목을 조르듯이 AP의 클럭을 꽉 조여서 발열을 억제하는 것을 말합니다.

 

PC의 CPU나 GPU, 모바일 AP 같은 반도체들은 일을 하면서 많든 적든 열을 발생시킵니다. PC의 CPU 같은 경우는 주로 팬을 쓰는 공랭식으로 열을 해결해서 발열이 일정 이상 넘어가면 PC에서 선풍기 돌아가는 굉음이 나게 됩니다.

 

그런데, 모바일 AP의 경우는 들어가는 곳이 스마트폰이나 태블릿 등의 협소한 공간이다 보니 발열을 잡기 위한 팬이나 기타 냉각 장치를 사용하기가 어렵습니다. 그렇다고 발열을 잡지 않을 수도 없는 만큼, 온도가 일정한 수준을 넘으면 AP의 작동 클럭을 강재로 낮춰서 발열을 억제합니다.

 

AP의 동작 클럭은 높으면 발열이 심해지지만 성능은 좋아집니다. 마찬가지로 클럭이 낮아지면 발열은 적어지지만 대신 성능도 함께 떨어지게 됩니다. 그러니 쓰로틀링으로 발열을 잡는다는 말은 결국 성능을 희생해서 급한 불을 끈다는 개념이지요. 모바일 AP에서 쓰로틀링이 심하다는 표현은 갑작스러운 성능 하락이 심하다는 말과 동의어입니다.

 

그러면, 이런 쓰로틀링이 심한 AP는 어떤 것이 있을까요? 아니면 쓰로틀링이 적은 AP는요? mobiledroid라는 사이트에서 현재 주로 쓰이는 상위 AP들을 상대로 쓰로틀링이 얼마나 심한지 측정해봤습니다.

 

일단 테스트에 참가한 모바일 AP들과 탑재하고 테스트에 임한 기기들의 명단은 아래 표와 같습니다.

 

탑재 기기제조사AP
iPhone 6애플A8
갤럭시 노트 4삼성엑시노스 5433
갤럭시 S6삼성엑시노스 7420
갤럭시 노트 4퀄컴스냅드래곤 805
LG G4퀄컴스냅드래곤 808
HTC One M9퀄컴스냅드래곤 810
ASUS Zenfone 2인텔아톰 Z3580

 

CPU 쓰로틀링 테스트




 

CPU 쓰로틀링 테스트는 GeekBench 결과를 기준으로 최고값에 대한 최저값의 비율을 구한 것입니다. 예를 들어 –40%라는 값은 최고값 100에 대해 최저값은 40이 낮다는 말입니다.

 

결과를 보면 애플 A8 프로세서와 퀄컴의 스냅드래곤 810 프로세서가 유난히 눈에 띕니다. 애플 A8의 경우 쓰로틀링 정도가 –10%를 조금 낮은 수준으로 거의 없다시피 하는데 반하여, 퀄컴 스냅드래곤 810의 경우 쓰로틀링 정도가 –60%를 넘는 것을 알 수 있습니다.

 


 

이것은 GeekBench 최고값과 평균값을 비교한 것으로 퀄컴 스냅드래곤 810의 심한 쓰로틀링이 더욱 두드러지게 나타납니다.

 

GPU 쓰로틀링 테스트

 

 

GPU 쓰로틀링 테스트는 GFXBench T-Rex Off-Screen 결과를 기준으로 CPU 때와 마찬가지로 최고값에 대한 최저값의 비율을 비교했습니다.

 

이번 결과에서도 애플 A8은 쓰로틀링이 거의 없는 모습을 보이며 압도적으로 우수한 면모를 나타냈습니다. 그리고 퀄컴의 스냅드래곤 808이 그 뒤를 이어 좋은 모습을 보여주는 군요.

 

CPU와 달리 GPU 쓰로틀링 테스트에서는 퀄컴 스냅드래곤 810이 평균적인 성적을 거뒀습니다. 좀 더 정확하게 말하면 우수한 성적을 남긴 애플 A8과 퀄컴 스냅드래곤 808, 그리고 인텔 아톰 Z3580을 제외한 나머지 AP들은 모두 비슷한 결과를 보여줍니다.

 

총평

사실 이 테스트에는 상당한 문제가 있습니다. 같은 AP라도 기기 제조사에 따라 쓰로틀링을 얼마나 걸지가 다릅니다. 어떤 제조사는 온도가 50도만 되도 클럭다운을 시작하고 어떤 제조사는 70도까지는 쓰로틀링을 걸지 않습니다. 그리고 같은 제조사라도 제품에 따라 쓰로틀링이 다릅니다. 비교적 여유 공간이 있는 패블릿은 상대적으로 발열에 강해서 다른 방법으로 AP를 냉각하고 스마트폰 같은 경우는 쓰로틀링 외에는 열에 대한 대책이 전혀 없으므로 클럭다운에 의존하는 바가 큽니다. 그러니 위의 테스트가 정확하게 AP의 쓰로틀링을 반영했다고 할 수는 없습니다.

 

그런 점을 생각하면서 결과를 보면 테스트는 사실 이미 전부터 알려져 있던 사실의 확인에 가깝습니다. 이번 테스트에서 최고의 성적을 기록한 애플 A8이 포함된 애플의 A 시리즈 칩셋들은 본래 쓰로틀링이 적기로 정평이 나있었습니다. 극단적일 정도로 애플의 iOS 기기에 최적화를 하고 비교적 낮은 클럭에서 작동해서 발열의 여지를 많이 줄였지요.

 

마찬가지로, 최악의 모습을 보여준 퀄컴의 스냅드래곤 810의 경우 이미 AP 제조사 퀄컴과 810을 사용하는 기기 제조사들을 제외하면 심각한 발열 문제 때문에 쓰로틀링이 심하기로 평판이 자자합니다.

 

이번 쓰로틀링 테스트는 새로운 사실을 알아 내었다기 보다는 기존에 알고 있었던 사실을 확인한 점에 의의를 두어야 할 것 같습니다.




ref : https://m.blog.naver.com/PostView.nhn?blogId=viperf40&logNo=220403927497&proxyReferer=https%3A%2F%2Fwww.google.co.kr%2F



반응형
반응형



CreateThread() 함수는 사용하지 말고 _beginthreadex()함수를 사용하라고하는데 CreateThread() 함수에는 어떤문제가 있는 것인가.




1. CreateThread()

CreateThread() 함수는 CRT 함수가 아닌 WinAPI 함수이다. 또한 이 함수는 멀티스레드가 고려되지 않은 시기에 만들어졌다.

그러므로 당연히 멀티스레드를 사용하는 상황에서 문제가 발생할 수 있다.

예를 들어 strtok() 함수 같은 경우 내부적으로 static 변수를 선언하고 사용하는데, 멀티스레드 환경에서 이러한 전역 변수를 동기화없이 공유하여 사용하게 된다면 문제가 될 수 있다. (물론, 멀티스레드가 고려된 strtok 함수는 내부가 좀 다를것이다.)






2. _beginthreadex()

그래서 나온것이 _beginthreadex() 함수이다. 

_beginthreadex() 함수도 내부적으로 CreateThread() 함수를 호출하지만 그에 앞서서 멀티스레드에서 활용될 독립적인 메모리 블록(_tiddata)을 CRT 힙에 할당한다.





할당한 이 메모리 블럭의 주소는 각 스레드의  TLS에 저장하여 연계시키며, 이는 각종 C/C++ 런타임 라이브러리 함수들의 멀티스레드 문제를 해결하는데 사용된다.


즉, Multi-Threaded 런타임 라이브러리 함수 호출 시 해당 스레드의 tiddata 메모리 블록을 찾아보게 될때 사용되는 것이다.






사실! CreateThread() 함수를 사용한다고 해서 tiddata 블록이 생성되지 않는 것은 아니다.

멀티스레드용 CRT 함수를 호출 시에 해당 스레드의 tiddata블록이 null 이라면 내부적으로 tiddata 블록을 할당하여 사용 하기 때문이다.


하지만, 문제는 CreateThread() 함수를 사용한 사용자가 _endthreadex() 함수를 호출하여 메모리 블럭을 해제하지 않을 것이기 때문에 문제가 발생하는 것이다. (분명 ExitThread() 함수로 종료 시킬것이다.


결국 현재 상황에서는 CreateThread() 함수의 호출은 메모리 누수와 관련이 있다고 볼 수 있는 것이다.





http://chfhrqnfrhc.tistory.com/entry/CreateThread-beginthreadex



다중스레드 사용하기 위한 필수 셋팅

 

 VC++ 로 만든 실행프로그램내에서 스레드를 1개만 사용하는 것이 아닌 경우 VC++ 프로젝트 속성 설정창에서 반드시 다중스레드를 지원하는 C run time library 를 사용하는 것으로 설정이 되어 있어야 한다. 

아래 그림처럼 프로젝트 속성창에서  "다중 스레드 DLL (/MD)" 를 선택해야한다. 프로젝트를 디버그 모드에서 개발중이라면  "다중 스레드 디버그 DLL (/MDd)" 를 선택하면된다.


이름에 DLL 이 붙어있는것은 C런타임 라이브러리를 동적으로 링크하는 설정이고, 이름에 DLL 이 붙어있지 않은 것은 정적으로 링크 한다는 의미이고, 이름에 dll이 붙어있는것은 동적으로 링크 한다는 설정인데, 이 설정은 프로젝트의 "속성  -> 일반 -> MFC 사용" 을 "정적 라이브러리에서 MFC사용" 으로 한 경우에는 런타임 라이브러리는  "다중스레드 (/MT)" 를 선택해야하고, "MFC사용" 선택을 동적  MFC사용으로 한경우에는 본 런타임 라이브러리에서는 "다중 스레드 DLL (/MD)" 를 선택해야한다. 이것 짝이 맞지 않은 경우에는 컴파일시에 에러가 나오므로 실수할 걱정은 하지 않아도 된다.

 

만일 아래 옵션이 단일스레드로 설정된 상태에서 _beginthreadex 나 _beginthread 를 사용하면 컴파일시 에러가 발생한다.






 http://igotit.tistory.com/entry/스레드-생성-방법-및-다중스레드-사용위한-필수-옵션-셋팅-VC [igotit]






2. 멀티 스레드 안전한 C/C++ Library
  역사적으로 C runtime-library 개발자는 멀티 스레드 어플리케이션에서 C runtime-library 를
사용하였을 때 발생하는 문제에 대해서는 전혀 고려하지 않았다. 멀티 스레드 어플리케이션
에서 전통적인 C runtime-library 를 사용하였을 때 문제가 발생할 수 있다.
따라서 Microsoft 는 이러한  문제를 해결하기 위해서 스레드 안전한 C/C++ runtime-library 를
제공하고 있다.
멀티 스레드 안전한 C/C++ run-time library 함수는 다른 스레들로부터 영향을 받지 않도록
자신을 호출한 스레드의 데이터 블록에만 접근 가능하게 한다.


3. Single-thread C/C++Library 와 Multi-thread C/C++ Library
Single-thread C/C++Library 는 단일 스레드 전용의 함수들을 말하고,
Multi-thread C/C++ Library 는 멀티 스레드 전용의 함수들을 말한다.

   ※ Visual Studio 2008 에서는 Multi-thread C/C++ Library 만 지원한다. 
       즉 더 이상 단일 스레드 전용의 C/C++ 라이브러리는 제공하지 않는다.



http://egloos.zum.com/heilow/v/83533

반응형
반응형

리눅스에서의 가상 메모리란?

윈도우랑 거의 같다

가상 메모리란?

리눅스는 가상 메모리(virtual memory)란 것을 지원한다. 이것은 메모리 사용량이 늘어남에 따라,

디스크의 일부를 마치 확장된 RAM처럼 사용할 수 있게 해주는 기술이다.

이 기술에 따르면, 커널은 실제 메모리(RAM)에 올라와 있는 메모리 블록들 중에 당장 쓰이지 않는 것을 디스크에 저장하는데,

이를 통해 사용가능한 메모리 영역을 훨씬 늘릴 수 있게 된다.


만일 디스크에 저장되었던 메모리 블록이 다시 필요하게 되면 그것은 다시 실제 메모리 안으로 올려지며, 대신 다른 블록이 디스크로 내려가게 된다.


그러나 이런 과정이 일어나고 있다는 것이 사용자에게는 전혀 보이지 않으며, 프로그램들에게도 그저 많은 양의 메모리가 있는 것처럼 보일 뿐이어서,

점유하고 있는 메모리가 디스크에 있는지 실제 메모리에 있는지 전혀 신경쓸 필요가 없게 된다.


그러나, 하드디스크를 읽고 쓰는 시간은 RAM보다 훨씬 느리기 때문에(보통 천배쯤 느리다), 프로그램의 실행은 그만큼 더디게 된다.

이렇듯 가상적인 메모리로 쓰이는 하드디스크의 영역을 `스왑 영역(swap space)'이라고 한다(swap은 바꿔치기를 한다는 뜻).


리눅스스왑 영역으로 일반적인 파일을 사용할 수도 있고 별도의 스왑을 위한 파티션을 사용할 수도 있다.

스왑 파티션은 속도가 빠른 반면에, 스왑 파일은 그 크기를 자유롭게 조절할 수 있다

(또한 스왑 파일을 사용하면, 리눅스 설치시에 파티션을 다시 해야 할 필요없이 모든 것을 그냥 설치할 수 있다).


스왑 영역이 얼마나 많이 필요한지를 미리 알고 있다면 그만큼 스왑 파티션을 잡으면 된다.

그러나 스왑 영역이 얼마나 필요할지 확실히 모른다면, 우선 스왑 파일을 사용해서 시스템을 가동해 보고

필요한 공간이 얼마인지 파악한 후에 스왑 파티션을 잡도록 하자.


또한 리눅스에서는 여러개의 스왑 파티션과 스왑 파일을 섞어서 사용할 수 있다.

이 방법을 이용하면, 언제나 큰 용량의 스왑 영역을 잡을 필요없이 그때 그때 필요한 만큼만 스왑을 늘려줄 수 있으므로 편리하다.


운영체제 용어에 관한 이야기 : 컴퓨터 과학에서는 스와핑(해당 프로세스 전체를 스왑 영역으로 내보냄)과 페이징(몇 킬로바이트의 작은 단위로 내보냄)을

구별하는 것이 일반적이다. 이 중에서 페이징이 좀더 효율적인 방법이며, 리눅스에서도 이 방법을 쓴다.

그러나 전통적인 리눅스 용어로는 이 두가지를 모두 뭉뚱그려서 스와핑이라고 흔히 불러왔다.


https://wiki.kldp.org/Translations/html/SysAdminGuide-KLDP/memory-management.html

반응형
반응형


기본적으로 멀티 프로그래밍(멀티 태스킹)을 지원하는 OS에서는 여러개의 프로세스(프로그램)이 함께 돌아가야 한다. 

즉 한 프로세스가 일방적으로 CPU를 독점하는 것이 아니라, 여러개의 프로세스가 잠깐씩 번갈아 실행되어 사용자 입장에서는

동시에 여러 프로세스가 돌아가는 것 처럼 느끼게 하는 것이다.

이 과정에서 필수적으로 프로세스 간의 문맥 전환(Context Switching)이 일어나고 문맥 전환에는 많은 비용이 든다.

가령 A라는 프로세스에서 B라는 프로세스로 CPU의 실행 권한이 넘어갈 때 , 다음 번 A의 실행이 재개되었을 때

실행의 연속성을 보장하기 위해 A 프로세스의 많은 정보를 어딘가에 보관해야 한다. (첫 번째로 메모리에 저장 시도)

 

만일 메모리가 무척 커서 A와 B 모든 코드나 데이터, 그리고 레지스터 값의 백업 데이터 등을 다 저장하고도 남는다면,

이 과정이 비교적 빠르게 진행될 수 있을 것이다. 하지만 불행히도 메모리가 두 프로세스 중 딱 하나의 정보만 수용할 수 있을 정도로

작다면, OS는 문맥 전환과 함께 스와핑(Swapping)이라는 과정을 거쳐야 한다.

 

스와핑이란 A프로세스의 문맥 정보, 즉 코드나 데이터, 레지스터 백업 값 등을 메모리에서 보조 기억장치(주로 하드디스크)로

옮겨둔 다음, 새로 실행이 재개되는  B프로세스의 문맥 정보들을 다시 하드디스크에서 읽어오는 것을 말한다.

하드디스크로 이런 프로세스의 문맥 정보를 옮기는 것을 Roll Out(롤 아웃), 반대로 재개되는 프로세스의 문맥 정보를 읽어서

메모리로 가져오는 것을 Roll In(롤 인)이라고 한다. 

이런 스와핑을 지원하는 OS라면 몇 개의 프로세스가 있더라도 이론적으로는 아무 문제없이 돌릴 수 있다.

롤인과 롤 아웃을 하는데 시간이 얼마나 걸리는지 굳이 따져보지 않더라도 경험적으로 충분히 문맥 전환 간격보다는 훨씬 클 거라 예상할 수 있다.

결국 그런 이유로 대부분의 시간을 스와핑하는데 다 쏟아 붓고 정작 프로그램을 돌리는 데에는 아주 적은 시간밖에 할애할 수 밖에 없는

아이러니한 상황이 된다.

 

이를 해결하기 위한 가장 쉬운 해결책은 대용량의 메모리를 사용하는 것이다.

그리고 한 프로세스가 모든 메모리를 차지하는 것이 아니라, 메모리가 가득 찰 때 까지 가능한 모든 프로세스를 다 메모리에 보관하고

스와핑 하지 않는 것이다.

메모리가 풀이 난 시점부터는 스와핑을 통해 문맥 전환을 할 수 있기도 하겠지만, 결국 문맥 전환할 때 마다 프로세스 단위로 롤인, 롤 아웃을

반복해야 한다면 다시 앞서의 배보다 배꼽이 더 커지는 문제점이 그대로 발생할 것이다.

 

이런 방식을 다중 분할 할당(Multiple-Partition Allocation) 이라 부른다.



만일 프로세스 2가 끝나기 전에 프로세스 4가 실행되려 한다면 앞의 프로세스 4는 자신의 크기만한 공간이 확보될 때까지,

즉 앞의 프로세스 중 누군가가 종료되어 공간이 날 때 까지 기다려야 할 것이다.

그나마 다행히 프로세스 2나 3이 종ㄹ되면 충분한 공간이 생기지만 프로세스 1은 종료되어 봐야 겨우 100바이트의 공간만

생길 뿐 200바이트 크기인 프로세스 4가 실행되기엔 역부족이다.

 

이런 문제를 단편화(Fragmentation)라 부른다.

즉 메모리 끝에 남아있던 100바이트와 프로세스 1이 종료되면서 생긴 100바이트를 합치면 200바이트니까 프로세스 4를 돌리기에

충분할 것 같지만, 두 공간이 나뉘어 있어서 실질적으론 쓸모없는 공간이라는 것이다.

이런 식의 단편화 문제를 뒤에서 보게 될 또 다른 형태의 단편화라는 것과 구분하기 위해

외부 단편화 라고 부른다. (External Fragmentation)

 

이러한 외부 단편화 문제를 해결하기 위해서 빈 공간을 활용하는 몇 가지 방법이 알려져 있고, 아예 자잘한 빈 공간들을 몰아서

큰 공간을 만드는 압축(Compaction)이라 불리는 방법이 있다.

빈 공간을 찾는 방법으로는 가장 먼저 발견되는 큰 공간을 사용할 것인지, 아니면 전체 빈 공간중에 새 프로세스의 크기와 가장 비슷한

곳을 활용하는 등의 몇 가지 방법이 있다. 이들은 실험적으로 어느 방법이 더 효율이 좋다 나쁘다 하는 것들이 알려져 있으나

이런 방식의 다중분할할당 자체가 그리 선호되는 방법이 아니다.

 

압축 역시 가장 확실한 방법이기는 하지만 현재 메모리상에 있는 프로세스들을 옮겨 연속되도록 붙여야 하는데,

앞에서 살펴본 컴파일 타임 혹은 적재 시간 주소 바인딩으로는 이를 해결하기 어렵다.

왜냐하면 두 방법 모두 프로세스가 실행되기 전에 메모리를 참조하는 인스트럭션들의 타겟 주소값이 모두 결정나버려

실행중에 있는 프로세스들이 옮겨지면 엉뚱한 메모리를 참조하게 되기 때문이다.


왼쪽 이미지에서 프로세스 1과 printf 함수 사이에는 빈공간이다 이것이 오른쪽 그림에서 압축 되면서 비어있던 공간이 아래로 배치됨

 


그래서 필요한 것이 바로 실행 시간 주소 바인딩(Execution Time Address Binding)이다.

실행시간 주소 바인딩에서는 call과 같은 인스트럭션의 타겟 메모리 주소가

실제 그 인스트럭션이 CPU에 의해 패치되기바로 직전에 결정나게 하는 방식이다.

이를 구현 하기 위해 컴파일러(혹은 링커)는 call printf부분의 코드를 생성할 때 이 프로세스가 항상 0번지를 기준으로

시작된다고 전제하고 타겟 주소를 생성한다. 그러면 printf루틴이 최종 컴파일 결과 코드의 제일 첫 부분에 위치하게 되었으므로

이를 호출하는 call printf 부분이 call 0x0로 기술될 것이다. 그리고 실제 이 코드가 CPU로 패치되는 시점에서

비로소 프로세스 2가 메모리에 위치한 시작 주소와 함께 더해져 최종 주소가 결정되는 것이다.


왼쪽 이미지에서 프로세스 1과 printf 함수 사이에는 빈공간이다 이것이 오른쪽 그림에서 압축 되면서 비어있던 공간이 아래로 배치됨



사실 이러한 실행시간 주소 바인딩은 (혹은 실시간 주소 바인딩) OS의 힘만으로는 구현하기 힘들다.

OS가 이를 해결하려면 매 인스트럭션 수행 때마다 OS로 다시 제어권이 넘어가 수행될 다음 인스트럭션의 주소 바인딩을 위해

인스트럭션을 수정해서 다시 메모리에 저장해야 하는데, 그 경우 속도가 어마어마하게 떨어지게 된다.

따라서 이런 경우 하드웨어 적인 지원이 필수적인데 최근의 고성능 CPU들은 대부분 이런 실행시간 주소 바인딩을 지원하기 위해

별도의 메커니즘을 내장하고 있다. 한 가지 간단한 예로 메모리를 참조하는 인스트럭션은 CPU가 읽어갈 때

그냥 읽어가는 것이 아니라 타겟 주소를 항상 특정한 레지스터 값과 더해서 읽는 것이다. (이 레지스터를 Relocation Register 라고 한다)

그러면 각 프로세스는 실행 될 때 필요한 자신의 시작 주소를 문맥이 전환될 때 마다 재위치 레지스터에 저장해 놓은 것 만으로

모든 메모리 주소를 참조하는 인스트럭션이 정상적인 메모리를 참조할 수 있다.


(call print 주소이동을 상대적으로 처리하겠다는 것)

 

하지만 압축을 한다 하더라도, 압축을 어느 시점에 해야할 지 고민이고

또 항상 압축된 상태로 있다해도 결국엔 프로세스 수가 늘어남에 따라 메모리가 가득차는 일이 벌어지고 말 것이다.

 

결국 다른 대안인 페이징 이라는 혁신적인 방법으로 해결 할 수 있다.

 


정보 출처 - 뇌를 자극하는 프로그래밍 원리 (CPU부터 OS까지) - 한세경 저


http://blog.naver.com/PostView.nhn?blogId=cnfldidhd&logNo=20171653078

 


반응형
반응형

캐시 메모리




1. 개요2. 배경3. 작동 원리4. 구조와 작동 방식5. 역사6. 유사한 것

1. 개요[편집]

Cache memory.

컴퓨터 시스템의 성능을 향상시키기 위해 주로 CPU 칩 안에 포함되는 빠르고 작고 비싼 메모리이다. 프로그램에서 직접적으로 읽거나 쓸 수 없고 하드웨어의 메모리 관리 시스템이 내부적으로 제어한다. 대부분 프로그램은 한 번 사용한 데이터를 다시 사용할 가능성이 높고, 그 주변의 데이터도 곧 사용할 가능성이 높은 데이터 지역성을 가지고 있다. 데이터 지역성을 활용하여 캐시보다는 느리지만 용량이 큰 메인 메모리에 있는 데이터를 캐시 메모리에 불러와 두고, CPU가 필요한 데이터를 캐시에서 먼저 찾도록 하면 시스템 성능을 향상시킬 수 있다.

흔히 1단계 캐시(L1 캐시)와 레지스터를 혼동하는데 완전히 다른 개념이다. L1 캐시에 있는 데이터도 궁극적으로는 레지스터에 올라가야 CPU에서 처리할 수 있으며, 레지스터 내 데이터는 프로그램 코드에서 직접 제어할 수 있다. 메모리 형태를 가지고 있는 L1 캐시와는 다르게 레지스터는 CPU 아키텍처에 따라서 종류와 의미가 다르다.

또한 가상메모리에서는 DRAM이 디스크의 캐시로서 동작한다.

2. 배경[편집]

CPU의 클럭 속도가 매우 빨라짐에 따라 CPU 밖에 있는 DRAM과의 속도 차이가 현저하게 증가하였는데, 이 때문에 CPU 클럭 속도를 아무리 올려도 DRAM에서 데이터를 빠르게 제공해 주지 못하여 전체 시스템 성능이 증가하기 어렵게 되었다. 메모리 기술은 주로 DRAM 기술, SRAM 기술로 나뉘는데, DRAM은 가격은 싸지만 속도가 느리고, SRAM은 속도는 빠르지만 가격이 비싸다는 단점이 있었다. 그래서 SRAM을 사용자가 직접 장착하게 하는 대신, CPU와 DRAM 사이에 SRAM을 별도로 두어서 DRAM의 데이터를 직접 접근하는 것보다는 빠르게 접근할 수 있도록 했다. 여기에 사용하는 SRAM을 캐시 메모리라고 한다. 폰 노이만 은 1946년 그의 논문 "Preliminary Discussion of the Logical Design of an Electronic Computing Instrument" 에서 캐시 메모리의 필요성을 예견했다. 역시 굇수 혼자 다 해먹네

3. 작동 원리[편집]

캐시 메모리는 데이터 지역성(Locality)의 원리를 사용한다. 데이터 지역성은 대표적으로 시간 지역성(Temporal locality)과 공간 지역성(Spatial Locality)으로 나뉘는데, 시간 지역성이란 for나 while 같은 반복문에 사용하는 조건 변수처럼 한 번 참조된 데이터는 잠시 후에 또 참조될 가능성이 높다는 것이고, 공간 지역성이란 A[0], A[1]과 같은 데이터 배열에 연속으로 접근할 때 참조된 데이터 근처에 있는 데이터가 잠시 후에 사용될 가능성이 높다는 것이다.

쉽게 예를 들자면 무지하게 지랄맞고 부지런한 상사가 2010년 재무결산 보고서를 가져오라고 했을 때, 무슨 일인지는 몰라도 또 가져오라고 할지도 모르니까 2010년 재무결산보고서를 일단 준비해 놓고, 2009년이나 2011년, 2012년 재무결산보고서도 가져오라고 할지 모르니까 그것도 준비해 놓는 식이다.

또 다른 예로는 캐시는 지갑이라고 생각하면 된다. 지갑 혹은 주머니가 없다면 우리가 현금이 필요할 때마다 매번 은행이나 ATM에 가야 할 것이다. 이는 당연히 매우 귀찮고 시간도 많이 걸린다. 하지만 우리가 현금을 지갑에 넣고 다님으로써 시간을 절약할 수 있다.

CPU가 메모리에 데이터를 요청할 때, DRAM에 접근하기 전에 일단 캐시 메모리에 접근하여 데이터 존재 여부를 확인한다. 캐시 메모리는 메인 메모리인 DRAM보다는 그 사이즈가 매우 작아서 데이터를 모두 저장할 수 없다. DRAM이 보통 4~16GB 정도인데 인텔 i5, i7에 들어가는 캐시 메모리는 32KB ~ 8MB 정도이다. 캐시 메모리는 DRAM의 데이터 일부를 가지고 있다가 CPU가 요청한 데이터가 캐시 메모리에 없으면 CPU를 잠시 기다리게 한 후 DRAM에서 해당 데이터를 가져온 후 CPU에게 넘겨 준다. CPU는 캐시의 존재를 알고 있지만, 그 위에서 실행되는 프로그램은 메모리 주소만 지정할 수 있지 프로그래머가 캐시를 직접 지정할 수는 없다. 이렇게 그 존재가 외부에 드러나지 않기 때문에 캐시 메모리는 CPU에 투명(transparent)하다고 한다. 투명하지 않은 작은 온칩 메모리는 Scratchpad Memory라고 부른다.

캐시에 데이터를 저장할 때 공간 지역성을 최대한 활용하기 위해 해당 데이터뿐만 아니라 옆 주소의 데이터도 같이 가져와 미래에 쓰일 것을 대비한다. DRAM에는 프로그램을 수행하는 명령어(Instruction)와 그 명령이 실행되는 데이터(Data)가 함께 들어 있는데, 명령어는 읽기만 하고 데이터는 읽기와 쓰기를 동시에 하므로 캐시 메모리 내에 이들을 각각 I-Cache(Instruction Cache)와 D-Cache(Data Cache)에 저장한다. 보통 L1 캐시에는 I-Cache와 D-Cache가 따로 있고, L2 캐시는 딱히 둘의 구분 없이 하나의 캐시 메모리로 구성된다. L1 캐시는 CPU에 직접 데이터를 공급해 주기 때문에 빠른 접근 지연 시간(Access latency)이 매우 중요한데, 명령어는 보통 공간 지역성이 높고 데이터는 보통 시간 지역성이 높다. 이 둘을 나누어 서로 다른 지역성을 이용할 수 있다. 또한 명령어와 데이터를 동시에 읽어올 수 있게 함으로써 CPU의 파이프라이닝 성능을 향상시킬 수 있다.

CPU가 데이터를 요청했을 때 캐시 메모리가 해당 데이터를 가지고 있다면 이를 캐시 히트라 부르고, 해당 데이터가 없어서 DRAM에서 가져와야 한다면 캐시 미스라 부른다. 

캐시 미스 발생시의 처리 방법은 캐시 정책에 따라 다르며, 데이터를 읽어 오는 시점으로 사용하기도 한다.

캐시 미스가 나는 경우는 대부분의 경우 3가지로 나눌 수 있는데

  • Compulsory miss(또는 cold miss) : 해당 메모리 주소를 처음 불렀기 때문에 나는 미스. 예를 들어 프로그램을 새로 켜거나 하는 경우 발생한다. 간혹 사용할 데이터를 미리 프리페치하는 경우가 아닌 이상 사실상 예방이 불가능한 캐시미스지만, 전체 컴퓨터 이용 시간에 비하면 굉장히 드물게 나는 미스 유형이라 전체적인 성능에 영향을 미치는 정도는 작다.

  • Conflict miss : 캐시 메모리에 A 데이터와 B 데이터를 저장해야 하는데, A와 B가 같은 캐시 메모리 주소에 할당되어서 나는 캐시 미스다. 예를 들어 내가 휴대폰과 따뜻한 커피캔은 항상 외투 오른쪽 주머니에만 넣는 습관이 있다고 하자. 평상시에는 오른쪽 주머니에 휴대폰만 넣고 다니는데, 어느날 친구에게 커피캔을 받아서 잠시 휴대폰을 가방 속에 넣어두고 커피캔을 오른쪽 주머니에 넣었다. 이때 휴대폰을 오른쪽 주머니에서 찾으려고 한다면 그때 conflict miss가 난다. direct mapped cache에서 가장 발생빈도가 높고, n-associative cache에서 n이 커질수록 발생빈도가 낮아지지만 대신 n이 커질수록 캐시 속도가 느려지고 파워도 많이 먹는다.

  • Capacity miss : 캐시 메모리에 공간이 부족해서 나는 캐시 미스. 위의 conflict miss는 캐시에 공간이 남아도는데도 불구하고 주소 할당때문에 나는 미스지만, capacity miss는 주소 할당이 잘 되어있더라도 공간이 부족하면 나는 미스다. 캐시 공간이 작아서 벌어지는 일이므로 캐시 크기를 키우면 해결되지만, 캐시 크기를 키우면 캐시 접근속도가 느려지고 파워를 많이 먹는다는 단점이 생긴다.


만약 대부분의 메모리 요청이 캐시 미스라면 캐시 메모리를 안 쓰는 게 더 빠르다. 하지만 다행히도 캐시 미스 비율이 대체로 평균 10% 안쪽이기 때문에 캐시 메모리를 통해 컴퓨터 시스템의 평균 성능을 크게 향상시킬 수 있으며 클럭 속도, 코어 개수와 함께 컴퓨터 성능에서 매우 큰 비중을 차지한다. 그러나 많은 사람들이 캐시 메모리에 대해 잘 모르며 이름부터 생소하니까 실제 캐시 메모리가 없이 클럭 속도가 더 높은 CPU가 클럭 속도는 낮지만 캐시 메모리가 있는 CPU보다 대체로 더 나쁜 성능을 보여준다. 셀러론이 싼데는 다 이유가

어떤 이는 컴퓨터에 사용된 도박의 원리라고 하기도 하는데, 이는 캐시 메모리의 작동 원리가 도박에서 돈을 거는 것과 유사하기 때문이다. 캐시 미스 비율 (질 확률)이 매우 낮아 거의 항상 이기는 도박이라 할 수 있다.

4. 구조와 작동 방식[편집]

  • Direct Mapped Cache


  • 가장 기본적인 캐시 구조. DRAM의 여러 주소가 캐시 메모리의 한 주소에 대응되는 다대일(n:1) 방식이다. 그림을 참고해서 간단히 설명하자면, 메모리의 공간이 32개(00000₂부터 11111₂)이고, 캐시 메모리의 공간이 8개(000₂부터 111₂)이면 메모리의 주소가 00000₂, 01000₂, 10000₂, 11000₂인 곳의 데이터를 캐시 메모리의 주소가 000₂인 곳에만 읽고 쓰는 방식이다. 이때 000₂을 인덱스 필드(Index Field), 인덱스를 제외한 나머지 비트 00₂, 01₂, 10₂, 11₂을 태그 필드(Tag Field)라 하고, 기본적으로 태그 비트와 인덱스 비트 뒤에 데이터를 저장하는 데이터 필드(Data Field)가 붙어있는 구조를 갖고 있다.


Direct Mapped Cache의 작동 방식은 간단하지않게 예를 들어, 캐시 메모리가 비워진 상태로 CPU가 메인 메모리의 00000₂ 주소의 데이터를 두 번 연속해서 읽는다고 하자. CPU는 먼저 캐시 메모리를 뒤적이는데 요청한 주소가 00000₂이므로 캐시 메모리의 주소가 000₂인 곳을 확인한다. 확인 후 아무것도 없는 것을 확인하고 직접 메인 메모리의 00000₂주소를 참조하여 데이터를 읽어온다. 이때 또 쓸지도 모르니까 캐시 메모리의 000₂인 공간에 00₂태그비트와 아까 읽은 메인 메모리 00000₂의 데이터를 저장해 놓는다. 그 다음 바로 같은 동작을 수행하면서 똑같이 캐시 메모리의 000₂ 인 곳을 확인하는데 이번엔 데이터가 들어있으니 그 데이터가 CPU가 요구한 주소의 데이터와 일치하는지 태그 비트를 비교하는 검사를 한 뒤 태그 비트마저 일치하면 캐시 메모리에서 데이터를 가져온다.

  • Set Associative Cache

  • Fully Associative Cache

5. 역사[편집]

초창기 캐시 메모리는 메인보드에 붙어서 CPU와 메인 메모리 사이에 존재했으나, CPU 집적도가 높아지면서 CPU 안에 캐시 메모리가 포함되게 되었다 오오 무어. 펜티엄용 메인보드는 CPU 근처에 별도의 캐시 메모리 슬롯이 있어서 필요한 경우에 L2 캐시 메모리를 증설할 수도 있었다. CPU 외부에 별도 장착되는 캐시 메모리는 CPU 속도와는 별개로 작동했기 때문에, 캐시 메모리 속도가 시스템 속도에 영향을 주었다.

펜티엄 이후 CPU에서 L2 이상의 캐시를 CPU 칩 패키징 내에 넣으려는 시도가 있었고, x86 아키텍처 중 인텔에서는 1995년 11월 출시된 펜티엄 프로에서, AMD에서는 1999년 출시된 K6-III에서 최초로 실현되었다. L2 캐시가 CPU 내에 통합되기 시작한 시기에는 캐시 메모리가 실장된 형태가 On-die, On-package로 구분되어 있었다. On-die 캐시는 하나의 다이 안에 CPU 코어와 L2 캐시가 함께 포함되어 있는 것이고, On-package 및 그와 유사한 용어는 CPU 코어와 L2 캐시 다이는 서로 나뉘어 있지만 하나의 패키징된 칩 안에서 서로 연결된 형태를 가리킨다.

인텔은 펜티엄 프로에서 하나의 칩 안에 CPU 코어와 L2 캐시 다이를 모두 올리는 형태를 사용했다. 그러나 가격 문제 때문에 일반 사용자를 대상으로 한 펜티엄 2와 초기 펜티엄 3 카트마이 및 셀러론 모델에서는 CPU 코어와 L2 캐시를 별도 칩으로 분리했고, CPU 형태도 소켓에서 슬롯으로 바꾸었다. 슬롯 1이 사용되던 시기의 인텔 CPU에서는 CPU 코어와 별개로 외부 제조사에서 만든 캐시용 SRAM이 붙어 있었고, 셀러론은 캐시 메모리가 붙어 있지 않거나 용량이 더 작았다. AMD는 K6-III에서 On-die L2 캐시를 도입했으나, 인텔과 같은 이유로 인해서 초기형 애슬론은 슬롯 형태로 전환했다. 그러나 기술이 발전하여 On-die 캐시를 만들기 쉬워졌고, 외부 캐시 메모리의 속도를 끌어 올리는 데에는 한계가 있기 때문에 인텔은 펜티엄 3 코퍼마인, AMD는 애슬론 선더버드부터 On-die L2 캐시로 복귀했다. 슬롯이 다시 소켓으로 돌아간 것은 보너스.

인텔은 초창기의 셀러론에서 L2 캐시를 없앴다가 엄청난 성능 저하를 보였다. 이후 인텔은 아무리 저가형 모델이라고 하더라도 L2 캐시는 용량이 작더라도 항상 달고 나오기 시작했다. 2015년 시점에서는 최소 2단계, 보통 3단계까지 존재한다. 3단계 캐시가 있는 CPU는 인텔은 펜티엄 4 Extreme Edition[1]인텔 코어 i 시리즈가 있으며 AMD는 AMD 페넘 시리즈에서 최초로 L3 캐시를 도입한 후, AMD 페넘 II 시리즈AMD FX 시리즈 등이 있다. 당연히 단계가 내려갈수록 속도는 상대적으로 떨어지게 되며, 반대급부로 용량은 증가한다. 물론 코어2 쿼드 같이 2단계 캐시가 12MB나 달린 깡패가 있긴 하다 인텔의 브로드웰에 와서는 4단계 캐시인 128MB eDRAM이 적용되었다. 이는 주로 내장그래픽 칩셋에서 사용한다. 속도가 빠르고 대역폭도 넓다보니 기존의 HD시리즈와는 차원이 다른 성능을 보여줬다.

6. 유사한 것[편집]

하드 디스크 사이의 속도 향상을 위해서 보통 16MB~64MB 정도의 메모리를 달아서 좀 더 빠른 입출력이 가능하도록 지원한다. 이를 '버퍼' 또는 '버퍼 메모리'라고 부르는데 캐시 메모리와 완전히 같진 않지만 거의 유사한 방식으로 동작한다. 

SSD에도 동일한 개념의 버퍼 메모리가 달려 있다. SSHD의 경우 하드 디스크의 버퍼를 아예 저용량의 SSD랑 섞어버린 것.

메인 메모리의 일부를 떼어서 HDD/SSD의 데이터를 캐싱하는것도 가능하며 이를 '디스크 캐시'라고 한다. DOS 시절 1M 메모리가 달린 286 이상의 컴퓨터에는 640kiB를 넘어가는 상위 메모리 영역의 일부를 디스크 캐시로 사용할 수 있도록 해 주는 유틸리티가 여러 종류 있었다. 이후 Windows 95로 넘어오면서 운영체제에서 자체적으로 디스크 캐시를 제공하게 되었다. Linux 등의 유닉스 계열 운영체제도 디스크 캐시를 기본 기능으로 보유하고 있다.



출처 : https://namu.wiki/w/%EC%BA%90%EC%8B%9C%20%EB%A9%94%EB%AA%A8%EB%A6%AC



반응형
반응형





http://blackrain.egloos.com/1224982

글 본문에 대한 내 생각을 밝히는 것이 아니기 때문에 트랙백은 남기지 않았다. 다만 글 쓰신 Gony님이 인용하신 '조엘 온 소프트웨어'의 대목에 많은 오류가 있어서 이렇게 글을 적어본다. 물론 좁은 지면과 독자의 배경 지식을 감안해서 쉽고 간단하게 malloc() 동작 원리를 썼으리라 생각한다. 그러나 실제 방식은 이와 많이 다르다. 혹시나 오해를 가질까 해서 이 글을 쓴다.

malloc이 어떻게 동작하는지 아십니까? malloc의 본질은 사용 가능한 메모리 블록을 연결 리스트linked list로 길게 연결한 자유 체인(free chain)입니다. malloc은 연결리스트를 따라가며, 요청 받은 메모리 양보다 큰 블록을 찾습니다. 이렇게 찾은 블록을 2개로 쪼개서, 하나는 호출한 사용자에게 반환하며, 쪼개고 난 다음에 남아있는 블록은 다시 연결 리스트에 넣어 둡니다. free를 호출할 때, free는 해제한 메모리를 자유 체인에 추가합니다. 결국 자유 체인은 자그마한 조각으로 잘게 쪼개지므로, 큰 조각을 요청하면 원하는 크기를 충족하는 조각이 없을 수도 있습니다. 이럴 경우 malloc이 타임아웃을 선언한 다음에 자유 체인 주위를 샅샅이 훑어 조각을 정렬하고 인접한 작은 자유 블록을 더 큰 블록으로 결합합니다. 이런 작업을 하다 보면 밤이 샐 것입니다. 결국 이 모든 혼란의 끝은 malloc의 성능 특성이 결코 아주 빠르다고 볼 수 없으며(malloc은 항상 자유 체인을 돌아다닙니다), 정리하는 과정에서 종종 예측할 수 없을 정도로 지독하게 느려질 수 있다는 사실로 귀결됩니다.

덧붙여 말하자면, 이런 현상은 가비지 컬렉션을 지원하는 시스템과 동일한 성능 특성이며, 시스템 성능 저하 원인이 가비지 컬렉션(garbage collection)이라는 주장이 전적으로 옳지만은 않다는 놀라운 결론에 도달합니다. 비록 정도는 약하지만, 전형적인 malloc구현을 따를 경우에도 가비지 컬렉션과 유사한 성능저하 문제가 생기니까요.

아 길다. 이걸 직접 타자하신 것 같은 Gony님께 감사의 말씀을 전한다 (띄어쓰기는 손을 좀 봤습니다). malloc의 작동 원리는 보기에는 간단하지만 실제로는 수 많은 최적화가 되어있어서 상당히 복잡하다. malloc 구현은 사실 여러 가지 버전이 있다. 대표적으로 리눅스의 libc에서 사용되는 Doug Lea의 dlmalloc이 있다. 이 외에도 Solaris 플랫폼의 버전도 있고 Win32 Heap 관리 알고리즘도 있다. 약간의 미세한 차이는 있지만 큰 틀은 차이가 없다. 내가 지적하는 것은 모두 Doug Lea의 malloc 구현 (버전 2.8.3)을 바탕으로 이야기 하는 것이다. 

malloc구현은 일단 큰 메모리를 시스템으로부터 얻어오는 것으로 시작한다. 리눅스라면 brk/sbrk가 있고, Win32라면 VirtualAlloc이 있다. 이들 함수가 user-level에서 사용할 수 있는 가장 원초적인 메모리 할당 함수들이다. 그러나 이들 메모리 할당 함수들은 그 할당 단위가 크다. 보통 페이지 단위이기 때문에 4KB 정도이다. 그리고 시작 메모리도 이 페이지 크기에 정렬이 되어있어야 하므로, 작은 메모리를 동적으로 할당 받고 쓰기에는 사실 상 불가능하다. 그래서 이렇게 크게 얻어온 메모리 덩어리를 잘게 썰어주고 관리하는 녀석들이 heap management library이다. 그리고 이 malloc 구현은 20년 가까이 된 것이고 속도와 메모리에 상당히 많은 최적화가 되어있다. 속도 보다는 메모리에 보다 더 많은 관심을 기울였다고 볼 수 있다.

또, malloc의 할당 단위는 보통 8byte이고 최소 할당 크기는 16byte 정도 된다. (시스템마다 32/64비트 마다 다를 수 있다). 그러니까 malloc(14)와 malloc(15)를 호출해도 부가 정보를 기록하기 위한 8바이트를 더해 다음 8바이트 단위로 align 시켜서 똑같이 24바이트가 할당 된다. 

예를 들어, malloc(16)을 호출해서 100번지의 주소를 반환 받았다면, 92번지에 현재 이 노드가 사용 중인가, 얼마나 할당 하였는가, 혹은 그 전 노드의 크기가 얼마인지를 저장한다. 이러한 노드는 'chunk'라는 구조체로 표현이 되는데 사실 이 보다 더 복잡하다. 비록 8바이트를 metadata를 위해 쓰지만 앞의 chunk가 사용 중이라면 맨 앞 4바이트는 앞 chunk의 데이터로 채워질 수 있다. 그러니까 겹쳐있는 부분이 있다. 메모리를 조금이라도 아끼기 위해 이런 방식을 취했는데 처음 소스 보면 이해하기 쉽지가 않다.

Malloc이 처음 나왔을 당시에는 그 누구도 이걸 악용해서 컴퓨터 제어권을 탈취하는 상황을 생각해 본 사람이 없다. 그래서 보안에는 사실 상 전혀 관심을 두고 있지 않았다. 물론 익히 알려진 consolidation attack등을 막기 위해 몇몇 알고리즘이 바뀌었고, Windows XP SP2에는 metadata 중 security cookie를 넣기도 하였다. 또한, layout obfuscation이라고 랜덤하게 위치를 바꾸어 공격을 어렵게 하는 방법도 채택이 되었다. 그러나 이러한 방식은 복잡도, entropy가 낮아서 결국 무식한 방법, brute-force로 충분히 뚫릴 수가 있다. 8비트 짜리 security cookie가 가지는 경우의 수가 고작 몇 가지 밖에 안되기 때문이다.


그렇다면 직접 조엘이 한 말을 한 문장씩 검증해보자.

malloc이 어떻게 동작하는지 아십니까? malloc의 본질은 사용 가능한 메모리 블록을 연결 리스트 linked list로 길게 연결한 자유 체인 free chain입니다. 

사용 가능한 메모리 블록 (free node)을 관리하는 것은 맞다. 그리고 이미 사용중인 메모리 블록은 따로 관리하지 않는다. 단순히 얼마나 많은 메모리가 사용 중인가 (footprint) 정도만 기억할 뿐이고, 실제 heap에 사용 중인 메모리를 확인하려면 일일이 heap을 돌아다니면서 체크해야 한다. 그런데, 이 free node를 절대 우리가 알고 있는 linked-list 형태로 관리하는 것이 아니다. 물론 chunk struct에 forward/backward로 list 스러운 모습이 있지만 결코 linked-list와 같은 형태로 작동되지는 않는다.

malloc은 연결리스트를 따라가며, 요청 받은 메모리 양보다 큰 블록을 찾습니다.

만약 앞 문장에서 독자가 흔히 알고 있는 linked-list를 떠올렸다면 이 대목은 아주 쉽게 이해가 된다. 즉, free node들을 쭉 이은 리스트를 선형 탐색 (혹은 더 똑똑한 탐색을 하던)으로 'first-fit'으로 하는 것으로 이해할 것이다. 그러나 free node는 그냥 무식하게 다 관리가 되는 것이 아니라 그 크기 별로 매우 효율적으로 관리되고 있다. Free node는 먼저, 512 byte보다 작은 경우에는 그 크기 별로 모두 별도로 관리 된다. 물론 이 때는 linked list 형태로 관리가 된다. 즉, 16 byte 짜리 free node 리스트, 24 byte 짜리 free node 리스트, … 이런 식으로 관리한다. 이들 포인터는 smallbins에 저장이 된다. 그래서 만약 512바이트 보다 작은 메모리가 요청 된다면 이 smallbins을 뒤져서 바로 free node 하나를 때어준다. 그러니 탐색을 할 필요 없고 그냥 상수 시간에 요청이 완료 된다.

그리고 이 보다 큰 경우에는 treebins라는 Trie 자료구조를 이용해서 저장이 된다. 그래서 임의의 크기가 들어왔을 때 'best-fit'으로 적합한 free node를 찾는데 결코 시간이 오래 걸리는 작업이 아니다. 트리 같은 구조를 순회를 하기는 하지만 Trie 자료구조 특성상 depth 에 비례하는 time complexity를 가진다. 일반 binary search tree처럼 전체 노드 개수 n과 관계가 없다.

마지막으로 페이지 단위보다 큰 메모리 요청이 들어오면 그냥 바로 mmap이나 VirtualAlloc을 불러버린다. 그래서 크기를 대략 3개의 큰 분류로 나누어서 처리한다. 게다가 비교적 최신 버전 malloc 구현에서는 designated victim이라는 변수를 두어 조금이라도 순회하는 양을 줄이려고 한다.

이렇게 찾은 블록을 2개로 쪼개서, 하나는 호출한 사용자에게 반환하며, 쪼개고 난 다음에 남아있는 블록은 다시 연결 리스트에 넣어 둡니다.

정확한 표현이다. 쓰고 남은 블록은 위에서 설명했듯이 크기가 크다면 treebins으로, 크기가 작다면 바로 리스트에 연결해준다. 그리고 locality를 보존해서 캐쉬 효율성을 높이려는 부분도 엿볼 수 있다.

free를 호출할 때, free는 해제한 메모리를 자유 체인에 추가합니다.

이 말은 맞으나, 단순히 해제한 메모리를 추가하는 것 이외에 coalescing, 즉 병합이라는 과정도 병행한다. 만약 free되는 노드 앞에도 free node라면 그 자리에서 바로 backward-consolidation을 수행한다. 초창기의 heap buffer overflow 어택은 바로 이 과정을 노린 것이었다. 여기에 포인터 연산이 들어가는데, 만약 이 metadata를 엄하게 고쳐놓으면 이상한 곳으로 점프할 수 있었다. 물론 이제는 이렇게 간단하게 heap integrity가 손상 받지는 않는다. 

결국 자유 체인은 자그마한 조각으로 잘게 쪼개지므로, 큰 조각을 요청하면 원하는 크기를 충족하는 조각이 없을 수도 있습니다. 이럴 경우 malloc이 타임아웃을 선언한 다음에 자유 체인 주위를 샅샅이 훑어 조각을 정렬하고 인접한 작은 자유 블록을 더 큰 블록으로 결합합니다. 이런 작업을 하다 보면 밤이 샐 것입니다.

첫 번째 문장이야 당연히 맞는 말이지만, 타임아웃 등의 이야기는 전혀 사실이 아니다. 적어도 Doug Lea의 구현에서는 저런 부분을 찾아볼 수가 없다. 위에서도 말했듯이 인접한 작은 자유 블록을 결합하는 것은 free를 할 때 마다 수행을 이미 했다. 그래서 안정된 힙 구조에서 (즉, malloc/free 호출이 끝난 뒤), 인접한 두 블록이 모두다 자유 블록인 경우는 절대 없다. 그래서 자유 체인 주위를 샅샅이 순회해가며 삽질하는 경우가 없다. 무한정 시간이 걸리는 부분은 한 곳도 없다. 그냥 찾다 찾아도 빈 공간이 없으면 (그래 봐야 3~4단계 작업이면 바로 알 수 있음), 그냥 sbrk등을 호출해서 메모리를 늘린다.

만약, 너무 많은 메모리 요구로 힙이 매우 단편화 되어있고, 그리고 최악의 경우가 발생해 매번 자유 블록 보다 큰 메모리가 들어온다고 해도 느려지는 것은 malloc 외적인 요소, 즉 스와핑이나 과도한 페이지 폴트로 인한 것이지 malloc 자체에서 병목현상은 거의 일어나지 않는다고 볼 수 있다. 작은 크기의 malloc/free는 1초당 5백만 번 이상 일어날 수 있을 정도로 상당히 빠르다. 그러나 일반적인 프로그램에서 이런 경우는 매우 찾아보기 힘들다. malloc 때문에 밤샌다는 표현은 비록 과장이지만 너무 터무니 없고 잘못된 인상을 심어주기에 충분하다.

결국 이 모든 혼란의 끝은 malloc의 성능 특성이 결코 아주 빠르다고 볼 수 없으며(malloc은 항상 자유 체인을 돌아다닙니다), 정리하는 과정에서 종종 예측할 수 없을 정도로 지독하게 느려질 수 있다는 사실로 귀결됩니다.

따라서, 이 결론도 완전히 거짓말이다. malloc은 결코 자유 체인을 항상 돌아다니지 않는다. 돌아다니는 것이 아니라 smallbin 뒤지고, treebin 뒤지는 과정만이 있고, 이 과정은 설명하였듯이 O(n)과 같은 시간이 걸리는 것이 아니다. 예측할 수 없을 정도로 느려진다면 이미 이 프로그램 자체가 매우 메모리를 과다하게 쓴다는 그 원초적인 문제 때문이다.

물론, 예전 에서도 잠깐 언급했는데, malloc은 멀티프로세서를 고려하지 않았다. 그래서 false sharing등이 큰 문제로 대두되어 여러 방법이 나오기도 하였다. 또, 최근 심각한 문제인 heap overflow vulnerability를 어떻게 막을 것인가도 여전히 숙제이다.

주절주절 거리다 보니 정말 글이 길어졌다. 과연 여기까지 얼마나 많은 분께서 다 읽으셨는지 궁금하다. 작년 가을 malloc 가지고 사투를 벌리면서 얻은 그래프나 하나 붙여본다. 3DMark 06 이 수행 도중에 얼마나 많은 malloc을 요청하는지 데이터를 뽑아보았다. 스레드 하나가 아니라 여러 개에서 부르다 보니 이 데이터 얻는데 꼬박 하루를 날렸었다. ㅠ 보면 초당 25만 번 까지도 malloc 요청이 일어나는데 이 정도로는 전혀 heap-intensive라고 부르지 않는다. 최소 2백만 번은 불러야 좀 빡센 프로그램으로 분류할 만큼 malloc은 빠르게 돌아간다.

정작 조엘이 언급하고 있는 가비지 콜렉터 이야기는 다음에 한 번… 흔히 자바에는 가비지 콜렉터 덕 분에 memory leak이 없는 것으로 알고 있지만 이는 잘못된 사실이다. 여전히 memory leak이 존재할 수 있다.




http://egloos.zum.com/minjang/v/1232908

반응형
반응형


MSDN에서 최적화된 코딩 방법에 대한 글을 일부 번역한 것입니다. 




처리속도가 빠른 코드를 만들기 위해서는 여러 방면으로 어플리케이션을 분석하고, 그 코드가 시스템과 어떤 상관관계를 취하고 있는지 조사 할 필요가 있습니다. 여기서는 코드의 타임 크리티컬한 부분의 퍼포먼스를 향상 시키기 위한, 몇가지의 코딩 테크닉에 대해서 설명합니다. 

타임 크리티컬한 코드를 고속화 할려면 이하의 점을 확인합니다. 
• 프로그램의 어느 부분을 고속화 해야되는가 
• 코드의 사이즈와 실행속도은 어느 정도인가. 
• 새로운 기능에 관계되는 코스트는 어느 정도인가 
• 처리를 완료 할려면 최소한 어느 정도의 작업이 필요한가 

코드 퍼포먼스에 관한 정보를 수집 할 때에는 퍼포먼스 모니터(perfmon.exe)를 사용합니다. 





캐쉬 미스와 페이지 폴트 

내부 캐쉬 및 외부 캐쉬를 히트 하지 않은 경우도, 페이지 폴트(프로그램 명령이랑 데이터를 2차원 스트레이지로 검색을 하는 것)가 발생한 경우와 같이 프로그램의 퍼포먼스가 저하된다. 

CPU 캐쉬가 히트 하지 않으면 10 ~ 20 클락 분의 로스가 발생한다고 계산된다. 
외부 캐쉬가 히트 하지 않으면 20 ~ 40 클락 분의 로스가 생긴다. 페이지 폴트가 발생한 경우는 프로세스가 매초 5억의 명령어를 처리하고, 1회 페이지 폴트로 2미리초가 걸린다고 가정하면, 100만 클락 분의 로스가 생긴다. 이를테면, 프로그램을 고속화 할려면, 캐쉬의 미스 히트와 페이지 폴트를 줄이는 세세한 주의를 줄 필요가 있다. 

프로그램이 느린 원인의 하나로써, 필요 이상으로 페이지 폴트랑 캐쉬의 미스 히트가 발생하고 있다고 생각 될 수 있다. 이것을 피하려면, 데이터 참조의 국소성(局所性 )이 높은 높은 데이터 구조체를 사용해야 되므로 관련이 있는 데이터를 모아 두는 것이 중요하다. 
일견 좋게 보이는 데이터 구조체라도, 데이터 참조의 국소성이 낮기 때문에 퍼포먼스가 나빠지게 되는 경우도 있고, 역으로 너무 나쁘게 보이는 데이터 구조체라도 데이터 요소의 국소성이 높기 때문에 고속으로 되는 것도 있다. 2개의 예를 다음에 나타내면. 

동적 메모리 할당으로 만들어진 링크드 리스트는 프로그램의 퍼포먼스를 저하 시키는 원인이 된다. 리스트 내의 아이템을 참조하기도 하고, 리스트 최후까지 이동하는 경우, 링크를 되돌아 갈 때마다 캐시의 미스 히트랑 페이지 폴트가 발생 하는 경우가 있기 때문이다. 실제, 단순한 배열로 구현딘 리스트쪽이 캐시가 유효하게 동작하여 페이지 폴트 회수가 낮기 때문에, 엄청나게 고속이다. 배열의 사이즈를 확장할 때마다 여분의 처리가 필요하다는 것을 참고하여도, 역시 이쪽이 고속이다. 

동적 메모리 할당으로 만들어진 링크드 리스트를 사용한 해쉬 테이블도 퍼포먼스를 저하시키는 원인이 된다. 이와 같은 링크드 리스트에 해쉬 테이블의 내용만을 저장하고 있는 경우라도, 예상이상을 퍼포먼스가 저하하는 경우도 있다. 실제, 배열을 사용한 단순한 리니아 서치 쪽이 최종적으로는 고속이라는 예도 있다. 배열 베이스의 해쉬 테이블(일명 “클로즈 해쉬 법)은 안 좋게 보이지만 퍼포먼스적으로는 뛰어난 방법이다. 



MFC 라이브러리와 클래스 라이브러리 

MFC를 사용하면, 코드의 기술이 간단하게 됩니다. 타임 크리티컬한 코드를 기술하는 경우, 클래스에 의한 오버헤드가 생길 가능성이 있으므로 주의가 필요합니다. 타임 크맅컬한 부분에서 사용하는 MFC 코드를 조사하여 그 코드가 요구하는 퍼포먼스에 합당한지 어떨지 판단한다. MFC 클래스 및 함수에 대한 주의점을 아래에 나타냅니다. 

• CString MFC는 C 런타임 라이브러리를 호출하여 CString의 메모리를 동적으로 할당합니다. 통상, CString은 동적으로 할당돤 문자열과 같은 역할을 합니다. CString은 동적으로 할당되어 그 외의 문자열과 같이 동적 활당하여 할당 해제 때에 오버헤드가 발생합니다. 많은 경우, 스택에서 단순한 chat 형의 배열을 잡으면, 같은 처리를 고속화 할 수 있습니다. CString은 정수 문자열의 저장에 사용하지 말아야 됩니다. 정수문자열에서는 const char*를 사용합니다. CString 오브젝트를 사용하는 처리에는 보통 오버헤드가 생김으로 런 타임 라이브러의 문자열 함수를 사용한 쪽이 고속화 할 수 있는 경우가 있습니다. 

• CArray는 통상의 배열에는 없는 유연성이 있지만 프로그램에 의해 그 유연성이 필요하지 않은 경우도 있습니다. 배열의 고유의 제한을 알고 있는 경우는 사이즈 고정형의 글로벌 배열을 사용 할 수 있습니다. CArray를 사용하는 경우는 CArray::SetSize를 사용하여 사이즈와 재 할당시의 증가 요소수를 지정합니다. SetSize를 사용하지 않으면, 요소를 추가 할 때마다 배열의 재 할당과 복사를 하기 때문에 실행속도가 저하되고 메모리가 단편화 합니다. 또 배열에 아이템을 삽입하면, CArray는 메모리 내의 후속 아이템을 이동하기 때문에 배열의 학장이 필요로 하는 경우가 있습니다. 이런 처리는 캐쉬의 미스 히트와 페이지 폴트의 원인이 됩니다. MFC에서 사용하는 코드를 조사하여 퍼포먼스의 향상에 무엇이 필료한지 확인하세요. 예를 들면, CArray는 템플리트이므로 특정의 형에 대해서 CArray의 특별한 형식을 지정 할 수 있습니다. 

• CList는 더블 링크드 리스트 이므로, 리스트의 선두, 꼬리, 및 지정된 위치로의 요소 삽입이 고속입니다. 정확히 값이랑 인덱스에 의해 요소를 검색할 때에는 연속적인 검색이 필요합니다. 이것은 리스트가 길어지면 시간이 걸립니다. 코드에 더블 링크드 리스트가 필요하지 않은 경우는 CList를 사용할 필요가 있을지 어떨지 재 검토해주세요. 연속 리스트를 사용하면, 여분의 포인터 때문에 메모리를 소비 할 일이 없고, 포인터의 갱신에 의한 오버헤드가 생기는 일도 없습니다. 여분 메모리의 소비자체는 그다지 큰 문제는 없습니다만, 캐쉬의 미스 히트랑 페이지 디폴트를 증가시키는 원인이 됩니다. 

• IsKindOf 이 함수는, 많은 함수 호출을 하는 것에 의해, 다른 데이터 영역에 다양한 메모리 접근을 합니다. 다시 말해, 데이터 참조가 국소적이지 않습니다. 이 함수는 디버그 빌드에서는 편리하여 ASSERT 호출등에서 사용됩니다. 릴리즈 빌드에서는 사용은 피해 주십시오 




출처 : http://coder.egloos.com/m/2804749


반응형
반응형

http://cafe.naver.com/ongameserver/3952



근래에 회사에서 만들고 있는 게임의 서버 애플리케이션에 LFH을 적용하면서 관련 자료를 좀 모아 보았습니다.


LFH ( Low fragmentation Heap )

1. 단편화 방지를 위해서 만들어진 것이지만 멀티코어 환경에서 특히 좋다.

2. 코어가 하나 있을 때는 오히려 10% 성능 하락도 있다( 신뢰 여부는 측정 불가 ^^; )

3. CRT에서는 크게 성능 향상을 볼 수 없다.
   (nhn Japan 블로그 글에서 CRT도 LFH를 적용했는지 정확하게 모르겠음
    CRT도 LFH 적용을 위해서는 CRT 힙 핸들을 HeapSetInformation에 적용해야 한다 )

4. LFH가 극상의 성능을 내기 위해서 Private Heap과의 조합이 최상이다.

5. Windows Server 2008/Vista 기본적으로 사용되도록 되어 있음
   : 이 때문에 메모리 측정(Private Bytes)를 할 때 정보가 좀 틀릴 수도 있음

6. LFH는 Windows XP, Windows 2000 Professional with SP4, Windows Server 2003 이상 가능



사용 방법

1. 프로세스 힙에 LFH 적용
ULONG ulEnableLFH = 2;
HeapSetInformation (
        GetProcessHeap(), 
        HeapCompatibilityInformation, 
        &ulEnableLFH, 
        sizeof(ulEnableLFH));

2. CRT 힙에 LFH 적용
intptr_t hCrtHeap = _get_heap_handle();
ULONG ulEnableLFH = 2;
if (HeapSetInformation((PVOID)hCrtHeap,
                           HeapCompatibilityInformation,
                           &ulEnableLFH, sizeof(ulEnableLFH)))
        puts("Enabling Low Fragmentation Heap succeeded");
    else
        puts("Enabling Low Fragmentation Heap failed");
    return 0;


반응형
반응형

http://valley.egloos.com/viewer/?url=http://byung.egloos.com/4812368


골치 아픈 Memory Fragmentation

byung.egloos.com/4812368

Application Debugging을 하다 보면간간히 들어오는 Issue OOM(Out Of Memory)에 대한 이슈인데이거 생각보다 골치가아프다사실 이를 위해서 DebugDiag  UMDH 라는 훌륭한 Tool에 의지하며, Allocation pattern을 Check 하는 것은debugging도 아니지만눈에 띄는 Allocation pattern이나 메모리상에 보유하는 있는 실제 Allocation(Committed) region이 그다지 높지 않은 데도 불구하고 OOM현상이 발생할 수 있다는 데에 어이없어 질 때가 있다그러면서 Application 개발자는 분명히Allocation/release 를 엄격하게 지켰을 뿐인데... 라고 말하기도 한다이런 경우 무엇을 할 수 있을 까.

이는 Memory Fragmentation 현상으로 판단한다상당히 자잘한 Memory Alloc이 셀 수 없이 반복되면서 메모리 조각화가 발생하는 것이다간혹큰 메모리 덩어리가 할당/해제 되기도 하고 그러면서 메모리 조각화는 더 할당 받을 수 있는 free 영역이 있음에도 불구하고 !address Check해보면연속된 여분의 메모리 블럭이 존재하지 않아서 OOM을 유발하게끔 한다이러한 경우에 일차적으로 할 수 있는 것은 1) 어찌됐든 불필요한 작고 많은 memory 조각들이 Leak으로 존재하는 부분을 제거해야만 한다.이는 DebugDiag Leak tracking을 통해서 Checking될 수 있다. DebugDiag에서 제공하는 Leak 분석에서 제공하는 Callstack 정보는 그 메모리 보유 Size가 크지 않더라도 Allocation Count Check하여 메모리상에 산재됨으로 인한 Fragmentation의 원인이 되진 않는 지 Check 해볼 수 있다.

2) 두 번째는 문서 http://support.microsoft.com/kb/315407 에서 제공하는 registry key, HeapDecommitFreeBlockThreshold 가 도움이 될 수 있다이는 레지스트리에 설정된 값 이상이 되는 경우가 아니면재사용을 위해 decommitted 되지 않는 상태로 유지하는 것이다.

 

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager

HeapDecommitFreeBlockThreshold REG_DWORD - 0x00040000 (권고)

 

3)사실 2번째 방법이 Application을 위해서 얼마나 효율적인지 경험해보진 못했다아마도 Windows XP/2003에서 도입된 LFH (Low Fragmentation Heap)을 사용하도록 Application을 구성하는 것이 가장 효율적일지 모르겠다이는 문서http://msdn.microsoft.com/en-us/library/aa366750.aspx 에도 언급되었지만, HeapSetInformation API를 이용해서HeapCompatibilityInformation(0) 값을 parameter 전달함으로써 Application Level에서 설정할 수 있다. http://msdn.microsoft.com/en-us/library/aa366705(VS.85).aspx 문서에는 다음과 같은 예제를 확인할 수 있다이는 Application에서 사용하는 모든 Heap을 LFH를 사용하도록 설정할 수 있다.

 

HANDLE heaps[1025];

  DWORD nheaps = GetProcessHeaps(1024, heaps);

 

  for (DWORD i = 0; i < nheaps; i++) {

    ULONG  HeapFragValue = 2;

    HeapSetInformation(heaps[i],

                       HeapCompatibilityInformation,

                       &HeapFragValue,

                       sizeof(HeapFragValue));

  }

이러한 것들이 Memory Fragmentation을 위해 도움이 될 듯 하다.


반응형
반응형
윈도우 힙 

  : 윈도우 힙은 모든 윈도우 응용프로그램이 공유하는 큰 메모리 공간으로, 주로 큰 메모리 블록을 할당 받고자
할때 사용합니다. 


CRT힙

  :  윈도우 힙과는 별도로, 프로그램마다 독립적으로 존재하는 힙. 각각의 프로세스는 윈도우 힙의 일부를
할당 받아 CRT힙으로 사용합니다.  CRT힙은 적은 양의 메모리 할당 요청에 대해 최적화 되어서 적은양의
메모리가 필요할때 CRT힙을 사용하는 것이 좋다.

C/C++ 언어의 표준 라이브러리 함수인 malloc 함수나  new 연산자를 사용한다.

초기에 64kb의 메모리 공간을 차지하며, 이 공간을 다 쓰게되면 메모리 공간을 두배씩 늘려갑니다.

동작은 malloc 보다 new 연산자가 빠르게 동작한다.
 


반응형
반응형

http://cafe.naver.com/jzsdn/17321


===== 19. 메모리 풀 =====
슈팅게임에서는 많은 오브젝트가 생성되고, 삭제되므로 
빠른 힙메모리 생성및 삭제가 필요하다.
그래서, 여기서는 MS가 만든 메모리 풀을 사용한다.


1. LFH사용설정
2. LFH호출
3. Low-Fragmentation Heap의 장점
4. Low-Fragmentation Heap사용시 참고

 

 


1. LFH사용설정
BOOL CheckLFH(VOID)
{
        ULONG  HeapFragValue = 2;

        if(HeapSetInformation(GetProcessHeap(),
                HeapCompatibilityInformation,
                &HeapFragValue,
                sizeof(HeapFragValue))
                )
        {
                return TRUE;
        }
        return FALSE;
}

프로세스의 힙핸들을 가져와서 설정하는것만으로 
메모리 풀을 사용하는 모든 절차는 끝이난다.

 

 

 


2. LFH호출
BOOL bLFH = CheckLFH();

if (bLFH)
        MessageBoxW( NULL, L"LFH를 사용합니다", L"정보", MB_OK );

프로그램이 시작할때 한번호출해준다.
디버거가 연결되면(F5) 디버그 힙을 사용하므로 CTRL+F5를 눌러야 
Low-Fragmentation Heap이 사용되고, TRUE가 리턴되는것을 알수있다.

 

 


3. Low-Fragmentation Heap의 장점
특별히 프로그래머는 신경쓰지 않아도, 메모리풀을 사용,
프로그램의 질을 높일수있다.
메모리 단편화를 예방할수있고, 힙메모리생성, 삭제시 효율이 높아진다.
특히, 멀티코어 시스템에서 높은 성능향상이 있다.

 

 

 


4. Low-Fragmentation Heap사용시 참고
이 함수가 호출되지 않아도, 이코드가 있으면 Win2000Pro Sp4이상의 
OS에서만 동작하므로, Win98을 지원하려고 한다면, 주석화시켜야 한다.

비스타는 이함수를 호출하지 않아도 기본동작한다.
이 함수는 하나의 프로세스에 메모리풀을 사용함으로 멀티프로세스로 동작하려면, 수정이필요하다.
16K보다 작은 메모리를 할당하면, LFH에 메모리가 할당, 그이상은 기존힙에서 할당된다.



  • 2009/07/28 10:30

    잘 봤습니다.
    음.. 궁금한 점이 있는데 new/delete도 HeapAlloc(GetProcessHeap()....) 으로 잡고
    HeapFree(GetProcessHeap(), ...)로 delete시키도록 오버로딩 시켜야 되지 않을까요??

  • 2009/07/28 21:01

    new/delete, malloc/free가 내부적으로 HeapAlloc/HeapFree를 호출합니다.


반응형
반응형

http://blog.naver.com/silver6688/60047045316



VOID GlobalMemoryStatus(

  LPMEMORYSTATUS lpBuffer   // pointer to the memory status structure

);

MEMORYSTATUS의 구조체를 보면


typedef struct _MEMORYSTATUS { 

    DWORD dwLength;                                     //구조체 크기

    DWORD dwMemoryLoad;                           //메모리 사용률

    SIZE_T dwTotalPhys;                                //전체 메모리

    SIZE_T dwAvailPhys;                                //사용한 메모리

    SIZE_T dwTotalPageFile; 

    SIZE_T dwAvailPageFile; 

    SIZE_T dwTotalVirtual;                              //총가상메모리

    SIZE_T dwAvailVirtual;                              //사용한 가상 메모리

} MEMORYSTATUS, *LPMEMORYSTATUS; 


라고 되어 있습니다.

MEMORYSTATUS ms;

GlobalMemoryStatus(&ms); 

ms.dwTotalPhys - ms.dwAvailPhys  

 

ex)

MEMORYSTATUSEX memStatus;

 memset( &memStatus, 0, sizeof( MEMORYSTATUSEX ) );

 memStatus.dwLength = sizeof( MEMORYSTATUSEX );

 

 GlobalMemoryStatusEx( &memStatus );

 char szPhysicalMemory[MAX_PATH] = _T("");

 sprintf( szPhysicalMemory, "메모리 용량 : %ld Mbytes", memStatus.ullTotalPhys / MEMORY_DIVIDER / MEMORY_DIVIDER );

 m_ctrSystemTree.SetInsertItem( hParent, szPhysicalMemory );

 char szVirtualMemory[MAX_PATH] = _T("");

 sprintf( szVirtualMemory, "가상 메모리 : %ld Mbytes", memStatus.ullTotalVirtual / MEMORY_DIVIDER / MEMORY_DIVIDER );

 m_ctrSystemTree.SetInsertItem( hParent, szVirtualMemory );

 char szPagingMemory[MAX_PATH] = _T("");

 sprintf( szPagingMemory, "페이징 파일 최대크기 : %ud", memStatus.ullTotalPageFile );

 m_ctrSystemTree.SetInsertItem( hParent, szPagingMemory );


반응형
반응형


http://goo.gl/u0UL1o


작업 관리자 메모리 열의 의미

작업 관리자에서 프로세스 탭에 표시되는 정보에 열을 추가하면 컴퓨터에서 실행 중인 프로세스를 모니터링할 수 있습니다. 이러한 열에는 프로세스의 현재 CPU(Central Processing Unit) 및 메모리 리소스 사용량 등 각 프로세스에 대한 정보가 표시됩니다.

  1. 작업 표시줄을 마우스 오른쪽 단추로 클릭한 다음 작업 관리자를 클릭하여 작업 관리자를 엽니다.

  2. 프로세스 탭을 클릭합니다. 작업 관리자에서 해당 사용자 계정으로 현재 실행 중인 프로세스를 표시합니다. 모든 사용자에 대해 실행 중인 프로세스를 표시하려면 모든 사용자의 프로세스 표시를 클릭합니다. 관리자 권한 필요 관리자 암호나 확인을 묻는 메시지가 표시되면 암호를 입력하거나 확인을 제공합니다.

  3. 기타 열을 추가하려면 보기를 클릭한 다음 열 선택을 클릭합니다. 표시할 열의 확인란을 선택한 다음 확인을 클릭합니다.

설명

PID(프로세스 식별자)

실행되는 동안 프로세스를 고유하게 식별하는 번호입니다.

사용자 이름

프로세스를 실행 중인 사용자 계정입니다.

세션 ID

프로세스의 소유자를 식별하는 번호입니다. 여러 명의 사용자가 로그온한 경우 각 사용자에 고유한 세션 ID가 있습니다.

CPU 사용

마지막 업데이트 이후 프로세스에서 CPU(Central Processing Unit)를 사용한 시간의 백분율이며 열 제목이CPU로 표시됩니다.

CPU 시간

시작된 이후 프로세스에서 사용한 총 프로세서 시간(초)입니다.

메모리 - 작업 집합

개인 작업 집합의 메모리 양과 프로세스에서 사용하고 있으며 다른 프로세스와 공유할 수 있는 메모리 사용량을 더한 것입니다.

메모리 - 최고 작업 집합

프로세스에서 사용하는 작업 집합 메모리 중 최대 메모리 양입니다.

메모리 - 작업 집합 변화량

프로세스에서 사용한 작업 집합 메모리의 변화량입니다.

메모리 - 개인 작업 집합

특히 프로세스에서 사용 중이며 다른 프로세스와 공유할 수 없는 메모리 양을 설명하는 작업 집합의 하위 집합입니다.

메모리 - 커밋 크기

프로세스에서 사용하려고 예약한 가상 메모리의 양입니다.

메모리 - 페이징 풀

하드 디스크 등의 다른 저장 미디어에 기록할 수 있는, 프로세스에 대해 커밋된 가상 메모리 양입니다.

메모리 - 비페이징 풀

다른 저장 미디어에 기록할 수 없는, 프로세스에 대해 커밋된 가상 메모리 양입니다.

페이지 폴트

메모리에 없으므로 디스크에서 프로세스의 데이터를 검색해야 하는 횟수입니다. 페이지 폴트 값은 프로세스가 시작된 시간부터 누적됩니다.

페이지 폴트 변화량

마지막 업데이트 이후 페이지 폴트 수의 변화입니다.

기본 우선 순위

프로세스의 스레드가 예약되는 순서를 결정하는 우선 순위입니다.

핸들

프로세스의 개체 표에 있는 개체 핸들 수입니다.

스레드

프로세스에서 실행 중인 스레드 수입니다.

USER 개체

현재 프로세스에서 사용 중인 USER 개체 수입니다. USER 개체는 창 관리자의 개체로, 창, 메뉴, 커서, 아이콘, 후크, 가속기, 모니터, 자판 배열 및 기타 내부 개체가 포함됩니다.

GDI 개체

그래픽 출력 장치에 대한 API(응용 프로그래밍 인터페이스)의 GDI(그래픽 장치 인터페이스) 개체 수입니다.

I/O 읽기

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 읽기 작업 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 읽기는 계산되지 않습니다.

I/O 쓰기

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 쓰기 작업 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 쓰기는 계산되지 않습니다.

I/O 기타

파일, 네트워크 및 장치 I/O를 포함하여 읽기 및 쓰기가 아닌 프로세스에서 생성한 입출력 작업 수입니다. 이러한 작업 유형의 예로 제어 기능이 있습니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 기타 작업은 계산되지 않습니다.

I/O 읽기 바이트

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 작업에서 읽은 바이트 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 읽기 바이트는 계산되지 않습니다.

I/O 쓰기 바이트

파일, 네트워크 및 장치 I/O를 포함하여 프로세스에서 생성한 입출력 작업에서 쓴 바이트 수입니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 쓰기 바이트는 계산되지 않습니다.

I/O 기타 바이트

파일, 네트워크 및 장치 I/O를 포함하여 읽기 및 쓰기가 아닌 프로세스에서 생성한 입출력 작업에서 전송된 바이트 수입니다. 이러한 작업 유형의 예로 제어 기능이 있습니다. CONSOLE(콘솔 입력 개체) 핸들로 보낸 I/O 기타 바이트는 계산되지 않습니다.

이미지 경로 이름

하드 디스크에 있는 프로세스의 위치입니다.

명령줄

프로세스를 만들기 위해 지정된 전체 명령줄입니다.

가상화

UAC(사용자 계정 컨트롤) 가상화가 사용되는지 여부 또는 이 프로세스에 대해 허용되지 않는지 식별합니다. UAC 가상화는 파일 및 레지스트리 쓰기 오류를 사용자별 위치로 리디렉션합니다.

설명

프로세스에 대한 설명입니다.

데이터 실행 방지

이 프로세스에 대해 데이터 실행 방지가 사용되는지 여부입니다. 자세한 내용은 데이터 실행 방지란?을 참조하십시오.


반응형
반응형

http://goo.gl/MRBRR




프로그램, 프로세스, 스레드의 차이를 알아보자.

일반적으로 프로그램(Program)은 메모장과 같은 하나의 실행 가능한 단위를 의미한다. 

그러나 아시다시피 메모장은 여러개를 동시에 실행할 수 있다. 이때 각각의 메모장은 서로 다른 프로세스(Process)에 의해서 실행이 된다.

즉, 프로세스는 프로그램을 객체화 시킨 것.

각 프로세스는 4GB의 개별 주소 공간과 파일, 메모리, 스레드를 소유하게 된다. 이 말에서도 나온것 처럼 스레드는 다시 프로세스에 속한다.

하나의 프로세스는 여러개의 스레드(Thread)를 소유할 수 있으며 프로세스 자체는 껍데기일 뿐이고 사실은 스레드가 모든 명령을 수행한다.

프로세스가 최초로 생성될때 각종 관련 변수와 메모리 등을 생성하면서 메인 스레드를 생성하게 되는데 이 메인 스레드가 사실 모든 명령들을 처리한다.

그러나 사용자가 원하면 멀티 스레드 (Multi-Thread)가 가능한데 즉, 하나의 프로세스가 여러개의 스레드를 가지고서 다양한 작업을 분산 시켜 처리하도록 하는 것이다.

사용자가 임의로 타이머를 만들어서 규칙적인 시간에 일정한 작업을 처리하는 것보다 (또는 PeekMessage를 이용한 방법보다) 운영체제가 알아서 시간을 쪼개에 스레드에게 작업을 나누어 주는 것이 훠어어얼씬 효율적이고 빠르단다.

다만, 멀티 스레드가 가지는 문제점은 하나의 프로세스안에 존재하는 스레드들은 같은 코드와 주소공간, 그리고 전역 변수를 '공유'하게 됨으로써 생기는 '자원 경쟁' 혹은 '비동기 문제'이다.

하나의 프로세스 안에 있는 1번 스레드가 전역 변수를 변경하고 있는 동안 다른 2번 스레드가 그 값을 참조하거나 다시 변경하게되면 사용자가 예상치 못한 이상한 값들로 바뀌어 버리는 문제가 생기게 된다.

그래서 이것을 해결하기 위해서는 가장 좋은 것이 스레드들이 공유하는 전역 변수 자체를 안만들거나, 혹은 스레드 지역 기억장소 (Thread Local Storage) 공간을 사용해야 한다.

TLS는 다른 말로 쓰자면 '하나의 스레드에 대해서만 전역 변수인 공간'이다. 

이렇게 복잡하게 설정을 해야 하는 이유는, C의 런타임 라이브러리가 애당초 Multi-Thread개념이 생기기 전에 만들어 졌기 때문이다.

기존 C 런타임 라이브러리는 함수들이 주로 Static 을 많이 사용하고 있는데 (특정 함수에서) 이 Static 변수들이 스레드 간에 '자원 경쟁' 혹은 '비동기' 때문에 엉뚱한 결과를 내는 현상을 가져왔다.

그래서 이걸 뜯어고치기되 기존 C 런타임 라이브러리를 동시에 유지하기 위해서 MLS가 도입된 것이다.


반응형
반응형
http://ezbeat.tistory.com/36

함수의 형태를 아래와 같이 생겼습니다.

DWORD WINAPI WaitForSingleObject(
  __in  HANDLE hHandle,
  __in  DWORD dwMilliseconds
);

위 함수는 커널 오브젝트의 상태 정보를 확인하는데 사용 됩니다.
즉, 해당 리소스의 커널 오브젝트가 Signaled인지 Non-Signaled인지 알 수 있습니다.

해당 리소스가 살아있으면 Non-Signaled (FALSE)이고 
해당 리소스가 종료되면 Signaled(TRUE)를 커널 오브젝트안에 가지고 있습니다.

첫번째 인자로는 해당 커널 오브젝트 핸들을 지정해 주고
두번째 인자는 Signaled 상태가 될 때까지 기다릴 수 있는 최대 시간을 밀리초 단위로 지정해 줍니다.
(INFINITE라고 인자를 주시면 해당 리소스가 종료 될 때까지 기다리고 있습니다. 
 즉, Signaled가 될 때까지 기다린다는 뜻입니다.)

자세한 내용은 MSDN을 참조해주세요 ^^

간단한 예제 소스로 결과를 확인해 봅시다.

===========================
예제 소스
===========================
#include <stdio.h>
#include <Windows.h>
#include <tchar.h>

int _tmain(int argc,TCHAR* args[])
{
STARTUPINFO start_info={0,};
PROCESS_INFORMATION process_info;

SetCurrentDirectory(_T("C:\\WINDOWS\\system32"));
TCHAR Command[] = _T("notepad.exe");

        /*메모장을 생성*/
CreateProcess(NULL,Command,NULL,NULL,TRUE,0,NULL,NULL,&start_info,&process_info);
       
       /*메모장이 종료 될때까지 멈춰있음*/
WaitForSingleObject(process_info.hProcess,INFINITE);

_tprintf(_T("Exit notepad. \n"));

return 0;
}

아마 실행해 보시면 메모장이 띄워지고 WaitForSingleObject 함수에서 멈춰 있는 것을 알 수 있습니다.
그리고 메모장을 끄시면 "Exit notepad."라는 문장이 출력되면서 예제소스 프로그램이 종료되는 것을 보실 수 있습니다.

======================================
쓰레드 동기화 시 사용되는 경우
======================================
쓰레드 동기화 시키는 방법에는 유저모드와 커널모드로 나눠지지만 WaitForSingleObject함수를 사용하는 경우는 
커널모드 쓰레드 동기화일 때만 사용합니다. 그에대한 2가지 방법으로  뮤텍스, 세마포어가 있습니다.

뮤텍스와 세마포어는 커널모드 쓰레드 동기화 방법 이므로 생성하면 커널 오브젝트 상태를 지닙니다.

1. 뮤텍스
임계영역에 들어가기 위해서는 무작정 들어가는 것이 아니라 키를 가지고 있어야만 들어갈 수 있는 것 입니다.
그럴 때 키는 얻는 함수로 사용되는 것이 WaitForSingleObject가 될 수 있습니다.
임계영역을 빠져나오면 키를 반환해야 하므로 그때 사용하는 함수는 ReleaseMutex가 되겠습니다.
간단하게 뮤텍스를 이용해 쓰레드들이 임계영역에 동시접근 하는 것을 막아보겠습니다.

상황을 간단하게 말하면
하나의 ATM기계가 있는데 10명의 사람들이 일을 보고 나오려고 하는 상황입니다.
각 사람들이 일보는 시간은 10~20초 사이입니다. ( 잘 안보이시면 클릭 )

위에 Working이라는 함수에서 WaitForSingleObject라는 함수를 사용하고 있는데 들어갈 수 있는 권한을 얻는 것입니다.
뮤텍스 커널 오브젝트도 마찬가지로 Non-Signed상태와 Signed상태가 있는데 해당 뮤텍스 커널 오브젝트가 Signed상태가 될때까지 기다리는 것입니다. Signed상태가 되면 기다리는 것들 중지하고 해당 쓰레드가 임계영역으로 들어가는 것입니다.

실행 결과 입니다.

동기화 방법 중에 뮤텍스 말고도 세마포어라는 것이 있는데 세마포어는 뮤텍스에 없는 카운트 기능이 있습니다.
즉, 임계영역에 동시에 들어 갈 수 있는 쓰레드의 개수를 정할 수 있는 기능이 있습니다.
하지만 WaitForSingleObject를 사용하는 방법은 같으므로 생략하도록 하겠습니다.

반응형
반응형


http://blog.naver.com/kimgudtjr/140152402219


[ 예외 ]

 

======= 고전적인 에러 처리 방법 =======

사람은 부정확한 존재이므로 항상 실수를 할 가능성이 있고 프로그래밍도 예외가 아니라고 함

일반 문법적인 예러

ex) if i==3  , siwtch(i) 

와 같은 문법적 에러는 컴파일러가 발견해주지만  논리적인 에러는 발견하기 힘들고 이런 것을 "버그"라고 한다


버그를 완전히 소탕해도 항상 에러를 일으킬 소지를 가지고 있는데 다음과 같은 경우라고 함

---------------------------------------------------------------------------------------------------
○ 존재하지 않는 파일을 열려고 함

○ 포인터가 빈 메모리르 가리키고 있음

○ 리소스나 메모리가 부족하여 작업을 계속 진행할 수 없다

○ 하드 디스크에 물리적인 손상으로 인해 사용할 수 없다. (베드 섹터)
---------------------------------------------------------------------------------------------------

 

고전적인 방법은

---------------------------------------------------------------------------------------------------
파일 핸들 = 파일 오픈(인자)

if(파일 핸들 != INVALiD_HANDLE_VALUE){
 파일 작업 진행
}
else
 에러 메시지 출력
---------------------------------------------------------------------------------------------------

위와 같은 방법은 함수의 리턴문을 비교하여 에러를 피하는 고전적인 방법이라고 함


상식적이고 쉽지만 불편하고 비효율적이라고 함

그래서 Win32에서는 에러만을 전문적으로 처리할 수 있는 매커니즘을 운영체제 차원에서 제공해준다고 함

 

======== 최후 에러 코드 ========


여러 윈도우 API함수들이 에러 발생시 리턴값으로 NULL을 리턴하여 알려주지만

구체적으로 어떤 에러가 발생했는지에 대한 자세한 정보는 알려주지 못한다고 함

그래서 아래와 같은 함수로 구체적인 에러내용을 알 수 있다.


---------------------------------------------------------------------------------------------------
DWORD GetLastError(VOID);
---------------------------------------------------------------------------------------------------

스레드는 최후 에러 코드를 기억하고 있으며 API 함수들은 에러 발생시 어떤 종류의 에러가 발생 했다는 것을 최후 에러 코드에

설정해 놓는다. 그러면 응용 프로그래램에서는 GetLastError 함수로 이 최후 에러 코드를 읽어 발생한 에러의 종류를

조사할 수 있다. 최후 에러 코드는 스레드별로 유지되므로 스위칭이 발생해도 최후 에러 코드는 그대로 유지된다.

그래서 다른 스레드에서 발생한 에러는 GetLastError로 알 수 없다.

최후 에러 코드에 대해 또 한가지 주의할 것은 이 코드는 실패에 대한 기록일 뿐이며 성공에 대해서는  리셋되지 않는 다는 것이다.

예를 들어 함수 A호출 결과 실패 코드 n이 기록되었다고 하자, 이어서 호출된 함수 B가 성공했다고 해서 A의 실패 코드 n이 0으로

리셋되지는 않으므로 이 값으로부터 B가 실패했다고 생각해서는 안된다. 함수 자체의 실패 여부는 리턴값으로 알 수 있으며

GetLastError는 최후 실패한 동작의 이유를 설명할 뿐이다.


함수 호출이 성공한 경우에도 최후 실패 코드가 설정되는 특수한 경우가 몇가지 있는데 성공할 수 있는 유형이 여러 개일 때

어떤 이유로 성공했는지도 GetLastError로 조사할 수 있다. 예를 들어 CrateMutex 함수는 뮤텍스를 생성하되

이미 있는 뮤텍스를 연 경우 최후 실패 코드를 ERROR_ALREADY_EXIST로 설정하여 새로 만든 뮤텍스가 아니라는 것을

리턴한다. 시스템이 정의하는 에러 코드 중 자주 발생하는 것은 다음과 같다.

 

---------------------------------------------------------------------------------------------------
에러 이름     코드   설명
---------------------------------------------------------------------------------------------------
ERROR_SUCCESS     0   성공

ERROR_INVALID_FUNCTION    1   함수가 없음

ERROR_FILE_NOT_FOUND    2   파일을 찾을 수 없음

ERROR_PATH_NOT_FOUND    3   경로를 찾을 수 없음

ERROR_TOO_MANY_OPEN_FILES   4   파일을 여려 수 없음

ERROR_ACCESS_DENIED    5   액세스 권한이 없음

ERROR_INVALID_HANDLE    6   핸들이 무효함

ERROR_NOT_ENOUGH_MEMORY    8   메모리가 부족함

ERROR_WRITE_PROTECT    19   쓰기 금지되어 있음

ERROR_FILE_EXISTS    80   파일이 이미 존재함

ERROR_DISK_FULL     112   디스크에 여유 공간이 없음
---------------------------------------------------------------------------------------------------

자주 발생하는 에러 몇 가지에 대해서만 썼지만  에러 종류는 아주 많으므로 래퍼런스를 참조바란다고 함

에러 코드는 시스템이 정의하여 사용하며 API 함수들이 적절히 설정하지만 사용자가 만든 함수도

에러 코드로 정의할 수 있다. 응용 프로그래램의 에러 코드는 29번 비트를 1로 설정하여 정의하며

SetLastError 함수로 최후 에러 코드를 설정하면 된다.  마이크로소프트가 정의한 에러 비트는 29번이 0으로 되어

있어 사용자가 정의한 커스텀 에러 코드와는 구분되게 해 놓았다. 이렇게 에러 코드를 정의하면

시스템의 최후 에러 코드를 응요요 프로글ㄻ이 사용할 수도 있다.

 

만약 에러 발생 사실만 사용자에게 알리려면 다음 함수로 에러 메시지 문자열을 구할 수 있다.

---------------------------------------------------------------------------------------------------
DWORD FormatMessage(DWORD dwFlags, LPCVOID lpSource, DWORD dwMessageld, DWORD dwLanguageld, LPTSTR lpBuffer, DWORD nSize, val_list *Arguments);
---------------------------------------------------------------------------------------------------

에러 메시지 문자열은 시스템에 정의되어 있기도 하지만 응용 프로그래램이 리소스에 메시지 테이플 형태로 정의할 수도 있고

함수로 즉시 조립할 수도 있따.또한 이 함수는 메시지버퍼를 내부에서  할당하기도 하고 서식 문자열도 지원하기 때문에

굉장히 복잡하고 사용방법도 어렵다.

자세한 사용방법은 래퍼런스를 참고하되 시스템 메시지 문자열만 얻고자 한다면 다음과 같이 단순화 하여 사용하면 된다.

---------------------------------------------------------------------------------------------------
FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,NULL,에러코드,0,버퍼,버퍼길이,NULL);
---------------------------------------------------------------------------------------------------

세 번째 인수에 GetLastError로 조사한 에러 코드, 다섯 번째 인수에 에러 메시지를 돌려받을 버퍼,

여섯번째 인수에 버퍼 길이를 전달하면 시스템이 정의하는 에러 메시지 문자열을 구할 수 있다.

 

예)
---------------------------------------------------------------------------------------------------
#include <windows.h>
#include <stdio.h>

int main(){

 HANDLE hFile;

 hFile = CreateFile("c:\\NotExistFile.txt",GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);

 if(hFile != INVALID_HANDLE_VALUE)
  CloseHandle(hFile);
 

 DWORD code; 
 code = GetLastError();
 char errMes[1024] = {0};

 FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,NULL,code,0,errMes,1024,NULL);

 
 printf("%s\n",errMes);

 return 0;
}

---------------------------------------------------------------------------------------------------

퀵 와치 윈도우에서 @err 라는 예약된 변수명을 확임함으로써 최후 에러 코드를 조사할 수 있다.

@err, hr 형식으로 문자열 형태의 에러 메시지를 읽을 수도 있다.

 


============================================================================================================

[ 예외의 정의 ]

예외(Exception)란 정상적인 코드 흐름의 외부 코드를 실해앟는 일종의 이벤트이다. 예외는 하드웨어에 의해

발생할 수도 있고 소프트웨어에 의해 발생할 수도 있다. 운영체제가 발생했을 때 예외를 처리하는 핸들러를 호출하여

프로그래램이 예외에 대처할 수 있도록 한다. 예외는 여러 가지 요인에 의해 발생ㅇ하는데 예외를 근본적으로 막을 수 있는 방법은 어벖다.

왜냐하면 프로그램이 CPU내부에 갇혀서 실행되는 것이 아니라 사용자와 상호작용하고 주변기기 등의 외부적인 환경과 어울려서

동작하기 때문이다. 운영체제가 예외 처리 메커니즘을 제공함으로써 다음과 같은 이점이 생겼다.

---------------------------------------------------------------------------------------------------
○ 운영체제 자체가 예외 처리를 사용함으로써 어떠한 상황에서도 시스템이 마비되는 사태를 막을 수 있도록

견고해졌다. 물론 아직까지 완벽하지 않지만 이런 견고성은 점점 더 확벽에 가까워질 것이다.


○ 예외 처리를 사용함으로써 응용 프로그램의 신뢰성이 훨씬 더 높아졌다. 사용자가 아무리 프로그램을 잘못 다루더라도

프로그램이 갑자기 이상 동작을 하지는 않는다.


○ 프로그램 고유의 작업과 오류를 처리하는 작업이 분리되었다.그래서 프로그래머는 작업에만 신경을 집중하고 나머지 에러는

예외로 처리하면 된다.
---------------------------------------------------------------------------------------------------


구체적으로 응용 프로그래램이 예외를 처리하는 예를 보기 위해 잠시 타갯ㄱ기를 테스트해 보자.

플로피 드라이브에 디스켓을 넣지 않고 A드라이브를 선택하면 탐색기는 에러메시지를 보여준다.

혹은 파일을 끌어서 다른 디렉토리에 떨어뜨리면 복사가 되는게 정상이지만

ROM과 같은 곳에 끌어다 놓으면 복사할 수 없다는 에러 메시지를 보여준다.

모름지기 프로그래램은 이렇게 짜야한다고 함...  사용자가 어떤 실수를 하더라도, 주변기기가 어떤 상황이더라도

프로그램은 이를 처리할 수 있어야 한다.  어떻게 하는가 하면 발생 가능한 예외에 대해 운영체제가 지원하는

방법대로 예외 핸들러를 작성하는 것이다.

예외 처리는 운영체제가 작업 지원하는 메커니즘이다. 하지만 예외가 제대로 동작하도록 코드를 생성하는 책임은

컴파일러에게 있다. 비록 운영체제가 예외 처리를 위한 준비를 완벽하게 하고 있더라도 응용 프로그래램이 이러한 예외 처리 서비스를

받을 수 있도록 작성되어 있지 않다면 완벽한 예외 처리를 불가능하다. 응용 프로그램이 예외 처리를 하도록 코드를 생성하는 것은 물론

컴파일러의 책임이다. 컴파일러는 운영체제가 예외 처리에 사용하는 정보를 응용 프로그램에 삽입해 두어야 하며 예외 발생시

운영체제가 호출할 수 있는 콜백함수를 작성해야 한다. 또한 예외 발생시 응용 프로그램이 비정상적으로 종료되지 않고 예외에

능동적으로 대처하도록 해야 할 책임도 컴파일러에게 있다.

그래서 예외 처리 루틴은 사용하는 컴파일러마다 다를 수밖에 없으며 Win32 API에 예외에 대한 명확한 규정이 없다.

일반적으로 많이 사용되는 예외처리 루틴은 두 가지 형식이 있다. 첫 번째는 C 형이며 두 번째는 C++ 형이다.

이 중 C 형을 구조화된 예외 처리(Structured Exception Handling)라고 하며 간단히 줄여 SEH라고 한다.

SEH 구문도 컴파일러마다 다른데 여기서는 비주얼 C++ 을 기준으로 설명한다.

C++형은 언어 차원에 제공되는 예외 처리 방식이며 C++ 국제 표준에 정의되어 있다.


============================================================================================================

[ 구조화된 예외 처리 ]

 

======== 예외 핸들러 ========

예외 핸들러는 1) 보호 구역, 2) 예외 필터, 3) 예외 핸들러의 세가지 요소로 구성된다.

문법은 다음과 같다.

---------------------------------------------------------------------------------------------------------

__try{
 보호구역
}
__except(예외 필터){
 예외 핸들러 
}
---------------------------------------------------------------------------------------------------------

___try와 __except( 밑줄이 두 개임에 유의할 것)는 키워드이며 {} 는 생략할 수 없다.

보호구역은 예외처리의 대상이 되는 코드이며 이 구역에서 예외가 발생하면 예외 필터와 예외 핸들러에 의해 보호된다.

예외가 발생할 만한 소지가 있는 코드를 보호구역으로 설정해 놓으면 된다.  예를 들어 다음 코드는 잠재적으로 예외가 발생할

소지가 있다.

---------------------------------------------------------------------------------------------------------
strcpy(str, "Test");
---------------------------------------------------------------------------------------------------------

str이 지정하는 메모리 번지로 "Test"라는 문자열을 복사하는 코드인데 만약 str이 NULL이거나 유효하지 않은 메모리를

가리키고 있다면 예외가 발생할 것이다. 이 예외를 처리하지 않고 그대로 두면 프로그래램음  에러 메시지 박스를 출력하며

종료되어 버릴 것이다.


<그림생략 p.2005>

참고로 strcpy는 무조건 지정한 포인터에 복사하므로 예외가 잘 발생하지만 lstrcpy는 메모리를 먼저 점검하는

내부적인 예외 처리를 하기 때문에 테스트용으로 쓸 수 없다. 이 코드는 다음과 같이 보호 구역으로 지정해야 한다.

---------------------------------------------------------------------------------------------------------
__try{
 strcpy(str, "Test"); 
}
---------------------------------------------------------------------------------------------------------

보호 구역 바로 다음에 __except와 함께 예외 필터가 온다. 예외 필터는 보호구역에서 예외 발생시

시스템이 예외를 처리하는 방법을 지정한다. 필터식의 결과에 따라 시스템은 예외를 핸들러에 넘기거나 다시 실행한다.

필터식은 다음 세 가지 중 한가지여야 하며 이 값에 따라 시스템이 예외를 처리하는 방식이 달라진다.


---------------------------------------------------------------------------------------------------------
값    상수   설명
---------------------------------------------------------------------------------------------------------
EXCEPTION_EXECUTE_HANDLER 1   예외 핸들러로 제어를 넘기고 예외 핸들러 다음 코드를 실행한다.

EXCEPTION_CONTINUE_SEARCH 0   예외 핸들러를 계속해서 찾는다.

EXCEPTION_CONTINUE_EXECUTION -1   예외가 발생한 지점을 다시 실행한다.
---------------------------------------------------------------------------------------------------------

예외 필터는 단순한 평가식이거나 아니면 별도의 함수로 빼낼 수 있다.  물론 필터 함수는 위 세가지 값중

한가지를 리턴해야 한다. 각 값은 매크로 상수가 정의되어 있기는 하지만 너무 길기 때문에 때로는

1,0,-1의 상수가 곧바로 사용되기도 한다. 예외 필터 각 값의 의미와 사용 예를 알아보자.

 

======== EXCEPTION_EXECUTE_HANDLER ========

예외 핸들러는 예외 필터식이 EXCEPTION_EXECUTE_HANDLER 일 때 실행되는 코드이다. 다음과 같이 코드를 작성했다고 해보자.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;
 __try{
  strcpy(str,"Test");
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"str pointer is invalid", "Exception",MB_OK);
 }
}
---------------------------------------------------------------------------------------------------------

str 포인터는 지역변수로 선언되기만 했으며 메모리가 할당되어 있지 않으므로 이 포인터에 문자열을 복사하는 것은

예외를 발생시킨다. 예외의 예를 보이기 위해 일부러 이런 코드를 작성하였다.

strcpy(str,"Text") 문이 보호 구역에 들어 있으므로 이 문장 실행중에 에러가 발생하면 예외 필터가 평가된다.

예외 필터식이 EXCEPTION_EXECUTE_HANDLER 이므로 예외 핸들러가 호출될 것이다. 예외 핸들러에서는

메시지 박스로 정중하게 사용자에게 예외가 발생한 이유를 알려주고 있다.

(독자는 메시지 박스가 바로 나오지 않고...  다른 에러가 발생하고 이것을 무시나 다시시도를 해야 메시지 박스가 나왔다.

왜그런지 아시는분.. 알려주세요....)


예외 처리를 하지 않으면 이 응용프로그래램은 Access Violation 에러를 내고 종료되어 버리겠지만

예외 처리를 함으로써 에러 메시지만 출력하고 실행을 계속할 수 있게 된다.

 

======== EXCEPTION_CONTINUE_EXECUTION ========

예외 필터식이 EXCEPTION_CONTINUE_EXECUTION 일 때는 예외가 발생한 문장을 다시 실행한다.

생각해 볼때 예외 필터란 예외가 발생했을 때 평가되는데 이 평가식에서 다시 예외가 발생한 지점으로

제어를 돌려 보내면 또 다른 예외가 발생할 것이다. 무한 루프에 빠지지 않으려면

EXCEPTION_CONTINUE_EXECUTION 이전에 뭔가 조치를 취해서 예외 상황이 다시 발생하지 않도록 해야 한다.

다음 예를 보자.
---------------------------------------------------------------------------------------------------------
void func(){

 int Div = 0,i;

 __try{
  i = 2/Div;
 }
 __except(Div=1,EXCEPTION_CONTINUE_EXECUTION){
 }
}
---------------------------------------------------------------------------------------------------------

보호 구역에서 0으로 나누기 예외가 발생하도록 해 두었다. 이때 예외 필터가 평가되며 예외 필터가

Div=1,EXCEPTION_CONTINUE_EXECUTION 을 리턴하므로 다시 나눗셈을 하는 보호 구역으로 돌아갈것이다.

그러나 예외 필터에서 그 전에 Div=1로 변경하기 때문에 예외 상황이 해결되었으며 i=2/Div는 정상적으로

실행될 수 있다. 이 경우 예외 핸들러는 절대로 실행될 수 어벖으므로 작성하지 않아도 된다.

EXCEPTION_CONTINUE_EXECUTION 의 의미는 예외 상황을 해결했으니까 이제 코드를 다시 실행해도 된다는

의미이다. 단 예외 필터에서는 이 값을 돌려보내기 전에 예외의 원인을 조사해서 해결해야 한다.

위 예제에서는 제수를 1로 바꿔 예외를 제거하였다.


그런데 예외 필터는 하나의 식이기 때문에 복문을 사용할 수 없다는 제약이 있다. 세가지 값중에

하나의 값으로 평가되어야 하며 코드를 가지도록 되어 있지는 않다. 하지만 C의 콤마 연산자와

삼항 연산자를 사용하면 약간의 코드를 구겨넣는 방법이 있다. 앞에서 보인 콤마 연산자가 그 좋은 예이다.

콤마 연산자는 왼쪽에서 오른쪽으로 식을 평가해 나가다가 제일 오른쪽 평가식의 결과를 리턴한다.


C의 기본 연산자중 하나이지만 잘 사용되지 않아 생소하게 느끼는 사람이 많을 것이다.

삼항 연산자를 사용하는 예를 보자. 다음은 0으로 나누기 예외와 액세스 위반 예외가 동시에 발생하는 코드이다.

---------------------------------------------------------------------------------------------------------
void func(){
 
 int Div = 0,i;

 TCHAR *str;
 __try{
  i = 2/Div;
  strcpy(str,"Test");
 }
 __except(GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? Div=1,EXCEPTION_CONTINUE_EXECUTION:EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"str pointer is invalid", "Exception", MB_OK);
 }


}
---------------------------------------------------------------------------------------------------------

예외 필터에 사용된 GetExceptionCode() 함수는 발생한 예외으 종류를 조사하는데 잠시 후에 별도로 알아 볼 것이다.


이 코드의 실행 순서는... 생략... (상식적으로 생각해보면 알수도 있을 것이라고 믿음 p.2007)


예외 필터가 좀 더 복잡해진다면 별도의 필터 함수를 작성한 후 예외 필터에서는 함수를 호출하면 된다.

필터 함수에는 반드시 0,-1,1 중 한 갑을 리턴해야 한다. 앞의 예제를 다음과 같이 바꾸어도 결과는 동일하다.

 

---------------------------------------------------------------------------------------------------------
int Div = 0;
DWORD ExcFilter(DWORD exCode){
 
 if(exCode == EXCEPTION_INT_DIVIDE_BY_ZERO){
  Div = 1;
  return EXCEPTION_CONTINUE_EXECUTION;
 }
 else{
  return EXCEPTION_EXECUTE_HANDLER;
 }
  
}

void func(){
 int i;
 TCHAR *str;
 
 __try{
  i = 2/Div;
  strcpy(str,"Test");
 }
 __except( ExcFilter(GetExceptionCode()) )
 {
  MessageBox(NULL,"str pointer is invalid","Exception",MB_OK);
 }
}

---------------------------------------------------------------------------------------------------------

ExcFilter 라는 필터 함수는 만들고 이 함수에서 예외 코드를 조사한 후 적절한 처리를 하고 평가식의 결과를 리턴했으며

예외 필터에서는 이 함수를 불러 필터식의 평가를 전담시켰다. 단 이때 GetExeceptionCode는 예외 필터 바깥에서는 호출할 수 없는

내재 함수이기 때문에 예외 필터에서 호출한 후 필터 함수로 결과를 넘기는 방식을 사용해야 한다.

 


======== EXCEPTION_CONTINUE_SEARCH ========

예외 평가식이 이 값이면 지금 실행되고 있는 코드 이전의 try 블록이 위쪽 try 블록에 대응되는 except 문의 예외 필터를 평가하도록 한다.

(아 먼소리냐.. 예제보면 이해됨..)

이 값은 함수 호출 등에 의해 예외 처리가 중첩되어 있는 경우에만 사용할 수 있다. 예를 들어 보호 구역 내에 또 다른 보호 구역이 있을 경우

안쪽 보호 구역에서 이 값을 사용한다. 이 값을 사용하는 예를 보이기 위해 예제를 하나 만들어 보자. 다음 예제는 0으로 나누는 연산을 하는 함수를

만들고 이 함수 호출문이 보호 구역에 들어 있다.


---------------------------------------------------------------------------------------------------------
int Div = 0;

int DivInt(int a, int b){

 int result;
 result = a/b;
 result result;

}

void func(){
 int i;

 __try{
  i = DivInt(2,Div);
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"Divide by zero", "Exception",MB_OK);
 }
}
---------------------------------------------------------------------------------------------------------

DivInt 함수 실행중에 예외가 발생하면 func 함수의 예외 필터가 평가될 것이며 예외 필터식의 결과

EXCEPTION_EXECUTE_HANDLER 이므로 예외 핸들러가 실행될 것이다. 그런데 만약 DivInt 함수 내부에서

또다른 예외 처리를 해야 할 필요가 있다고 하자. 다음처럼 말이다.


---------------------------------------------------------------------------------------------------------

int Div = 0;

int DivInt(int a, int b){

 int result;

 
 __try{
  result = a/b;
  strcpy(str,"Test");
 }
 __except( GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_CONTINUE_SEARCH:EXCEPTION_EXECUTE_HANDLER){

  MessageBox(NULL,"str pointer is invalid","Exception",MB_OK);

 }
 result result;
}

void func(){
 int i;

 __try{
  i = DivInt(2,Div);
 }
 __except(EXCEPTION_EXECUTE_HANDLER){
  MessageBox(NULL,"Divide by zero", "Exception",MB_OK);
 }
}

---------------------------------------------------------------------------------------------------------


이 경우 DivInt에서는 0으로 나누기 예외와 액세스 위반 예외가 동시에 발생하는데 액세스 위반 예외는 DivInt 내부에서

처리할 수 있지만 0으로 나누기 예외는 이 함수를 호출한 func 함수에서 처리하고자 한다. 이때 사용되는 예외 평가값이

바로 EXCEPTION_CONTINUE_SEARCH 이다. 평가식이 이 값이면 이전의 try 블록과 대응되는 예외 필터를 평가하여 그 결과에 따른다.

 

==============================================================================================================================

[ 내재 함수 ]

예외 처리는 __try, __except 키워드에 의해 지원되닌다. 이 키워드 외에 예외 처리를 보조하는 몇 가지 함수가 있다.

앞에서 본 GetExceptionCode 등의 함수가 이에 해당한다. 근데 이 함수들은 다른 API 함수들과는 본질적으로

다른 함수들이다. 함수의 본체가 정의되어 있지 않으며 컴파일 결과 함수 호출문이 삽입되지 않고 직접 이 함수의 코드가 삽입된다.

동작 방식으로 볼 대 인라인 함수와 유사하지만 그것과도 다르다.  함수의 형태를 띠고 있지만 사실은 컴파일러가 직접처리하는 키워드이다.

이런 함수를 내재 함수(Intrinsic Function)라고 한다. 이름만 함수고 사용한는 방법도 함수와 동일하지만 사실은 함수가 아니다. 무늬만 함수이다.


함수와 구체적으로 어떤 점이 다른가 하면 내재 함수는 사용할 수 있는 곳이 정해져 있다. 예를 들어 GetExceptionCode는 예외 필터에서만

사용되어야지 예외 필터 바깥에서는 사용할 수 없다. 왜냐하면 예외 처리 코드 이외의 부분에서는 이 함수가 필요할 리도 없고 이 함수가

정보를 구할 방법도 어벖기 때문이다. 예외가 발생해야 어떤 예외가 발생했는지 알 수 있지 않겠는가.

 

======== GetExceptionCode ========

이 함수는 예외 필터와 예외 핸들러에서만 사용할 수 있으며 지금 발생한 예외의 종ㄹ를 조사한다.

인수는 취하지 않으며 리턴값으롱 ㅖ외의 종류를 알려준다. 발생가능한 예외의 종류는 다음과 같다.


---------------------------------------------------------------------------------------------------------
예외    설명
---------------------------------------------------------------------------------------------------------
EXCEPTION_ACCESS_VIOLATION 허가되지 않은 메모리 영역을 읽거나 쓰려고 시도하였다. 
    가장 일반적으로 많이 발생하는 예외

EXCEPTION_BREAKPOINT  중단점을 만났다.

EXCEPTION_DATATYPE_MISALIGNMENT 정렬을 지원하지 앟는 하드웨어에 잘못된 정렬된 값을 읽거나 쓰려고 하였다.
    예를 들어 16비트 값은 반드시 2바이트 경계로 정렬되어야 하고 32비트값은 반드시
    4바이트 경계로 정렬되어야 한다.

EXCEPTION_SINGLE_STEP  단계 실행을 위한 신호이다.

EXCEPTION_ARRAY_BOUNDS_EXCEEDED 배열의 범위를 초과하여 배열 요소를 읽거나 쓰려고 하였다. 단 이예외는 
    하드웨어 배열 범위 점검을 하는 경우에만 발생한다.

EXCEPTION_FLT_DENORMAL_OPERAND 부동 소수점 연산의 피 연산자중 하나가 잘못되었다.
    예를 들어 너무 작은 값이기 때문에 표준 부동 소수점 형식으로 표현할 수 없는 값이다.

EXCEPTION_FLT_DIVIDE_BY_ZERO 실수를 0으로 나누었다.

EXCEPTION_FLT_INEXACT_RESULT 부동 소수점 연산의 결과 정수부를 정확하게 표현할 수 없다.

EXCEPTION_FLT_OVERFLOW  연산의 결과가 허용된 범위보다 더 크다.

EXCEPTION_FLT_STACK_CHECK 연산의 결과 스택이 오버 플로우 되었거나 언더 플로우 되었다.

EXCEPTION_FLT_UNDERFLOW  연산의 결과가 허용된 범위보다 더 작다.

EXCEPTION_INT_DIVIDE_BY_ZERO 0으로 나누기 정수 연산을 하였다.

EXCEPTION_INT_OVERFLOW  성수 연산의 결과가 허용된 범위보다 더 크다.

EXCEPTION_INT_PRIV_INSTRUCTION 현재 기계 모드에서 사용할 수 없는 명령을 실행하려고 하였다.

EXCEPTION_NONCONTINUABLE_EXCEPTION 실행을 계속할 수 없는 예외를 다시 실행하려고 하였다.
---------------------------------------------------------------------------------------------------------

 

======== GetExceptionInformation ========

이 함수는 예외가 발생했을 때 예외에 대한 상세한 정보와 예외 발생시의 기계 상태에 대한 정보를 조사한다.

예외에 대한 정보를 조사하는 것이기 때문에 예외 필터밖에서는 이 함수를 사용할 수 없다.

원형은 다음과 같다.

---------------------------------------------------------------------------------------------------------
LPEXCEPTION_POINTERS GetExceptionInformation(VOID)
---------------------------------------------------------------------------------------------------------

인수는 취하지 않으며 리턴값 하나를 돌려주는데 이 값은 복잡한 정보를 가진 구조체의 포인터이다.

예외가 발생하면 운영체제는 이 구조체를 작성하여 스택에 넣어주며 사용자는 예외 필터에서

GetExceptionInformation 함수를 호출하여 이 구조체를 읽을 수 있다.


---------------------------------------------------------------------------------------------------------
typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord;
    PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
---------------------------------------------------------------------------------------------------------

이 구조체는 또 다른 두 개의 구조체를 멤버로 가지고 있다. EXCEPTION_RECORD 멤버는 예외 자체에 대한 정보를

가지며 프로그램이 실행되는 플랫폼에는 독립적이다.  ContextRecord 멤버는 예외가 발생한 당시의 기계 상태,

즉 레지스터의 값들을 가지는데 레지스터 구성이 플랫폼마다 다르므로 이 구조체도 플렛폼에 따라 달라진다.

ExceptionRecord 구조체는 다음과 같이 선언되어 있다.


---------------------------------------------------------------------------------------------------------
typedef struct _EXCEPTION_RECORD {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

---------------------------------------------------------------------------------------------------------

모두 6개의 멤버를 가진다. 이 구조체의 멤버를 읽으므로써 예외의 종류, 발생 위치, 계속 여부 등의 정보를 얻을 수 있다.

---------------------------------------------------------------------------------------------------------
멤버    설명
---------------------------------------------------------------------------------------------------------
ExceptionCode   어떤 종류의 예외인지를 설명한다. GetExceptionCode 함수의 리턴값과 동일하다.

ExceptionFlags   예외가 계속 실행될 수 있는 종류인가 그렇지 않은가를 나타낸다. 0이면 예외는 계속 진행될 수 있다는 뜻이며
    EXCEPTION_NONCONTINUABLE이면 계속 진행될 수 없는 예외라는 뜻이다.
    진행해서는 안 되는 예외를 계속 진행하려고 해서는 안 된다.


ExceptionRecord   예외 처리 루틴이 중첩되어 있을 겨웅 다른 예외에 대한 정보를 가진다.  예외 처리를 하는 중에도
    예외가 발생할 수 있는데 이때는 예외끼리 발생 순서에 따라 상호 연결된다. 
    마치 연겨려 리스트의 노드가 서로 연결되는 것과 같다.

ExceptionAddress  예외가 발생한 번지이다.


NumberParameters  ExceptionInformation 배열의 크기이다. 대개의 경우 0이다.


ExceptionInformation  예외를 설명하는 추가적인 32비트값의 배열이다. 대부부느이 예외를 이 배열에 값을 정의하지 않으나
    소프트웨어 예외에서는 이 값을 사용할 수도 있다.
    EXCEPTION_ACCESS_VIOLATION 예외에서는 배열의 첫 번재 요소는 예외를 일으킨 여산의 종류를 지정한다.
    이 값이 0이면 읽을 수 없는 데이터를 읽으려고 한 것이며 이 값이 1이면 쓸 수 없는 데이터를 쓰려고 시도한 것이다.
    두 번째 요소는 읽기/쓰기를 시도한 가상 메모리상의 번지를 지정한다.
    
---------------------------------------------------------------------------------------------------------


CONTEXT 구조체는 플랫폼에 따라 달라지며 플랫폼의  종류가 많기 때문에 여기에 그 리스트를 보이지 않겠다.

이 구조체를 보고 싶은 사람은 winnt.h를 직접 열어서 보기 바라되 열어보면 괜히 봤다는 생각이 들지도 모르겠다.

Intel CPU의 CONTEXT 구조체에는 Intel CPU의 레지스터 값들을 담는 멤버들이 있다. 이 값을

읽으면 예외가 발생했을 때의 CPU 상태를 그대로 덤프해 볼 수 있을 것이다. 단 그렇게 하면 예외처리의

일부분이 플랫폼에 종속되어 버린다.

==============================================================================================================================

[ 종료 핸들러 ]

종료 핸들러(Termination handler)는 어떠한 상황에서도 특정 부분이 반드시 실행될 것을 보장하는 예외 처리 구조이다.

예외 핸들러와 마찬가지로 컴파일러에 의해 제공되며 다음과 같은 구조를 가진다.

---------------------------------------------------------------------------------------------------------
__try{
 보호구역
}
__finally{
 종료 핸들러
}
---------------------------------------------------------------------------------------------------------

여기서 보호 구역은 종료 처리가 제대로 이루어지지 않을 롹률이 있는 문장이 포함된다.

예를 들어 메모리를 할당한다거나 파일을 오픈한다거나 크리티컬 섹셕으로 들어간다거나 하는 등의 문장이다.

이런 문장들은 반드시 짝을 이루는 해제 문장이 따라 오는데 해제 문장이 종료 핸들러에 배치된다.

이 구조를 사용하면 보호 구역에서 어떠한 원인으로 종료 처리를 하지 못할 상황이 발생하더라도 종료 핸들러에 의해

반드시 종료 처리됨을 보장받을 수 있다.

예외 처리와 다른 점은  예외 핸들러는 예외가 발생하지 않으면 핸들러가 전혀 실행될 필요가 어벖지만

종료 핸들러는 정상적이나 예외시나 항상 실행된다. 종료 핸들러는 사용하는 간단한 실제 예를 보자.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;
 str = (TCHAR*)malloc(20);

 if(1/*어떤 조건*/){
  return;
 } 
 free(str);

}
---------------------------------------------------------------------------------------------------------

이 예에서는 메모리를 할당한 후 이 메모리를 사용하고 그리고 해제한다. 코드가 정상적으로 실행된다면

아무 문제가 없겠으나 만약 어떤 조건에 의해 함수가 중간에 리턴되어야 할 상황이 발생한다면

free 함수가 실행되지 못하기 때문에 할당된 메모리가 그대로 할당된 채로 있게 된다.

결과적으로 메모리 누수를 유발하며 시스템 리소스의 부족과 속도 저하로 이어진다.

이런 문장이 확실하게 동작하려면 다음과 같이 종료 핸들러로 싸야 한다.

---------------------------------------------------------------------------------------------------------
void func(){
 TCHAR *str;

 __try{
  str = (TCHAR*)malloc(20);

  if(1/*어떤 조건*/){
   return;
  } 
 }
 __finally{
  free(str);
 }

}
---------------------------------------------------------------------------------------------------------

메모리르 할당하는 부분이 보호 구역으로 설정되어 있으며 메모리를 해제하는 부분이 종료 핸들러로 지정되어 있다.

그래서 함수가 중간에 리턴하거나 혹은 goto 문에 의해 제어가 다른 곳으로 옮겨가더라도 __finally 블록은 반드시

실행된다. 독자중에 조건을 완벽하게 테스트하여 반드시 free까지 가도록 하면 된다거나 리턴하기 전에

free를 실행하면 되지 않느냐고 반문할 사람이 있을 것이다. 하지만 실제 예에서는 항상 그것이 가능하지 않기 때문에

문제가 되는 것이다. 여기서 /*어떤 조건*/이 라고 되어 있는 부분은 실제로는 무척 복잡한 문장이 될 수 있다.

예외를 일으킬 가능성이 있는 문장 일 수도 있고 또는 함수 호출문이라 함수 내부에서 무슨 일이 일어날지 알 수 없는 경우 등등

여러 가지 경우를 가정해 볼 수 있다.

컴파일러는 종료 핸들러가 있으면 어떤 경우라도 종료 핸들러가 호출될 수 있게 코드를 작성한다.

설사 함수 중간에 return 문이 있더라도 함수의 종료를 미루고 종료 핸들러를 호출한 후 리턴하으로써

함수 실행 중 반드시 종료 핸들러를 거쳐가도록 코드를 컴파일 하기 때문에 종료 핸들러를 사용하면 어떠한 경우라도 정상적인

종료 처리가 된다고 보장할 수 있다. 단 스레드나 프로세스가 ExitThread, ExitProcess에 의해 강제로 종료될 때는

예외이다. 이때는 "나 죽었어요"하는 시점이기 때문에 종료 처리고 뭐고 뒤도 안볼아본다.


종료 핸들러는 정상적인 코드 흐름에서도 실행되며 보호 구역에서 예외가 발생하여 일찍종료되었을 때도 실행된다.

만약 이 두 경우를 구분하려면 AbnormalTernimation 함수를 사용한다. 이 함수는 __finally 블록 내에서만 실행될 수 있는

내재 함수이다. 만약 종료 핸들러가 정상적인 흐름을 따라 실행된다면 이 함수는 FALSE를 리턴하고 보호 구역에서 바로

종료핸들러로 넘어 왔다면  이 함수는 TRUE를 리턴한다. 사실 이 함수가 사용되는 실례를 찾기 어렵지만 억지로라도 예를 들어보자.

비정상적인 종료 처리를 할 경우 메시지를 출력하고 싶다면 다음과 같이 한다.

---------------------------------------------------------------------------------------------------------
__finally{
 if(AnormalTernimation() == TRUE)
  MessageBox(NULL,"뭔가 이상하네요. 이 프로그램 쓰지마세요","어라",MB_OK);
 free(str);
}
---------------------------------------------------------------------------------------------------------


종료 핸들러는 다음과 같은 용도로도 사용된다. 만약 한 함수에서 여러 가지 자원을 할당한 후 함수 종료 전에

해제해야 한다고 해 보자. 그러면 코드는 아래와 같이 될 것이다.

<코드 1>
---------------------------------------------------------------------------------------------------------
if(크리티컬 섹션으로 들어감 == 성공){

 if(핸들 열기 == 성공){
  if(메모리 할당 == 성공){
   if(오브젝트 생성 == 성공){
   
    오브젝트 해제
   } 
  
   메모리 해제 
  }

 핸들 닫기
 }


 크리티컬 섹션 나옴
}
---------------------------------------------------------------------------------------------------------

전통적인 방법이긴 하지만 뭔가 복잡해 보이고 지저분해 보인다.


<코드 2>
---------------------------------------------------------------------------------------------------------
if(크리티컬 섹션으로 들어감 == 실패){
 return;
}

if(핸들 열기 == 실패){
 크리티컬 섹션 나옴
 return;
}

if(메모리 할당  == 실패){
 핸들 닫기
 크리티컬 섹션 나옴
 return;
}

if(오브젝트 생성 == 실패){
 메모리 해제
 핸들 닫기
 크리티컬 섹션 나옴
 return;

하고 싶은 일 
오브젝트 해제
---------------------------------------------------------------------------------------------------------

참고로 위의 코드는 그냐양 웃자고 보인 것인데 결과는 동일하지만 0점짜리 코드다.

위의 처럼 짜면 안된다.

<코드 1>은 그나마 네 가지 할당을 해서 그렇지 만약 10개 정도의 자원을 할당한다면

정말 복잡해 보일 것이며 브레이스의 if else의  짝을 찾기 어려울 것이다.  종료 핸들러를 사용한다면

이런 상황은 훨씬 더 깔끔하게 처리할 수 있다.

---------------------------------------------------------------------------------------------------------
__try{
 크리티컬 섹션으로 들어감
 핸들열기
 메모리 할당
 오브젝트 생성
 하고 싶은 일
}
__finally{
 오브젝트 파괴
 메모리 해제
 핸들 닫기
 크리티컬 섹션 나옴
}
---------------------------------------------------------------------------------------------------------

한마디로 모든 종료 처리를 종료 핸드러로 모아놓은 것이다. 이렇게 되면 중간에 한 자원이 할당에 실패하더라도

자원은 제대로 해제된다. 단 __finally 에서는 무조건 자원을 해제해서는 안 되며 할당된 자원에 대해서만 해제를 하도록 조건 점검을 해 보아야 한다.

종료 핸들러를 이런 목적에 사용하는 것을 지원하기 위해 비주얼 C++은 __leave 라는 키워드를 제공한다. 이 문장이 사용되면

__try 블록 내에서 곧바로 __finally 블록으로 제어를 옮겨 종료 처리를 한다. __leave를 쉽게 표현하면 goto __finally 이다.

 

반응형
반응형


http://3dmpengines.tistory.com/614 에도 SEH 에 대해 올린글이 있음







구조적 예외 처리 - Structured Exception Handling(SEH)  Window 

2011/08/01 11:14

복사http://blog.naver.com/yoonga87/10114826647





일반적으로 c++에서 사용하는 예외처리 문법에는 try, catch, throw이 있다. 하지만 windows c++환경에서 사용되는 또 하나의 예외처리 문법이 있는데, __try, __except, __finally가 그것이다. 이 둘의 차이점은 분명하게 나누어진다. try, catch, throw는 사용자 애플리케이션 단위의 예외처리 문법이라면 __try, __except, __finally는 windows 운영체제 단위의 예외처리 문법이다. try, catch, throw는 내부적으로 __try, __except, __finally를 사용해 구현된다.

 

일반적으로 운영체제(os) 단위에서 발생하는 예외로는 null pointer 참조나 0나누기를 들 수 있다. 이런 운영체제 단위의 예외를 처리하고 싶을 때 c++에서 사용하는 예외처리기가 아닌 os 단위의 예외처리기를 사용해야 한다.

 

__try, __except, __finally의 기본적인 문법은 다음과 같다.

 

__try

{

...

 //예외를 감시할 루틴

...

}

__except( 예외 필터 )

{

...

//예외 처리

...

}

 

__try

{

...

return; // return이 발생해도 __finally는 실행됨.

}

__finally

{

// 무조건 실행해야하는 루틴

}

 

여기서 주의해야 할 점은 __except와 __finally는 하나의 __try블록에 같이 쓸 수 없다는 점이다.

 

__except 안에 들어가는 예외 필터에는 다음과 같이 3가지가 있다.

EXCEPTION_EXECUTE_HANDLER

EXCEPTION_CONTINUE_SEARCH

EXCEPTION_CONTINUE_EXECUTION

 

* EXCEPTION_EXECUTE_HANDLER

 

예외가 발생한 지점에서 빠져나와 __except 안에있는 루틴을 실행한다. __except 안에 있는 루틴의 실행이 끝나면 계속해서 다음 루틴을 실행해 나간다.

 

* EXCEPTION_CONTINUE_SEARCH

 

예외가 발생한 지점에서 빠져나와 call stack에서 다음 __try 블록을 찾는다. 만약 __try를 못찾으면 kernel에게 예외가 보내지고 에러메시지와 함께 application이 비정상종료하게 된다.

 

* EXCEPTION_CONTINUE_EXECUTION

 

예외가 발생한 지점을 다시 실행한다. 만약 다시 실행할 때 문제가 해결된 상태가 안된다면 무한 루프를 돌게 된다.

 

이 3가지 예외 필터의 순서도를 그려보면 다음과 같다.

 

 







http://ezbeat.tistory.com/122




DWORD GetExceptionCode(void);

예외가 발생했을 때 어떠한 종류의 예외가 발생했는지를 확인하기 위해서 사용

example code)
__except(EXCEPTION_CONTINUE_HANDLER){
DWORD exptType = GetExceptionCode();

switch( exptType )
{
case EXCEPTION_ACCESS_VIOLATION:
_tprintf(_T(" Access violation \n\n"));
break;
case EXCEPTION_INT_DIVIDE_BY_ZERO:
_tprintf(_T(" Divide by zero \n\n"));
break;
}
}

EXCEPTION_ACCESS_VIOLATION     (0xC0000005) 메모리 참조 오류를 구분하기 위해서 정의해 놓은 정수값
EXCEPTION_INT_DIVIDE_BY_ZERO  (0xC0000094) 정수를 0으로 나누는 예외를 너타내기 위해 정의해 놓은 정수값
EXCEPTION_BREAKPOINT             (0x80000003) 중단점 발생하면!

위 함수보다 더욱 자세하게 예외에 대한 내용을 알고 싶다면
LPEXCEPTION_POINTERS GetExceptionInformation(void);
함수를 호출한다.

구조체 주소를 리턴한다. EXCEPTION_POINTERS구조체를 봐보겠다.

typedef struct _EXCEPTION_POINTERS {
  PEXCEPTION_RECORD ExceptionRecord; //예외에 대한 정보가 담김
  PCONTEXT          ContextRecord; //프로세서의 레지스터 데이터를 비롯한 프로세서의 종속적인 정보
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode; //RaiseException함수의 첫번째 인자가 들어옴
  DWORD                    ExceptionFlags; //두번째 인자
  struct _EXCEPTION_RECORD *ExceptionRecord;
  PVOID                    ExceptionAddress;
  DWORD                    NumberParameters; //세번째 인자
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //네번째 인자
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;


반응형
반응형

http://carstart.tistory.com/98


ESP 와 EBP

[Public] 프로그래밍/C


함수를 호출 할때 어셈블리언어를 보면 함수의 크기보다 4 + 4 바이트가 더 붙는 것을 알 수 있다. 

어셈으로 접근해보면 그게 무엇인지 알수가 있다. EBP에 대하여 알아 보자


ESP (Extended Stack Pointer)

 -  현재 스택의 가장 위에 들어있는 데이터를 가리키고 있는 포인터

 -  Intel CPU에서는 스택이 거꾸로(리틀엔디언)  자라므로 데이터가 하나 PUSH 될때마다 ESP 값은 감소

 -  ESP는 다음 번 DATA를 Push 할 위치가 아니라 다음에 POP 했을 때 뽑아낼 데이터의 위치를 가리 킴  



EBP (Extended Base Pointer)

 -  현재 스택에 가장 바닥을 가리키는 포인터

 -  새로운 함수가 호출될 때마다 EBP 레지스터 값이 지금까지 사용했던 스택 꼭대기의 위에 위치하게 되고

     새로운 Stack이 시작

 -  따라서 EBP는 새로운 함수가 호출이거나 현재 실행중인 함수가 종료되어 리턴될 때마다 값이 달라짐


 

ex) 어셈 예

push    ebp   - 이전 스택의 Base 주소를 저장

mov     ebp, esp - 현재 스택의 꼭대기를 새로운 스택의 base설정 (새로운 스택 시작)


반응형
반응형

http://www.cyworld.com/widwolf/6075176


Shared Lock(공유 잠금), Exclusive Lock(독점 잠금)

Noveloper 2012-12-10 13:35:49 주소복사
조회 14  스크랩 0
여러 트랜잭션의 동시 접근성을 보장하기 위해 Lock을 사용한다.
각 트랜잭션이 수행을 시작하고, 데이터 항목을 접근할 때 마다 요청한 Lock에 대한 정보는 Lock Table에 저장된다.

트랜잭션이 갱신을 목적으로 접근할때는 Exclusive Lock 을 요청하고
그냥 읽을 목적으로 데이터를 접근할때는 Shared Lock을 요청한다.

Exclusive Lock이 걸려있는 항목에 대해 다른 트랜잭션이 Exclusive Lock 이나 Shared Lock 을 요청하는 경우에 Lock을 허용하지 않는다.

트랜잭션이 데이터 항목에 대한 접근을 끝난 후에 Lock 을 해제한다.

Lock 단위가 작을수록 Locking 에 따른 OverHead 가 증가한다.
- 각 튜플에 대해 Lock을 요청할때마다 Lock Table을 입력하고, 트랜잭션이 끝나면 Lock Table 에서 삭제해야 하고, 그런 작업을 반복하기 때문

하지만 Lock 단위가 작을수록 동시성의 정도는 증가한다. 

반응형

'운영체제 & 병렬처리 > 시스템프로그래밍' 카테고리의 다른 글

구조적 예외 처리 - Structured Exception Handling(SEH)  (0) 2012.12.25
ESP 와 EBP  (0) 2012.12.15
메모리 컨트롤  (0) 2012.11.01
표준 검색 경로  (0) 2012.11.01
FAQ - 윈도우프로그래밍  (0) 2012.11.01
반응형

메모리 상태 :


페이지의 개수 = 가상 메모리의 크기 / 페이지 하나당 크기

페이지 개수는 가상 메모리의 크기에 비례하며(가상 메모리는 몇 비트 환경인지에 비례 (ex. 32비트 4GB)),

모든 페이지는 Reserve, Commit, Free 세가지 중 하나의 상태를 지닌다.

Commit : 물리 메모리에 할당된 상태

Reserve : Free 와 Commit 의 중간상태이다. 해당 번지에 대해 예약을 한다.
              다른 메모리 함수가 물리 메모리에 해당 번지에 할당하지 못하도록 한다.
              하지만 물리 메모리의 소비는 발생하지 않는다.
                
Free : 물리 메모리 할당이 이뤄지지 않은 상태


메모리 할당의 시작점과 단위 확인 :

가상 메모리 시스템은 페이지 단위로 관리된다.

페이지의 중간 위치에서부터 할당을 시작할수 없으며, 페이지 크기의 배수 단위로 할당한다.

Allocation Granularity Boundary : 메모리 할당의 시작 주소가 되는 기본 단위
(참고 : 메모리가 지나치게 조각나는것과 관리의 효율성을 이유로 페이지 크기의 배수보다 더 넓은 값을 가진다)

Allocation Granularity Boundary 과 페이지 크기 확인 :

#include <stdio.h>
#include <tchar.h>
#include <windows.h>

int _tmain(int argc, TCHAR *argv[])
{
          SYSTEM_INFO si;
          DWORD allocGranularity;
          DWORD pageSize;
 
          GetSystemInfo(&si);
          pageSize = si.dwPageSize;
          allocGranularity = si.dwAllocationGranularity;

          _tprintf(_T("Page Size : %u Kbyte \n"), pageSize/1024);
          _tprintf(_T("Allocation granularity : %u Kbyte \n"), allocGranularity/1024);
 
          return 0;
}



가상 메모리 컨트롤 :

페이지 상태를 RESERVE 나 COMMIT 로 변경할때 사용하는 함수

LPVOID VirtualAlloc(
          LPVOID lpAddress,       // 예약 및 할당하고자 하는 메모리의 시작 주소
                                            // (일반적 NULL 전달, RESERVED -> COMMIT 일때 해당페이지 시작 주소 지정)
          SIZE_T dwSize,                // 할당하고자 하는 메모리의 크기를 바이트 단위로 지정
                                                 // 메모리의 할당은 페이지 크기 단위로 결정
          DWORD flAllocationType,    // 메모리 할당의 타입.
                                                 // RESERVE : MEM_MRESERVE,          COMMIT : MEM_COMMIT 
          DWORD flProtect                // 페이지별 접근방식에 제한을 두는 용도
                                                 // RESERVE 상태로 변경할때는 접근을 허용하지 않는 PAGE_NOACCESS ,
                                                 // COMMIT 상태로 변경할때는 읽고/쓰기 모드인 PAGE_READWRITE 전달
);

함수 호출이 성공하면 할당이 이뤄진 메모리의 시작 주소를 리턴


할당된 페이지를 되돌리는 함수

BOOL VirtualFree(
           LPVOID lpAddress,            // 해제할 메모리 공간의 시작 주소 
           SIZE_T dwSize,                // 해제할 메모리 크기를 바이트 단위로 지정
           DWORD dwFreeType         // MEM_DECOMMIT : 해당 페이지의 상태를 RESERVE 상태로 되돌린다.
                                                  // MEM_RELEASE : 해당 페이지의 상태를 FREE 상태로 되돌린다.
                                                  //                           두번째 전달인자 dwSize 는 반드시 0 이어야 한다.
);



힙 컨트롤 :

디폴트 힙(Default Heap) : 

디폴트 힙을 구성하는 페이지들은 RESERVE 상태이다. 일부는 성능을 위해 COMMIT 상태일수도 있다.

C 언어의 malloc 함수와 free 함수, C++ 언어의 new와 delete 를 사용해서 힙영역을 사용할 경우

프로세스를 생성할때 생성되는 힙, 즉 1M 바이트 크기의 디폴트 힙에 메모리를 할당한다.
(COMMIT 와 RESERVE 상태를 오간다)

프로세스에 기본적으로 할당되는 힙이며 프로세스 힙(Process Heap)라고도 한다.


디폴트 힙 컨트롤 :

디폴트 힙의 기본 크기는 1MB 이다. 링커(Linker)옵션을 통해 크기를 변경할 수 있다.

/HEAP : reserve, commit  (ex. /HEAP 0x200000, 0x10000  :  디폴트 힙의 크기 : 2MB,       COMMIT 크기 : 64KB)

힙은 동적이라 기본 크기 1MB 로 생성이 된 후 필요에 따라 그 크기가 Windows 시스템에 의해 자동으로 늘어난다.



Windows 시스템 힙(Dynamic Heap) : 

Windows 시스템 함수를 통해 여러 힙을 추가로 생성할 수 있다.

가상 메모리 범위 내에서 프로세스 생성시 만들어지는 디폴트 힙 이외에 필요로 하는 힙을 얼마든지 생성할 수 있다.


힙(Dynamic Heap) 생성이 가져다 주는 이점 :

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



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

힙은 쓰레드가 공유하는 메모리 영역이다. 따라서 둘 이상의 쓰레드가 동시접근 할때 문제가 발생할수 있으므로

Windows 내부적으로 동기화처리를 해준다.(여기서 동기화는 메모리 할당과 해제)

같은 주소 번지에 둘 이상의 쓰레드가 동시에 메모리를 할당 및 해제하는 상황이 발생할 경우 메모리 오류(Corrupt)가 
발생하므로 디폴트 프로세스 힙은 동기화 처리를 하는데 쓰레드마다 독립된 힙을 가지고 있다면

이러한 동기화가 필요없으므로 성능이 향상된다.


힙(Dynamic Heap) 컨트롤 :

힙을 생성하는 함수 :

HANDLE HeapCreate(
         DWORD flOptions,                 // 생성되는 힙의 특성을 부여 (0 을 전달시 가장 일반적인 힙 생성)
                                                   // HEAP_GENERATE_EXCEPTIONS : 오류 발생시 NULL이 아닌 예외 발생
                                                   // HEAP_NO_SERIALIZE : 생성된 힙의 메모리 할당과 해제에 대해
                                                   //                                   동기화 처리를 하지 않는다.
                                                   // 둘 이상의 속성을 비트 단위 연산자 OR( | )로 동시 지정 가능
         SIZE_T dwInitialSize,            // dwMaximumSize 에서 지정한 메모리 중에서
                                                   // 초기에 할당할 COMMIT 페이지를 지정한다 
         SIZE_T dwMaximumSize      // 생성되는 힙의 크기 결정.
                                                   // 지정하는 크기에 해당하는 페이지의 수만큼 RESERVE 상태가 된다.
                                                   // 0이 아닐 경우 힙은 증가가능한 메모리(Growable Heap)가 된다
);

힙을 소멸하는 함수 :

BOOL HeapDestroy(
         HANDLE hHeap                    // 반환하고자 하는 힙의 핸들
);  

힙에 메모리를 할당하는 함수 :

LPVOID HeapAlloc(
         HANDLE hHeap,               // 메모리 할당이 이뤄질 힙의 핸들
         DWORD dwFlags,             // HEAP_GENERATE_EXCEPTIONS : 오류 발생시 NULL이 아닌 예외 발생
                                               // HEAP_NO_SERIALIZE : 함수호출시 동기화 처리되지 않는다.
                                               // (HeapCreate 함수호출에서 지정했다면 중복지정할 필요는 없다)
                                               // HEAP_ZERO_MEMORY : 할당된 메모리는 0으로 초기화
                                               // 둘 이상의 속성을 비트 단위 연산자 OR( | )로 동시 지정 가능
         SIZE_T dwBytes               // 할당하고자 하는 메모리의 크기를 지정
                                               // (증가가능한 힙이 아닐 경우 최대 크기 0x7FFF8)
); 

힙에 메모리를 해제하는 함수 :

BOOL HeapFree(
         HANDLE hHeap,                // 해제할 메모리를 담고 있는 힙을 지정
         DWORD dwFlags,              // HEAP_NO_SERIALIZE 가 인자로 올 수 있다,     일반적 : 0
                                                // (HeapCreate 함수호출에서 지정했다면 중복지정할 필요는 없다)
         LPVOID lpMem                 // 해제할 메모리의 시작 주소 지정
);

반응형
반응형

vs 는 파일 실행파일 경로를 순차적으로 아래 순서대로 검색해나간다

 

반응형
반응형

http://cs.knou.ac.kr/~khkim/faq/file.asp?code=aa027&num=1&snum=15


▣ LNK1104 : cannot open file "Debug/MiniMFC.exe"
▣ Mini MFC 단원에서 

[질문] =========================== 

Linking...
LINK : fatal error LNK1104 : cannot open file "Debug/MiniMFC.exe"
Error executing link.exe.
MiniMFC.exe - 1 error(s), 0 warning(s)

해결하는 방법은?

[답변] ===============================

해결방안 1 : 대부분의 경우 한번 실행한 후 실행된 윈도우를 닫지 않고 다시 실행하면 이런 에러가
발생합니다. 

해결방안 2 : 다운 받은 모든 파일의 속성을 열어서 읽기 속성을 쓰기로 바꿉니다. 

해결방안 3 : Visual C++을 닫고 프로젝트 폴더 내의 디버그 폴더를 삭제한 후 
Visual C++을 열고 나서 F5키를 눌어 실행시켜 줍니다.

또는

디버그 폴더를 삭제한 후 Build 메뉴에서 Rebuild All을 클릭하여 재컴파일 및 링크를 합니다.

-> 위와 같은 방법으로 해결이 안되는 경우는 다른 부분에 문제가 있는 것입니다. 
프로그램 문제는 아닙니다.

반응형

+ Recent posts