페이지 교체 비용은 작업 집합이 물리 메모리를 넘는 순간 1000배가 된다

메모리가 모자라면 성능이 조금씩 나빠질 것 같다. 실제로는 그렇지 않다. 물리 메모리를 512MiB로 묶어 두고 프로그램이 실제로 쓰는 메모리만 늘려 가며 같은 작업을 세 바퀴씩 돌렸다

  작업 집합  384 MiB |  3바퀴       7 ms | 부 폴트        3 | 주 폴트       0 | 스왑 0
  작업 집합  640 MiB |  3바퀴    7854 ms | 부 폴트   430029 | 주 폴트   61502 | 스왑 130M

작업 집합이 67% 늘었는데 걸린 시간은 1,122배가 됐다. 그 사이에 성능이 완만하게 떨어지는 구간은 없다. 들어가느냐 넘치느냐뿐이다

넘치기 전까지는 아무 일도 일어나지 않는다는 점도 눈여겨볼 만하다. 384MiB짜리 작업을 세 바퀴 도는 동안 페이지 폴트가 세 번밖에 없었다. 필요한 페이지만 올려 두고 나머지는 건드리지 않는 구조가 그만큼 잘 맞아떨어진다는 뜻이고, 그 구조가 디맨드 페이징이다

필요할 때만 올리는 방식, 꽉 찼을 때 무엇을 내보낼지 정하는 정책, 그리고 내보낸 것을 계속 다시 불러오게 되는 상태를 차례로 다룬다. 페이지 교체 비용이 언제 0에 가깝고 언제 전부를 잡아먹는지가 이 세 가지로 갈린다. 페이지 테이블 항목의 비트들은 직접 켜고 끄고 읽어 가며 확인했다

이 글에서 자주 나오는 용어

  • 디맨드 페이징(Demand Paging): 실제로 접근할 때까지 물리 메모리를 붙이지 않는 방식
  • 페이지 폴트(Page Fault): 접근한 페이지가 물리 메모리에 없을 때 발생하는 인터럽트
  • 부 폴트 / 주 폴트(minor / major fault): 저장장치를 읽지 않고 끝나는 폴트가 부 폴트, 스왑이나 파일에서 실제로 읽어 와야 하는 폴트가 주 폴트다
  • 스왑인 / 스왑아웃: 스왑 영역에서 물리 메모리로 가져오는 것과 그 반대
  • 접근 비트 / 변경 비트: 그 페이지를 읽거나 실행했는지, 썼는지를 하드웨어가 표시해 두는 비트
  • 작업 집합(Working Set): 프로세스가 지금 실제로 돌아가며 쓰고 있는 페이지의 묶음
  • 쓰레싱(Thrashing): 페이지를 내보내고 다시 불러오는 데 시간을 다 쓰느라 일이 진행되지 않는 상태

메모리를 할당해도 물리 메모리는 붙지 않는다

프로그램이 1GiB를 달라고 하면 운영체제는 주소 공간만 표시해 두고 물리 메모리는 붙이지 않는다. 실제로 그 주소를 건드릴 때가 되어서야 페이지 하나씩 붙는다. 1GiB를 잡고, 한 번 훑고, 다시 한 번 훑으면서 재 봤다.

  시작                        VmRSS     1424 kB   부 폴트       76
  1 GiB 할당 직후              VmRSS     1748 kB   부 폴트       87
  1 GiB 전체를 한 번 만진 뒤     VmRSS  1050388 kB   부 폴트   262233
  → 첫 접근 6823 ms  (페이지 262144개, 한 페이지당 26.03 us)
  같은 곳을 다시 만진 뒤          VmRSS  1050452 kB   부 폴트   262234
  → 두 번째 접근 8 ms  (한 페이지당 0.03 us)

할당 직후 VmRSS는 324KiB만 늘었다. 1GiB를 요청했는데 붙은 물리 메모리는 사실상 없다. 전체를 한 번 만지고 나서야 1,050,388KiB가 되고 부 폴트가 262,146번 늘었다. 1GiB를 4KiB로 나누면 정확히 262,144개이므로, 페이지마다 한 번씩 폴트가 났다는 뜻이다

시간 차이가 이 구조의 값을 보여 준다. 첫 바퀴는 페이지당 26.03마이크로초, 두 번째 바퀴는 0.03마이크로초다. 870배 차이다. 첫 바퀴의 26마이크로초가 폴트를 처리하고 물리 페이지를 찾아 붙이고 0으로 채우는 비용이고, 두 번째 바퀴에는 그 일이 이미 끝나 있다

읽기만 하는 경우는 한 단계 더 나간다. 갓 할당한 256MiB를 전부 읽기만 했을 때의 Rss 변화다

  시작                        Rss    1744 kB
  익명 256 MiB를 읽기만 한 뒤    Rss    1748 kB
  같은 익명 영역에 쓴 뒤          Rss  263892 kB

256MiB를 전부 읽었는데 물리 메모리가 4KiB만 늘었다. 아직 아무것도 쓰지 않은 페이지는 전부 0이므로, 커널이 0으로 채워진 페이지 하나를 만들어 두고 모든 읽기를 거기로 보낸다. 쓰기가 일어나는 순간에야 그 페이지에만 진짜 메모리를 준다. 물리 메모리를 아끼는 장치가 할당 시점, 읽기 시점, 쓰기 시점에 걸쳐 세 겹으로 깔려 있는 셈이다

이 방식이 성립하는 근거는 지역성이다. 프로그램은 주소 공간 전체를 고르게 훑지 않고 좁은 구역을 집중해서 쓴다. 방금 접근한 자리 근처를 다시 건드릴 확률이 높은 것이 공간 지역성이고, 방금 접근한 자리를 다시 건드릴 확률이 높은 것이 시간 지역성이다. 두 성질이 있으니 전부 올려 두지 않아도 대부분의 접근이 이미 올라온 페이지에서 끝난다. 실제로 이 글 뒤쪽의 쓰레싱 실험에서, 작업 집합이 물리 메모리에 들어가는 동안에는 수백만 번 접근하는 세 바퀴 내내 페이지 폴트가 세 번밖에 나지 않았다. 지역성이 무너지면 어떻게 되는지도 같은 표에 함께 있다

페이지 테이블 항목은 프레임 번호만 담고 있지 않다

이 동작이 가능하려면 페이지 테이블 항목이 “지금 물리 메모리에 있는가”를 표시할 수 있어야 한다. 실제 항목에는 프레임 번호 말고도 비트가 여럿 들어 있다

비트
존재(유효) 비트이 페이지가 지금 물리 메모리에 있는가
접근 비트메모리에 올라온 뒤 읽거나 실행한 적이 있는가
변경 비트메모리에 올라온 뒤 쓴 적이 있는가
읽기·쓰기·실행 비트이 페이지에 허용된 접근 종류

존재 비트의 방향은 헷갈리기 쉬운데 규약은 하나다. 1이면 물리 메모리에 있고 0이면 없다. 리눅스가 /proc/PID/pagemap으로 노출하는 형식이 그대로 그 규약을 따른다. 63번 비트가 존재, 62번 비트가 스왑이고, 존재하는 페이지는 하위 비트에 프레임 번호가, 스왑에 나간 페이지는 같은 자리에 스왑 위치가 들어간다(pagemap, Linux Kernel documentation)

접근 비트는 직접 켜고 끄며 확인할 수 있다. /proc/PID/clear_refs에 1을 쓰면 그 프로세스의 모든 페이지에서 참조 표시와 하드웨어 접근 비트가 지워지고(proc_pid_clear_refs(5)), smaps_rollupReferenced 항목으로 몇 킬로바이트가 표시돼 있는지 볼 수 있다. 256MiB를 잡고 네 단계로 재 봤다

  256 MiB 전체를 만진 직후          Referenced   263568 kB
  clear_refs 로 접근 비트를 지운 뒤  Referenced      112 kB
  앞쪽 16 MiB만 다시 만진 뒤        Referenced    16608 kB
  지운 뒤 256 MiB를 읽기만 한 뒤     Referenced   262256 kB

전부 만지면 263,568KiB가 표시되고, 지우면 112KiB로 떨어지고, 앞쪽 16MiB만 다시 만지면 정확히 그만큼인 16,608KiB만 올라온다. 운영체제가 “최근에 어떤 페이지를 썼는가”를 알아내는 방법이 이것이다. 일정 시점에 비트를 지워 두고 나중에 다시 보면 그사이에 쓰인 페이지만 켜져 있다

마지막 줄이 접근 비트와 변경 비트의 차이를 가른다. 지운 뒤 읽기만 했는데 262,256KiB가 켜졌다. 접근 비트는 읽어도 켜진다. 변경 비트는 쓰기에만 켜지고, 쓴 적이 없는 페이지는 스왑 영역에 굳이 써 둘 필요 없이 버려도 되므로 교체할 때 값이 싸다. 뒤에서 나올 향상된 클락 알고리즘이 이 두 비트를 함께 보는 이유다

스왑에 나간 페이지는 주소 대신 위치를 들고 있다

물리 메모리가 꽉 찼는데 새 페이지를 올려야 하면, 지금 올라와 있는 페이지 중 하나를 저장장치의 스왑 영역으로 내보낸다. 내보내는 것이 스왑아웃, 다시 가져오는 것이 스왑인이다. 나간 페이지를 다시 건드리면 페이지 폴트가 나고, 이번에는 저장장치를 읽어야 하므로 주 폴트로 분류된다

내보낸 페이지의 페이지 테이블 항목은 어떻게 될까. 실제로 확인했다. 물리 메모리를 512MiB로 묶어 둔 상태에서 768MiB를 훑은 뒤, 자기 페이지들을 하나씩 조회했다

  768 MiB 중  물리 메모리에 있음  123982개 ( 484 MiB)
              스왑에 나가 있음    72626개 ( 283 MiB)
              어디에도 없음          0개

  물리에 있는 페이지 예: 가상 7f5ee2200000  엔트리 810000000012c3d0
     → 63번 비트(존재) 1, 프레임 번호 1229776
  스왑에 나간 페이지 예: 가상 7f5ef0942000  엔트리 40000000009446a0
     → 62번 비트(스왑) 1, 스왑 장치 0번의 오프셋 303669

두 엔트리의 첫 글자가 다르다. 8로 시작하는 쪽은 63번 비트가 켜져 있어 물리 메모리에 있고, 하위 비트에 프레임 번호 1229776이 들어 있다. 4로 시작하는 쪽은 62번 비트가 켜져 있어 스왑에 나가 있고, 같은 자리에 프레임 번호 대신 스왑 장치 번호와 오프셋이 들어 있다. 페이지 테이블 항목 하나가 “메모리 어디” 또는 “디스크 어디” 둘 중 하나를 가리키는 구조다

프로세스가 이 주소를 다시 건드리면 순서는 이렇게 된다

  프로세스가 접근
        │
        ▼
  페이지 테이블 확인 → 존재 비트 0
        │
        ▼
  Page Fault 인터럽트 → 프로세스는 대기 상태로
        │
        ▼
  빈 프레임 확보 (없으면 다른 페이지를 스왑아웃)
        │
        ▼
  스왑 영역에서 읽어 와 프레임에 넣는다
        │
        ▼
  페이지 테이블 갱신 (존재 비트 1, 프레임 번호 기록)
        │
        ▼
  프로세스를 준비 상태로 되돌려 같은 명령을 다시 실행

중간의 “빈 프레임 확보”에서 무엇을 내보낼지 정하는 것이 페이지 교체 정책이다

어떤 페이지를 내보낼지는 미래를 모른 채 정해야 한다

교체 정책은 몇 가지로 나뉜다. 무작위로 고르는 방법은 자주 쓰는 페이지도 걸리므로 거의 쓰이지 않는다. 가장 오래 머문 페이지를 내보내는 FIFO는 구현이 간단하다. 앞으로 가장 오랫동안 쓰이지 않을 페이지를 내보내는 Optimum은 미래를 알아야 하므로 구현할 수 없고, 다른 정책의 성능을 재는 기준으로만 쓴다. 가장 오랫동안 쓰이지 않은 페이지를 내보내는 LRU는 시간 지역성에 기대는 방법이다

같은 참조 순서를 네 정책으로 돌려 페이지 폴트를 세 봤다

  참조 순서 A B C A C D A D C A B   (프레임 3개)
    Optimum  Page Fault  5회
    LRU      Page Fault  5회
    Clock    Page Fault  6회
    FIFO     Page Fault  6회

LRU가 Optimum과 같은 5회를 냈다. 최근에 쓴 것이 앞으로도 쓰일 확률이 높다는 가정이 이 참조 순서에서는 잘 맞은 것이다. 다만 이 결과는 참조 순서 하나에 대한 것이고, 지역성이 없는 접근 패턴에서는 LRU도 Optimum과 멀어진다

빌레이디의 역설은 FIFO만의 문제가 아니다

FIFO에는 이상한 성질이 있다. 프레임을 늘리면 폴트가 줄어야 할 것 같은데 오히려 늘어나는 참조 순서가 존재한다. 빌레이디의 역설이다

  참조 순서 1 2 3 4 1 2 5 1 2 3 4 5
                    프레임 3개    프레임 4개
    FIFO              9회         10회  ← 프레임을 늘렸는데 폴트가 늘었다
    LRU              10회          8회
    Optimum           7회          6회

FIFO만 9회에서 10회로 늘었고 LRU와 Optimum은 줄었다. 여기까지는 잘 알려져 있는데, 이것이 FIFO만의 성질이라고 하면 정확하지 않다. 무작위 참조열을 만들어 가며 각 정책에서 같은 현상이 일어나는 순서를 찾아봤다

프레임을 하나 늘렸을 때 오히려 Page Fault가 늘어나는 참조열이 있는가

  FIFO     있음:  3 2 1 1 4 2 3 2 0 3 2 1 4 0
           프레임 3개 → 9회,  프레임 4개 → 10회
  Clock    있음:  1 1 0 2 4 0 1 4 0 3 1 1 0 2
           프레임 3개 → 7회,  프레임 4개 → 8회
  LRU      30000개 무작위 참조열에서 찾지 못했다
  Optimum  5000개 무작위 참조열에서 찾지 못했다

클락 알고리즘에서도 나온다. LRU와 Optimum에서는 탐색한 범위 안에서 나오지 않았다. 못 찾은 것이 아니라 존재하지 않는데, 이유를 OSTEP이 스택 속성으로 설명한다

LRU has what is known as a stack property. For algorithms with this
property, a cache of size N + 1 naturally includes the contents of a
cache of size N. Thus, when increasing the cache size, hit rate will
either stay the same or improve. FIFO and Random (among others) clearly
do not obey the stack property, and thus are susceptible to anomalous
behavior.

출처는 OSTEP, Swapping: Policies다. 프레임 N+1개짜리 캐시가 항상 N개짜리 캐시의 내용을 포함하면 프레임을 늘렸을 때 적중률이 나빠질 수 없다. LRU와 Optimum이 이 성질을 갖고 FIFO는 갖지 않는다. 클락은 FIFO를 접근 비트로 보정한 방식이라 이 성질을 물려받지 못했고, 그래서 위 탐색에서 걸렸다. 기억해 둘 것은 “FIFO에서만”이 아니라 “스택 속성이 없는 정책에서”다

클락 알고리즘은 접근 비트 하나로 LRU를 흉내 낸다

LRU가 좋다면 그대로 쓰면 될 것 같지만 구현이 걸린다. 페이지마다 마지막 사용 시각을 기록하려면 페이지 테이블 항목에 시간을 담을 비트가 필요하고, 그 비트를 충분히 두어도 오래 돌면 넘친다

그래서 앞 절에서 확인한 접근 비트 하나만 쓴다. 페이지들을 원형으로 늘어놓고 포인터 하나를 돌린다. 포인터가 가리키는 페이지의 접근 비트가 1이면 0으로 바꾸고 지나가고, 0이면 그 페이지를 내보낸다

        ┌──▶ 페이지 A (접근 1) ──┐
        │                     ▼
   페이지 D (접근 0)        페이지 B (접근 1)
        ▲                      │
        └──── 페이지 C (접근 0) ◀─┘
                  ▲
              클락 핸드

  핸드가 A를 본다 → 접근 비트 1 → 0으로 바꾸고 다음으로
  핸드가 B를 본다 → 접근 비트 1 → 0으로 바꾸고 다음으로
  핸드가 C를 본다 → 접근 비트 0 → C를 내보낸다

비트를 지우는 일은 따로 도는 청소 작업이 아니라 핸드가 지나가면서 한다. 한 바퀴 도는 사이에 다시 접근된 페이지는 비트가 켜져 살아남고, 그렇지 않은 페이지가 걸린다. 시간을 기록하지 않고도 “최근에 안 쓴 것”을 골라내는 방법이다

향상된 클락은 여기에 변경 비트를 더해 네 등급으로 나눈다. 내보내기 좋은 순서는 접근 0·변경 0, 접근 0·변경 1, 접근 1·변경 0, 접근 1·변경 1이다. 접근도 변경도 없는 페이지는 스왑에 쓰지 않고 그냥 버려도 되므로 가장 싸다. 2차 기회 알고리즘은 같은 발상을 FIFO에 적용해, 폴트 없이 접근에 성공한 페이지를 큐의 맨 뒤로 보내 수명을 늘려 준다

접근 비트를 하드웨어가 지원하지 않는 시스템에서는 이 방법들을 쓸 수 없고 FIFO로 돌아가야 한다. FIFO의 성능을 높이려는 연구가 계속 나온 이유가 그것이다

리눅스는 페이지를 활성 목록과 비활성 목록 두 개로 나눠 관리하는 방식으로 LRU를 근사해 왔고, 6.1에서 세대 개념을 도입한 Multi-Gen LRU가 선택 가능한 대안으로 들어갔다(Linux 6.1, KernelNewbies). 가장 차가운 세대부터 회수하는 구조다(Multi-Gen LRU, Linux Kernel documentation). 이 글을 쓴 환경의 커널에는 /sys/kernel/mm/lru_gen이 없어 활성화 여부를 확인하지 못했다

작업 집합이 물리 메모리를 넘는 순간 성능이 절벽에서 떨어진다

교체 정책이 아무리 좋아도 물리 메모리 자체가 부족하면 소용이 없다. 내보낸 페이지를 곧바로 다시 불러오게 되면 일은 진행되지 않고 스왑만 오간다. 쓰레싱이다

이 지점을 재려고 cgroup으로 물리 메모리 한도를 512MiB로 묶고 작업 집합만 키웠다. 프로그램은 매번 같은 일을 한다. 잡은 영역의 페이지를 처음부터 끝까지 순서대로 훑는 것을 세 바퀴 반복한다

물리 메모리 한도를 512 MiB로 묶고, 작업 집합만 키워 가며 3바퀴씩 돈다

  작업 집합  256 MiB |  3바퀴       6 ms | 부 폴트        3 | 주 폴트       0 | 스왑 0
  작업 집합  384 MiB |  3바퀴       7 ms | 부 폴트        3 | 주 폴트       0 | 스왑 0
  작업 집합  512 MiB |  3바퀴    2235 ms | 부 폴트   235672 | 주 폴트    4456 | 스왑 1.5M
  작업 집합  640 MiB |  3바퀴    7854 ms | 부 폴트   430029 | 주 폴트   61502 | 스왑 130M
  작업 집합  768 MiB |  3바퀴    9932 ms | 부 폴트   516067 | 주 폴트   73768 | 스왑 259M

384MiB까지는 폴트가 세 번뿐이고 7밀리초에 끝난다. 한도에 닿는 512MiB에서 2,235밀리초로 뛰고, 640MiB에서 7,854밀리초가 된다. 주 폴트가 0에서 61,502로 올라가는 자리가 정확히 그 지점이다

주 폴트가 하는 일을 생각하면 곡선의 모양이 설명된다. 부 폴트는 저장장치를 읽지 않고 끝나지만 주 폴트는 스왑에서 실제로 읽어 와야 하고, 그동안 프로세스는 대기 상태로 내려간다. 순서대로 훑는 접근 패턴에서는 방금 내보낸 페이지를 다음 바퀴에 다시 부르게 되므로, 한도를 넘는 순간부터 거의 모든 접근이 주 폴트가 된다

이 상태는 커널이 따로 재고 있기도 하다. 리눅스는 메모리 때문에 멈춰 있던 시간의 비율을 /proc/pressure/memory에 적어 둔다. 768MiB로 돌리는 동안 읽은 값이다

  some avg10=13.76 avg60=9.40 avg300=3.48 total=12942581
  some avg10=14.53 avg60=9.68 avg300=3.58 total=13228705
  some avg10=14.43 avg60=9.82 avg300=3.65 total=13500763

some은 일부 작업이 메모리 때문에 멈춰 있던 시간의 비율이고(PSI, Linux Kernel documentation), 실험 전에는 avg10이 0.00이었다. 10초 평균으로 14% 가까이가 메모리를 기다리는 데 쓰였다는 뜻이다. 같은 실험이 남긴 또 다른 흔적은 회수됐다가 다시 불려 들어온 페이지 수다

$ grep workingset_refault_anon /proc/vmstat
workingset_refault_anon 4909474

490만 페이지가 내보내졌다가 되돌아왔다. 작업 집합이라는 개념이 커널 안에서 이런 카운터로 존재한다. 프로세스가 지금 돌아가며 쓰는 페이지들을 한 묶음으로 보고 그 묶음을 메모리에 올려 두려는 것이 워킹셋이고, 묶음이 들어가지 않으면 위 표의 아래쪽 줄들이 된다

여기서 따라 나오는 실무적인 판단이 하나 있다. 램을 늘리면 쓰레싱이 시작되는 지점이 뒤로 밀리므로 위 표에서 아래쪽에 있던 작업이 위쪽으로 올라온다. 반대로 이미 위쪽에 있는 작업, 즉 작업 집합이 이미 물리 메모리에 들어가는 상태라면 램을 두 배로 늘려도 6밀리초가 3밀리초가 되지는 않는다. 메모리 증설로 얻는 이득은 연속적이지 않고, 절벽의 이쪽에 있느냐 저쪽에 있느냐로 갈린다

측정은 cgroup으로 한도를 건 것이고 시스템 전체의 메모리가 실제로 고갈된 상황은 아니다. 시스템 전체가 같은 상태에 빠지면 커널이 어느 시점에 OOM 킬러를 불러 프로세스 하나를 종료시키는데, 그 지점까지는 재지 않았다

리눅스는 물리 메모리보다 많이 약속한다

가상 메모리의 크기를 물리 메모리와 스왑 영역의 합으로 설명하는 경우가 있다. 실제로 담을 수 있는 양은 그 합이 맞지만, 프로세스가 요청할 수 있는 양은 그것과 다르다. 물리 메모리 8GiB에 스왑 2GiB인 이 장비에서 요청해 봤다

  이 시스템의 물리 메모리 + 스왑 = 약 10 GiB

     16 GiB 요청 → 성공
           VmSize  16779916 kB   VmRSS     1564 kB
   1024 GiB 요청 → 성공
           VmSize 1073744524 kB   VmRSS     1756 kB

1TiB를 요청해도 통과한다. 주소 공간만 잡아 두고 물리 메모리는 건드릴 때 붙이므로, 요청 시점에는 있을 수 없는 양도 약속할 수 있다. 이것을 오버커밋이라고 하고, 리눅스의 기본값은 명백히 터무니없는 요청만 거절하는 휴리스틱 방식이다(Overcommit Accounting, Linux Kernel documentation). 약속을 지키지 못하는 상황이 실제로 오면 그때 OOM 킬러가 처리한다

그래서 두 문장을 갈라 둬야 한다. 프로세스 하나가 쓸 수 있는 주소 공간의 폭은 CPU의 주소 비트가 정하고, 시스템이 실제로 담을 수 있는 양은 물리 메모리와 스왑의 합이 정한다. 앞의 것이 뒤의 것보다 훨씬 크고, 그 차이를 메우는 장치가 디맨드 페이징이다

마무리

디맨드 페이징은 약속과 실제를 분리한다. 1TiB를 약속해도 실제로 붙는 것은 건드린 페이지뿐이고, 그래서 384MiB짜리 작업이 세 바퀴 도는 동안 폴트가 세 번으로 끝난다

분리가 무너지는 곳이 작업 집합이 물리 메모리를 넘는 지점이다. 그 앞뒤로 7밀리초와 7,854밀리초가 나뉜다. 메모리가 부족한지 아닌지를 판단할 때 사용률보다 먼저 볼 것은 주 폴트 횟수다. 0이면 아직 절벽 이쪽이다


다음 편 예고

마지막 편은 입출력 장치와 파일 시스템이다. 버스와 DMA, 하드디스크와 플래시 메모리의 구조, 파일 디스크립터가 실제로 가리키는 것, 그리고 파일을 지워도 데이터가 남는 이유를 다룬다


출처와 범위

이 시리즈는 인프런의 그림으로 쉽게 배우는 운영체제(감자님) 강의를 따라 정리한 것이고, 강의 전체를 일곱 편으로 나눈 여섯 번째 편이다. 개념 구성과 예시의 출처는 강의이며, 커널 동작과 인터페이스를 말하는 문장은 본문에 건 커널 문서와 man 페이지로 확인했다. 코드 블록에 실은 /proc/self/pagemap, smaps_rollup, /proc/pressure/memory, /proc/vmstat 출력과 시간 측정값은 물리 메모리 8GiB에 스왑 2GiB를 붙인 2코어 x86-64 리눅스 가상 머신에서 직접 돌린 결과 그대로다. 쓰레싱 실험은 cgroup으로 물리 메모리 한도를 512MiB로 건 것이고 시스템 전체를 고갈시킨 것이 아니다. 페이지 교체 정책 비교와 빌레이디의 역설 탐색은 직접 구현한 파이썬 시뮬레이터로 계산했고, 무작위 탐색은 각각 30,000개와 5,000개 참조열까지만 돌렸으므로 “찾지 못했다”는 그 범위 안에서의 결과다. 측정에 쓴 C·파이썬 코드는 본문에 인용한 부분 외에는 싣지 않았고 공개 저장소도 없다

참고 자료