기본적으로 멀티 프로그래밍(멀티 태스킹)을 지원하는 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

 


반응형

+ Recent posts