사전 MTRR
개념

MTRR

gabury1고친 사람 github-actions[bot]

MTRR 은 프로세서에게 메모리 주소의 구간마다 캐시를 어떻게 쓸지 알려 줍니다. 보통 메모리 구간은 캐시에 담아 씁니다. 장치와 이어진 구간은 캐시를 건너뛰고 곧장 장치로 보냅니다. 컴퓨터가 켜질 때 펌웨어가 이 구간 표를 채워 둡니다.

쉽고 빠른 이해

MTRR 은 메모리 주소를 구간으로 나눠 둡니다. 구간마다 캐시를 쓸지 말지를 적어 두는 표입니다. 그래픽 카드의 화면 메모리 구간이라면 「쓰기를 모았다가 한 번에 보낸다」고 적습니다.

이 표가 없으면 프로세서는 장치의 값을 캐시 속 옛 복사본으로 읽게 됩니다. 주소만 봐서는 그 구간이 메모리인지 장치인지 프로세서가 모릅니다. 그래서 누군가 알려 줘야 합니다.

어떻게 도는가:

  1. 컴퓨터가 켜질 때 펌웨어가 구간과 다루는 방식을 적어 둡니다
  2. 프로세서가 메모리를 읽거나 쓸 때 그 주소가 어느 구간인지 봅니다
  3. 그 구간에 적힌 방식대로 캐시를 쓰거나 건너뜁니다

대가도 있습니다. 구간을 적을 칸이 몇 개뿐입니다. 구간 크기도 마음대로 못 정합니다. 그래서 요즘은 페이지 단위로 정하는 다른 방식이 이 일을 대부분 넘겨받았습니다.

상세

큰 건물의 자료실을 떠올려 봅니다. 벽에는 층 범위마다 서류를 다루는 방식을 적은 표가 붙어 있습니다. 1층부터 20층의 서류는 창구 서랍에 사본을 두고 꺼내 줍니다. 사본을 고치면 원본에는 나중에 옮겨 적습니다.

지하 기계실 앞으로 가는 쪽지는 서랍에 넣지 않습니다. 오는 즉시 들고 내려갑니다.

MTRR 이 그 벽에 붙은 표입니다.

앞에서 말한 프로세서를 흔히 CPU(Central Processing Unit, 중앙처리장치)라고 부릅니다. 이 절부터는 CPU 라고 적습니다.

CPU 가 메모리를 읽고 쓸 때 내는 주소를 물리 주소라고 합니다. 이 주소는 메모리 칩의 칸을 가리키는 번호입니다.

그런데 물리 주소의 구간을 받는 것이 메모리만은 아닙니다. 그래픽 카드나 네트워크 카드 같은 장치도 구간을 하나씩 받습니다. 그래서 한 주소 공간 안에 성격이 다른 구간이 섞여 있습니다.

block-beta
columns 1
  a["맨 앞 1MB · 롬과 옛 화면 메모리"]
  b["보통 메모리"]
  c["그래픽 카드의 화면 메모리"]
  d["장치 레지스터"]

위 그림은 한 컴퓨터의 물리 주소를 낮은 쪽부터 세로로 쌓은 예입니다. 구간마다 담긴 것이 다르니 CPU 가 다루는 방식도 달라야 합니다. 그림의 이름표 셋을 차례로 풉니다.

롬은 ROM(Read-Only Memory, 읽기 전용 메모리)입니다. 한 번 적어 두고 고쳐 쓰지 않는 메모리입니다.

화면 메모리는 그래픽 카드가 화면에 뿌릴 픽셀 값을 담는 메모리입니다. 흔히 프레임버퍼라고도 부릅니다. 맨 앞 1MB 에 있는 것은 초기 개인용 컴퓨터가 쓰던 작은 화면 메모리입니다. 지금 컴퓨터도 그 주소 배치를 지킵니다.

레지스터는 칩 안에 둔 아주 작은 저장 칸입니다. CPU 안에도 있고 장치 안에도 있습니다.

장치 레지스터는 장치 안에 있는 레지스터입니다. CPU 가 여기에 값을 쓰면 장치가 움직입니다. 여기서 값을 읽으면 장치의 상태를 압니다.

CPU 캐시는 CPU 안에 둔 작은 저장소입니다. 메모리에서 읽은 값의 복사본을 담아 둡니다. 다음에 같은 주소를 읽으면 메모리까지 가지 않고 이 복사본을 씁니다.

어느 주소를 캐시에 담을지와 쓰기를 언제 내려보낼지를 정한 규칙을 메모리 타입이라고 부릅니다. 구간마다 메모리 타입이 하나씩 붙습니다.

MTRR(Memory Type Range Register, 메모리 타입 범위 레지스터)은 물리 주소의 구간마다 메모리 타입을 적어 두는 레지스터 묶음입니다. MTRR 은 x86 계열 CPU 안에 들어 있습니다.

메모리 타입 다섯

MTRR 이 구간마다 적을 수 있는 메모리 타입은 다섯입니다. 타입마다 읽기와 쓰기를 다루는 방식이 다릅니다.

메모리 타입 읽을 때 쓸 때 흔히 붙는 구간
write-back 캐시에 담는다 캐시에만 쓰고 메모리에는 나중에 내려보낸다 보통 메모리
write-through 캐시에 담는다 캐시와 메모리에 같이 쓴다 쓰임이 드물다
write-protect 캐시에 담는다 캐시를 거치지 않고 메모리로 내보낸다 고쳐 쓰지 않는 롬
write-combining 캐시에 담지 않는다 여러 번의 쓰기를 모아 한 번에 내보낸다 그래픽 카드의 화면 메모리
uncacheable 캐시에 담지 않는다 매번 장치까지 보낸다 장치 레지스터

표의 첫 두 줄은 캐싱에서 말하는 쓰기 정책과 같은 이름입니다. write-through 는 쓸 때마다 원본까지 같이 고칩니다. write-back 은 캐시에만 고쳐 두었다가 나중에 원본으로 내려보냅니다.

나중에 내려보낸다는 발상은 소프트웨어 캐시의 write-behind와 닮았습니다. write-behind 는 애플리케이션이 캐시에 먼저 쓰고 데이터베이스에는 나중에 모아 쓰는 방식입니다.

캐시에 담으면 안 되는 구간

장치 레지스터 하나를 들어 MTRR 이 왜 필요한지 보입니다. 캐시에 담으면 읽기도 쓰기도 틀어지는 구간입니다.

많은 장치는 자기 레지스터를 물리 주소 구간에 내놓습니다. CPU 는 그 주소를 메모리처럼 읽고 써서 장치를 부립니다. 이 방식을 메모리 맵 입출력이라고 합니다.

장치 레지스터의 값은 장치가 스스로 바꿉니다. 네트워크 카드의 상태 레지스터는 패킷이 도착하면 값이 바뀝니다. 이 주소를 캐시에 담으면 CPU 는 캐시 속 옛 복사본만 읽습니다. 바뀐 값을 끝내 못 봅니다.

sequenceDiagram
    participant CPU
    participant 캐시
    participant 장치
    Note over 장치: 패킷이 와서 상태 값이 바뀐다
    CPU->>캐시: 상태 레지스터를 읽는다
    캐시-->>CPU: 캐시에 남은 옛 값
    Note over 캐시,장치: 읽기가 장치까지 가지 않는다

쓰기도 같습니다. 장치에 보낸 명령이 캐시에 머물러 있으면 장치는 명령을 받지 못합니다. 그래서 이런 구간은 uncacheable 로 둡니다. 읽기와 쓰기가 매번 장치까지 갑니다.

어려운 점은 CPU 가 주소만 보고는 그 구간이 메모리인지 장치인지 모른다는 것입니다. 누군가 알려 줘야 합니다. MTRR 은 그 알림을 주소 구간 단위로 적어 두는 방법입니다.

화면 메모리와 write-combining

화면 메모리 구간은 장치 레지스터와 성격이 또 다릅니다. 두 타입이 쓰기를 내보내는 횟수를 견줘 봅니다.

CPU 는 화면 메모리에 픽셀 값을 잔뜩 쓰기만 합니다. 다시 읽는 일은 드뭅니다. 그러니 캐시에 담아 봐야 얻는 것이 없습니다.

그렇다고 uncacheable 로 두면 쓰기 하나하나가 따로 장치까지 갑니다. 작은 쓰기가 줄줄이 나가서 느립니다.

sequenceDiagram
    participant CPU
    participant 화면 메모리
    Note over CPU,화면 메모리: uncacheable
    CPU->>화면 메모리: 픽셀 1
    CPU->>화면 메모리: 픽셀 2
    CPU->>화면 메모리: 픽셀 3

write-combining 은 가까운 주소의 쓰기를 CPU 안 버퍼에 모았다가 큰 덩어리 하나로 내보냅니다. 같은 쓰기 셋이 한 번에 나갑니다.

sequenceDiagram
    participant CPU
    participant 버퍼
    participant 화면 메모리
    Note over CPU,화면 메모리: write-combining
    CPU->>버퍼: 픽셀 1
    CPU->>버퍼: 픽셀 2
    CPU->>버퍼: 픽셀 3
    버퍼->>화면 메모리: 픽셀 1~3 을 한 덩어리로
    Note over 버퍼,화면 메모리: 나가는 순서는 쓴 순서와 다를 수 있다

대가는 순서입니다. 모은 쓰기가 나가는 순서가 프로그램이 쓴 순서와 다를 수 있습니다. 그래서 명령의 순서가 중요한 장치 레지스터에는 이 타입을 쓰지 않습니다.

고정 구간과 가변 구간

MTRR 레지스터는 구간을 적는 방식에 따라 두 무리로 나뉩니다. 가변 구간 쪽은 주소의 비트를 그려서 봅니다.

하나는 고정 구간 MTRR 입니다. 물리 주소의 맨 앞 1MB 를 미리 정해 둔 조각들로 잘라 둡니다. 조각마다 메모리 타입만 적습니다. 이 1MB 에는 롬과 옛 화면 메모리가 좁게 붙어 있어서 잘게 나눠 둡니다.

다른 하나는 가변 구간 MTRR 입니다. 레지스터 두 개가 한 쌍으로 구간 하나를 맡습니다. 한쪽에는 시작 주소와 메모리 타입을 적습니다. 다른 쪽에는 구간의 크기를 정하는 마스크를 적습니다.

비트 마스크는 주소의 어느 비트를 볼지 고르는 비트 묶음입니다. CPU 는 주소에서 마스크가 고른 비트만 떼어 내 시작 주소와 견줍니다. 같으면 그 주소가 이 구간에 든다고 봅니다.

마스크는 주소의 윗비트만 고릅니다. 아랫비트 몇 개는 고르지 않습니다. 고르지 않은 아랫비트는 무슨 값이어도 비교에 안 들어갑니다. 그래서 윗비트가 같은 주소는 모두 한 구간에 듭니다.

block-beta
columns 2
  hi["윗비트 · 시작 주소와 견준다"] lo["아랫비트 k 개 · 안 본다"]

안 보는 아랫비트가 k 개면 그 비트로 만들 수 있는 값은 2^k 가지입니다. 그래서 구간은 2^k 바이트가 됩니다. 아랫비트 27 개를 안 보면 2^27 바이트, 곧 128MB 가 한 구간입니다.

이 방식이라 구간 크기는 2의 거듭제곱이어야 합니다. 시작 주소의 안 보는 아랫비트는 모두 0 이어야 하므로 시작 주소도 그 크기의 배수여야 합니다.

3GB 구간은 한 쌍으로 못 적습니다. 3GB 는 2의 거듭제곱이 아니기 때문입니다. 2GB 한 쌍과 1GB 한 쌍으로 나눠 적습니다.

block-beta
columns 1
  y["0 ~ 2GB · 2GB 한 쌍"]
  x["2GB ~ 3GB · 1GB 한 쌍"]

주소 하나가 어느 타입을 받는지는 아래처럼 정해집니다. CPU 는 가변 구간 쌍을 모두 견준 뒤에 타입을 고릅니다.

flowchart TD
    A["맨 앞 1MB 밖의 물리 주소"] --> B["가변 구간 쌍을 모두 견준다"]
    B --> C{"맞는 쌍이 몇 개인가"}
    C -->|없다| F["기본 타입을 쓴다"]
    C -->|하나| D["그 쌍의 메모리 타입을 쓴다"]
    C -->|여럿| E["겹침 규칙으로 하나를 고른다"]

맞는 쌍이 여럿이면 구간이 겹친 것입니다. 겹친 쪽 가운데 하나가 uncacheable 이면 uncacheable 이 이깁니다. 가변 구간 쌍은 CPU 마다 몇 개뿐입니다.

어느 구간에도 들지 않는 주소는 기본 타입을 따릅니다. 기본 타입은 따로 정하는 레지스터가 있습니다.

펌웨어와 커널의 몫

MTRR 은 보통 펌웨어가 채웁니다. 펌웨어는 컴퓨터를 켜면 운영체제보다 먼저 도는 프로그램입니다. 메모리가 얼마나 꽂혔는지와 장치가 어느 주소를 받는지를 펌웨어가 가장 먼저 압니다.

MTRR 은 MSR(Model-Specific Register, 모델별 레지스터)의 하나입니다. MSR 은 CPU 설정을 담는 레지스터입니다. 전용 명령으로만 읽고 씁니다. 이 명령은 커널만 쓸 수 있습니다.

커널은 펌웨어가 채운 값을 읽어서 씁니다. 장치 드라이버가 요청하면 구간을 더하기도 합니다. 일반 프로그램은 MTRR 을 못 고칩니다.

sequenceDiagram
    participant 펌웨어
    participant MTRR
    participant 커널
    participant 드라이버
    펌웨어->>MTRR: 켜질 때 구간을 채운다
    커널->>MTRR: 채워진 값을 읽는다
    드라이버->>커널: 구간을 더해 달라고 한다
    커널->>MTRR: 구간을 더한다
    Note over MTRR,드라이버: 일반 프로그램은 MTRR 을 못 고친다

PAT 로 넘어간 역할

MTRR 에는 약점이 둘 있습니다. 구간을 적을 쌍이 몇 개뿐입니다. 구간 크기도 2의 거듭제곱에 묶여 있습니다. 장치가 늘고 주소 배치가 복잡해지면 칸이 모자랍니다.

페이지는 메모리를 일정한 크기로 자른 덩어리입니다. 운영체제는 메모리를 이 단위로 나눠 다룹니다.

페이지 테이블은 프로그램이 보는 주소의 페이지가 어느 물리 주소로 가는지 적은 표입니다. 프로그램이 보는 이 주소를 가상 주소라고 합니다.

PAT(Page Attribute Table, 페이지 속성 테이블)는 페이지 테이블의 줄마다 메모리 타입을 고르게 해 줍니다. 페이지마다 타입을 따로 적으니 구간 개수에 묶이지 않습니다.

요즘 x86 컴퓨터에서는 MTRR 이 하던 일을 PAT 가 넘겨받았습니다. 그래도 펌웨어는 MTRR 로 큰 틀을 잡아 둡니다. 그래서 한 주소에 두 겹의 타입이 올라갑니다.

flowchart TD
    A["물리 주소 하나"] --> B["MTRR · 이 주소가 든 구간의 타입"]
    A --> C["PAT · 페이지 테이블 줄에 고른 타입"]
    B --> D["CPU 가 둘을 합친다"]
    C --> D
    D --> E["최종 메모리 타입"]

두 겹이 다른 타입을 말하면 CPU 는 대개 캐시를 덜 쓰는 쪽을 따릅니다. 한쪽이 write-back 이고 다른 쪽이 uncacheable 이면 그 주소는 uncacheable 이 됩니다.

리눅스에서 보이는 MTRR

리눅스에서는 /proc/mtrr 파일로 지금 걸린 가변 구간을 볼 수 있습니다. 한 줄이 가변 구간 한 쌍입니다.

reg00: base=0x00000000 (   0MB), size= 128MB: write-back, count=1
reg01: base=0x08000000 ( 128MB), size=  64MB: write-back, count=1

첫 줄은 0 부터 128MB 까지를 write-back 으로 둡니다. 둘째 줄은 그 뒤 64MB 를 write-back 으로 둡니다. 둘 다 크기가 2의 거듭제곱입니다. 시작 주소도 크기의 배수입니다. 칸마다 읽는 법은 아래와 같습니다.

칸 뜻
reg00 몇 번째 가변 구간 쌍인가
base 구간이 시작하는 물리 주소. 괄호 안은 같은 값을 메가바이트 단위로 적은 것
size 구간의 크기
write-back 이 구간의 메모리 타입
count 이 구간을 쓰겠다고 등록한 수

예전에는 그래픽 드라이버가 이 파일에 줄을 써서 화면 메모리를 write-combining 으로 걸었습니다. 요즘 드라이버는 PAT 를 먼저 씁니다.

서버에서 MTRR 을 마주치는 때

애플리케이션 코드에는 MTRR 을 가리키는 문법이 없습니다. 펌웨어와 커널이 알아서 채웁니다. 백엔드 개발자가 이 이름을 보는 것은 대개 무언가 어긋났을 때입니다.

보통 메모리 구간이 잘못해서 uncacheable 로 잡히면 그 구간을 쓰는 모든 것이 캐시 없이 돕니다. 서버가 까닭 없이 눈에 띄게 느려집니다.

관련 항목

MTRR 이 구간마다 고르는 메모리 타입

write-back · write-through · write-protect · write-combining · uncacheable

MTRR 의 타입 이름이 빌려 온 캐싱 쓰기 정책

캐싱 · write-behind · write-around · 캐시 무효화

MTRR 이 캐시 사용을 정해 주는 하드웨어

CPU 캐시 · 캐시 라인 · 캐시 일관성 · 쓰기 버퍼 · 메모리 배리어

MTRR 이 캐시에서 떼어 두는 장치 메모리

메모리 맵 입출력 · 프레임버퍼 · 장치 레지스터 · DMA · 그래픽 카드

MTRR 이 구간을 가를 때 쓰는 주소 개념

물리 주소 · 주소 공간 · 비트 마스크 · 메모리 정렬 · 가상 주소

MTRR 을 담는 레지스터와 그 상위 구조

x86 · CPU · 레지스터 · MSR · 특권 명령

MTRR 의 역할을 넘겨받은 페이지 단위 수단

페이지 속성 테이블 · 페이지 테이블 · 페이지 테이블 엔트리 · 페이지

MTRR 을 채우고 고치는 소프트웨어

펌웨어 · BIOS · UEFI · 커널 · 디바이스 드라이버 · 하이퍼바이저

다른 이름: Memory Type Range Register · Memory Type Range Registers · 메모리 타입 범위 레지스터