프로그래밍 2013. 8. 4. 22:21


1. 프로세스와 가상 메모리

모든 프로세스는 자신만의 가상 주소 공간을 가지고 있다.
32비트/64비트 프로세스는 각 비트수에 맞게 최대 4GB/16EB의 주소 공간을 가진다.

모든 프로세스들은 자신만의 주소 공간을 가지기 때문에,
특정 프로세스 내에서 쓰레드가 수행될 때 해당 쓰레드는 프로세스가 소유하고 있는 메모리에 대해서만 접근이 가능하다.
다른 프로세스에 의해 소유된 메모리는 숨겨져 있으며, 접근이 불가능하다.

윈도우에서는 운영체제 자체가 소유하고 있는 메모리 또한 숨겨져 있다.
이는 특정 프로세스의 수행 중인 쓰레드가 운영체제의 데이터에 접근하는 것이 불가능함을 의미한다.

따라서, A 프로세스가 0x12345678 주소에 무엇인가를 저장하였지만,
B 프로세스 역시 0x12345678 주소에 무엇인가를 저장할 수 있으며, 이 주소들은 완전히 독립되어 있는 것이다.

즉, 정리하면...

가상 메모리는 프로세스의 logical memory와 physical memory를 분리하기 위해 생겨난 것이라 할 수 있다.

이를 이용하여, logical memory가 physical memory보다 커지는 것을 가능케 할 수 있다.

하나의 프로세스 logical memory가 physical memory보다 커지는 것도 가능하며,
여러 프로세스의 logical memory 총합이 physical memory보다 커지는 것도 가능한 것이다.

램이 꼴랑 1기가인 PC를 생각해 보라.
이 PC에서 포토샵도 띄우고, 일러트스레이터도 띄으고, 3D 맥스도 띄운다.
느리다. 하드가 졸라 버벅대지만, PC는 멈추지 않고 돌아는 가게 된다.

이처럼, 프로세스가 실제 필요로 하는 부분만 메모리로 올리는 Demand-Paging 기법을 사용한다.
(이에 대해서는 아래에서 자세히 설명하겠다)

그렇다면, 포토샵도 띄우고, 3D 맥스도 띄웠는데 이 툴들의 작업 내용은 어디에 저장될까?
이미 메모리는 소진되고 없는데 말이다.

이를 기억해두기 위해 최근의 운영체제들은 디스크 공간을 메모리처럼 활용할 수 있는 기능을 가지고 있다.
디스크 상에 존재하는 이러한 파일을 paging file (또는 swap file)이라고 하며, 
모든 프로세스가 사용할 수 있는 가상 메모리로 사용된다.

용량 졸라 큰 게임을 실행하면 하드를 미친듯이 읽어댄다.
우리는 이것을 "하드 스왑 쩌네~"라고 표현하고...

여기서 말하는 하드 스왑이라는 페이징 파일에서 실제 물리 메모리로 올리고 내리고 하는 일련의 작업을 이야기하는 것이다.
(정식 명칭은 Disk thrashing이라고 하는데...이 역시 후반에 자세하게 설명하겠다)

애플리케이션 관점에서 보면, 페이징 파일을 사용하면 애플리케이션이 사용할 수 있는 램의 크기가 증가한 것과 같은 효과를 가져온다.
만일 PC에 4GB의 램이 있고, 디스크 상에 4GB의 페이징 파일이 있디면, 수행 중인 애플리케이션은 PC에 8GB의 램이 있는 것과 동일한 효과를 누릴 수 있는 것이다 (물론, 8GB 램보다는 4GB 램+ 4GB 페이징 파일이 더 느리겠지만)

자!
내용이 너무 길었다.
한번 더 정리하자.

한정된 물리 메모리의 한계를 극복하고자, 디스크와 같은 느린 저장장치를 활용해,
애플리케이션들이 더 많은 메모리를 활용할 수 있게 해 주는 것이 가상 메모리이다.


2. 프로세스 가상 주소 공간의 분할

각 프로세스의 가상 주소 공간은 분할되어 있으며, 각각의 분할 공간을 파티션(partition)이라고 한다.

주소 공간의 분할 방식은 운영체제의 구현 방식에 따라 서로 다를 수 있으며,
윈도우 계열에서도 커널이 달라지면, 그 구조가 조금씩 달라지곤 한다.

처음 32비트 프로세스는 4GB의 주소 공간을 가질 수 있다고 했다.
하지만, 사용자가 이 4GB를 모두 사용할 수 있는 것은 아니고, 약 2GB밖에 사용하지 못한다.
32비트 주소 공간이 아래와 같이 분할되어 있기 때문이다.
  • Null 포인터 할당 파티션 : 0x00000000 ~ 0x0000FFFF
  • 유저 모드 파티션 0x00010000 ~ 0x7FFEFFFF
  • 64KB 접근 금지 파티션 : 0x7FFF0000 ~ 0x7FFFFFF
  • 커널 모드 파티션 : 0x80000000 ~ 0xFFFFFFFF
1) Null 포인터 할당 파티션

프로그래머가 NULL 포인터 할당 연산을 수행할 경우를 대비하기 위해 준비된 영역이다.
만일 프로세스의 특정 쓰레드가 이 파티션에 대해 읽거나 쓰기를 시도하게 되면 접근 위반(access violation)이 발생한다.

2) 유저모드 파티션

프로세스의 주소 공간 내에서 유일하게 자유롭게 활용될 수 있는 파티션이다.
0x00010000 ~ 0x7FFEFFFF의 범위이므로, 2047MB의 크기이다.

모든 애플리케이션에 대해 프로세스가 유지해야 되는 대부분의 데이터가 저장되는 영역이며,
.exe / .dll 파일이 이 파티션에 로드된다.


3. Page

Page란, 가상 메모리를 사용하는 최소 크기 단위이다.
최근의 윈도우 운영체제는 모두 4096 (4KB)의 페이지 크기를 사용한다.

만약, 페이징 파일에서 물리 메모리로 데이터를 로드할 때, 
아무 위치에나 필요한 크기 만큼(무작위) 로드한다고 가정을 해 보자.

이런 경우, 로드하고 언로드하는 데이터의 크기가 모두 제각각이므로,
이를 반복하다 보면 메모리 공간에 fragmentation이 발생하게 된다.

결국 메모리는 남아 있지만, 정작 원하는 크기의 데이터를 물리 메모리로 로드하지 못하게 되는 상황이 생길 수 있는 것이다.

이를 막기 위해, 운영체제가 만든 것이 page라는 최소 크기 단위인 것이다.


4. Demanding-Page

Demanding-page는 실제로 필요한 page만 물리 메모리로 가져오는 방식을 이야기한다.

필요 page에 접근하려 하면, 결국 가상 메모리 주소에 대응하는 물리 메모리 주소를 찾아내어,
물리 메모리 주소를 얻어와 하는데, 이 때 필요한 것이 페이지 테이블(page table)이다.

페이지 테이블에 valid bit 라는 것을 두고, 해당 page가 물리 메모리에 있으면 set, 그렇지 않으면 invalid로 설정한다.

Page 접근 요청을 하였는데, physical memory에 없는 상태, 

즉 valid bit가 clear 되어있는 상황을 Page fault 라 하며 아래와 같은 처리 과정을 거친다.


1) 페이징 하드웨어는 page table entry를 보고 invalid인 것을 확인한 후 OS에게 trap으로 알린다.

2) OS는 정말로 메모리에 없는 것인지 아니면 잘못된 접근인지 확인한 후 잘못된 접근이었으면 종료시킨다.
3) Empty frame (free page)을 얻는다.
4) Page를 frame으로 swap한다.
5) 프로세스의 page table과 TLB를 page-in/page-out 된 것을 토대로 재설정한다.
6) Page fault를 야기했던 인스트럭션부터 다시 수행한다.


3번 과정에서 empty frame을 얻어와야 하는 상황에서 물리 메모리가 이미 모두 사용중이라면,
그 사용중인 frame 중 하나를 선택해서 page-out (페이징 파일로 이동) 시키고, 그 frame을 사용해야 한다.

이와 같이 victim frame을 선택하는 과정에서 Page Replacement Algorithm을 사용한다.


5. Page replacement alogorithm

페이지 교체 알고리즘은 꽤 다양한 종류가 있으며, 그 종류와 특징은 아래와 같다.

1. FIFO

가장 먼저 page-in 되어, 오래된 page를 page-out 시키는 방식.
단순하고 깔금하나, memory locality 측면에서 영 꽝이다.

2. LRU (Least Recent Used)

가장 오랫동안 사용되지 않았던 녀석은 앞으로도 사용되지 않을 것이라는 기대로 page-out 시키는 방식.
LRU는 두 가지 방식으로 보통 구현하는데,
  • 카운트 방식 : 4. Counting에서 이 방식을 쓴다.
  • 스택 방식 : 그 많은 페이지를 관리하기 위해, doubly-linked-list를 차용한 스택을 쓰기엔 메모리, 연산량이 너무 아깝다.
3. LRU approximation (이 방식이 굳)

LRU 개념이긴 하지만, 하드웨어의 도움을 받아 reference bit을 사용한다.
(레퍼런스 카운트 기법과 유사하다)

페이지들을 circular queue 형태로 나타내고 만약 reference가 일어나면 이 bit 를 set 한다. 
그리고 victim 페이지를 선정할 때는 하나씩 접근하여 reference bit 를 확인한다.
만약 bit 가 clear하면 그 페이지를 victim으로 선정하며, set 되어 있으면 그 bit 를 clear시키고 다음 페이지를 확인한다. 
해당 페이지에 Second-chance를 주는 것이므로 Second-chance algorithm이라고도 한다.

이 방식이 가장 많이 사용되고 효율이 좋은 것으로 알려져 있다.

4. Counting

Page가 참조될 때마다 카운팅을 하는 방식
  • MFU : Most frequent used
  • LFU : Least frequent used

6. Thrashing

가상 메모리를 사용하다 보면 실제 물리 메모리 공간보다 논리적 메모리 공간이 큰 것 처럼 사용될 수 있기 때문에 효율적이다. 

그런데, 효율적이라는 이유로 멀티 프로세싱의 degree를 계속 늘리다 보면, 
실제 실행하는 시간보다 page replacement를 하는 시간이 더 많아지는 순간이 오며, 결국엔 CPU 사용율이 떨어지게 된다. 
(하드가 미친듯이 돌아간다. 하드 스와핑이라고도 불리우는...)

이와 같이 프로그램을 제대로 수행하지 못하고, 실행 시간보다 페이지 교체 시간이 많아지는 현상을 Thrashing이라 한다.

이 thrashing이 발생하는 이유는 locality의 크기의 합이 실제 메모리의 크기보다 커졌기 때문이다.

따라서, 이 thrashing을 해결하기 위해서, Working set model 이란 것을 적용한다.
Working set model 은 locality 를 approximate한 것으로 페이지 넘버로 관리한다.
그러다 working set의 크기가 실제 메모리보다 커지면 하나의 프로세스를 종료하는 방식으로 thrashing이 생기지 않도록 할 수 있다.

하지만, 위 방법은 접근되는 페이지를 관리해야 하므로 불편한 감이 있다.
따라서, thrashing이 생겼다는 것은 즉, Page fault 가 많아졌다는 것이기 때문에
그것의 정도를 지정하고 Page fault 의 횟수가 어느 한계점을 넘어가면 프로세스를 종료하는 방식으로 구현한다.

무엇보다, Thrashing이 발생할 때 가장 좋은 솔루션은 램을 추가로 설치하는 것이다.


7. 가상 메모리의 부가 장점

가상 메모리를 사용하면서 생기는 부가 장점으로 다음과 같은 것들이 있다.
  • 공유 메모리 사용
  • Copy-on-write 메커니즘
  • Memory mapped file
위 장점들에 대해 하나씩 살펴보면...

1. 공유 메모리 사용

여러 프로세스 간의 communication의 한 가지 방법으로 공유 메모리를 사용할 수 있는데, 
demand-paging 기법을 사용할 경우 다른 프로세스의 각각의 페이지가 같은 프레임을 가리키도록 하면 공유 메모리를 사용할 수 있다.

윈도우의 dll이나 리눅스의 so 역시 이 방식으로 물리 메모리 프레임을 같이 가리키게 하여, 전체 메모리를 절약하게 되는 것이다.
아래 그림에서 프로세스 A의 1번 페이지, 프로세스 B의 7번 페이지가 물리 메모리 5번 프레임을 같이 가리키는 것을 볼 수 있다.


2. Copy-on-write 메커니즘

부모 프로세스를 clone하여 자식 프로세스를 생성하였을 때, 
처음에는 같은 메모리를 사용하도록 하다가 그곳에 Write가 발생하였을 때 메모리를 copy하는 것으로 
이것 또한 공유 메모리처럼 같은 프레임을 가리키도록 하였다가 복사가 되었을때 새로운 프레임을 할당하면 된다.


3. Memory mapped file

file을 접근하는 것을 메모리 접근하듯이 페이지를 할당하여 할 수 있도록 하는 것이며, 
메모리 접근 속도가 훨씬 더 빠르므로 효율적이라 할 수 있다.


'프로그래밍' 카테고리의 다른 글

git 명령어 및 이용  (0) 2014.03.23
Git 을 사용한 소스 버전관리  (0) 2014.03.17
IA32 레지스터에 관한 글-1  (0) 2013.08.04
이클립스 vim 플러그인  (0) 2012.12.08
이클립스 vim 플러그인  (0) 2012.12.01
//