CSAPP 9장을 참고했습니다.
가상메모리를 공부하기 전에 컴퓨터에서 각 프로세스가 어떻게 실행되며, 어떤 공간을 공유하고 어떤 공간을 독립적으로 사용하는 지 되짚어보자.
시스템의 프로세스들은 CPU와 메인 메모리를 공유한다.
사실 하드웨어 자원은 한정적이기 때문에 모두 공유한다고 봐야 한다.
그런데 어떤 것은 독립적으로 사용한다고 말한다. 이건 뭘까?
프로세스 간의 메모리는 독립적으로 할당된다. 프로세스는 각자의 영역에 침범할 수 없다.
즉, 메인 메모리와 CPU는 공유하지만 그 안에서 돌아가는 프로세스는 가상화(가상 메모리)를 사용하여 독립적인 환경으로 메모리를 점유하게 한다.
메인 메모리는 M개의 연속적인 1byte 크기의 배열로 구성된다. 각 바이트는 고유의 물리 주소(PA)를 가진다.
CPU가 이러한 물리 주소에 직접 접근하는 방식을 물리 주소 방식 이라고 부른다.
초기 PC가 주로 물리 주소 방식을 채택하였으며, 현재는 임베디드 시스템 등에서 계속 사용하고 있다.
CPU가 가상 주소를 생성하고, OS가 관리하는 테이블을 사용하여 물리 주소로 변환된다. 이렇게 변환된 물리 주소로 메모리에 접근한다. 이렇게 주소를 번역해주는 일은 CPU 칩 내, MMU가 수행한다.
그러면 이 주소 공간은 어느만큼의 크기를 가질까? 처음부터 정해져 있는 걸까?
이는 우리가 어떤 컴퓨터를 샀는지! 쓰고 있는지!에 따라 달라진다. 현대 시스템은 보통 32bit 혹은 64bit 시스템이다. 만약 32bit 시스템을 사용한다고 가정했을 때,
이 컴퓨터의 CPU는 한 번에 32bit를 처리할 수 있다. 1bit는 0과 1의 두 가지 경우가 있으니, 32bit면 2^32 만큼의 경우를 다룰 수 있다.
따라서 32bit 시스템에서의 메모리 주소 공간도 0 ~ (2^32) - 1이다.
디스크 안의 배열 정보는 메인 메모리에서 캐시되어 보다 빠르게 사용할 수 있다.
이 말은, CPU가 디스크에 접근해서 데이터를 가져오는 데 시간이 오래걸리니까, 디스크 보다 가까운 메인 메모리에 저장해두었다가 보다 더 빠르게 가져다 쓴다는 말이다.
그래서 이 캐시는 계층구조 안에서 블록 단위로 분할된다. 디스크에서는 데이터를 블록 단위로 관리한다. 디스크와 메인 메모리 사이에서 징검다리 역할을 원활하게 수행하기 위해서 VM 시스템의 단위 또한 블록이다. 블록 단위로 분할하여 관리한다.
그리고 이렇게 분할된 각 블록들을 가상 페이지라고 부른다.(참고로, 물리 메모리도 프레임이라고 불리는 블록 단위로 분할하여 관리된다.)
📙 가상 페이지의 집합
- Unallocated: VM 시스템에 의해 아직 할당되지 않은 페이지들 → 디스크 상에 공간을 차지하지 않는다
- Cached: 물리 메모리에 캐시되어 할당된 페이지들 → RAM에 캐시되었다.
- Uncached: 물리 메모리에 캐시되지 않은 할당된 페이지들 → 디스크에만 저장되어있다.
이거 왜 해?
DRAM?
가상페이지를 메인 메모리로 캐싱하는 VM 시스템의 캐시
SRAM?
CPU와 메인 메모리 사이에서 L1, L2, L3 등의 캐시 메모리
메모리 계층구조에서 DRAM의 캐시 미스가 발생하면 다시 디스크에서 처리되지만 이는 엄청난 비용이 발생한다. 따라서 이를 줄이기 위해 가상 페이지가 더 커지고 있다. 현대 시스템에서는 보통 4KB를 사용하지만 2MB까지의 값을 가질 수도 있다.
완전 결합성 fully associative
자잘하게 쪼개진 모든 메모리 공간을 사용할 수 있도록 채택한 방법이다. 어디든 갈 수 있어서 하드웨어 비용이 크지만 충돌이 적고 효율적인 저장이 가능하다. 충분히 많은 메모리 공간에 충돌 없이 저장되어야 하기 때문에 DRAM은 이 방식을 채택한다.
"직접 매핑형 Direct Mapped"
데이터를 지정된 딱 한 공간에만 넣을 수 있다.
index로 바로 찾을 수 있어, 빠르지만 충돌이 많다.
"집합 연관형 Set Associative"
특정 구역 내의 메모리 공간에 넣을 수 있다.
캐시에 있는 데이터를 수정할 때, 언제 메인 메모리에 그 변경을 반영할지 결정하는 방식
내가 찾고자 하는 데이터가 DRAM에 캐시 되어있는지 어떻게 알 수 있을까?
그니까, 어떤 물리페이지들을 DRAM에 캐싱 했는지 결정하고, 미스가 존재했을 때 어떻게 페이지를 교체해야할지는 바로 이 페이지 테이블에서 제공된다.
페이지 테이블은 물리 메모리에 저장된 자료구조이다. 운영체제가 관리한다.
페이지 테이블을 수정하면서 페이지들을 디스크와 DRAM 사이에서 넘겨주고 받는 것을 관장한다.
페이지 테이블은 PTE(Page Table Entry)의 배열로 이루어져 있다.
PTE의 구성은 다음과 같다.
| 필드 이름 | 설명 |
|---|---|
| Present bit | 해당 페이지가 메모리에 존재하는지 여부 (0이면 페이지 폴트 발생) |
| Read/Write bit | 페이지의 읽기/쓰기 권한 설정 |
| User/Supervisor bit | 유저 모드에서 접근 가능한지 여부 (0이면 커널 모드만 접근 가능) |
| Accessed bit | 페이지가 최근에 접근되었는지 표시 (페이지 교체 알고리즘에 사용) |
| Dirty bit | 페이지가 수정되었는지 여부 (쓰기 연산 시 1로 설정) |
| Page Frame Number (PFN) | 실제 물리 메모리 주소의 상위 비트 (프레임 번호) |
| Cache Disable, Write-Through 등 | 캐싱 관련 속성 지정 |
| NX bit (No Execute) | 페이지에서 코드 실행을 허용할지 여부 (보안 목적) |
CPU가 DRAM에 캐시되어 있는 데이터를 읽는 것
DRAM에 캐시되지 않았다면 커널 내의 페이지 오류 핸들러를 호출해서 희생자 페이지와 새로운 페이지를 교체한다.
이 때, 희생자 페이지에 변경 사항이 있다면 이를 다시 디스크에 복사한다(write-back)
그리고, PTE를 수정해서 희생자 페이지가 더이상 캐시되지 않는다는 사실을 반영하고, 새로운 페이지는 찾아갈 수 있도록 한다.
미스가 발생할 때, 디스크에서 메모리로 페이지를 불러오는 방식이다.
프로그램이 메모리를 실제로 접근할 때까지 해당 페이지를 물리 메모리에 로드하지 않고 기다리는 기법이다.
디스크와 메모리 사이에서 페이지를 전송하는 동작
만약, malloc을 호출하여 주소를 배정받았다면 값을 할당하는 시점에서 페이지 할당이 일어난다.
디스크 상에 공간이 없었기 때문에 1. 디스크 상의 공간을 만들고 2. PTE를 디스크의 새로운 페이지를 가리키도록 할당한다.
페이지 미스는 매!! 우 비효율적이라는 것을 알 수 있다. 그런데 이 가상 메모리는 왜 쓰는 걸까?
우리가 생각하는 것만큼 페이지 미스는 잦게 일어나지 않는다. 이유는 바로 지역성이다.
지역성이란?
프로그램은 최근에 접근한 데이터나 코드 근처를 곧 다시 접근하는 경향이 있다는 성질
int sum = 0;
for (int i = 0; i < 100; i++) {
sum += arr[i];
}
따라서 우리 프로그램이 좋은 시간 지역성을 가지고 있으면, 가상메모리 시스템은 상당히 잘 동작할 것이다.
좋은 시간 지역성을 가진다면, 자주 사용하는 페이지는 TLB에 저장되고, 더하여 페이지 폴트 없이 사용할 수 있다. 그렇다면 가상 메모리 시스템의 효율이 전반적으로 증가한다.
링킹을 단순화한다.
각 프로세스의 주소공간은 모두 동일한 메모리 포맷을 유지하고 있다.

64bit 주소 공간에서 코드 세그먼트는 항상 0x400000에서 시작한다. 데이터 세그먼트는 일정 거리를 띄워두고 코드 세그먼트 다음에 위치한다.
링킹의 과정에서 링커는 심볼 해석과 재배치 과정을 각 영역에 찾아가서 하게 된다. 이러한 메모리 구조의 통일성은 링커의 설계와 구현을 매우 단순화해준다.
로딩을 단순화한다.
목적파일의 .data, .text 섹션들을 새롭게 생성된 프로세스에 로드하기 위해서 로더는 가상의 페이지를 할당하고 캐시되지 않은 상태로 표시하고 PTE를 목적파일의 해당 위치를 가리키게 한다. 이 과정에서 로더는 실제로 디스크로부터 메모리에 데이터를 전혀 복사하지 않아도 된다.

이 또한 연속된 가상페이지를 임의의 위치로 매핑하는, 메모리 매핑 기술을 사용한다.
공유를 단순화한다.

각 프로세스는 다른 프로세스와 공유할 수 있는 라이브러리가 있다. 예컨대, printf같은 표준 C 라이브러리를 호출할 때는 매 프로세스의 코드 영역에 포함시키기 보다, 다수의 프로세스가 공유할 수 있도록 한다.
메모리 할당을 단순화한다.
프로세스가 추가적인 힙 공간을 요청할 때, OS는 일정 부분의 연속적인 가상메모리 페이지를 할당하고 이를 물리 메모리 내에 위치한 임의의 물리페이지로 매핑한다. OS의 페이지 테이블 덕분에 물리 메모리에 흩어진 페이지를 힘들게 찾을 필요가 없다.

가상 메모리를 사용하면 이 모든 것이 쉬워진다.
하나씩 해결해보자.
이제 접근을 제어하는 방법만 생각해보면 된다.
CPU가 가상 주소(VA)에서 물리 주소(PA)로 변환할 때마다, PTE를 읽기 때문에 PTE에 허가 비트를 추가해서 해당 페이지로의 접근이 가능한지 판단할 수 있으면 어떨까?

위의 사진처럼 각 PTE에 세 개의 허가 비트를 추가한다.
SUP 비트는 커널 모드만 허용한다.(사용자 모드는 제한)
READ, WRITE는 각각 이 페이지에 대한 읽기, 쓰기 접근을 제어한다.
만약 이러한 허가사항을 위반한다면, CPU는 일반 보호 오류를 발생시켜서 SIGSEGV 시그널을 위반한 프로세스로 보내서 커널 내의 예외 핸들러로 제어를 이동시킨다.
그런 다음, 리눅스 쉘은 세그먼트 오류 segmentation fault를 반환한다.
책으로 이해가 어려워서 GPT 선생님의 가르침으로 정리했다.
| 개념 | 설명 |
|---|---|
| lazy allocation | 실제 접근 전까지 메모리 할당/초기화하지 않음 |
| Copy-on-Write (COW) | 읽기만 할 땐 공유, 쓰면 진짜 메모리 생성 |
| demand paging | 접근할 때만 물리 메모리 할당 |
| zero page | 0으로 가득 찬 공유 페이지, read-only 매핑용 |
Lazy Allocation (게으른 할당)
“실제로 접근할 때까지 메모리를 진짜로 만들지 않는다.”
[ 장점 ]
zero-on-demand
“페이지를 처음 접근할 때 운영체제가 0으로 초기화된 물리 메모리를 할당해주는 방식”
[ 장점 ]
copy-on-write
“읽을 땐 공유하고, 쓸 때 복사한다.”
[ 장점 ]
demand paging
“페이지는 필요한 순간에만 메모리에 올린다.”
[ 장점 ]
zero page
“0으로 가득 찬 페이지. 초기화 용도로 공유된다.”
[ 장점 ]