가상메모리 | 컴퓨터의 주소 공간은 어떻게 관리될까?

설현아·2025년 4월 23일

CSAPP 9장을 참고했습니다.

가상메모리를 공부하기 전에 컴퓨터에서 각 프로세스가 어떻게 실행되며, 어떤 공간을 공유하고 어떤 공간을 독립적으로 사용하는 지 되짚어보자.

시스템의 프로세스들은 CPU와 메인 메모리를 공유한다.
사실 하드웨어 자원은 한정적이기 때문에 모두 공유한다고 봐야 한다.

그런데 어떤 것은 독립적으로 사용한다고 말한다. 이건 뭘까?
프로세스 간의 메모리는 독립적으로 할당된다. 프로세스는 각자의 영역에 침범할 수 없다.

즉, 메인 메모리와 CPU는 공유하지만 그 안에서 돌아가는 프로세스는 가상화(가상 메모리)를 사용하여 독립적인 환경으로 메모리를 점유하게 한다.

들어가기

📙 가상 메모리의 주요 기능

  1. 메인 메모리를 디스크에 저장된 주소 공간에 대한 캐시로 취급
  2. 각 프로세스에 통일된 주소공간 제공
  3. 각 프로세스의 주소공간을 다른 프로세스에 의한 손상으로부터 보호

컴퓨터의 주소 공간은 어떻게 관리 될까?

메인 메모리는 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이다.

메인 메모리는 VM(디스크)의 캐시이다

디스크 안의 배열 정보는 메인 메모리에서 캐시되어 보다 빠르게 사용할 수 있다.

이 말은, CPU가 디스크에 접근해서 데이터를 가져오는 데 시간이 오래걸리니까, 디스크 보다 가까운 메인 메모리에 저장해두었다가 보다 더 빠르게 가져다 쓴다는 말이다.

그래서 이 캐시는 계층구조 안에서 블록 단위로 분할된다. 디스크에서는 데이터를 블록 단위로 관리한다. 디스크와 메인 메모리 사이에서 징검다리 역할을 원활하게 수행하기 위해서 VM 시스템의 단위 또한 블록이다. 블록 단위로 분할하여 관리한다.

그리고 이렇게 분할된 각 블록들을 가상 페이지라고 부른다.(참고로, 물리 메모리도 프레임이라고 불리는 블록 단위로 분할하여 관리된다.)

📙 가상 페이지의 집합
- Unallocated: VM 시스템에 의해 아직 할당되지 않은 페이지들 → 디스크 상에 공간을 차지하지 않는다
- Cached: 물리 메모리에 캐시되어 할당된 페이지들 → RAM에 캐시되었다.
- Uncached: 물리 메모리에 캐시되지 않은 할당된 페이지들 → 디스크에만 저장되어있다.

이거 왜 해?

  1. 매번 디스크로 가서 참조하기에 디스크는 매우 느리다.
  2. 전체 프로세스의 가상 주소 공간은 매우 크다(64bit 시스템에서는 2^64)
  3. 실제 물리 메모리는 작기 때문에, 필요할 때만 가져와야 한다. 빠르다고 해서 디스크의 모든 데이터를 물리 메모리에 둘 수는 없다.

DRAM의 구성

DRAM?

가상페이지를 메인 메모리로 캐싱하는 VM 시스템의 캐시

SRAM?

CPU와 메인 메모리 사이에서 L1, L2, L3 등의 캐시 메모리

메모리 계층구조에서 DRAM의 캐시 미스가 발생하면 다시 디스크에서 처리되지만 이는 엄청난 비용이 발생한다. 따라서 이를 줄이기 위해 가상 페이지가 더 커지고 있다. 현대 시스템에서는 보통 4KB를 사용하지만 2MB까지의 값을 가질 수도 있다.

DRAM의 캐시 설계

완전 결합성 fully associative

자잘하게 쪼개진 모든 메모리 공간을 사용할 수 있도록 채택한 방법이다. 어디든 갈 수 있어서 하드웨어 비용이 크지만 충돌이 적고 효율적인 저장이 가능하다. 충분히 많은 메모리 공간에 충돌 없이 저장되어야 하기 때문에 DRAM은 이 방식을 채택한다.

"직접 매핑형 Direct Mapped"
데이터를 지정된 딱 한 공간에만 넣을 수 있다.
index로 바로 찾을 수 있어, 빠르지만 충돌이 많다.

"집합 연관형 Set Associative"
특정 구역 내의 메모리 공간에 넣을 수 있다.

쓰기 정책 write-back

캐시에 있는 데이터를 수정할 때, 언제 메인 메모리에 그 변경을 반영할지 결정하는 방식

  1. write-through
    : 매번 캐시와 메모리 둘 다 갱신
  2. write-back
    : 캐시에서 먼저 수정하고 나중에 한 번에 메모리에 갱신(해당 블록이 캐시에서 제거 될 때 다시 쓴다.)

VM의 페이지

페이지 테이블

내가 찾고자 하는 데이터가 DRAM에 캐시 되어있는지 어떻게 알 수 있을까?
그니까, 어떤 물리페이지들을 DRAM에 캐싱 했는지 결정하고, 미스가 존재했을 때 어떻게 페이지를 교체해야할지는 바로 이 페이지 테이블에서 제공된다.
페이지 테이블은 물리 메모리에 저장된 자료구조이다. 운영체제가 관리한다.
페이지 테이블을 수정하면서 페이지들을 디스크와 DRAM 사이에서 넘겨주고 받는 것을 관장한다.

페이지 테이블은 PTE(Page Table Entry)의 배열로 이루어져 있다.
PTE의 구성은 다음과 같다.

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)페이지에서 코드 실행을 허용할지 여부 (보안 목적)

PTE 활용

  • PTE의 주소필드에 주소가 있다면 해당 주소로 찾아가면 된다.
  • 주소필드가 비어있다면 해당 가상페이지(DISK)의 시작 부분을 가리킨다.
  • NULL이 들어있다면, 가상페이지가 아직 할당되지 않았음을 나타낸다.

페이지 적중 page hit, 페이지 오류 page fault

페이지 적중 page hit

CPU가 DRAM에 캐시되어 있는 데이터를 읽는 것

페이지 오류 page fault

DRAM에 캐시되지 않았다면 커널 내의 페이지 오류 핸들러를 호출해서 희생자 페이지와 새로운 페이지를 교체한다.
이 때, 희생자 페이지에 변경 사항이 있다면 이를 다시 디스크에 복사한다(write-back)
그리고, PTE를 수정해서 희생자 페이지가 더이상 캐시되지 않는다는 사실을 반영하고, 새로운 페이지는 찾아갈 수 있도록 한다.

요구 페이징 demand paging

미스가 발생할 때, 디스크에서 메모리로 페이지를 불러오는 방식이다.
프로그램이 메모리를 실제로 접근할 때까지 해당 페이지를 물리 메모리에 로드하지 않고 기다리는 기법이다.

스와핑 swapping

디스크와 메모리 사이에서 페이지를 전송하는 동작

페이지의 할당

만약, malloc을 호출하여 주소를 배정받았다면 값을 할당하는 시점에서 페이지 할당이 일어난다.

디스크 상에 공간이 없었기 때문에 1. 디스크 상의 공간을 만들고 2. PTE를 디스크의 새로운 페이지를 가리키도록 할당한다.

지역성

페이지 미스는 매!! 우 비효율적이라는 것을 알 수 있다. 그런데 이 가상 메모리는 왜 쓰는 걸까?
우리가 생각하는 것만큼 페이지 미스는 잦게 일어나지 않는다. 이유는 바로 지역성이다.

지역성이란?

프로그램은 최근에 접근한 데이터나 코드 근처를 곧 다시 접근하는 경향이 있다는 성질

int sum = 0;
for (int i = 0; i < 100; i++) {
    sum += arr[i];
}
  • 시간 지역성 최근에 접근한 데이터에 곧 다시 접근할 가능성이 높다. for문 안에서 sum 값이 계속 변경되고 있다. 같은 데이터를 반복해서 쓰는 성질인 시간 지역성으로 sum은 곧 다시 접근할 가능성이 높다.
  • 공간 지역성 근처의 데이터도 곧 접근할 가능성이 높다. 위에서 arr 배열은 각 원소를 메모리에 연속해서 접근할 가능성이 높다.

따라서 우리 프로그램이 좋은 시간 지역성을 가지고 있으면, 가상메모리 시스템은 상당히 잘 동작할 것이다.

좋은 시간 지역성을 가진다면, 자주 사용하는 페이지는 TLB에 저장되고, 더하여 페이지 폴트 없이 사용할 수 있다. 그렇다면 가상 메모리 시스템의 효율이 전반적으로 증가한다.

VM은 이런 걸 돕는다!

메모리 관리를 위한 도구

  • 링킹을 단순화한다.
    각 프로세스의 주소공간은 모두 동일한 메모리 포맷을 유지하고 있다.

    64bit 주소 공간에서 코드 세그먼트는 항상 0x400000에서 시작한다. 데이터 세그먼트는 일정 거리를 띄워두고 코드 세그먼트 다음에 위치한다.
    링킹의 과정에서 링커는 심볼 해석과 재배치 과정을 각 영역에 찾아가서 하게 된다. 이러한 메모리 구조의 통일성은 링커의 설계와 구현을 매우 단순화해준다.

  • 로딩을 단순화한다.
    목적파일의 .data, .text 섹션들을 새롭게 생성된 프로세스에 로드하기 위해서 로더는 가상의 페이지를 할당하고 캐시되지 않은 상태로 표시하고 PTE를 목적파일의 해당 위치를 가리키게 한다. 이 과정에서 로더는 실제로 디스크로부터 메모리에 데이터를 전혀 복사하지 않아도 된다.

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

  • 공유를 단순화한다.

    각 프로세스는 다른 프로세스와 공유할 수 있는 라이브러리가 있다. 예컨대, printf같은 표준 C 라이브러리를 호출할 때는 매 프로세스의 코드 영역에 포함시키기 보다, 다수의 프로세스가 공유할 수 있도록 한다.

  • 메모리 할당을 단순화한다.

    프로세스가 추가적인 힙 공간을 요청할 때, OS는 일정 부분의 연속적인 가상메모리 페이지를 할당하고 이를 물리 메모리 내에 위치한 임의의 물리페이지로 매핑한다. OS의 페이지 테이블 덕분에 물리 메모리에 흩어진 페이지를 힘들게 찾을 필요가 없다.

메모리 보호를 위한 도구

  • read-only code segment 영역은 프로세스가 수정하도록 허용되지 않는다.
  • 또한 커널 내의 코드와 데이터 구조들도 읽거나 수정할 수 없어야 한다.
  • 그리고 다른 프로세스의 사적 메모리를 읽거나 쓸 수 없어야 한다.

가상 메모리를 사용하면 이 모든 것이 쉬워진다.
하나씩 해결해보자.

  • read-only code 영역은 읽기 전용으로 설정한다.
  • 커널 메모리는 사용자 모드에서 접근 불가로 설정한다.
  • 다른 프로세스 메모리는 접근 불가로 설정한다.

이제 접근을 제어하는 방법만 생각해보면 된다.

CPU가 가상 주소(VA)에서 물리 주소(PA)로 변환할 때마다, PTE를 읽기 때문에 PTE에 허가 비트를 추가해서 해당 페이지로의 접근이 가능한지 판단할 수 있으면 어떨까?

위의 사진처럼 각 PTE에 세 개의 허가 비트를 추가한다.
SUP 비트는 커널 모드만 허용한다.(사용자 모드는 제한)
READ, WRITE는 각각 이 페이지에 대한 읽기, 쓰기 접근을 제어한다.

만약 이러한 허가사항을 위반한다면, CPU는 일반 보호 오류를 발생시켜서 SIGSEGV 시그널을 위반한 프로세스로 보내서 커널 내의 예외 핸들러로 제어를 이동시킨다.

그런 다음, 리눅스 쉘은 세그먼트 오류 segmentation fault를 반환한다.

메모리 매핑 주요 키워드

책으로 이해가 어려워서 GPT 선생님의 가르침으로 정리했다.

개념설명
lazy allocation실제 접근 전까지 메모리 할당/초기화하지 않음
Copy-on-Write (COW)읽기만 할 땐 공유, 쓰면 진짜 메모리 생성
demand paging접근할 때만 물리 메모리 할당
zero page0으로 가득 찬 공유 페이지, read-only 매핑용
  1. Lazy Allocation (게으른 할당)

    “실제로 접근할 때까지 메모리를 진짜로 만들지 않는다.”

    • 프로세스가 malloc() 또는 mmap()으로 메모리를 요청해도 → 운영체제는 가상 주소만 예약하고 → 진짜 메모리(RAM)는 붙이지 않음
    • 메모리에 실제 접근이 일어나는 순간! → 페이지 폴트 발생 → 그때서야 물리 메모리를 만들어서 연결

    [ 장점 ]

    • 메모리 낭비를 줄이기 위해 (진짜 안 쓰면 할당도 안 됨)
    • 속도 향상 (할당 속도 빨라짐)
  2. zero-on-demand

    “페이지를 처음 접근할 때 운영체제가 0으로 초기화된 물리 메모리를 할당해주는 방식”

    • mmap(MAP_ANONYMOUS)로 메모리를 만들면 → 처음엔 실제 메모리에 아무것도 없음
    • 해당 가상 주소에 처음 접근할 때 → 페이지 폴트 발생
    • OS가 깨끗한 물리 페이지를 하나 할당하고, 그 내용을 전부 0으로 채운다
    • 그 뒤부터는 정상 접근 가능!

    [ 장점 ]

    1. 보안 : 이전에 다른 프로그램이 쓰던 메모리를 그대로 주면 안 되니까 무조건 0으로 깨끗하게 초기화해서 줘야 함
    2. 프로그래머 기대 일치 : mmap()이나 malloc()으로 할당한 메모리는 0이거나 쓰레기값이면 안 됨
    3. 성능 최적화 : 처음 접근할 때만 실제 초기화하여 불필요한 메모리 초기화 줄이기
  3. copy-on-write

    “읽을 땐 공유하고, 쓸 때 복사한다.”

    • 부모 프로세스를 fork()로 복제할 때 → 모든 메모리를 복사하지 않고 → 부모-자식이 같은 물리 메모리 블록을 읽기 전용으로 공유
    • 둘 중 하나가 메모리에 쓰기 시도하면그 블록만 복사해서 따로 사용

    [ 장점 ]

    • fork() 성능 향상
    • 메모리 절약 (쓸 때만 복사)
  4. demand paging

    “페이지는 필요한 순간에만 메모리에 올린다.”

    • 프로그램 실행 시 전체를 다 메모리에 올리지 않음
      • 실행 파일의 .text, .data 등이 다 올라가는 게 아님
      • 접근되는 페이지만 OS가 디스크에서 읽어 메모리에 올림
    • 어떤 페이지가 처음 접근될 때페이지 폴트 발생 → 디스크에서 해당 코드나 데이터 페이지를 읽어서 메모리에 올림

    [ 장점 ]

    • 실행 속도 빠르게 (시작 시 전부 로드 안 해도 됨)
    • 메모리 사용량 절약
  5. zero page

    “0으로 가득 찬 페이지. 초기화 용도로 공유된다.”

    • 어떤 페이지가 아직 쓰인 적 없고, 초기화만 필요할 때 → OS는 0이 가득 찬 페이지를 하나 만들어서 여러 프로세스와 공유
    • 단, 쓰기 시도하면 Copy-On-Write! → 새로운 물리 페이지 할당됨

    [ 장점 ]

    • 0 초기화된 메모리를 메모리 낭비 없이 제공하기 위해
    • 읽기만 한다면 여러 프로세스가 같은 zero page 공유 가능
profile
어서오세요! ☺️ 후회 없는 내일을 위해 오늘을 열심히 살아가는 개발자입니다.

0개의 댓글