- 기억장치(DRAM): 주소를 가지는 워드(또는 바이트)들로 구성
- 프로그램이 실행되기 위해서는 기억장치에 적재되어야 한다(적재된 프르그램 → 프로세스)
- 프로세스의 일반적인 실행과정
- Program Counter register에서 가리키고 있는 주소에서 명령어를 CPU로 가져옴(fetch)
- 명령어를 해독(decode)
- 기억장치에서 피연산자를 CPU로 가져와서 피연산자에 대해 명령어를 실행(execute)
- 실행한 결과를 기억장치에 다시 저장(store)
Process 생성 과정
- 프로세스 구성요소
- 코드(code)
- 데이터(data): 전역 변수들을 저장
- 스택(stack): 지역 변수 및 함수의 매개변수 저장
- 힙(heap): 동적 메모리 할당 영역
- CPU 레지스터
- PC(Program Counter): 프로그램의 다음에 실행할 명령어의 주소 값을 가짐
- CS(Code Segment: Code 영역 시작 주소를 가리킴)
- DS(Data Segement: Data 영역의 시작 주소를 가리킴)
- SS(Stack Segment: Stack 영역의 시작 주소를 가리킴)
문제 상황
논리 주소를 기준으로 line 2에 있는 id = number + tmp 명령어를 실행한다고 가정해보자. 그럼 CPU는 line 2의 명령어를 수행하기 위해서 논리 주소를 기준으로 104 번지에 있는 id 값에 100번지에 있는 number 값에다가 tmp 값을 더해서 저장하라는 명령어를 수행하기 위해 실제 100 번지로 가면 100번지는 실제 프로세스 영역이 아니다. 이렇게 논리 주소랑 물리 주소의 주소 값이 서로 매칭이 안 되는 문제를 어떻게 해결할까?
주소 바인딩(Address Binding)
- 프로그램의 명령어와 데이터를 메모리에 적재할 때, 그것들의 메모리 주소를 결정
- 프로그램에서 사용하는 심볼(전역변수, 함수)은 메모리의 어떤 주소가 있어야, 실행될 때 그 주소를 사용하여 접근(읽기/쓰기)할 수 있음
- e.g.
- 컴파일러는 심볼의 주소를 재배치 가능 주소(논리 주소, Relocatable address)로 바인딩한다(예를 들면, 실행파일의 처음부터 14바이트 위치)
- 실행파일을 실행하면, 운영체제는 재배치 가능 주소를 절대 주소(실제 주소, absolute address)로 바인딩한다(예를 들면, 메모리의 24014 address)
주소 바인딩 종류
- 컴파일 시간(compile time) 바인딩
- 프로그램 내부에서 사용하는 주소와 물리 주소가 같다
- 컴파일 시간에 절대 코드(absolute code)를 생성
- 프로그램을 메모리 내에 적재할 위치를 컴파일 시간에 결정
- 말 그대로 컴파일 할 때 물리적 메모리 주소가 결정되는 바인딩이다. 프로그램의 물리적 주소를 변경하려면 다시 컴파일해야 하므로 잘 사용하지 않는다. 하지만 아두이노 같이 OS가 없는 엠베디드 시스템 환경에서는 컴파일 타임 바인딩이 사용된다. 왜냐하면 아두이노에는 내가 만든 프로그램 말고 다름 프로그램이 사용될 일이 없기에...
- 적재 시간(load time) 바인딩 (요즘에 쓰는 방식)
- 컴파일 시간에 재배치 코드(relocatable code)를 생성
- 코드 시작 주소를 0번지부터 시작.
- 적재 시간에 적재할 위치를 결정
- 로더가 메모리 주소를 부여하고 프로세스가 종료될 때까지 물리 주소가 고정된다.
- 위 그림에서는 data는 0번지부터 98000번 위치에 있으므로, 이 프로그램이 메모리에 로딩될 때, 예를 들어 10만 번지에 로딩이 되면 data 주소는 10만에다가 98000을 더하면 되므로 198000이 된다. 이처럼 로드 타임 바인딩은 프로그램 안에서 사용되는 메모리 주소를 이 프로그램 전체를 로딩할 때 이 프로그램이 메모리 어느 위치에 로딩되는지에 따라서 주소를 바꾼다.
- 문제점: 로드 타임 바인딩을 하려면 프로그램 안의 code segment 명령어들이 엄청나게 많은데 여기에 메모리 참조하는 명령어도 더하면 오버헤드가 너무 심할 것이다. 이는 결국 메모리 로딩 타임이 현저히 증가하므로 로드 타임 바인딩은 실제로는 쓰이지 않는다.
- 실행 시간(execution time) 바인딩
- 프로세스가 실행한 후에도 물리 주소가 변경될 수 있는 바인딩 방식이다. 앞서 말한 로드 타임 바인딩은 로딩 타임이 너무 오래 걸린다는 단점이 있었는데, 런 타임 바인딩은 로딩 타임에 해주던 작업을 런 타임에 해준다. CPU가 주소를 참조할 때마다 해당 데이터가 물리적 메모리의 어느 위치에 존재하는지 주소 매핑 테이블을 이용해 주소 바인딩 점검한다. 그러기 위해서 주소 매핑 테이블, 기준 레지스터, 한계 레지스터, MMU(Memory Management Unit)가 필요하다.
- 기준 (relocation register): 프로세스의 물리적 메모리의 시작 주소를 가지고 있다
- 한계 레지스터: 현재 CU에서 수행 중인 프로세스의 논리적 주소의 최대 값 및 프로세스의 크기를 가지고 있다.
- MMU(Memory Management Unit): 논리적 주소를 물리적 주소로 매핑해주는 하드웨어,프로세스가 생성하는 논리 주소에 relocation register의 값을 더해서 물리 주소를 구한다. OS는 어떤 프로세스에게 CPU를 할당하여 실행시킬 때, 그 프로세스의 시작 위치를 MMU의 relocation register에 set. 이후, 프로세스가 CPU에 의해 실행될 때 생성되는 논리 주소는 MMU에 의해 물리 주소로 변환됨.
- 로드 타임 바인딩 vs 런 타임 바인딩: 로드 타임 바인딩은 주소 변환을 로딩할 때 전부 다 해놓는 대신, 런 타임 바인딩은 프로그램을 실행해서 그 코드가 변활될 소요가 있을 때마다 주소 변환을 해준다. 어떻게 보면 런 타임 바인딩이 퍼포먼스가 더 나빠 보일 수 있는데 이제는 하드웨어 성능이 너무 좋아져서 하드웨어상 로직이 있기 때문에 런 타임 바인딩의 퍼포먼스는 걱정할 필요가 없다.
MMU 동작 방식
프로세스는 고유한 주소 공간을 가지고 있다. 그리고 논리적 주소 값은 프로세스마다 독립적으로 할당된다. 프로세스 A에도 100번 논리 주소가, 프로세스 B에도 100번 논리 주소가 있을 수 있다 그렇지만 프로세스 A의 100번 논리 주소에 매핑되는 실제 물리 주소와 프로세스 B에 매핑 되는 실제 물리 주소는 다를 것이다. 따라서 MMU 기법에서는 문맥 교환이 일어날 때마다 재배치 레지스터의 값을 바뀌는 프로세스에 해당되는 값으로 재설정 해주어야 한다.한계 레지스터가 필요한 이유 - MMU 방식에서는 기준 레지스터 값 + 논리적 주소 값을 통해서 주소 바인딩을 한다. 만약 해당 값이 해당 프로세스의 주소 범위를 넘어가는 값이 된다면 어떻게 할까? 프로세스가 접근해서는 안 되는 영역(예컨대 OS 영역)을 접근할 수도 있다. 이런 상황을 방지하기 위해 한계 레지스터를 사용한다. 한계 레지스터에 최대 논리적 주소 값을 저장하고 CPU가 논리적 주소를 요청할 때마다 한계 레지스터 값보다 작은 값인지 검사한다.
- 생각
- 재배치 레지스터에는 현재 CPU에서 수행중인 프로세스의 물리적 메모리 시작 주소가 저장되어 있다. CPU가 논리적 주소 245에 있는 데이터를 요청하게 되면 재배치 레지스터에 저장된 물리적 시작 주소와 해당 논리적 주소를 더한다. 그렇게해서 실제 메모리의 8245에 있는 데이터를 꺼내오면 된다.
- OS는 프로세스를 메모리 내의 가용 공간에 로드
- 프로세스들이 로드되고, 제거되면서 가용 공간이 곳곳에 생김
- OS는 메모리의 사용 상태를 관리: 어떤 프로세스가 어느 공간을 사용하고 있고, 어느 공간은 가용 상태인지 관리해야 함.
- 프로세스를 로드할 때 가용 공간 중에서 어느 가용 공간에 로드할 것인가?
- 최초 적합(first-fit): 메모리가 로드 되기에 충분한 공간 중에서 첫 번째 가용 공간에 할당
- 최적 적합(best-fit): 메모리가 로드 되기에 충분한 공간 중에서 가장 작은 공간에 할당. 즉 효율성 추구
- 최악 적합(worst-fit): 메모리가 로드 되기에 충분한 공간 중에서 가장 큰 가용 공간에 할당. 가장 큰 hole(빈 공간) 발생.
단편화(Fragmentation) 문제- 외부 단편화(external fragmeatation): 연속 할당기 법에서 발생
- 프로세스들이 로드되고 제거되면서, 가용 메모리 공간이 작은 조각들로 나누어진다.
- 어떤 프로세스를 위해서 가용 메모리 공간의 총량은 충분한데, 공간이 연속적이지 않기 때문에 할당 불가 문제 발생(연속 할당 기법은 프로세스를 쪼개지 않고 메모리에 로드시키므로)
- 내부 단편화(internal fragmeatation)
- 할당한 기억 공간에서 사용되지 않는 공간이 남을 때 발생하는 단편화
- 압축(compaction): 외부 단편화 솔루션
- 프로세스들을 이동시켜서 모든 가용 공간을 하나의 가용 공간으로 만듦
- 프로세스를 런 타임에 주소를 옮겨야 하므로 주소 바인딩 방법이 런 타임 바인딩이어야 가능
- 프로세스를 페이지(page)라는 단위로 쪼개서 메모리에 로드하는 기법
- 물리적 메모리는 프레임(frame)이라 부르는 블록으로 나누어 진다(프레임 크기는 보통 512B ~ 8 KB)
- 프로세스의 논리 주소 공간은 페이지(page)라 부르는 블록으로 나누어 진다(페이지 크기 = 프레임 크기)
- 외부 단편화 문제는 발생하지 않으나, 내부 단편화 문제가 발생할 수 있다
- OS는 각 프로세스마다 페이지 테이블(PT) 생성
- OS는 어떤 프로세스에게 CPU를 할당하여, 실행시킬 때, 그 프로세스의 페이지 테이블의 시작 위치를 MMU의 레지스터에 세팅한다. 이 후, 프로세스가 CPU에 실행될 때 생성되는 논리 주소는 MMU에 의해 물리 주소로 변환됨
- MMU는 레지스터에 세팅되어 있는 페이지 테이블 정보를 이용해서 (p, d)로 구성된 논리 주소를 물리 주소에 변환
- 프로세스가 실행한 후에도 물리 주소가 변경될 수 있는 바인딩 방식이다. 앞서 말한 로드 타임 바인딩은 로딩 타임이 너무 오래 걸린다는 단점이 있었는데, 런 타임 바인딩은 로딩 타임에 해주던 작업을 런 타임에 해준다. CPU가 주소를 참조할 때마다 해당 데이터가 물리적 메모리의 어느 위치에 존재하는지 주소 매핑 테이블을 이용해 주소 바인딩 점검한다. 그러기 위해서 주소 매핑 테이블, 기준 레지스터, 한계 레지스터, MMU(Memory Management Unit)가 필요하다.
- MMU의 레지스터에는 OS의 PCB에 연결되어 있는 Page Table의 주소 값을 넣어준다. 그리고 CPU 타임을 해당 프로세스에게 할당하면(CS regster, SS register, DS register, PC register 등에 해당 프로세스의 주소 정보 값이 할당) 해당 프로세스는 컴파일러가 논리 주소로 컴파일 해놓았기 때문에 논리 주소 위치로 찾아갈 것이다. CPU는 처음에는 논리 주소로 접근을 할 텐데, 이 논리 주소 값이 MMU에 입력이 되면 MMU는 해당 논리 주소를 물리 주소로 바꾼 다음, 그 변환된 물리 주소로 찾아가기 위해서 페이지 테이블을 참조(초기 MMU에 페이지 테이블의 주소 값을 할당 했기 때문에 페이지 테이블의 주소 값을 알고 있다)한 다음 페이지 번호를 프레임 번호로 바꿔서 실제 주소 값에 접근한다.
- MMU와 페이지 테이블
- 페이징(Paging): 대부분의 OS들이 페이징 기법으로 메모리 할당
- 연속 할당 기법: 프로세스를 나누지 않고(페이징, 세그먼트 같은 방법은 프로세스를 쪼개서 메모리에 적재한다) 메모리에 적재하는 방법( 컴퓨터 초창기에 사용)
- OS는 프로세스가 메모리에 로드되어 실행될 수 있도록 프로세스에게 메모리를 할당해야 하는데 그 할당 기법은 크게 4가지 -연속 할당 기법, 페이징(paging), 세그먼트(segmentation), 페이지화된 세그먼테이션(paged segmeatation) 가 있다.