메모리 = 현대 컴퓨터 시스템 운영의 중심. 주소가 할당된 일련의 바이트로 구성
CPU는 프로그램 카운터(pc)가 지시하는 대로 메모리로부터 데이터를 가져오거나, 내보낸다.
명령어 실행은 이러한 CPU의 동작 원리를 이용한다.
- 메모리로부터 한 명령어를 가지고 온다.
- 명령어를 해독하고 메모리에서 피연산자를 가지고 온다.
- 피연산자에 대해 명령어를 실행한다.
- 계산 결과를 메모리에 다시 저장한다.
이 장에서는 이러한 메모리를 관리하기 위해 필요한 것들을 소개한다.
기본 하드웨어
cpu와 메모리가 제대로 돌아가기 위해서는 여러가지 부수적인 하드웨어가 필요하다.
1. 빠른 접근 → 캐시 메모리
CPU가 직접 접근할 수 있는 저장장치 = 메인 메모리 & 레지스터
- 기계 명령어들은 메모리 주소만을 인수로 취하기 때문.
- 데이터가 메모리에 없으면, 이를 메모리로 이동시켜야 함.
레지스터와 메모리는 몇 가지 차이가 있다.
- 레지스터 = CPU가 좀 더 빠르게 접근할 수 있다.
- CPU 클록의 1사이클 내로 접근 가능.
- 일부 코어는 클록 틱 당 1개 또는 그 이상의 속도로 레지스터 속 명령어를 처리 가능
- 메모리 = CPU의 접근 속도가 느리다.
- 많은 CPU 클록이 필요.
- CPU가 필요한 데이터가 없어서 명령어를 수행하지 못하는 지연현상 발생 가능
- 특히 메인 메모리에 접근을 많이 하는 경우에 더 위험.
그래서 CPU와 메인 메모리 사이에 빠른 속도의 메모리를 추가한다.
이를 캐시 메모리(cache memory)라고 하며, 하드웨어는 이를 통해 메모리 접근 속도를 향상시킨다.
2. 올바른 작동 → 기준/상한 레지스터
운영체제가 CPU와 메모리 간의 접근 중 개입한다면 성능이 떨어짐.
또, 시스템의 올바른 동작을 위해서 APP과 OS 사이를 분리해야 함.
이를 위해 각 프로세스가 독립된 공간을 가짐. 독립된 공간을 설정하기 위해 각 프로세스 만의 합법적인 메모리 주소 영역을 설정하고, 해당 프로세스가 여기만 접근할 수 있도록 설정.
- 독립된 공간의 위치를 설정해주는 것 = 기준 레지스터
- 독립된 공간의 크기를 설정해주는 것 = 상한 레지스터
- 사용자 모드에서 만들어진 주소와 레지스터를 비교하면서 메모리 공간을 보호.
두 레지스터의 등록은 여러가지 특권 명령을 사용하는 운영체제에 의해 적재.
- 이러한 특권 명령은 운영체제에 의해 커널 모드에서만 수행됨.
- 사용자 프로그램이 레지스터의 내용을 변경하는 것을 막음.
따라서, 운영체제는 OS 메모리 영역 & App 메모리 영역에 마음대로 접근할 수 있음.
- 이를 이용해 App을 적재하고, 오류가 생기면 내보내는 등 다양한 일을 할 수 있게 됨.
- ex) 멀티코어 OS : 한 프로세스의 상태를 레지스터로부터 메인 메모리로 저장하고, 다음 프로세스의 문맥을 메인 메모리로부터 레지스터로 저장하는 문맥교환을 실행.
주소의 할당
프로그램이 실행되는 과정에서 어떻게 주소를 할당받는지를 보자.
그 전에, 바인딩이라는 용어에 대해 미리 알아보자.
💡 바인딩 = 컴퓨터 프로그래밍에서 각종 값들이 확정되어 더 이상 변경할 수 없는 구속(bind) 상태가 되는 것.
주소를 할당하는 것 또한 어떠한 값을 확정짓는 것이므로, 바인딩이라고 할 수 있다.
프로그램 실행 과정
- 실행 전 : 바이너리 파일(=이진수로 된 파일)로 보조 저장장치에 있다.
- 메모리로 가지고 온 후 프로세스 문맥 내로 배치한다.
- 실행 : 메모리에서 명령 및 데이터에 접근
- 실행 후 : 메모리가 회수됨.
이를 다시 한 번 주소의 관점에서 보자.
- 처음 프로그램의 주소 = 심볼 주소
- 심볼 주소 → 재배치 가능한 주소로 바인딩 (by 컴파일러)
- 재배치 가능한 주소 → 절대 주소로 바인당 (by 링커 & 로더)
바인딩의 종류
전통적으로 메모리 주소 공간에서 명령어와 데이터의 바인딩은 바인딩이 이뤄지는 시점에 따라 다음과 같이 구분된다.
- 컴파일 시간 바인딩 : 컴파일을 하는 시점에 해당 프로그램이 물리적 메모리의 몇 번지에 위치할 것인지를 결정한다. 절대 코드를 생성하는 바인딩이라고도 부른다. 물리적인 메모리 위치를 변경하려면 컴파일을 다시해야한다.
- 적재 시간 바인딩 : 프로그램의 실행이 시작될 때에 물리적 메모리 주소를 결정한다. 적재 타임 바인딩 방식에서는 로더(loader)의 책임 하에 물리적 메모리 주소가 부여되며 프로그램이 종료될 때까지 물리적 메모리 상의 위치가 고정된다.
- 실행 시간 바인딩 : 프로그램이 실행을 시작한 후에도 그 프로그램이 위치한 물리적 메모리 상의 주소가 변경될 수 있는 바인딩 방식이다. CPU가 메모리 주소를 참조하기 위해 주소 매핑 테이블을 이용해 바인딩을 점검한다.
컴퓨터 시스템에서 이러한 바인딩들을 어떻게 효과적으로 구현하고, 이를 위한 하드웨어 지원에 대해서 알아보는 것이 9장의 핵심 요점!
주소공간 : 논리 vs 물리
💡 논리 주소 : CPU가 생성하는 주소 물리 주소 : 메모리가 취급하는 주소 (= 메모리 주소 레지스터, RAM에 주어지는 주소)
- 컴파일 시간 바인딩 : 논리 주소와 물리 주소가 같다.
- 적재 시간 바인딩 : 논리 주소와 물리 주소가 같다.
- 실행 시간 바인딩 : 논리 주소와 물리 주소가 다르다.
그래서, 프로그램 실행 중에는 논리 주소를 물리 주소로 번역해주는 장치가 필요하다.
MMU : 논리주소 → 물리주소
이러한 번역기를 메모리 관리 장치, MMU라고 한다.
MMU의 동작 원리는 여러가지가 있지만, 가장 간단한 방법은 논리 주소에 특정 값을 더해 물리 주소로 만드는 것이다.
이때, 사용자 프로그램은 실제적인 메모리 주소를 절대 건드리지 않는다.
즉, 논리 주소는 사용되기 전 반드시 물리 주소로 바꿔져야 한다.
동적 적재
아까 프로그램이 실행되는 과정에서, 프로세스가 실행되기 위해서는 프로세스 전체가 미리 메모리에 올라와야 한다고 했다.
그런데, 프로세스의 크기가 메모리의 크기보다 크다면?
이러한 문제를 해결하기 위해 동적 적재를 해야 한다.
동적 적재 : 과정
💡 루틴? 함수라고 생각하면 될듯
-
각 루틴은 메모리에 올라오지 않고 재배치 가능한 상태로 보조 장치에서 대기
-
main 프로그램이 실행.
-
이 루틴이 다른 루틴을 호출.
-
이미 있는 루틴인지 검사
없으면 재배치 가능 연결 적재기가 불러서 메모리에 올림.
이 변화를 메모리 테이블에 기록.
동적 적재 : 특징
- 루틴이 필요한 경우에만 적재.
- 사용자가 직접 동적 적재를 할지 선택해야 함.
동적 연결
동적 적재 : 프로그램 실행 시, 적재(loading)을 실행할 때까지 미룬다.
동적 연결 : 프로그램 실행 시, 연결(linking)을 실행할 때까지 미룬다.
이를 도와주는 라이브러리를 동적 연결 라이브러리(DLL), 또는 공유 라이브러리라고 한다.
- 주로 표준 C언어 라이브러리와 같은 시스템 라이브러리에 사용
- 실행 가능 이미지의 크기를 줄이고, 메인 메모리 낭비를 막음
- 라이브러리를 여러 프로세스 간에 공유할 수 있어 메인 메모리에 DLL 인스턴스가 하나만 있어도 됨.
프로그램이 동적 라이브러리에 있는 루틴은 참조하려면 로더는 DLL을 찾아 필요한 경우 메모리에 적재. 그 뒤 동적 라이브러리의 함수를 참조하는 주소를 DLL이 저장된 메모리의 위치로 조정.
DLL의 특징
- 라이브러리 갱신으로 확장 가능
- 추가만 하면 DLL을 쓰는 모든 프로그램이 알아서 새 버전으로 갱신
- 일반적으로 운영체제의 도움을 받아 실행
- 운영체제만이 기억 공간에 루틴이 있는지를 검사
- 운영체제만이 여러 프로세스가 같은 메모리 주소를 공용할 수 있도록 해줌.