Chapter sebelumnya menutup dengan pertanyaan tersisa: siapa yang mencegah proses “root” di user namespace menghabisi RAM mesin? Jawabannya ada pada subsistem pengelola memori kernel — wasit yang membagi RAM fisik kepada semua proses tanpa pernah mempercayai salah satunya. Di luar sana beredar mitos bahwa Linux boros memori karena kolom free yang selalu tampak rendah; setelah memahami cara kernel bekerja, kita justru akan menganggap free yang tinggi sebagai pemborosan. Pada chapter ini kita bedah virtual memory dan demand paging, page cache, perilaku swap, skor OOM killer, hingga Transparent Huge Pages.

Alamat Virtual, Page Table, dan Frame Fisik #

Setiap proses Linux tidak pernah menyentuh alamat RAM fisik secara langsung. Program beroperasi pada alamat virtual miliknya sendiri — ruang raksasa yang tampak milik proses itu seorang diri — dan kernel menerjemahkan setiap akses lewat tabel penerjemah bernama page table, empat kilobyte demi empat kilobyte (satu page). Penerjemahan ini tiga keuntungan sekaligus: proses saling terisolasi, ruang alamat bisa jauh lebih besar dari RAM fisik, dan kernel bebas memindahkan isi page ke disk atau memadatkannya kapan pun diperlukan.

Karena itu dua ukuran memori proses yang kita pantau melalui /proc/<pid>/status berbeda makna: VmSize adalah total ruang alamat virtual yang telah dipetakan, sedangkan VmRSS (resident set size) hanya menghitung page yang benar-benar menempati RAM. Rincian seluruh field ini terdokumentasi di proc(5) . Berikut contoh tampilan beberapa field Vm* untuk sebuah proses biasa:

text
VmPeak:   8236 kB
VmSize:   7932 kB
VmRSS:     980 kB
VmData:   1120 kB
VmSwap:      0 kB

VmSize yang mendekati 8 MB dengan VmRSS hanya sekitar 1 MB adalah pemandangan normal: sebagian besar ruang virtual belum pernah disentuh, dan kernel tidak menganggarkan RAM untuknya.

Demand Paging: Memori Baru Nyata Saat Disentuh #

malloc() di C kelihatan seperti membeli RAM, padahal ia hanya mencadangkan ruang alamat virtual. RAM fisik baru dialokasikan ketika page pertama kali ditulis — momen ini memicu page fault yang ditangani kernel dengan menyediakan frame fisik kosong lalu memperbarui page table. Program kecil berikut membuktikannya dengan membaca VmRSS milik dirinya sendiri di tiga titik waktu:

c
/* vmrss-demo.c: membuktikan demand paging lewat VmRSS di /proc/self/status */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static long baca_vmrss_kb(void)
{
    FILE *f = fopen("/proc/self/status", "r");
    char baris[256];
    long kb = -1;

    if (f == NULL) {
        return -1;
    }
    while (fgets(baris, sizeof baris, f) != NULL) {
        if (sscanf(baris, "VmRSS: %ld kB", &kb) == 1) {
            break;
        }
    }
    fclose(f);
    return kb;
}

int main(void)
{
    const size_t ukuran = (size_t)8 * 1024 * 1024; /* 8 MiB */
    char *p = malloc(ukuran);
    long sebelum, setelah_alokasi, setelah_sentuh;

    if (p == NULL) {
        fprintf(stderr, "malloc gagal\n");
        return 1;
    }
    sebelum = baca_vmrss_kb();
    setelah_alokasi = baca_vmrss_kb();
    memset(p, 1, ukuran); /* sentuh setiap byte -> page fault per page */
    setelah_sentuh = baca_vmrss_kb();

    printf("malloc 8 MiB, belum disentuh : RSS naik >= 8192 kB? %s\n",
           (setelah_alokasi - sebelum >= 8192) ? "ya" : "tidak");
    printf("memset 8 MiB, menyentuh      : RSS naik >= 8192 kB? %s\n",
           (setelah_sentuh - setelah_alokasi >= 8192) ? "ya" : "tidak");
    free(p);
    return 0;
}

Program di atas tersimpan sebagai vmrss-demo.c pada bundle chapter ini. Kompilasi dan jalankan:

bash
gcc -o vmrss-demo vmrss-demo.c && ./vmrss-demo

Output yang Dihasilkan:

text
malloc 8 MiB, belum disentuh : RSS naik >= 8192 kB? tidak
memset 8 MiB, menyentuh      : RSS naik >= 8192 kB? ya

Bedah program di atas:

  1. baca_vmrss_kb(): membuka /proc/self/status lalu memindai baris berawalan VmRSS:; sscanf mengambil angka di antara nama field dan satuan kB.
  2. malloc(ukuran): mencadangkan 8 MiB ruang virtual — belum ada satu page pun di RAM.
  3. memset(p, 1, ukuran): menulis setiap byte sehingga 2048 page (8 MiB ÷ 4 kB) terisi berurutan; inilah pemicu page fault yang memindahkan VmRSS.
  4. Pemeriksaan >= 8192: program tidak mencetak nilai VmRSS mentahnya karena angka itu berubah-ubah antar eksekusi; yang stabil dan bermakna adalah apakah kenaikannya melewati 8192 kB (8 MiB).

Angka VmRSS yang berbeda pada setiap eksekusi itu normal — ia bergantung pada page yang kebetulan sudah terpakai oleh libc dan posisi alokasi. Yang penting bagi kita adalah polanya: alokasi murni tidak menaikkan RSS, sentuhan pertamalah yang menghabiskan RAM.

Page Cache: Mengapa free Rendah Itu Normal #

Setiap kali proses membaca file, kernel tidak membuang datanya begitu selesai — page berisi isi file disimpan di page cache supaya bacaan berikutnya tidak menyentuh disk lagi. Page-page ini terhitung sebagai “terpakai” pada statistik memori, padahal kernel melepasnya seketika saat aplikasi membutuhkan RAM. Maka free yang rendah bukan tanda bahaya, melainkan tanda kernel memanfaatkan RAM yang menganggur — persis seperti cache CPU pada chapter CPU monitoring , hanya saja untuk disk. Simulasi berikut membaca snapshot /proc/meminfo lalu menghitung porsinya:

bash
cat > meminfo.txt <<'FIX'
MemTotal:        4096000 kB
MemFree:          262144 kB
MemAvailable:    3328000 kB
Buffers:            51200 kB
Cached:           2200000 kB
SwapCached:             0 kB
SReclaimable:      148480 kB
FIX
awk '/^(MemTotal|MemFree|Buffers|Cached|SReclaimable):/ { v[$1] = $2 }
END {
    total = v["MemTotal:"]
    bebas = v["MemFree:"]
    cache = v["Cached:"] + v["Buffers:"] + v["SReclaimable:"]
    printf "RAM total            : %d kB\n", total
    printf "benar-benar lowong   : %d kB (%d%%)\n", bebas, bebas * 100 / total
    printf "dipakai page cache   : %d kB (%d%%)\n", cache, cache * 100 / total
}' meminfo.txt

Output yang Dihasilkan:

text
RAM total            : 4096000 kB
benar-benar lowong   : 262144 kB (6%)
dipakai page cache   : 2399680 kB (58%)

Mari kita baca blok di atas:

  1. v[$1] = $2: awk menyimpan angka ke array asosiatif dengan nama field (misalnya MemTotal:) sebagai kunci — pola v[...] ini pola yang sama dengan chapter cgroups .
  2. cache = Cached + Buffers + SReclaimable: tiga komponen page cache — isi file biasa, buffer metadata, dan slab yang bisa direklamasi.
  3. Baris printf terakhir: hampir 60% RAM “terpakai” oleh cache yang sepenuhnya bisa dibebaskan — hanya 6% yang benar-benar lowong, dan keduanya sehat.

Swap dan Dorongan swappiness #

Saat RAM menipis, kernel memindahkan page yang lama tak disentuh ke area swap di disk, lalu memanggilnya kembali bila dibutuhkan — mekanisme lengkapnya dibedah pada chapter Swap Space . Seberapa agresif kernel melakukan ini diatur sysctl vm.swappiness bernilai 0–200, dengan 60 sebagai bawaan: makin tinggi nilainya, makin berani kernel menukar page anonim yang lama tak disentuh ke swap demi mempertahankan page cache yang masih ramai dipakai. Lingkungan latihan kita sendiri berjalan tanpa swap sama sekali (/proc/swaps hanya mencetak baris judulnya), sehingga tekanan memori langsung bermuara ke wasit terakhir di bawah ini.

OOM Killer: Hakim Saat Memori Habis #

Ketika permintaan alokasi tidak bisa dipenuhi dan tidak ada swap untuk diselamatkan, kernel menjatuhkan keputusan drastis: memilih satu proses korban dan membunuhnya agar sisanya selamat — OOM killer (out of memory). Kandidat korban dinilai lewat skor pada /proc/<pid>/oom_score yang kira-kira sebanding dengan porsi RAM yang dipakai proses; sysadmin boleh memiringkan pengadilan lewat oom_score_adj dari -1000 (hampir kebal) hingga +1000 (santapan pertama). Simulasi berikut meniru rumus pilihannya atas snapshot daftar proses:

bash
cat > proses-aktif.txt <<'FIX'
# pid  nama          rss_kB  oom_score_adj
    1  systemd         10000      0
  812  server-web     300000      0
  940  database       400000      0
1055  backup-notbenar 250000    800
1200  shell-budi       40000      0
FIX
awk 'NR > 1 {
    total += $3
    baris[NR] = $0
}
END {
    printf "total RSS semua proses: %d kB\n\n", total
    printf "%-5s %-15s %8s %5s %6s\n", "pid", "nama", "rss_kB", "adj", "skor"
    for (i = 2; i <= NR; i++) {
        split(baris[i], f, " ")
        skor = f[3] * 1000 / total + f[4]
        printf "%-5s %-15s %8d %5d %6.0f\n", f[1], f[2], f[3], f[4], skor
        if (skor > maks) { maks = skor; korban = f[2] }
    }
    printf "\nkorban OOM killer: %s (skor %.0f)\n", korban, maks
}' proses-aktif.txt

Output yang Dihasilkan:

text
total RSS semua proses: 1000000 kB

pid   nama              rss_kB   adj   skor
1     systemd            10000     0     10
812   server-web        300000     0    300
940   database          400000     0    400
1055  backup-notbenar   250000   800   1050
1200  shell-budi         40000     0     40

korban OOM killer: backup-notbenar (skor 1050)

Bedah simulasi di atas:

  1. total += $3: menjumlahkan RSS semua proses sebagai dasar pembagian — di kernel sungguhan pembaginya total halaman sistem.
  2. skor = f[3] * 1000 / total + f[4]: skor pokok proporsional terhadap porsi RSS, lalu digeser oleh oom_score_adj.
  3. if (skor > maks): korban adalah skor tertinggi — di sini backup-notbenar yang RSS-nya sedang, tetapi adj 800 menjadikannya kambing hitam.
  4. Baris split dan baris[NR]: menyimpan dulu seluruh baris agar tabel bisa dicetak setelah total diketahui — trik dua fase yang sama dengan alat agregasi di chapter sebelumnya.

Transparent Huge Pages: Halaman Raksasa Otomatis #

Menerjemahkan alamat virtual empat kilobyte demi empat kilobyte membebani cache penerjemah alamat CPU (TLB). Fitur Transparent Huge Pages (THP) mengizinkan kernel memakai page 2 MiB untuk pemakaian memori besar secara otomatis — lebih sedikit entri TLB, lebih sedikit page fault. Setelannya terbaca dari sysfs:

bash
printf 'swappiness (dorongan ke swap) : %s\n' "$(cat /proc/sys/vm/swappiness)"
printf 'Transparent Huge Pages        : %s\n' "$(cat /sys/kernel/mm/transparent_hugepage/enabled)"

Output yang Dihasilkan:

text
swappiness (dorongan ke swap) : 60
Transparent Huge Pages        : [always] madvise never

Nilainya tiga pilihan, dan kernel menandai yang aktif dengan tanda kurung siku: always, madvise (hanya program yang meminta), atau never. THP terdengar seperti kemenangan tanpa syarat, namun tidak selalu: pengelola basis data kerap mematikannya karena upaya kernel memadatkan halaman raksasa menimbulkan jeda latensi mendadak — rincian desainnya ada di dokumentasi THP , dan konsep memori per-prosesnya dirangkum di dokumen konsep memory management kernel.

Dengan ini rangkaian pengelola memori lengkap: ruang virtual murah, RAM nyata saat disentuh, cache yang lapar, dan wasit terakhir yang galak. Namun memori bukan satu-satunya resource yang dijadwalkan kernel — sektor kecil di bawah ini juga punya antreannya sendiri. Kita beralih ke penjadwal I/O.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

vmrss-demo.c

Unduh berkas
c
/* vmrss-demo.c: membuktikan demand paging lewat VmRSS di /proc/self/status */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static long baca_vmrss_kb(void)
{
    FILE *f = fopen("/proc/self/status", "r");
    char baris[256];
    long kb = -1;

    if (f == NULL) {
        return -1;
    }
    while (fgets(baris, sizeof baris, f) != NULL) {
        if (sscanf(baris, "VmRSS: %ld kB", &kb) == 1) {
            break;
        }
    }
    fclose(f);
    return kb;
}

int main(void)
{
    const size_t ukuran = (size_t)8 * 1024 * 1024; /* 8 MiB */
    char *p = malloc(ukuran);
    long sebelum, setelah_alokasi, setelah_sentuh;

    if (p == NULL) {
        fprintf(stderr, "malloc gagal\n");
        return 1;
    }
    sebelum = baca_vmrss_kb();
    setelah_alokasi = baca_vmrss_kb();
    memset(p, 1, ukuran); /* sentuh setiap byte -> page fault per page */
    setelah_sentuh = baca_vmrss_kb();

    printf("malloc 8 MiB, belum disentuh : RSS naik >= 8192 kB? %s\n",
           (setelah_alokasi - sebelum >= 8192) ? "ya" : "tidak");
    printf("memset 8 MiB, menyentuh      : RSS naik >= 8192 kB? %s\n",
           (setelah_sentuh - setelah_alokasi >= 8192) ? "ya" : "tidak");
    free(p);
    return 0;
}