가상메모리: 메인 메모리의 추상화. 각 프로세스에 하나의 크고 통합된, private한 가상 주소공간을 제공한다.
초기의 cpu들은 물리 주소 방식을 사용했다. 따라서 프로세스가 메인 메모리에 직접 접근할 수 있었고, 이는 메인 메모리의 용량 한계로 인한 프로그램 확장성 제한, 프로세스 간 메모리 영역 침범, 일관되지 않은 주소 등의 문제로 이어졌다.
일반적으로 한 시스템의 여러 프로세스들은 CPU, 메인 메모리를 공유한다.
CPU를 공유하는 부분에 대해서는 일반적으로 순서를 기다리느라 단지 느려질 뿐이고 심각한 오류는 발생하지 않는다.
그러나 프로세스들이 존재하는 메모리가 여유가 없이 지나치게 많은 요구에 의해 오염될 경우, 프로그램의 논리와 무관하게 오류가 난다. 이를 방지하기 위한 기술이 바로 가상메모리이다.
따라서 현대의 프로세스는 가상주소방식을 사용한다 (1960년대 본격적으로 도입). 이는 곧 가상메모리 라는 개념을 사용한다는 말과 같으며, 가상메모리는 메인메모리의 추상화이다.
앞선 포스팅에서 살펴본 바 있지만, 가상주소방식을 사용하기 위해 CPU 칩 내부에는 MMU 라는 하드웨어(가상주소를 물리주소로 번역하는 장치)가 장착되기 시작했다. 또한 TLB 라는 장치가 추가되었으며, 메인메모리 안에는 PTE 영역이 포함되게 되었다. 스왑 개념이 등장함에 따라 보조기억장치 안에는 스왑 영역이 생겨나게 되었다.
왜 이렇게 복잡한 과정을 거쳤을까? 가상 메모리 자체가 캐싱 아이디어에서 비롯된 개념이기 때문이다.
결국 위 장치들은 효과적인 캐싱을 가능케하는 장치들이다.
캐시 미스가 매우 빈번하지만 않는다면 캐시는 데이터 이동의 효율을 극적으로 개선한다.
이를 위해 미래에 쓸 것들을 잘 예측해서 가까이에 두는 게 중요한데, 그 예측 과정에 있어서 지역성이라는 속성이 매우 큰 역할을 한다.
지역성은 프로그램이 갖는 여러 속성 중 하나이다. 지역성의 종류는 크게 두 가지가 있다: 1) 시간 지역성, 2) 공간 지역성
시간 지역성
공간 지역성
좋은 지역성을 갖는 프로그램이 나쁜 지역성을 갖는 프로그램보다 더 빨리 동작한다. 왜냐하면, 위 가상 메모리만 봐도 그렇고 그 외에도 수많은 현대의 하드웨어, 운영체제, 응용프로그램은 지역성을 활용하도록 설계되었기 때문이다. (즉, 좋은 개발자라면 좋은 지역성을 갖도록 프로그램을 짤 수 있어야 한다.)
+) 프로그램 내에서 지역성을 정성적으로 평가하는 간단한 일부 규칙
# 낮은 Stride
for i in range(N):
for j in range(N):
print(a[i][j])
# 높은 Stride
for i in range(N):
for j in range(N):
print(a[j][i])
다시 가상 메모리로 돌아와서, 각 프로세스는 가상 메모리를 부여받는다고 했는데 프로세스 입장에서 바라본 가상 메모리는 어떤 모습일까?
이를 가상 주소를 갖고 있는 가상 주소 공간(Virtual Address Space, VAS)이라고 표현한다.
가상 주소공간의 구조는 앞선 포스팅(동적 메모리 할당 | (1) 기본 개념, 메모리 누수, 단편화)에서 잠깐 본 바 있는데, 잠시 복습해보자.
프로세스에 할당된 가상 메모리는 크게 네 부분으로 나눌 수 있다. 다른 프로그래밍 언어와 실행 환경도 비슷한 구조를 사용한다.
간단한 C 코드를 통해 각 구성요소가 어디에 저장되는지 확인해보면 더 이해가 빠를 것 같다.
#include <stdio.h>
#include <stdlib.h>
int initialized_global_var = 5; // 데이터 세그먼트
static int static_initialized_var = 10; // 데이터 세그먼트
int uninitialized_global_var; // BSS 세그먼트
static int static_uninitialized_var; // BSS 세그먼트
void my_function(int function_param) {
static int static_var_in_function = 3; // 데이터 세그먼트
static int static_uninitialized_var_in_function; // BSS 세그먼트
int local_var = 2; // 스택
int *heap_var = malloc(sizeof(int)); // 힙 (포인터는 스택에 저장됨)
*heap_var = 4; // 힙에 저장된 값
printf("Local variable = %d\n", local_var);
free(heap_var); // 할당된 힙 메모리를 해제
}
int main() {
int local_var_in_main = 1; // 스택
my_function(local_var_in_main);
return 0;
}
여기서, main() 함수와 my_function() 함수의 정의, 그리고 이 함수들 내부의 명령어들(예: printf(), malloc(), free() 호출, 변수 할당 등)은 코드 영역에 저장된다.
CPU는 코드 세그먼트의 기계어 코드를 읽고, 그때그때 필요한 데이터를 스택, 힙, 데이터 세그먼트에서 가져오는 식으로 프로그램이 동작한다.
보조기억장치에 있던 실행파일(컴파일 과정을 거친 바이너리 파일. Linux 계열에서는 ELF 형식이 표준)이 프로세스화 될 때의 대략적인 과정은 다음과 같다.
1) 커널은 이와 같은 구성의 가상주소공간을 할당하고,
2) 보조기억장치의 ELF 파일에서 코드(.text)와 데이터(.data) 세그먼트의 내용을 파싱하고 메모리의 해당 영역으로 복사한다. 이 과정에서 파일의 바이너리 내용이 메모리에 로드된다. 동시에, 커널에 의해 코드 세그먼트는 실행 가능하도록 설정되며, 데이터 세그먼트는 읽기/쓰기 가능하도록 설정된다.
3) 메모리(커널의 버퍼)에 로드된 코드들을 1에서 할당한 주소공간에 매핑(mapping)한다.
가상메모리 | (3) 세그멘테이션, 페이징 (0) | 2023.11.15 |
---|---|
가상메모리 | (1) 개발자 코드가 물리 메모리에 닿는 과정 (2) | 2023.11.14 |
동적 메모리 할당 | Malloc Lab | (4) 할당기 배치 전략 개선 (next fit) (0) | 2023.11.14 |
동적 메모리 할당 | Malloc Lab | (3) 기초적인 할당기 작동 원리, 구현 (0) | 2023.11.12 |
동적 메모리 할당 | Malloc Lab | (2) 과제 소개 (0) | 2023.11.11 |