메모리
컴퓨터 사이언스 부트캠프 with 파이썬을 보고 정리한 내용입니다.
메모리 계층 구조
- 빅 엔디언(big-endian)과 리틀 엔디언(little-endian): 왼쪽부터 / 오른쪽부터
- 컴퓨터에는 다양한 종류의 메모리가 있음
- CPU안에도 메모리가 있고(레지스터), RAM과 하드디스크, CPU와 메인 메모리 사이의 캐시라는 메모리가 있음
- 다양하게 있는 이유는 속도와 비용 때문
- 위로 올라갈수록 속도는 빨라지지만 용량은 작아지고, 아래로 내려올수록 속도는 느리지만 용량은 커짐
- 계층 구조의 특징은 전달되는 데이터가 아래쪽에 있을 경우 CPU에 도달하려면 위에 있는 모든 계층을 거쳐야 한다는 것
- 다 거치면 속도가 느려질 것 같지만 지역성이라는 개념으로 인해 오히려 성능이 향상됨
지역성과 캐시 히트
- 캐시가 없던 시절에는 CPU가 데이터를 요청하면 메모리에서 레지스터로 바로 가져왔음
- CPU가 레지스터에서 데이터를 가져올 때는 1사이클이 걸리는 반면, 메인 메모리에서 가져올 때는 20~100사이클이 걸리는데 CPU 처리 속도가 빨라지면서 메모리에서 가져오는 이 시간이 매우 부담스러워짐
- 따라서 캐시를 사용해서 CPU가 데이터를 요청했을 때 해당 데이터와 함께 인접 데이터로 이루어진 메모리 블록을 캐시로 가져옴 = 캐시 행(cache line)으로 64 ~ 128바이트 정도
- CPU가 다시 다른 데이터로 요청하면 메인 메모리가 아닌 캐시부터 확인함 → 있으면 이를 캐시 히트(cache hit)라고 하고 없으면 캐시 미스(cache miss)로 그냥 메인 메모리에서 가져와야 함
- 캐시에 있다면 3사이클이 걸리므로 메인메모리에서 가져오는 것보다 훨씬 빠르고 캐시 히트가 일어나면 날수록 성능이 좋아짐
- 캐시 히트가 많아지려면? 요청한 데이터들이 대부분 가까이에 있어야 함
- 지역성의 원리: 데이터 접근이 같은 메모리 공간이나 인접한 메모리 공간에서 자주 일어난다는 의미
- 공간적 지역성: 이번에 접근할 데이터는 이전에 접근했던 데이터 근처에 있을 확률이 높다
- 시간적 지역성: 특정 데이터에 한번 접근했을 때 곧 다시 그 데이터에 접근할 가능성이 높음
- 실제로 캐시에 필요한 데이터가 존재할 확률이 90% 이상이라고 함
- 지역성의 원리: 데이터 접근이 같은 메모리 공간이나 인접한 메모리 공간에서 자주 일어난다는 의미
가상 주소 공간
- 프로그램을 실행하면, 하드디스크에 있던 프로그램이 메인 메모리에 올라오면서 프로세스가 생성되고 (32비트 운영체제라면) 실행되는 순간 4GB 메모리를 할당받음 (실제 4GB는 아니지만 프로세서는 실제로 운영체제에게 4GB를 할당받은 것처럼 사용함)
- 4GB 중 2GB는 운영체제가 담당 = 커널 영역
- 나머지 2GB는 프로그램에 담당 = 유저 영역
- 유저 영역은 다시 4개의 세그먼트로 나뉨
- 코드 세그먼트
- 프로그램의 인스트럭션이 저장되는 공간
- 우리가 작성한 함수나 클래스 정의 코드는 인스트럭션으로 변환되어 하드디스크에 저장되어 있다가 프로그램 실행 시 코드 세그먼트에 올라감
- 함수를 호출한다면 프로그램 카운터가 함수의 인스트럭션이 있는 메모리 주소를 가리켜 함수를 실행함
- 데이터 세그먼트
- 전역 변수가 저장되는 공간
- 전역 변수 프로세스 실행 시 데이터 세그먼트에 올라가고 프로세스가 종료될 때 소멸 (즉 프로그램이 실행되는 동안 계속 있음)
- 따라서 프로세스 실행 전에 이미 그 크기를 알 수 있음 (이건 코드 세그먼트도 마찬가지)
- 힙 세그먼트
- 프로그래머가 자유롭게 메모리를 할당하고 해제할 수 있는 공간
- 스택 세그먼트와 다르게 여기에 할당을 하면 해제하지 않는 한 메모리 공간에 계속 남아 있음
- 여기 할당해놓고 해제하지 않아 메모리가 계속 남아 있는 걸 메모리 누수(memory leak)이라고 함
- 스택과 달리 늘어날 수 있는 최대 크기가 정해져 있지 않음
- 스택 세그먼트
- 지역 변수가 저장되는 공간
- 함수를 호출했을 때 그 스택 프레임이 스택 세그먼트에 생성된다는 의미
- 그림 상 화살표는 스택이 늘어날 수 있는 방향을 의미함. 함수가 호출되면 그 함수의 스택 프레임이 스택 세그먼트에 생기고, 함수 실행 도중 다른 함수를 호출하면 다시 호출된 함수의 스택 프레임이 호출한 함수의 스택 프레임 위에 쌓임 (=늘어난다)
- 그러나 최대로 늘어날 수 있는 최대 크기는 정해져 있으며 프로그래머가 직접 정하지 않으면 1MB가 할당되며, 최대 크기를 넘기게 되면 스택 오버플로(Stack over flow) 오류가 발생함
스택 프레임
int adder(int a, int b, int n) {
int c = a * n;
int d = b * n;
int e = c + d;
return e;
}
int main(void) {
int a = 10;
int b = 20;
int n = 2;
int res = adder(a, b, n);
return 0;
}
스택 프레임 할당
- 함수가 호출되어 스택 프레임에 할당될 때 지역 변수가 쌓이는 순서
- 인자 오른쪽에서 왼쪽 순서로, 함수 내부에서는 위쪽에서 아래쪽으로 차례대로 쌓임
- esp(extended stack pointer): 스택 세그먼트의 맨 위를 가리키는 스택 포인터 레지스터
- mov A, B: B에서 A로 복사
- push A: A의 데이터를 스택에 쌓는 명령어
- eax는 범용 레지스터
- 즉 main 스택 프레임의 n 값을 범용 레지스터 eax로 복사한 다음 다시 스택에 쌓음 (main 스택 프레임에서 adder 스택 프레임으로 바로 복사하는 것이 아니라 레지스터를 거침)
- a와 b도 n처럼 순서대로 쌓임
- call func(함수 인스트럭션 주소 값): 함수 호출이 끝나고 돌아올 주소 값을 스택에 쌓고, func 함수의 인스트럭션이 있는 곳으로 점프
- 이때 스택 프레임에는 레지스터 중 프로그램 카운터(PC) 값이 쌓임 = 다음에 실행할 인스트럭션의 주소 값 저장 (함수 호출이 끝났을 때 다시 돌아와서 실행해야 함)
- ebp(extended base pointer): 프레임 포인터라는 레지스터로, 스택 프레임의 기준이 됨
- 이 경우 main 스택 프레임의 프레임 포인터 값
- 지역 변수에 접근할 때 이 프레임 포인터를 이용해 접근 (b는 ebp + 12를 하면 b의 주소값을 얻음)
- 마지막으로 sub A, B : A에서 B를 빼서 다시 A에 저장
- 스택 포인터가 esp에서 일정 메모리르 빼서 나머지 지역 변수의 공간을 확보함
- 왜 뺄셈을 하냐면 스택의 위쪽은 낮은 주소고 아래쪽은 높은 주소이기 때문, 스택은 위쪽으로 확장함
- 즉 스택에 데이터를 쌓는다는 것은 스택 포인터가 가리키는 주소가 점점 낮아진다는 것 → 낮아지게 하려면 뺄셈을 하면 됨
스택 프레임 해제
- 함수 호출이 끝나고 스택 프레임이 해제되는 첫번째 과정
- ebp 값을 esp에 대입함으로써 실제 메모리를 지우지는 않지만 결국 해제한 것과 같음
- 이후 다른 함수를 호출할 때 스택 포인터가 가리키는 곳부터 스택 프레임이 생기므로 이전 데이터를 덮어씌우게 됨
- pop A 는 스택 맨 위에 있는 데이터를 꺼내 A로 옮기는 명령어
- 스택프레임에 저장해둔 ebp 값을 꺼내 ebp에 할당함
- ret 는 스택에 저장해 둔 프로그램 카운터 값을 복원하는 명령어
- 함수 호출을 종료하고 adder() 함수 호출 이후에 실행할 인스트럭션으로 돌아감
- 메모리를 직접 삭제하지 않고 스택 포인터를 인자가 차지했던 메모리만큼 더해서 해제
- 스택 쌓을 때 스택 포인터에서 일정 메모리를 빼서 낮은 주소로 옮겼으므로 해제할 땐 덧셈을 하게 됨
가상 메모리와 페이징
- 운영체제는 대부분 멀티태스킹을 할 수 있음
- 프로세스 한번 실행 시 4GB를 쓴다고 했는데 어떻게?
- 프로세스는 메인 메모리에서 데이터를 가져옴(하드디스크에서 가져오면 매우 느리기 때문에)
- 하지만 하드디스크에도 데이터 저장 기능이 있으므로 하드디스크를 메인 메모리처럼 사용할 수 있지 않을까? → 가상 메모리
- 가상 메모리란, 메인 메모리를 확장하기 위해 페이지 파일로 불리는 하드디스크의 일정 부분을 메인 메모리처럼 사용하는 것을 말함
- 메인 메모리와 이 페이지 파일을 합쳐 물리 메모리라고 함
- 가상 주소 공간: 프로세스에 주어지는 가상 메모리 공간
- 가상 주소 공간의 메모리 주소 = 논리 주소, 메인 메모리의 메모리 주소 = 물리 주소
- 프로세스를 실행하려면 실제로 데이터와 코드를 올릴 물리 메모리가 필요하고 이를 위해 논리 주소를 물리 주소로 변환해 메인 메모리를 사용해야 하는데 이때 MMU(Memory Management Unit)이 필요함 (*CPU 내부의 하드웨어)
- 가상 메모리 관리 기법은 가상 주소 공간을 쪼개는 기준에 따라 세그멘테이션 기법과 페이징 기법으로 나눌 수 있으며 오늘날의 운영체제는 대부분 두 가지를 함께 사용
페이징
- 가상 주소 공간과 메인 메모리를 일정한 크기로 나누어 다룸
- 가상 주소 공간을 일정한 크기로 쪼개는데 이 쪼개진 한 부분을 페이지라고 함 (보통 1~8KB, 32비트 시스템에서는 4KB)
- 가상 주소 공간에는 페이지가 몇 개 있을까?
- 페이지 개수 = 가상 주소 공간 크기 / 페이지 크기 = 4GB / 4K = 2^20개
- 2^20을 2진수로 표현하려면 스무 개의 비트가 필요함, 이 비트를 페이지 넘버 = VPN(virtual page number) 라고 부름
- 페이지 크기가 4,096바이트일 때 페이지 안에 있는 바이트 하나를 가리키려면 비트가 열 두개 필요함, 페이지 안에서 특정 바이트를 가리키는 이 비트를 오프셋이라고 부름
- 페이지 넘버와 오프셋을 더하면 논리 주소가 됨 → CPU의 프로그램 카운터가 이 값을 저장
- 가상 주소 공간에는 페이지가 몇 개 있을까?
- 페이지 프레임
- 메인 메모리도 가상 주소 공간과 같은 크기로 쪼갬 = 실제로 존재하지 않는 페이지를 실제로 존재하는 프레임에 할당하기 위해서
- 쪼개진 부분 하나를 페이지 프레임이라고 부름
- 프레임 순서를 나타내는 비트를 프레임 넘버 = PPN (physical page number)라고 부름
- 페이지 테이블
- 어떤 프로세스의 VPN, PPN, 상태(control bits)등을 저장하는 테이블
- 모든 프로세스는 저마다 페이지 테이블이 있고 메인 메모리에 저장됨
- CPU에는 페이지 테이블의 싲작 주소를 가리키는 PTBR(Page table base register)라는 레지스터가 있음
- 페이지는 페이지 테이블을 통해 프레임으로 대응되어야만 실제 메모리를 사용할 수 있음
- 유효 비트(valid bit): 프레임이 메인 메모리에 있는지 하드 디스크에 있는지를 나타냄, 메인 메모리에 존재하면 1
- MMU가 논리 주소(프로그램 카운터에 저장된 인스트럭션의 주소 값, VPN+오프셋)을 물리 주소로 바꿈
- PTBR에 저장된 페이지 테이블의 시작 주소를 참조
- 프로그램 카운터에서 VPN 비트(상위 20비트)를 참조
- 페이지 테이블에서 VPN을 찾고 이에 상응하는 PPN을 가져와 프로그램 카운터의 오프셋 비트와 합쳐 물리 주소를 만듦
- 이 물리 주소를 또다른 레지스터인 MAR(Memory addreser register)에 저장
- CPU는 이 레지스터의 주소값을 읽어와 메인 메모리에서 인스트럭션을 가져오고 실행함
- 요구 페이징 (demand paging)
- 프로세스를 실행할 때 모든 페이지를 프레임에 맵핑하는 것이 아니라 필요한 페이지만 메인 메모리에 올려 실행하는 것!
- 프로세스가 처음 실행될 때 운영체제는 페이지 테이블을 메인 메모리에 만들고 실행이 필요할 것 같은 페이지만 먼저 맵핑 = 프리페어링
- 맵핑한 페이지는 페이지 테이블의 유효 비트를 1로 바꿈
- 아직 필요하지 않아 프레임에 맵핑하지 않은 페이지는 하드디스크에 있는 페이지 위치로 초기화하고 유효 비트를 0으로 (나중에 요청하면 맵핑)
- 페이지 폴트 (page fault)
- CPU가 요청한 페이지가 메인 메모리에 없을 때 발생
- 요청을 받아서 빈 프레임에 할당하려고 했는데 메인 메모리에 빈 프레임이 없다면? 페이지 교체 알고리즘에 의해 메인 메모리에 있는 페이지를 하드디스크로 내리고 요청된 페이지를 메인 메모리로 올림
- 내려지는 페이지를 희생 페이지(victim page), 내리는 것을 페이지 아웃(page-out), 올리는 것을 페이지 인(page-in)이라고 함
- 페이지 폴트가 자주 일어나면 프로그램 성능이 상당히 떨어짐(하드디스크에서 읽어오는 건 매우 늘이기 때문에)
- 페이지 폴트가 일어날 확률을 줄이려면 지역성을 고려하면서 프로그래밍해야 함
- 변환 색인 버퍼 (Translation Lookaside Buffer, TLB)
- 변환 속도를 높이기 위한 일종의 캐시
- 최근 사용된 페이지 테이블 일부를 저장해두고, MMU가 페이지 테이블에서 프레임 넘버를 읽어와야 할 때 먼저 TLB에 있는지 확인함 (있으면 TLB 히트, 없으면 TLB 미스)
- 마찬가지로 지역성의 원리로 TLB 히트의 확률은 99%에 달함
This post is licensed under CC BY 4.0 by the author.