https://www.youtube.com/watch?v=82h8ndWuaXk&list=PLVsNizTWUw7E2KrfnsyEjTqo-6uKiQoxc&index=57 

 

.

 

 

가상 메모리가 물리 메모리에 매핑될 때 페이지 단위(4kb)로 할당이 되는데

 

기준이 되는 가상 메모리 페이지에 윈도우즈는 상태를 부여한다.

 

Commit - 할당되어있는 상태(malloc)

Reserve - 할당을 예약해둔 상태

Free - 할당되어있지 않은 상태(Free)

 

 

 

근데 실제로 사용되는건 페이지 1~2개 정도인데, 가끔 사용량이 높아질 때를 대비해 몇 배 크게 할당해준다는건 메모리 낭비라는 것이다.

 

문제1. 연결된 메모리 공간 필요

문제2. 메모리를 미리 할당하면 너무 낭비

 

그래서 Reserve 상태라는게 나왔다.

 

 

주로 실제 사용되는 페이지만 Commit하고, 나머지 예비 페이지는 Reserve 상태로 두자는 것이 그 개념이다.

 

 

아무튼 그렇다 치고, 메모리 할당에 관련해 더 살펴보면

 

Allocation Granularity Boundary라는 것이 있다.

 

이건 메모리를 할당할 때 페이지 크기의 몇 배수 크게 할당하겠다는 의미인데

 

페이지 단위 그 자체로 메모리에 할당해버리면 메모리 단편화가 생길 가능성이 크다.

 

메모리를 할당해서 ABAB같이 자료가 떨어져서 할당되면 문제가 된다.

 

그래서 AABB처럼 뭉쳐서 할당하자는 취지다.

 

 

그 크기는 최소 1페이지부터 시작된다.

 

기본값은 64kb이다.

 

(기본 페이지 4kb)

 

 

4kb를 할당해달라고 하면 64kb번지부터 4kb를 할당하고,

 

그 뒤 8kb를 할당해달라고 하면 다음 번지인 128kb번지부터 8kb를 할당한다.

 

만약 6kb를 할당해달라고 한다면(잘못된 값이다. 기본 페이지 단위는 4kb이다) 윈도우는 지가 알아서 8kb로 바꿔서 할당하기도 한다

 

 

이 함수는 메모리를 데이터 스택 디폴트 힙, 다이나믹 힙 밖의 다른 동적 메모리 영역에 할당한다

 

 

 

VirtualAlloc & VirtualFree는 Reserve 상태를 지원해주는 시스템 함수다

 

3번째 인자를 잘 보면 나온다.

 

 

VirtualFree는 Free 하거나 Release(Reserve 상태로 변환) 시킨다.

 


 

 

 

힙엔 두가지 힙이 있는데 하나는 디폴트 힙이고 나머지 하나는 다이나믹 힙이다.

 

기존 디폴트 힙에서 리스트와 같은 자료구조로 메모리를 할당하면

 

할당 해제시 리스트를 다 순회하며 하나씩 지워야 하는데

 

다이나믹 힙에 이러한 할당을 하게 되면

 

굳이 힙 내부에 들어가서 하나씩 할당을 해제할 필요가 없이

 

힙 자체를 삭제시켜버린다.

 

 

 

메모리 단편화 해소

 

사용자가 직접 메모리를 할당해주면 디폴트 힙처럼 윈도우 맘대로 할당하느라 생긴 단편화를 피할 수 있다.

 

그 결과로 로컬리티가 괜찮아 진다는 것이다

 

동기화 문제에서 자유

 

쓰레드별로 힙을 생성해 메모리 침범이 일어나지 않는다

 

다이나믹 힙은 데이터 스택 디폴트 힙을 벗어난 다른 메모리 공간에 할당한다.

 

HeapCreate 함수를 사용하면 핸들이 나온다. 그 핸들로 HeapAlloc을 사용하면 다이나믹 힙에 할당이 되는 것이다.

 

HeapFree를 하면 할당된 메모리 힙을 통채로 날려버린다. 리스트로 하나씩 순회하면서 메모리를 지울 필요가 없는 것이다.

 


 

메모리를 파일에 매핑시키겠다는 개념이다.

파일의 일부 메모리 공간을 프로세스의 가상메모리에 연결시킨다

연결이 일단 되면 프로세스의 가상메모리에 내용을 쓰면 그 위치만큼 이동하여 파일에 데이터를 대신 써준다

이점은 일단 성능 이슈도 있지만, 프로그램의 편리성에 이슈를 둘 수 있다

만일 데이터를 sorting 해야 한다고 치면 파일에 있는 데이터를 메인메모리로 불러들여 sorting하고 다시 저장해야한다.

하지만 이 기능을 사용한다면, 데이터가 파일에 매핑이 됐으므로 그 자리에서 sorting만 하면된다.

 

 

편하다!

 

데이터 매핑을 시켜버리면 데이터 write시, 알아서 파일에 저장된다.

파일에 매번 데이터를 쓸때마다 반영해야 할 필요가 있을까?

메모리에 데이터를 write, read 하므로 실질적인 최신의 데이터는 메모리가 가지고 있다.

즉, 데이터가 변경되었을 때, 파일에 정보를 반영할 필요가 없다는 것이다.

주기적으로, 또는 특별한 상황이 됐을 경우에만 파일에 반영해도 된다.

 

 

ㅁㅁㅇㄴㄹ

'운영체제 > 윈도우 시스템' 카테고리의 다른 글

윈도우 프로시저  (0) 2020.05.15
I/O와 CPU 클럭의 관계  (1) 2019.10.16
파일 I/O와 디렉터리 컨트롤  (0) 2019.10.15
예외처리(SEH)  (0) 2019.10.15
메모리 계층  (0) 2019.10.15


A와 B 컴퓨터가 있고, A의 클럭은 100Hz, B의 클럭은 200Hz



I/O 버퍼가 있고, 버퍼를 비우는 기준은 10클럭에 한번, 그래서 A컴퓨터는 초당 10번, B컴퓨터는 초당 20번.



A컴과 B컴에 A, B, C를 입력했을 때, A컴퓨터는 버퍼에 자료가 모두 들어가고 나서 버퍼가 비워진다.


이 때, 만약 자료의 이동이 네트워크를 통해 다른 컴퓨터에 전달될 시 A, B, C가 들어있는 버퍼는 한번의 통신 싸이클(Ack, TCP/IP)로 해결할 수 있다.


그러나 B컴퓨터는 A가 들어오고 버퍼를 바로 비우고, B가 들어오고 비우고, C가 들어올 때 비운다. 그러면 통신 싸이클을 세번을 각기 다르게 해줘야 한다.



I/O(Bus 클럭) 연산이 묶이는 경우에는 CPU 클럭에 영향이 덜하다


Buffer는 I/O의 기본 메커니즘이다.




동기 I/O

함수의 호출과 데이터 전송이 동기화

함수의 반환과 전송 끝이 동기화


바로 반환을 안함, 아무것도 못함


이게 동기화 I/O





write 함수 호출 시작과 동시에 반환하고 내부적으로 계속 전송하는 것


클라이언트가 서버에게 요청을 하는 경우를 예를 들면,

클라이언트가 서버에게 요청을 보내고 서버가 그 결과를 보낼 때까지

다른 일을 하지 않고 기다렸다가 결과를 받으면 다음 단계로 진행하는

방식을 "동기" 방식이라고 합니다.

반대로 클라이언트가 서버에게 요청을 보낸 후, 그 결과가 넘어올 때까지

기다리지 않고 다른 작업을 하다가 결과값이 왔다는 신호를 받으면

결과값을 받아 처리하는 방식을 "비동기" 방식이라고 합니다.


출처: https://linuxism.ustd.ip.or.kr/757 [linuxism]



I/O는 느려서 상대적으로 빠른 CPU는 I/O 처리를 기다리는 동안 다른 일을 할 수 있음



동기 I/O의 CPU 사용을 보면 CPU가 부분부분 쉬는것이 보이고


비동기 CPU 사용을 보면 여유를 두고 꾸준히 사용중인 것을 볼 수 있다


속도가 중요한 프로그램이 아니면 동기 I/O 모델로 프로그램을 만드는 것이 가독성 면에서도 좋다


대신 아니라면 비동기 I/O가 중요하다.


비동기 I/O가 필요한 프로그램은 50%가 채 안된다고 함



I/O를 여러개 두고 중첩시켜서 CPU를 진행시킨다. 느린 I/O처리를 기다릴 바에 이렇게 중첩시켜 처리하겠다는 뜻



A컴퓨터에 B, C컴의 네트워크 I/O 빨대 꽂고 처리를 해도


I/O는 느려터져서 그냥 B, C 꽂고 동시처리 해도 처리가 가능하다는 것


대신 동시처리중인 I/O의 작업이 완료되었는지 확인을 해줘야 한다.


근데 I/O들의 목적이 다 다르기 때문에 이 목적 확인이 힘들다고 함



그래서 나온게 완료루틴이다.


I/O가 끝나면 해당 I/O에 묶인 함수를 실행한다. C I/O가 끝나면 함수 F를 실행하는 방식



 


Offset과 OffsetHigh는 유니온


완료루틴 I/O는 중첩 I/O의 확장형, OVERLAPPED함수가 둘 다 필요한 이유


hEvent가 EVENT 객체를 가리킨다. EVENT 객체가 상태를 변경하면 hEvent 핸들값이바뀌고 연산의 끝을 알 수 있다.



WriteFileEx 함수는 중첩 I/O방식과 같지만 완료루틴 콜백함수가 추가되어있다.


WriteFileEx는 CompletionRoutine과 인자(PIPE) I/O를 연결시켜준다.


콜백이기 때문에 함수의 호출과 인자의 전달을 윈도우가 해준다.


lpOverLapped를 보면 hEvent를 받는다.



그래서 완료루틴 I/O에도 기존 중첩 I/O 함수를 사용해서 중첩 I/O에 사용되던 hEvent 핸들에 추가로 데이터를 완료루틴 함수로 전달할 수 있다.





A가 I/O 요청했다. 이 I/O가 연산이 끝나면 CompletionRoutine 함수를 호출한다.

즉, A I/O 작업이 끝나면 자동적으로 CompletionRoutine 함수 시작될 것이기 때문에 A는 작업을 계속해야 한다.

근데 I/O 작업이 언제 끝날지는 모른다.

I/O 연산 끝나면 CompletionRoutine 을 하러 가야하는데, 프로그래머 입장에서 보면 일을 하다가 CompletionRoutine 에게 우선순위를 뺏기는 셈이다.

이 일의 우선순위를 A에게 줄지, CompletionRoutine에게 줄지 결정할 수 있어야 한다.

이게 가능해야 안정적으로 SW를 디자인할 수 있다.

만일, I/O 작업이 끝나 CompletionRoutine을 해야할 때, 지금 하고 있는 일에 상관 없이 CompletionRoutine에 우선순위를 주어

CompletionRoutine을 시작하도록 하고 싶다.

이 상태를 알람이 가능한 상태, Alertable State라 한다.

그러면 이를 명시적으로 선언해야 하는데, 이게 그림의 세가지 함수이다.

이 함수들을 호출하면, I/O 작업이 끝났을 때 CompletionRoutine이 시작하게 할 수 있다.


APC Queue라는 것이 있다.  이는 각각의 쓰레드에 독립적이다.

쓰레드가 알람 가능한 상태가 되었을 때, 호출할 콜백 함수들을 모아둔 queue이다.

즉, Alertable State가 되었을 때 호출할 콜백 함수들을 모아둔 queue이다.

그러면 큐에 있는 것은 대상이 Function&Param에 들어가있는 정보를 참조하여 해당 함수를 호출하게 되는데

언제 호출하게 되냐면 쓰레드가 Alertable State가 되었을 때 함수들이 다 호출이 된다.

총 세개의 함수가 등록되어 있다 해서 쓰레드가 Alertable State로 세번 들어가야 하는 건 아니다 .

무조건 한번 들어가면 queue는 전부 지워진다.

WirteFileEx() 함수의 경우에도 I/O가 완료되었을 때 단순하게 APC 큐에 콜백함수 정보를 입력시킨다.




'운영체제 > 윈도우 시스템' 카테고리의 다른 글

윈도우 프로시저  (0) 2020.05.15
메모리 관리 (가상 메모리, 힙, MMF)  (0) 2019.10.18
파일 I/O와 디렉터리 컨트롤  (0) 2019.10.15
예외처리(SEH)  (0) 2019.10.15
메모리 계층  (0) 2019.10.15


ANSI는 껍데기다.


사용자가 fopen 함수를 호출하면 ANSI는 내부적으로 해당 OS 시스템 함수를 호출한다.



ANSI는 공통된 부분만 담는다.






검사

dwPtrLow = SetFilePointer( . . . . . )

if( (dwPtrLow == INVALID_SET_FILE_POINTER) && (GetLastError() != NO_ERROR) )

{

진짜 오류..

}

'운영체제 > 윈도우 시스템' 카테고리의 다른 글

메모리 관리 (가상 메모리, 힙, MMF)  (0) 2019.10.18
I/O와 CPU 클럭의 관계  (1) 2019.10.16
예외처리(SEH)  (0) 2019.10.15
메모리 계층  (0) 2019.10.15
쓰레드 동기화 기법  (0) 2019.10.11




CPU 내부에는 CPU가 설정해놓은 예외 상황이 존재한다(예를 들어, 0 나누기 등)


그러한 예외들을 1, 2, 3, 4, ...n개 등록해 놓는것을 하드웨어 예외라고 한다.


소프트웨어 또한 그러한 예외 상황을 지정해두고 있다.


예외가 발생해 CPU가 예외를 알리면 소프트웨어는 해당 예외를 확인해 추후 상황을 결정한다.


즉 CPU는 예외를 알리기만 하지만, 그에 대한 결정은 소프트웨어가 결정한다.


예외의 결정은 운영체제가 하거나, 사용자가 제작한 어플리케이션이 한다



즉, CPU가 발생시킨 하드웨어 예외를 OS가 받아 소프트웨어 예외를 처리한다.


이를 처리하는 방식은 OS 자체적으로 해결하거나 / 다시 전달해 사용자가 전달받아 사용자 어플리케이션에서 처리한다.


이러한 예외를 SEH라고 한다. (소프트웨어 예외 / 어플리케이션 예외)



윈도우 기반에서 APP 예외는 잘 안다룬다고 함




SEH



예외 처리 코드는 거추장스럽따



예외 처리 코드를 따로 분리하면 가독성에 상당히 도움이 된다.




종료 핸들러



예외 핸들러




result = num1 / num2; 에서 예외가 발생하면 바로 __except로 넘어간다.



스택 풀기의 개념


C++ try catch 참고


아무런 except가 없으면 윈도우는 프로세스를 종료시킨다. 이게 윈도우 예외처리 방식


std::exception 참고



return EXCEPTION_CONTINUE_EXECUTION; 에 의해 에러가 난 코드부터 다시 실행한다.


그냥 함수 스택 풀어버리고 기존 함수로 이동해서 예외처리해버림


하드디스크는 저장, 메인 메모리엔 실행으로 알고 있지만 하드디스크는 실행의 기능도 있다.




프로그램의 실행은 지역적인 특성이 있다.


메모리 크기가 1 ~ 100까지 있다면 난수처럼 산발적으로 실행되는 것이 아니라,


한 부분이 실행되면 그 주변을 돌면서 작동하고 시간이 지나면 다른 곳으로 점프해서 그 주변을 실행한다.


그 주변을 더 빠르게 실행하기 위해 나온 것이 캐시로, 메인 메모리보다 CPU에 더 가깝게 위치한다. 속도는 빠르지만 용량은 메인 메모리보다 작다


이 방법이 효과가 좋았기 때문에, 메모리를 또 놨다.



기존 캐시보다 더 빠르지만, 용량은 더 작다.


하드디스크 또한 이러한 구조와 같다.


하드디스크는 실행을 빨리 하기 위해 메인 메모리에 데이터를 넣고,


메인메모리는 더 실행을 빨리 하기 위해 L2캐쉬에 데이터를 넣는다.


ALU는 L1에 데이터를 요청하면, L1은 L2에 요청하고 ... 하드디스크에 요청한다.


하드디스크에 데이터가 있으면 그 데이터를 메인 메모리, L2, L1, 레지스터에 올려 실행시킨다.


즉, 하드디스크의 저장이라는 관점은 파일 시스템이다. 메모리 매니지먼트 측면으로 이해하면 안된다.





모든 프로그램은 지역성을 갖는다.



CPU가 데이터를 요청하면 캐쉬에 그 데이터가 있을 확률이 90%가 넘는다


와우!


좋은 프로그램은 지역성 또한 좋을 것이다.



캐쉬 메모리로 성능이 향상되는 이유는 바로 Locality 때문


Temporal Locality :

만약 int a를 선언했다면 CPU는 int a에 다시 접근할 가능성이 높다


Spatial Locality :

int a에 접근하는 경우 int a와 함께 선언할 int b에도 접근할 가능성이 높다.



로컬리티의 예


Temporal Locality와 Spatial Locality를 만족하는 것을 볼 수 있다.



캐시에 적절한 데이터가 있을 확률이 90%가 넘는 이유는 Spatial Locality의 특성때문이다.


하드가 메인메모리에 데이터를 넘겨줄 때, 하드가 메모리에, 메모리가 L2에, L2가 L1에, L1이 레지스터에 넘겨주는 데이터는 특정 용량의 기준이 있다.




10mb도 5mb도 2mb도 아니지만 예를 들어 이러한 기준이 있다고 보면


메모리 계층간 데이터 이동은 블록단위로 이동한다.


그래서 캐시 히트가 90% 넘는 확률로 발생하는 이유가 바로 블록단위 데이터 이동 때문이다.


일반적으로 짜는 코드 역시 이러한 방식이기도 한데, 캐시 친화적인 코드란 메모리를 순서대로 읽게 되는 코드를 뜻한다.


하지만 캐시에 대해 공부하려면 그 양이 너무 방대해 밑도끝도 없는 공부를 하게 될수도 있다고 하네





동대문좌


실제로 필요한 메모리가 20개인데 실제 가지고 있는 메모리는 10개라면 소유중인 메모리를 자유자재로 매핑할 필요가 있다.



메인 메모리가 부족하면 하드 디스크까지 확장하는 것을 가상 메모리라고 한다.




선 할당으로 인한 부담


특정 프로세스가 선 할당으로 메모리 4gb를 점유해버리면

다른 프로세스는 메모리 사용을 못하고 발만 동동 구르게 된다.


느린 속도의 개선



1gb는 램, 1gb는 하드에 데이터가 올라가 있으면 언제는 빠르고 언제는 느리고 할 것이다.

하지만 속도가 일정해야 하니 이것을 개선해야 한다.



CPU가 어떤 데이터 할당을 요청하면 MMU는 물리 메모리(램)에 가상 메모리를 할당한다.


1K번지부터 20바이트를 할당을 요청하면 MMU는 가상 메모리로 0K ~ 4K를 매핑해 물리 메모리 0 ~ 4K에 할당하고,


36K번지부터 20바이트를 할당을 요청하면 MMU는 가상 메모리로 36K ~ 40K를 매핑해 물리 메모리 4 ~ 8K에 할당한다.


만약 CPU가 36K번지를 요청하면 MMU는 이를 가상 메모리에서 물리 메모리로 치환해서 실제로 데이터가 존재하는 4K번지에 있는 것을 CPU로 전달한다.


가상 메모리 관점으로 페이지, 물리 메모리 관점으로 페이지 프레임이라고 하네


만약 프로세스를 실행해서 가상 메모리에 2gb를 할당할 일이 있다면, 하드디스크에 2gb를 다 넣어둔다. 그리고 필요한 데이터를 램에다 올리는 것이다.


즉, 램을 일종의 캐쉬처럼 사용하는 것이다.



램이 꽉 찬 상태에서 4 ~ 8K를 CPU로부터 요청받으면


LRU는 램에서 사용된지 오래된 것을 하드디스크에 스토어한다.( 8 ~ 12K )


그리고 4~8K를 부른다.


하지만 그러자마자 8 ~ 12K를 다시 요청받으면


램에 존재하는 단위 중 사용된지 가장 오래된것을 다시 하드에 스토어하고, 하드에 있는 8 ~ 12K를 다시 로드한다.


하드에 데이터를 저장할 때 파일 시스템, 즉 파일로 저장한다. 이러한 임시 데이터를 스왑파일이라고 한다.



그림이 지존 멋있군


컨텍스트 스위칭은 이러한 경우에도 일어난다.



임계 영역 - 둘 이상의 쓰레드가 동시에 접근할 위험 요소를 가지고 있는 코드 블록


동시에 접근하는 것을 막는 것이 메모리 동기화


커널 쓰레드 동기화는 여러 기능이 있다. 심지어 프로세스 쓰레드간 동기화도 존재한다



예를 들어, A 와 B 라는 사람이 화장실 앞에서 마주쳤다.

A는 큰 일을 볼 사람이고, B 는 작은 일을 볼 사람이다.

이런 상황에서는, 보통 B 가 양보를 한다

그럼 B 는 이제 어떻게 할까!? 이것이 바로 순서가 무너진 상황이다.

쓰레드도 실행 순서가 중요한 경우가 있다.

만약, A 쓰레드가 계산하고, 그 결과를 B 쓰레드가 출력 한다고 할 때, 반드시 A 쓰레드가 먼저 실행되어야 한다.


실행 순서 동기화



한 순간에 하나의 쓰레드만 접근해야 하는 메모리 영역이 존재한다.

대표적으로 데이터 영역과 힙 영역이다.

앞서 _beginthreadex 함수를 소개하며, 메모리에 동시 접근할 때 어떤 문제가 생기는지 설명했다.

즉, 이렇게 메모리 접근에 있어서 동시 접근을 막는 것 또한 쓰레드의 동기화에 해당한다.

위의 것의 차이점을 보자.

실행 순서의 동기화는, 이미 순서가 정해져 있는 상황인 반면에, 메모리 접근의 동기화는, 실행 순서가 중요한 것이 아니라 한 순간에 하나의 쓰레드만 접근하면 되는 상황이다.


Windows 에서 제공하는 동기화 기법은 제공하는 주체에 따라 크게 두 가지로 나뉘는데, 하나는 유저 모드 동기화 기법이고, 또 하나는 커널 모드 동기화 기법이다.

유저 모드 동기화

동기화 과정에서 커널의 코드를 사용하지 않는 동기화 기법이다.

따라서 커널 모드로의 전환이 불필요하므로 성능상 이점이 있지만, 그 만큼 기능상 제한이 있다.

커널 모드 동기화

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

동기화 관련 함수가 호출될 때 마다 커널 모드로의 변경이 필요하고, 때문에 성능의 저하가 있다.

하지만 그 만큼 유저모드 동기화에 비해 기능이 많다.


굳이 크리티컬섹션 함수 안에 어떻게 구현이 되어 있는지 알아볼 생각은 하지 말고 주어진걸 쓸 생각 하라고 하네


그리고 임계영역 구성은 최소한의 부분만!


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


앞의 예제처럼, 전역으로 선언된 변수 하나의 접근 방식을 동기화 하는 것이 목적이라면, 인터락 함수도 괜찮다.

인터락 함수는 내부적으로 한 순간에 하나의 쓰레드에 의해서만 실행되도록 동기화 되어있다.

LONG InterlockedIncrement(LONG volatile* Addend);

이 함수의  인자는, 값을 하나 증가 시킬 32비트 변수의 주소값을 전달한다.

둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 증가시킬 경우, 동기화 된 상태에서 접근하는 것과 동일한 안정성을 보장받을 수 있다.

LONG InterlockedDecrement(LONG volatile* Addend);

둘 이상의 쓰레드가 공유하는 메모리에 저장된 값을 이 함수를 통해 증가시킬 경우, 동기화 된 상태에서 접근하는 것과 동일한 안정성을 보장받을 수 있다.

위 함수들은, 한 순간에 하나의 쓰레드만 접근하는 것을 보장하는 함수이다.

따라서, 이 함수를 사용할 시 동시에 둘 이상의 쓰레드 접근에 의한 문제는 결코 없다.

앞의 크리티컬 섹션 동기화 기법도 내부적으로는 인터락 함수 기반이다.

이러한 함수들도 유저 모드 기반이므로, 속도는 상당히 빠르다.


크리티컬섹션 작동 방식은


인터럽트를 막아버리는 방식으로 구현할 수도 있다.


하드웨어가 운영체제에 시간을 보내주는 것을 인터럽트로 알려주는데 이 때 인터럽트를 막아버려서 시간을 알려주지 않으면 임계영역을 신나서 동작시킨다는 것




커널 모드 동기화

뮤텍스는 세마포어의 일부다



뮤텍스는 키가 1개다



세마포어는 키가 여러개다



A쓰레드가 키를 가지고 정상 반환을 하지 못했을 때, 윈도우는 그 코드를 받아서 바로 앞에서 대기중이던 B쓰레드에 쥐어준다.


열쇠 소유의 개념이 있기 때문에 가능한 것이다.


하지만 세마포어는 키가 여러개고, 키를 어떤 쓰레드가 가져갈지 모르기 때문에 소유의 개념이 없다.


그래서 위와 같은 예외처리가 불가능하다.


그렇다

HANDLE

CreateThread(

    LPSECURITY_ATTRIBUTES lpThreadAttributes,

    SIZE_T dwStackSize,

    LPTHREAD_START_ROUTINE lpStartAddress,

    LPVOID lpParameter,

    DWORD dwCreationFlags,

    LPDWORD lpThreadId

    );


첫 번째 인자는 프로세스 생성 할 때도 본 것이다핸들의 상속 여부를 결정한다.

두 번째 인자는 쓰레드의 스택 크기를 지정하기 위한 매개변수이다

0을 전달하면 디폴트 사이즈인 1M가 적용된다.

세 번째 인자는 쓰레드의 main 역할을 하는 함수를 지정하는 인자이다.

인자타입 LPTHREAD_START_ROUNTE 인데반환타입이 DWORD 이고 매개변수 타입은 LPVOID(void *) 인 함수 포인터로 형변환 되어있다.

네 번째 인자는 쓰레드 함수에 전달할 인자를 지정하는 용도이다.

lpStartAddress 가 가리키는 함수 호출 시 전달할 인자를 지정하는 것이다.

Main  argv 생각하면 된다.

다섯 번 째 인자는 쓰레드의 생성 및 실행을 조절하기 위한 전달인자이다

CREATE_SUSPENDED 가 전달되면생성과 동시에 Blocked 상태가 된다.

그러나 아래에서 나올 함수 ResumeThread 가 호출되면 실행한다.

XP 이상에서는 인자로 STACK_SIZE_PARAM_IS_A_RESERVATION 을 전달 할 수 있는데이 경우 dwStackSize 를 통해 전달되는 값의 크기는 reserve 메모리 크기를 의미하게 되고그렇지 않을 경우 commit 메모리 크기를 의미한다(이 내용은 나중에 설명한다그냥 넘어가자)

여섯 번 째 인자는 쓰레드 ID 를 전달받기 위한 변수의 주소값을 전달한다.

굳이 필요없다면 NULL 을 전달하면 되는데, ME 이하에서는 NULL을 전달할 수 없다.

이 함수가 실행되면쓰레드의 핸들이 반환된다.

ID는 PC에 고유하다.


운영체제마다 다른데 윈도우는 제한이 없어서 유저 레벨에 허용된 메모리만큼 생성 가능


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/*
    CountThread.cpp
*/
 
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
 
#define MAX_THREADS (1024*10)
 
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD threadNum = (DWORD)lpParam;
 
    while (1)
    {
        _tprintf(_T("thread num: %d \n"), threadNum);
        Sleep(5000);
    }
 
    return 0;
}
 
DWORD cntOfThread = 0;
 
int _tmain(int argc, TCHAR* argv[])
{
    DWORD dwThreadId[MAX_THREADS];
    HANDLE hThread[MAX_THREADS];
 
 
    while (1)
    {
 
        hThread[cntOfThread] =
            CreateThread(
                NULL,
                0,
                ThreadProc,
                (LPVOID)cntOfThread,
                0,
                &dwThreadId[cntOfThread]
            );
 
        if (hThread[cntOfThread] == NULL)
        {
            _tprintf(_T("MAXIMUM THREAD SIZE: %d \n"), cntOfThread);
            break;
        }
 
        cntOfThread++;
    }
 
    for (DWORD i = 0; i < cntOfThread; i++)
    {
        CloseHandle(hThread[i]);
    }
 
    return 0;
}
cs





쓰레드의 소멸



리턴코드로 깔끔하게 쓰레드를 끝내는 경우

(가장 권장한다고 한다)



GetExitCodeThread


GetExitCodeThread 의 인자는 쓰레드의 핸들, 종료코드를 저장할 메모리 주솟값 이다.





break마냥 쓰레드 내에서 종료하는거


그러나 잘 디자인된 프로그램의 경우를 보면 쓰레드 함수의 역할이 명확하다.


소프트웨어 디자인적으로 메인 스레드가 기타 쓰레드들의 라이프사이클을 책임지는 것이 좋다는 것.


메인 쓰레드가 B 쓰레드를 생성하는 것까진 괜찮으나, B 쓰레드가 C쓰레드를 또 생성해버린다면 별로 좋지 않다는 것이다.


디자인의 범위를 벗어나고 동작이 명확하지 않은 프로그램이 된다고 함. 결론은 쓰지말자




만약 C 함수에서 쓰레드를 종료한다고 하면, return 으로 종료하려면 다시 B , A 로 가야 하지만, ExitThread 함수를 호출하면 바로 종료가 된다.

하지만, C++ 로 할 경우, A,B 함수의 스택 프레임에 C++ 객체가 존재한다고 가정 할 경우, 그 객체의 소멸자는 호출되지 않는다. 따라서 메모리 누수가 날 수도 있다.

이로써, C,C++ 구분 없이 return 문이 가장 좋은 것 같다.


쓰지말자


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/*
           ThreadAdderOne.cpp
*/
 
#include <stdio.h>
#include <windows.h>
#include <tchar.h>
 
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
    DWORD * nPtr = (DWORD *)lpParam;
 
    DWORD numOne = *nPtr;
    DWORD numTwo = *(nPtr + 1);
 
    DWORD total = 0;
 
    for (DWORD i = numOne; i <= numTwo; i++)
    {
        total += i;
    }
 
    return total;
}
 
 
int _tmain(int argc, TCHAR* argv[])
{
 
    DWORD dwThreadID[3];
    HANDLE hThread[3];
 
    DWORD paramThread[] = { 1347810 };
    DWORD total = 0;
    DWORD result = 0;
 
    hThread[0=
        CreateThread(
            NULL0,
            ThreadProc,
            (LPVOID)(&paramThread[0]),
            0&dwThreadID[0]
        );
 
    hThread[1=
        CreateThread(
            NULL0,
            ThreadProc,
            (LPVOID)(&paramThread[2]),
            0&dwThreadID[1]
        );
 
    hThread[2=
        CreateThread(
            NULL0,
            ThreadProc,
            (LPVOID)(&paramThread[4]),
            0&dwThreadID[2]
        );
 
 
    if (hThread[0== NULL || hThread[1== NULL || hThread[2== NULL)
    {
        _tprintf(_T("Thread creation fault! \n"));
        return -1;
    }
 
    WaitForMultipleObjects(3, hThread, TRUE, INFINITE);
 
    GetExitCodeThread(hThread[0], &result);
    total += result;
 
    GetExitCodeThread(hThread[1], &result);
    total += result;
 
    GetExitCodeThread(hThread[2], &result);
    total += result;
 
    _tprintf(_T("total (1 ~ 10): %d \n"), total);
 
    CloseHandle(hThread[0]);
    CloseHandle(hThread[1]);
    CloseHandle(hThread[2]);
 
    return 0;
}
cs


68 : 메인쓰레드의 종료 -> 프로세스의 종료

나머지 쓰레드들이 종료될때까지 메인쓰레드의 진행을 멈추겠다는 의미




쓰레드의 성격과 특성


동시접근의 문제점



total이 10일때, A쓰레드에서 연산을 진행해 ALU에서 6을 더하고 그 값을 레지스터 r0에 저장한 채로 컨텍스트 스위칭이 일어나서,


B쓰레드에서 total의 값은 바뀌지 않았는데 또 그 total값에 9를 더해 다시 total에 저장한 채로 다시 A쓰레드로 컨텍스트 스위칭이 일어나면


레지스터 r0에 있던 값 16이 B쓰레드에서 연산한 total 19를 덮어씌워 total의 값은 16이 된다.



쓰레드의 UC는 프로세스의 그것과 같은 원리다


쓰레드가 죽을 때 쓰레드의 커널 오브젝트는 계속 누적되어서 메모리를 차지할 가능성이 있다.


그래서 쓰레드를 생성할 때 그 부모에서 CloseHandle 함수를 호출 해 쓰레드의 UC를 하나만 남겨둬야


쓰레드가 종료될 때 쓰레드 커널 오브젝트도 같이 사라진다. 이것을 쓰레드 분리라고 한다.


다만 이렇게 되면 부모가 종료 코드를 얻기 힘들어지기 때문에 그러한 경우엔 주의깊게 사용해야 한다.




1
2
3
4
5
6
7
8
9
10
11
12
Strtok 함수를 호출되면서 처음에 등록된 문자열은 어딘가에 저장되어야만 한다.
그래서 두 번째 부터는 NULL 을 인자로 줘서 출력할 것이다.
우리는 전역, 혹은 static 으로 선언된 배열에 문자열이 저장되어 있음을 예측할 수 있다.
이 경우, 메모리의 동시 참조의 문제가 발생할 수 있다.
해결책은 MS 에서 멀티 쓰레드에 안전한 ANSI 표준 라이브러리를 제공하고 있다. 
따라서, 프로젝트 설정 – C/C++ / Code Generation 에서 런타임 라이브러리를 Multi-hreaded Debug DLL 로 바꿔주면 된다.
이제 한 가지 일을 더 해줘야 한다.쓰레드를 생성 할 때,CreateThread 함수 대신 _beginthreadex 함수를 사용하면 된다..
이 함수는 내부적으로 CreateThread 를 호출하지만, 그 전에 독립적 메모리 블록을 할당한다.
Multi- 로 시작하는 이름의 표준 C 라이브러리 함수는 이렇게 할당된 쓰레드 각각의 메모리 블록을 기반으로 연산한다.
이로써 멀티 쓰레드 기반에서 안정성이 확보되는 것이다.
_beginthreadex 함수는 전달인자의 순서와 의미가 CreateThread 함수와 동일하다.
다만 선언된 매개변수 자료형과 반환형에 차이가 있기 때문에, 약간 형 변환이 요구된다.
cs


실행 결과는 차이가 없다.

앞으로는 멀티 쓰레드 기반 프로그래밍을 한다는 가정하에 _beginthreadex 함수를 사용한다.

한가지 주의할 점은 쓰레드를 종료하는 방법이다.

만약 ExitThread 함수를 활용하고자 하면, _endthreadex 를 사용하기 바란다.

인자는 동일하다.

이 함수는 _beginthreadex 함수에서 메모리를 할당했으므로, 종료할 때는 그 메모리를 반환해야 한다. 그래서 사용하는 함수이다.

그럼 return 문을 이용하면 메모리 반환이 안될까

된다! 쓰레드 함수에서 return 문을 이용할 경우, _endthreadex 함수가 자동 호출된다.

그냥 return 에 의한 종료가 만사Ok 이다!.

 

참고,

_beginthread , _endthread 라는 이름의 함수가 있는데, 이 것들은 쓰레드 생성을 간결하게 하려고 만든 함수인데, 기능도 제한적이고 핸들도 사용 할 수 없다.

이러한 문제점들 때문에 많은 전문가들이 가급적이면 사용하지 말라고 하는 함수이다.





쓰레드의 상태 컨트롤


쓰레드의 상태는 프로그램이 실행되는 과정에서 수도없이 변경된다.

이 것은 상황에 따라 운영체제의 관리방법에 따른 것이므로, 프로그래머가 건드리는 것은 아니다.

그러나, 경우에 따라서 프로그래머가 직접 변경해 주어야 하는 상황도 있다.

특정 쓰레드를 지목하면서, 그 쓰레드의 실행을 잠시 멈추기 위해 Blocked 상태로 만들거나, 다시 실행시키기 위해 Ready 상태로 두기 위해서 필요하다.



쓰레드 함수를 Blocked 상태로 두거나 Ready 상태에 둘 때 사용하는 함수


별로 권장하지 않는다고 한다ㅇㄹㅇㄴㅁ



Windows 에서는 프로세스가 우선순위를 갖는 것이 아니라, 프로세스 안의 쓰레드가 우선순위를 가진다.

9장에서 말한 프로세스의 우선순위를 가리켜 기준 우선 순위라고 한다.

(IDLE_PRIORITY_CLASS , NORMAL_PRIORITY_CLASS 등..)

쓰레드는 상대적인 우선순위를 갖는다.

 

#define THREAD_PRIORITY_LOWEST          -2

#define THREAD_PRIORITY_BELOW_NORMAL    -1

#define THREAD_PRIORITY_NORMAL          0

#define THREAD_PRIORITY_ABOVE_NORMAL    1

#define THREAD_PRIORITY_HIGHEST         2

 

쓰레드의 우선순위는 프로세스의 기준 우선순위와 쓰레드의 상대 우선순위를 조합해서 결정된다.

참고로 위 표의 우선순위 상수값은 Windows 버전별 차이가 있다.

따라서 우선순위가 정확히 몇 이라고 이해하기 보다는 어느것 보다는 높겠다 정도로만 이해하면 된다.

프로세스 내 모든 쓰레드의 상대 우선순위는 THREAD_PRIORITY_NORMAL 이다.

이를 변경 , 참조할 때는 다음 두 함수를 쓴다.

BOOL SetThreadPriority(HANDLE hThread, int nPriority);

Int GetThreadPriority(HANDLE hThread);

이 함수에 대해서는 설명하지 않아도 알 것이다.



쓰레드 우선순위


나중에 다시바야하려나?



과거 OS는 멀티 프로세스를 지원하는 OS, 지원하지 않는 OS가 있었고

거기서 분리되어 쓰레드를 지원하는 OS, 지원하지 않는 OS(Unix)가 있었다

현대에는

쓰레드를 지원하지 않는 OS

쓰레드를 지원하는 OS가 있다.


지원하는 OS가 무슨 의미냐면 커널에서 쓰레드를 지원해준다는 것이다.


지원하지 않는 OS는 커널에서 쓰레드를 지원하지 않는다. 그래서 라이브러리 형태로 커널을 사용해야 한다.

-> 유저가 만들어서 사용하는 쓰레드다.



라이브러리는 유저 영역이다. 쓰레드를 지원하는 라이브러리 역시 유저 영역이다.



커널이 쓰레드를 지원해준다는 것은 커널이 각각의 쓰레드를 스케줄링 해준다는 것이다

이를 커널 레벨 쓰레드라고 한다.


반대로, 지원해주지 않는다는 것은 커널이 각각의 프로세스를 스케줄링 해준다는 것이다

이를 유저 레벨 쓰레드라고 한다.


그래서 쓰레드 지원 OS는 총 쓰레드 갯수를 감안해서 스케줄링하지만, 지원하지 않는 OS는 총 프로세스 갯수를 감안해서 스케줄링한다.


그 결과, 유저 레벨 쓰레드에서 만약 내부 쓰레드가 Blocked 상태로 빠지게 되면 스케줄러는 해당 프로세스가 Blocked 상태로 빠졌다고 생각하고 해당 프로세스의 실행 권한을 뺏어가버린다.


한 쓰레드의 상태로 프로세스 전체가 멈춰버리게 되는 것이다.


그렇다고 유저 레벨 쓰레드가 안좋다는 것은 아니다. 유저 레벨 쓰레드는 빠르다!는 장점이 있다




커널 모드와 유저 모드



유저가 커널에 접근해서 커널을 뒤죽박죽 해놓으면 개망하기 때문에 평소에는 유저 모드로 커널 영역에 접근을 제한시킨다.


커널 모드는 모든 메모리에 접근이 가능한 상태를 의미하고


유저 모드는 유저 레벨만 접근이 가능한 상태를 의미한다.


커널 모드로 변환되는 경우는 스케줄러가 동작하거나, 프로세스를 생성하거나, 쓰레드 지원 OS라면 쓰레드를 사용하는 경우 등등, 커널이 필요한 경우에 커널 모드로 변경된다.



커널 레벨 쓰레드는 쓰레드간 실행 권한이 변경될 때, 쓰레드 간의 컨텍스트 스위칭이 일어날 때마다 유저 모드에서 커널 모드로 변경되어야 한다.


그로 인해 속도가 저하되는 것이다.


하지만 유저 레벨 쓰레드는 커널 영역으로 들어갈 필요가 없다. 그로 인해 속도적인 손해를 보지 않는다.



코드 영역엔 프로그램 코드

데이터 영역엔 전역

힙 영역엔 동적

스택 영역엔 지역

 

이렇게 메모리를 프로세마다 할당하면 프로세스간의 컨텍스트 스위칭 CS가 발생, 부담스럽다

 

근데 흐름을 분리하기 위해 필요한 것들은 각각의 독립된 코드가 있는데,

 

하나의 독립된 코드는 각각 필요한 함수를 호출한다. 함수를 호출하기 위해서는 스택이 필요하다.

 

스택 영역을 공유할 수는 있지만 흐름별로 스택을 독립시켜서 사용하면 관리가 한결 편해질 것이다.

 

코드 영역은 하나의 프로세스가 별개로 나눠서 사용 가능하다. 하지만 스택은 한 프로세스가 나눠서 사용할 수 없다.

 

 

 

그래서 나온게 쓰레드다.

 

메인 프로세스의 코드 영역에 각각의 쓰레드의 코드를 전세내서 방을 얻어내고, 데이터랑 힙은 같이 써도 크게 문제될 것이 없어 공유한다.

 

하지만 스택은 공유가 힘드니, 각각의 흐름을 위한 별도의 스택 메모리 공간만 마련해두면 하나의 프로세스로 별개의 흐름을 가질 수 있을 것이라는 아이디어에서 나타난 것이 쓰레드이다.

 

 

쓰레드 사용 시 코드 영역에 올라가는 코드를 적당히 살펴보면

 

전역함수

메인함수

m2

m3

 

있다고 해보면

 

함수 중에 add()함수가 있다. add함수는 메인함수에서 호출이 가능하다. 또한 m2, m3 코드에서도 add함수 호출이 불가능하다.

 

크게 보면, 하나의 프로세스이기 때문에 Code 영역, Data영역, Heap 영역에 각각의 쓰레드에서 해당 영역에 접근이 가능하다.

 

하지만 멀티 프로세스 상황에서 그러한 접근은 불가능하니, IPC같은 기법을 사용해서 접근하는 것이다.

 

멀티 프로세스는 구시대적인 것이고, 쓰레드는 진보되어있는 것이 아니라 서로 담당하는 영역이 다르다는 것으로 알아두어야 한다.

 

각각 적용하는 영역이 조금씩 다르다. 멀티 프로세스를 대체하는 것이 쓰레드라는 것은 아니라는 것.

 

프로세스가 담당하는 영역이 있고, 쓰레드가 담당하는 영역이 있다.

 

근데 이건 Windows 쓰레드가 아니다. 윈도우 쓰레드는 좀 다른가봄

 

 


 

윈도우 프로세스 쓰레드

 

 

사실 윈도우 OS의 스케줄러는 프로세스 스케줄링을 하는 것이 아니라, 쓰레드를 스케줄링 한다.

 

또한 프로세스의 상태정보는 프로세스에 존재하는 것이 아니라, 쓰레드가 가지고 있다.

 

하지만 프로세스만 있다고 봤을 때는, 프로세스가 상태 정보를 가지고 있다고 봐도 된다...

 

 

프로세스 A에서 쓰레드 1번과 2번이 엎치락뒤치락 실행중이다.

 

그 말은 쓰레드 3번은 우선순위가 낮거나, I/O에 의한 Blocked 상태에 있다는 것이다.

 

 

프로세스란?

일반인 : 실행중인 프로그램

개발자 : 메모리 구조(OS에 의해 할당되는 메인 메모리의 리소스 뿐 아니라 가상 메모리의 리소스까지 포함), 실행중인 프로그램에 독립적인 레지스터 Set, etc

 

하지만 쓰레드는 명확한 정의를 내리기 힘들다. 다만 쓰레드를 구성하는 것은 쓰레드의 메인 코드와, 독립된 스택이 있다.

쓰레드의 독립된 스택과 코드만으로 쓰레드라고 하기는 힘든게, 프로세스와 공유하기 때문에 이것이 쓰레드라고 하긴 어렵다고 함

 

결국은 쓰레드는 실행의 흐름이다. 정도가 그나마 명확한 정의라고 볼 수 있다.

 

실행 흐름의 주체는 프로세스가 아니다. 메인함수 또한 메인 쓰레드가 진행시키는 것이다. 기존에 공부했던 코드는 하나의 쓰레드, 하나의 메인 쓰레드가 총괄하는 것이다.

 

쓰레드가 실행할 수 있는 환경을 제공하는 것이 프로세스, 그 환경 안에서 쓰레드가 동작한다.



https://mm5-gnap.tistory.com/60


ESP - Stack pointer register


ESP 레지스터 : 스택의 크기를 조정할 때 사용되는 레지스터. 스택의 최상단 주소값을 가진다.

 -> 스택의 크기를 나타냄


Intel CPU에서는 스택이 거꾸로 자란다.


ESP는 다음 번 DATA를 PUSH할 위치가 아닌, 다음에 POP 했을 때 뽑아낼 데이터의 위치를 가리킨다.

 -> std::stack::top()의 주소라고 생각하면 될듯.


어셈블리에서 esp에 PUSH를 하면 esp 값이 n감소한다.


 n? : msvc 2017 기준, 0C0h(192) 감소한다. 밑의 디스어셈블리 참고



감소하는 이유는 다음과 같다. 높은주소 → 낮은주소


EBP - Base pointer register


EBP 레지스터 : 스택프레임 형태로 저장된 함수의 지역변수, 전달 인자를 참조 & 값의 수정 시 사용되는 레지스터.


현재 스택의 가장 바닥을 가리키는 포인터.


새로운 함수 B가 호출되면, EBP 레지스터 값은 지금까지 사용했던 스택 A의 위를 가리킨다. 그리고 새로운 스택(함수 공간)이 시작


따라서 EBP는 새로운 함수가 호출되거나, 현재 실행중인 함수가 종료되면 값이 달라진다.


새로운 함수를 호출할 때, EBP 레지스터 값

전달 인자를 ESP 레지스터로 참조할 수는 있지만 어셈블리 코드 유지가 힘들다.


EBP는 고정적이지만 ESP는 명령을 수행 시 값이 변하기 때문에 매번 수정해주어야 하기 때문.



sp(Stack Pointer register) 는 esp다.

fp(Frame Pointer register) 는 ebp다.


fp 는 sp의 백업 포인터다.


sp가 반환을 알리면 fp 위치로 돌아간다. 이 때 메모리는 반환작업을 거치지 않고 그냥 덮어쓰는 방식으로 작동한다.



fp의 중복 문제는 스택에 fp를 백업하는 방식으로 해결한다





PUSH & POP 명령어 디자인



기존 STORE 명령어는


STORE 대상(레지스터), 목적지(메모리 주소)

이지만


문제는

STORE 7(숫자), sp(레지스터)

이다.


해결책은 7을 레지스터에 넣고 sp를 메모리에 넣는것



첫 번째 문제점 7을 레지스터에 넣는것은


ADD r1, 7, 0

으로 해결한다.


보통 MOV 명령어가 있거나 그렇던데 그렇더라.


두번째 문제는 sp를 메모리에 STORE 한다


STORE sp, 0x40


그 후 레지스터와 메모리 주소를 STORE한다


STORE r1, [0x40]


[] 기호는 인다이렉트, 포인터 연산이다.


0x40에는 sp메모리 위치 0x10이 저장되어 있다


[0x40]이 아니라 0x40에다가 r1을 STORE 해버리면 포인터 변수의 위치에 값을 넣어버리는 것과 같은 꼴이다.


[] 기호로 0x10에 접근하자.


그리고 ADD로 sp 메모리 위치를 4바이트 올리자. 만약 64비트라면 8을 더해줘야 한다.


POP을 만드는 두가지 방법



호출 규약 실행 이동




CPU 내부에선 IR(Instruction) register에 코드영역 내부에 존재하는 명령어를 하나씩 fetch한다.


PC(Program Counter) register는 fetch해야할 명령어, Command n을 가리킨다.


즉 CPU는 PC에 저장된 값을 Fetch Decode Execution 사이클을 죙일 반복한다.



pc 레지스터도 스택포인터, 프레임포인터와 같이 함수호출이 되면 돌아갈 위치를 저장해야 하는데 이를 LR(Link Register)라고 부른다.


LR도 똑같이 Stack에 쌓는다.




32비트의 함수호출규약은 한계가 있어서 하나도 빠짐없이 암기할 필요까진 없다


Parameter order는 C스타일이다. <-

(파스칼스타일은 -> 이다 근데 안중요)


스택을 비우는게 32비트에선 Caller와 Callee가 각각 다른 것을 볼 수 있는데 64비트에선 Caller 통일댓다


중요한 점은 64비트에서 Parameters in registers 카테고리의 레지스터 사용 방식이 레지스터에 상당히 의존하는 것을 볼 수 있다.


32비트에서 빠르다는 __fastcall이 레지스터 두개를 쓴다. 64비트에선 기본적으로 4개를 쓴다. 리눅스에선 지존많이쓴다.


그래서 64비트는 클럭수준으로 속도만 빠른게 아니라 이러한 함수호출규악까지 포함해서 32비트보다 속도가 빠르다.






핸들테이블의 작동 방식, 프로세스가 메일슬롯 함수를 호출하면 메일슬롯 커널오브젝트 핸들값을 호출한다.


핸들값은 프로세스의 핸들테이블에 들어가고 핸들테이블은 프로세스별로 독립적이고 해당 프로세스에게만 의미가 있다.



부모 프로세스가 자식 프로세스를 생성할 때, 상속이 가능한 핸들에 따라 자식 프로세스에 부모 프로세스의 핸들을 상속할 수 있다.


여기서, 사용자는 bnheritHandles를 통해 상속유무를 직접 설정할 수 있다.



메일슬롯의 CreateMailSlot 함수와 CreateFile 함수에는 이러한 인자들이 있다.


1
2
3
4
5
6
HANDLE CreateMailslot(
           LPCTSTR lpName,
           DWORD nMaxMessageSize,
           DWORD lReadTimeout,
           LPSECURITY_ATTRIBUTES lpSecurityAttributes
);
cs


CreateFile 함수는 첫번째, 두번째, 다섯번째 인자만 알고 있으면 된다.

첫 번째 인자는 파일 이름이다.

두 번째 인자는 개방 모드이다. (Write , Read 등등) 여기선 쓰기모드로 GENERIC_WIRTE를 인자로 주었다.

다섯 번째 인자는 파일의 생성방식을 결정짓는 용도이다.


라고 적어놨는데, 4번째 인자에 위 그림의 SECURITY_ATTRIBUTES 구조체가 포인터가 들어간다.


여기서 SECURITY_ATTRIBUTES 구조체의 핸들 중 BOOL bInheritHandle; 요 변수가 상속여부를 결정한다.


또한 이 함수는 CreateProcess의 다섯번째 인자이기도 하다.




https://mm5-gnap.tistory.com/72


CreateProcess 함수의 선헌 형태를 보자.


지금은 이걸 다 이해할 필요는 없다. 그냥 보자


1. LPCTSTR lpApplicationName


생성할 프로세스의 실행파일 이름을 인자로 전달한다.


경로명을 추가로 지정할 수 있다. 경로명을 지정하지 않을 경우 현재 디렉터리에서 찾는다.


. . .

5. BOOL bInheritHandles


전달인자가 TRUE 이면, 생성되는 자식 프로세스는 부모 프로세스가 소유하는 핸들 중 상속 가능한 핸들들을 상속받는다.


. . .

10. LPPROCESS_INFORMATION lpProcessInformation


생성하는 프로세스 정보를 얻기 위해 사용되는 정보이다.


메일슬롯 이외의 리소스(함수?)에도 저러한 SECURITY_ATTRIBUTES 구조체 포인터가 들어가는 부분이 있다. 넣어주면 된다.



부모 프로세스가 어떤 커널오브젝트를 사용하는 리소스(함수?)나 프로세스를 호출한다면 UC는 1이 된다.


여기서 자식 프로세스에 특정 프로세스, 리소스를 상속시킨다면 UC는 2가 된다.



MailSender가 CreateFile로 Receiver에 연결하는 리소스를 생성하면 Sender의 CreateFile(0x1700번지) UC는 1이다.


여기서 상속으로 자식 프로세스에 0x1700번지 커널 오브젝트를 상속하면 UC가 2로 증가한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
        MailSender2_1.cpp
*/
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
 
#define SLOT_NAME _T("\\\\.\\mailslot\\mailbox")
 
int _tmain(int argc, LPTSTR argv[])
{
    HANDLE hMailSlot;
    TCHAR message[50];
    DWORD bytesWritten;  // number of bytes write
 
    SECURITY_ATTRIBUTES sa;
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;
 
    hMailSlot = CreateFile(SLOT_NAME, GENERIC_WRITE, FILE_SHARE_READ, &sa,
        OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
 
    if (hMailSlot == INVALID_HANDLE_VALUE)
    {
        _fputts(_T("Unable to create mailslot!\n"), stdout);
        return 1;
    }
 
    _tprintf(_T("Inheritable Handle : %d \n"), hMailSlot);
    FILE* file = _tfopen(_T("InheritableHandle.txt"), _T("wt"));
    _ftprintf(file, _T("%d"), hMailSlot);
    fclose(file);
    STARTUPINFO si = { 0, };
    PROCESS_INFORMATION pi;
    si.cb = sizeof(si);
    TCHAR command[] = _T("Branch2.exe");
    CreateProcess(NULL,
        command,
        NULL,
        NULL,
        TRUE,  // 자식 프로세스에게 핸들을 상속!
        CREATE_NEW_CONSOLE,
        NULL,
        NULL,
        &si,
        &pi
    );  //CreateProcess
    while (1)
    {
        _fputts(_T("MY CMD>"), stdout);
        _fgetts(message, sizeof(message) / sizeof(TCHAR), stdin);
        if (!WriteFile(hMailSlot, message, _tcslen(message) * sizeof(TCHAR), &bytesWritten, NULL))
        {
            _fputts(_T("Unable to write!"), stdout);
            CloseHandle(hMailSlot);
            return 1;
        }
        if (!_tcscmp(message, _T("exit")))
        {
            _fputts(_T("Good Bye!"), stdout);
            break;
        }
    }
    CloseHandle(hMailSlot);
    return 0;
}
cs


16 ~ 19 : 상속 관련된 SECURITY_ATTRIBUTES를 정의하고 있다.


31 ~ 33 : 핸들을 저장하고있다.


38 : CreateProcess로 자식 프로세스를 생성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/*
        MailSender2_2.cpp
*/
#include <windows.h>
#include <tchar.h>
#include <stdio.h>
 
int _tmain(int argc, LPTSTR argv[])
{
    HANDLE hMailSlot;
    TCHAR message[50];
    DWORD bytesWritten;  // number of bytes write
        /************* 핸들을 얻는 코드 *****************/
    FILE* file = _tfopen(_T("InheritableHandle.txt"), _T("rt"));
    _ftscanf(file, _T("%d"), &hMailSlot);
    fclose(file);
    _tprintf(_T("Inheritable Handle : %d \n"), hMailSlot);
    /**********************************************/
    while (1)
    {
        _fputts(_T("MY CMD>"), stdout);
        _fgetts(message, sizeof(message) / sizeof(TCHAR), stdin);
        if (!WriteFile(hMailSlot, message, _tcslen(message) * sizeof(TCHAR), &bytesWritten, NULL))
        {
            _fputts(_T("Unable to write!"), stdout);
            _gettchar();
            CloseHandle(hMailSlot);
            return 1;
        }
        if (!_tcscmp(message, _T("exit")))
        {
            _fputts(_T("Good Bye!"), stdout);
            break;
        }
    }
    CloseHandle(hMailSlot);
    return 0;
}
cs

자식 프로세스는 다음과 같다.


14 ~ 16 : 핸들을 얻어오고 있다. 핸들은 다음과 같이 생성된 txt파일을 열어서 읽고 종료한다.


커널이 좀 더 깊게 관여할 줄 알았더니 마냥 그런것도 아니다...


그래서 


23 : 읽은 핸들로 문자열을 전송하고있다.



프로세스의 핸들은 가짜핸들이다. 임의 프로세스 A의 커널 오브젝트 핸들값은 -1인가 그렇다고 했는데, 그게 바로 가짜핸들이다.


자기 자신을 의미하는 핸들일 뿐이지 실제 핸들 테이블에 등록되는 핸들값이 아니라는 것이다


그래서 가짜 핸들을 진짜 핸들로 넘기려면 위와 같은 코드를 사용해야한다.



DublicateHandle?

 번째 인자는 복제할 핸들을 소유하는 프로세스를 지정한다.

 

 번째 인자는 복제할 핸들을 지정한다.

 

 번째 인자는 복제된 핸들을 소유할 프로세스를 지정한다.

 

 번째 인자는 복제된 핸들값을 저장할 변수의 주소를 지정한다.


두번째 인자의 핸들값을 지정한다 하더라도, 세번째 인자의 핸들값이 두번째 인자의 핸들값과 동일하다는 보장은 되어있지 않다.


하지만 두 프로세스의 핸들값(256, 364)이 가리키는 주소는 동일하다. 이를 통해 커널 오브젝트에 간접 접근이 가능하다.


1. 등록하는 방법


자식 프로세스에 상속시키기 위해서는 부모 테이블의 핸들을 자기 자신의 핸들 테이블에 등록시켜야 한다.

그 때, 사용하는 것이 DublicateHandle 함수를 사용한다.


DuplicateHandle(

    HANDLE hSourceProcessHandle,

    HANDLE hSourceHandle,

    HANDLE hTargetProcessHandle,

    LPHANDLE lpTargetHandle,

    DWORD dwDesiredAccess,

    BOOL bInheritHandle,

    DWORD dwOptions

    );

 

 번째 인자는 복제할 핸들을 소유하는 프로세스를 지정한다.

 

 번째 인자는 복제할 핸들을 지정한다.

 

 번째 인자는 복제된 핸들을 소유할 프로세스를 지정한다.

 

 번째 인자는 복제된 핸들값을 저장할 변수의 주소를 지정한다.


다섯 번째 인자는 접근 권할을 지정한다.일단 0  전달한다. 자세한건 MSDN 참고


여섯 번째 인자는 복제된 핸들의 상속 여부를 지정한다.

ㅇㅇ


2. 왜 상속을 시키려고 하나


부모 프로세스의 핸들을 자식 프로세스에 상속시키면 자식 프로세스도 부모 프로세스가 종료되길 기다릴 수 있다.

Signal vs Non-Signal 편의 WaitForSingleObject 함수사용 참고



1. 등록하는 방법 이어서



첫번째 인자와 세번째 인자가 같다. 자기 자신에게 256을 복사한다는 것이다. 하지만 256은 고유하기 때문에 핸들은 다른 값으로 복사된다. 이 때 UC는 증가한다

CloseHandle을 두번 호출해줘야 한다!!



자기 자신을 기준으로 자기 자신에 자기 자신의 핸들값을 복사하는 방식이다.


여기서 두번째 인자는 가짜 핸들값이 나오는데, 가짜 핸들을 복사하게 되면 해당 프로세스의 핸들 테이블에 진짜 핸들값이 구성되어서 들어간다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
/*
    DuplicateHandleOne.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hProcess;
    TCHAR cmdString[1024];
    DuplicateHandle(
        GetCurrentProcess(), GetCurrentProcess(), GetCurrentProcess(),
        &hProcess, 0, TRUE, DUPLICATE_SAME_ACCESS
    );
    _stprintf(cmdString, _T("%s %u"), _T("Branch2.exe"), (unsigned)hProcess);
    STARTUPINFO si = { 0, };
    PROCESS_INFORMATION pi = { 0, };
    si.cb = sizeof(si);
    BOOL isSuccess = CreateProcess(
        NULL, cmdString, NULLNULL, TRUE, CREATE_NEW_CONSOLE, NULLNULL&si, &pi);
    if (isSuccess == FALSE)
    {
        _tprintf(_T("CreateProcess failed \n"));
        return -1;
    }
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    _tprintf(_T("[Parent Process]\n"));
    _tprintf(_T("ooooooooooooooooooooooopps! \n"));
    return 0;
}
cs


13 : 보면 이러케 나온다. hProcess에 핸들값이 담긴다.  번째 인자는 복제된 핸들값을 저장할 변수의 주소를 지정한다.

그 핸들값을 일케 절케 해서 프로세스를 생성한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/*
    DuplicateHandleChildProcess.cpp
*/
#include <stdio.h>
#include <stdlib.h>
#include <windows.h>
#include <tchar.h>
 
int _tmain(int argc, TCHAR* argv[])
{
    HANDLE hParent = (HANDLE)_ttoi(argv[1]);
    DWORD isSuccess = WaitForSingleObject(hParent, INFINITE);
    _tprintf(_T("[Child Process] \n"));
    if (isSuccess == WAIT_FAILED)
    {
        _tprintf(_T("WAIT_FAILED returned!"));
        Sleep(1000);
        return -1;
    }
    else
    {
        _tprintf(_T("General Lee said, \"Don't inform the enemy my death\""));
        Sleep(1000);
        return 0;
    }
}
cs


12 : 전달받은 핸들을 넣고 부모 프로세스가 끝날때까지 기다린다.


자식 프로세스는 부모 프로세스가 끝날때까지 기다리고 부모 프로세스가 끝나면 20 행부터 출력된다.


으아으아으ㅏ으아으아ㅡ아으ㅏㅡ아으ㅏㄹ암나ㅣㅓㄹㅇ남ㄴㄹ어ㅏㅣㄻㅇ니;ㄹㄴㅁ아ㅓ;ㅣㄴㄹ머ㅏ이;ㅏ먼;ㅣㅇ란ㄹ어;민ㅁㄹ어ㅏ;ㅣ나어;림ㅁㄴㄹ아ㅓ;ㅣㄹ망;ㅣㅓㄴㅁㄹ아ㅓ;ㄴㄻ아ㅓㅣ;ㅁㄴㅇ러ㅏ;ㅣㄹㅇㅁ너ㅏㅣ;ㅁㄴㅇ러ㅏ;ㅣㄴㅁㅇ러;ㅏㅣㅇ러ㅏ;ㅣㄴㅇㄹ머ㅏ;ㅣㅁㄴㅇ라ㅓ;ㅣㄴㅁㅇ라ㅓㅣ;ㅁㄴㅇㄻㄹㄴ어ㅏ;ㅣㄴㅁㅇ라;ㅣㅓㅁㄴㅇ라;ㅓㄴㅇㅁ라ㅓ;ㄴㅁㅇㄹ;ㅏㅓㅣㄴㅇㄻ;ㅏㅓㅣㄴㅁㅇ라;ㅣㅓㅁㄴㅇ라ㅓ;ㅣㅁㄴㄹ어ㅏㄴㅇㅁ라ㅓ;ㅁㄴㅇㄹ;ㅓㅏㅣㄴㅁㄹ아ㅓ;ㅁㄹㄴ아;ㅓㅣㅁㄴㅇ러ㅏㄴㅁㅇ라ㅓ;ㅣㅁㄴㅇㄹ;ㅓㅏㅣㄴㄹㅇㅁ;ㅏㅣㅓㄴㅇ러마ㅣㅓㅁㄴㅇ라ㅣㅓㅁ;ㅏㄴㄹㅇ;ㅓㅏㅣㄴㅇㄹㅇㄴ롬ㄹㄴ옴ㄴㄹㅇㅁ놈ㄴㅇㄻㄴㄹ이ㅗㅁㄹ뇌미로민롬ㄹㄴ오롬ㄴ옴ㄹㄴ어ㅏㄹㄴㅇ몲님ㄴㄹ엄ㄴ옮ㄹ나ㅣㅣㄻㄴ엄ㄴㅇ리아아앙러ㅏㄹ어ㅏㄹㄴ어ㅏㄴㅇ러ㅏㅣㄴㅇ라ㅓㅣㄴㅇ라ㅓㅣㅇㄹ나ㅓㅇㄴ라ㅓㅁㄴ;ㅁ어ㅏㅁㄹㄴ어ㅏ;ㅁㄹㅇ너ㅏㅁㄹㅇ너ㅏ;ㅓㅏㄻㄹㄹ나ㅓㅁ아ㅓㅏㄹㅇㄴ머ㅏㄻ;나ㅣ어ㅏ러라;ㅁㄴ어ㅏㅓㅏ;ㅁㄴ러ㅏㅓ;ㅏㅣㄴㄹㅇ머ㅏ;ㅣ;ㅓㅏㅣㅁㄹㄴ어ㅏㅓ;만ㅇ리ㅓㅏㅣㅁㄴ이ㅓㅏㅁ나ㅣ;ㅏㅓㅁㅇ나ㅓㄹ;ㅏㅓㅣㅏㅓㅣㅓㅏ;;러안;ㅏㅁㄴ어ㅏ러ㅏㅁ;닝ㄹ;ㅓㅏㅓㅏㅁ;ㄴㅇ리ㅓ;ㅏㄹㄴㅇ머;ㅏㅓㅁㄴㅇ라ㅏ;ㅁㄴ;ㅇ러ㅏㅓㅏㅇㄴㅁ러;ㅏㅁㄹ너ㅏ;ㅓㅏ러ㅏㅁㄹㅇ너ㅏㅣㅁ러ㅏㅓㅏㄹ;ㅇ;ㅓㅏㅁㄹ어ㅏ;ㅁㄹㄴㅇ;ㅓㅏㅣㅓㅁ라;이;ㅓㅏㅁㄹㅇ너ㅏ;ㄹ엄나;ㅓㅏ;너ㅏ;ㄴㅇㅁ러ㅏ;ㅁㄴㄹ어ㅏㅁㄴㅇ라;ㅣㅓㅁㄹㄴ어ㅏ;ㅣㅁㅇㄴ러ㅏㅓ마;ㄴㄹ이ㅏㅓ;나ㅓ;ㅁㄹㅇ나ㅓㄹㅇㄴ머ㅏㄹ멍ㄴ;ㅏ;ㅓ아;ㅓ;ㅓㅏㅁㄴㅇ러;ㅏㅁ;ㅓㅏ러;ㅏㅁ어나러ㅏ;ㅁㄴ;ㅓㅏㄴㅁㅇㄹ;ㅓㅏㅣㅏㅓㅣㄴ;ㅓㅏㄹ;ㅓㅏㅁ날어;ㅏㅓㅁㄹ너ㅏㄴ어ㅏㅓㅁㄴㅇ라ㅓ마ㅓ나ㅓㅏㅓㅣㅁ너ㅏㄹㅇ;ㅓㅏ머ㅏ;어ㅏ;ㅁㄴ어라;ㅓㅏㅁㄹ어ㅏㅣㅁ너ㅏ이ㅓㅏㅓㅏ;ㄹㅇㄴ머ㅏ나ㅓㅣ;ㅓㅏㅣㄴ;ㅓㅏㅣㄹ어;ㅏㅣㄹㄴ안ㅁㅇ러ㅏㅣ;ㅏㅓㅁ;리너;ㅏ어ㅏ;ㅁㄴ어;ㅏㄹ;ㅏㅓㅣㅇㅁㄴㄹ;ㅓㅏ;만;ㄹ아ㅏㅣㅇ나ㅓㅁㄹ;ㅓㅁ나;ㄹ어ㅏㅁㄹㄴ어ㅏㅣ;ㅓㅏㄻ너ㅏ;ㅣ머ㅏ;ㅣㅓㅏ;;ㅓㅏㅁㄴㅇㄹ;ㅓㅏ;란ㅇㅁ;ㅓㅏㅣㄹㄴ어ㅏ;ㅣㅇㄻㄴ;ㅓㅏ;ㅓㅏㅁㄴㅇㄹ;ㅓㅏㅁㄴㄹ어ㅏ



흐으흐으그

'운영체제 > 윈도우 시스템' 카테고리의 다른 글

프로세스와 쓰레드, 윈도우 쓰레드  (0) 2019.10.08
컴구세번째 함수호출  (0) 2019.10.08
Signaled Non-Signaled  (0) 2019.09.27
프로세스간 통신  (0) 2019.09.26
커널 오브젝트, 핸들의 종속관계  (0) 2019.09.26

+ Recent posts