Apa yang mungkin terjadi jika suatu proses "mati karena RAM rendah"?
Terkadang dikatakan bahwa linux secara default tidak pernah menolak permintaan untuk lebih banyak memori dari kode aplikasi - mis malloc()
. 1 Ini sebenarnya tidak benar; default menggunakan heuristik dimana
Overcommits ruang alamat yang jelas ditolak. Digunakan untuk sistem tipikal. Ini memastikan alokasi liar yang serius gagal sementara memungkinkan terlalu banyak komitmen untuk mengurangi penggunaan swap.
Dari [linux_src]/Documentation/vm/overcommit-accounting
(semua kutipan berasal dari pohon 3.11). Tepatnya apa yang dianggap sebagai "alokasi liar yang serius" tidak dibuat eksplisit, jadi kami harus melalui sumber untuk menentukan detailnya. Kita juga dapat menggunakan metode eksperimental pada catatan kaki 2 (di bawah) untuk mencoba dan mendapatkan beberapa refleksi heuristik - berdasarkan itu, pengamatan empiris awal saya adalah bahwa dalam keadaan ideal (== sistem idle), jika Anda tidak t memiliki swap apa pun, Anda akan diizinkan untuk mengalokasikan sekitar setengah RAM Anda, dan jika Anda memiliki swap, Anda akan mendapatkan sekitar setengah RAM Anda ditambah semua swap Anda. Itu lebih atau kurang per proses (tetapi perhatikan batas ini dinamis dan dapat berubah karena keadaan, lihat beberapa pengamatan di catatan kaki 5).
Setengah RAM Anda plus swap secara eksplisit merupakan standar untuk bidang "CommitLimit" di /proc/meminfo
. Inilah artinya - dan perhatikan itu sebenarnya tidak ada hubungannya dengan batas yang baru saja didiskusikan (dari [src]/Documentation/filesystems/proc.txt
):
CommitLimit: Berdasarkan rasio overcommit ('vm.overcommit_ratio'), ini adalah jumlah total memori yang saat ini tersedia untuk dialokasikan pada sistem. Batas ini hanya dipatuhi jika penghitungan overcommit ketat diaktifkan (mode 2 dalam 'vm.overcommit_memory'). CommitLimit dihitung dengan rumus berikut: CommitLimit = ('vm.overcommit_ratio' * RAM Fisik) + Swap Sebagai contoh, pada sistem dengan 1G RAM fisik dan 7G swap dengan 'vm.overcommit_ratio' dari 30 akan menghasilkan Batas Komisi 7,3G.
Doc-akuntansi overcommit yang dikutip sebelumnya menyatakan bahwa standarnya vm.overcommit_ratio
adalah 50. Jadi jika Anda sysctl vm.overcommit_memory=2
, Anda dapat menyesuaikan vm.covercommit_ratio (with sysctl
) dan melihat konsekuensinya. 3 Mode default, ketika CommitLimit
tidak ditegakkan dan hanya "ruang alamat overcommits jelas ditolak", adalah ketika vm.overcommit_memory=0
.
Sementara strategi default memang memiliki batas heuristik per-proses yang mencegah "alokasi liar yang serius", itu membuat sistem secara keseluruhan bebas untuk menjadi liar, alokasi yang bijak. 4 Ini berarti pada titik tertentu kehabisan memori dan harus menyatakan kebangkrutan untuk beberapa proses melalui pembunuh OOM .
Apa yang dibunuh pembunuh OOM? Belum tentu proses yang meminta memori ketika tidak ada, karena itu belum tentu proses yang benar-benar bersalah, dan yang lebih penting, belum tentu proses yang paling cepat akan mengeluarkan sistem dari masalah yang ada.
Ini dikutip dari sini yang mungkin mengutip sumber 2.6.x:
/*
* oom_badness - calculate a numeric value for how bad this task has been
*
* The formula used is relatively simple and documented inline in the
* function. The main rationale is that we want to select a good task
* to kill when we run out of memory.
*
* Good in this context means that:
* 1) we lose the minimum amount of work done
* 2) we recover a large amount of memory
* 3) we don't kill anything innocent of eating tons of memory
* 4) we want to kill the minimum amount of processes (one)
* 5) we try to kill the process the user expects us to kill, this
* algorithm has been meticulously tuned to meet the principle
* of least surprise ... (be careful when you change it)
*/
Yang sepertinya alasan yang layak. Namun, tanpa mendapatkan forensik, # 5 (yang berlebihan dari # 1) tampaknya seperti implementasi penjualan yang sulit, dan # 3 berlebihan dari # 2. Jadi mungkin masuk akal untuk mempertimbangkan penurunan ini ke # 2/3 dan # 4.
Saya memahami sebuah sumber baru-baru ini (3.11) dan memperhatikan bahwa komentar ini telah berubah untuk sementara:
/**
* oom_badness - heuristic function to determine which candidate task to kill
*
* The heuristic for determining which task to kill is made to be as simple and
* predictable as possible. The goal is to return the highest value for the
* task consuming the most memory to avoid subsequent oom failures.
*/
Ini sedikit lebih eksplisit tentang # 2: "Tujuannya adalah untuk [membunuh] tugas yang menghabiskan sebagian besar memori untuk menghindari kegagalan berikutnya," dan dengan implikasi # 4 ( "kami ingin membunuh jumlah minimum proses ( satu ) ) .
Jika Anda ingin melihat pembunuh OOM beraksi, lihat catatan kaki 5.
1 Khayalan Gilles untungnya membebaskan saya dari, lihat komentar.
2 Inilah sedikit C yang langsung yang meminta potongan memori yang semakin besar untuk menentukan kapan permintaan untuk lebih banyak akan gagal:
#include <stdio.h>
#include <stdint.h>
#include <stdlib.h>
#define MB 1 << 20
int main (void) {
uint64_t bytes = MB;
void *p = malloc(bytes);
while (p) {
fprintf (stderr,
"%lu kB allocated.\n",
bytes / 1024
);
free(p);
bytes += MB;
p = malloc(bytes);
}
fprintf (stderr,
"Failed at %lu kB.\n",
bytes / 1024
);
return 0;
}
Jika Anda tidak tahu C, Anda bisa mengkompilasi ini gcc virtlimitcheck.c -o virtlimitcheck
, lalu jalankan ./virtlimitcheck
. Ini benar-benar tidak berbahaya, karena prosesnya tidak menggunakan ruang yang diminta - yaitu, ia tidak pernah benar-benar menggunakan RAM apa pun.
Pada sistem 3.11 x86_64 dengan sistem 4 GB dan 6 GB swap, saya gagal pada ~ 7400000 kB; jumlahnya berfluktuasi, jadi mungkin keadaan adalah faktor. Ini kebetulan dekat dengan CommitLimit
di /proc/meminfo
, tetapi memodifikasi ini melalui vm.overcommit_ratio
tidak ada bedanya. Pada sistem 3.6M 32-bit ARM 448 MB dengan 64 MB swap, bagaimanapun, saya gagal ~ 230 MB. Ini menarik karena dalam kasus pertama jumlahnya hampir dua kali lipat jumlah RAM, sedangkan pada yang kedua adalah sekitar 1/4 itu - sangat menyiratkan jumlah swap adalah faktor. Ini dikonfirmasi dengan mematikan swap pada sistem pertama, ketika ambang kegagalan turun ke ~ 1,95 GB, rasio yang sangat mirip dengan kotak ARM kecil.
Tetapi apakah ini benar-benar per proses? Tampaknya begitu. Program singkat di bawah ini meminta sepotong memori yang ditentukan pengguna, dan jika berhasil, menunggu Anda untuk kembali - dengan cara ini Anda dapat mencoba beberapa contoh bersamaan:
#include <stdio.h>
#include <stdlib.h>
#define MB 1 << 20
int main (int argc, const char *argv[]) {
unsigned long int megabytes = strtoul(argv[1], NULL, 10);
void *p = malloc(megabytes * MB);
fprintf(stderr,"Allocating %lu MB...", megabytes);
if (!p) fprintf(stderr,"fail.");
else {
fprintf(stderr,"success.");
getchar();
free(p);
}
return 0;
}
Berhati-hatilah, bagaimanapun, bahwa ini bukan semata-mata tentang jumlah RAM dan swap terlepas dari penggunaan - lihat catatan kaki 5 untuk pengamatan tentang efek dari status sistem.
3 CommitLimit
mengacu pada jumlah ruang alamat yang diizinkan untuk sistem ketika vm.overcommit_memory = 2. Agaknya, jumlah yang dapat Anda alokasikan harus dikurangi dengan apa yang sudah dilakukan, yang tampaknya adalah Committed_AS
bidang.
Eksperimen yang berpotensi menarik yang menunjukkan ini adalah untuk menambah #include <unistd.h>
bagian atas virtlimitcheck.c (lihat catatan kaki 2), dan fork()
tepat sebelum while()
perulangan. Itu tidak dijamin berfungsi seperti yang dijelaskan di sini tanpa sinkronisasi yang melelahkan, tetapi ada kemungkinan yang layak, YMMV:
> sysctl vm.overcommit_memory=2
vm.overcommit_memory = 2
> cat /proc/meminfo | grep Commit
CommitLimit: 9231660 kB
Committed_AS: 3141440 kB
> ./virtlimitcheck 2&> tmp.txt
> cat tmp.txt | grep Failed
Failed at 3051520 kB.
Failed at 6099968 kB.
Ini masuk akal - melihat tmp.txt secara rinci Anda dapat melihat proses berganti alokasi yang lebih besar dan lebih besar (ini lebih mudah jika Anda melemparkan pid ke dalam output) sampai satu, jelas, telah mengklaim cukup bahwa yang lain gagal. Pemenang kemudian bebas untuk mengambil semuanya hingga CommitLimit
minus Committed_AS
.
4 Perlu disebutkan, pada titik ini, jika Anda belum memahami pengalamatan virtual dan paging permintaan, bahwa yang membuat komitmen lebih mungkin di tempat pertama adalah bahwa apa yang dialokasikan kernel untuk proseslandland bukanlah memori fisik sama sekali - itu adalah ruang alamat virtual . Misalnya, jika suatu proses menyimpan 10 MB untuk sesuatu, itu ditata sebagai urutan alamat (virtual), tetapi alamat tersebut belum sesuai dengan memori fisik. Ketika alamat tersebut diakses, ini menghasilkan kesalahan halamandan kemudian kernel mencoba untuk memetakannya ke memori nyata sehingga dapat menyimpan nilai nyata. Proses biasanya menyimpan lebih banyak ruang virtual daripada yang sebenarnya mereka akses, yang memungkinkan kernel membuat penggunaan RAM yang paling efisien. Namun, memori fisik masih merupakan sumber daya yang terbatas dan ketika semuanya telah dipetakan ke ruang alamat virtual, beberapa ruang alamat virtual harus dihilangkan untuk membebaskan beberapa RAM.
5 Pertama peringatan : Jika Anda mencoba ini vm.overcommit_memory=0
, pastikan Anda menyimpan pekerjaan Anda terlebih dahulu dan menutup semua aplikasi penting, karena sistem akan dibekukan selama ~ 90 detik dan beberapa proses akan mati!
Idenya adalah untuk menjalankan bom garpu yang keluar setelah 90 detik, dengan garpu mengalokasikan ruang dan beberapa dari mereka menulis sejumlah besar data ke RAM, sambil melaporkan ke stderr.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/time.h>
#include <errno.h>
#include <string.h>
/* 90 second "Verbose hungry fork bomb".
Verbose -> It jabbers.
Hungry -> It grabs address space, and it tries to eat memory.
BEWARE: ON A SYSTEM WITH 'vm.overcommit_memory=0', THIS WILL FREEZE EVERYTHING
FOR THE DURATION AND CAUSE THE OOM KILLER TO BE INVOKED. CLOSE THINGS YOU CARE
ABOUT BEFORE RUNNING THIS. */
#define STEP 1 << 30 // 1 GB
#define DURATION 90
time_t now () {
struct timeval t;
if (gettimeofday(&t, NULL) == -1) {
fprintf(stderr,"gettimeofday() fail: %s\n", strerror(errno));
return 0;
}
return t.tv_sec;
}
int main (void) {
int forks = 0;
int i;
unsigned char *p;
pid_t pid, self;
time_t check;
const time_t start = now();
if (!start) return 1;
while (1) {
// Get our pid and check the elapsed time.
self = getpid();
check = now();
if (!check || check - start > DURATION) return 0;
fprintf(stderr,"%d says %d forks\n", self, forks++);
// Fork; the child should get its correct pid.
pid = fork();
if (!pid) self = getpid();
// Allocate a big chunk of space.
p = malloc(STEP);
if (!p) {
fprintf(stderr, "%d Allocation failed!\n", self);
return 0;
}
fprintf(stderr,"%d Allocation succeeded.\n", self);
// The child will attempt to use the allocated space. Using only
// the child allows the fork bomb to proceed properly.
if (!pid) {
for (i = 0; i < STEP; i++) p[i] = i % 256;
fprintf(stderr,"%d WROTE 1 GB\n", self);
}
}
}
Kompilasi ini gcc forkbomb.c -o forkbomb
. Pertama, coba dengan sysctl vm.overcommit_memory=2
- Anda mungkin akan mendapatkan sesuatu seperti:
6520 says 0 forks
6520 Allocation succeeded.
6520 says 1 forks
6520 Allocation succeeded.
6520 says 2 forks
6521 Allocation succeeded.
6520 Allocation succeeded.
6520 says 3 forks
6520 Allocation failed!
6522 Allocation succeeded.
Di lingkungan ini, bom fork semacam ini tidak terlalu jauh. Perhatikan bahwa angka dalam "kata N garpu" bukan jumlah total proses, itu adalah jumlah proses dalam rantai / cabang yang mengarah ke yang itu.
Sekarang coba dengan vm.overcommit_memory=0
. Jika Anda mengarahkan stderr ke file, Anda dapat melakukan beberapa analisis kasar sesudahnya, misalnya:
> cat tmp.txt | grep failed
4641 Allocation failed!
4646 Allocation failed!
4642 Allocation failed!
4647 Allocation failed!
4649 Allocation failed!
4644 Allocation failed!
4643 Allocation failed!
4648 Allocation failed!
4669 Allocation failed!
4696 Allocation failed!
4695 Allocation failed!
4716 Allocation failed!
4721 Allocation failed!
Hanya 15 proses gagal mengalokasikan 1 GB - menunjukkan bahwa heuristik untuk overcommit_memory = 0 adalah dipengaruhi oleh negara. Berapa banyak proses yang ada di sana? Melihat akhir tmp.txt, mungkin> 100.000. Sekarang bagaimana sebenarnya bisa menggunakan 1 GB?
> cat tmp.txt | grep WROTE
4646 WROTE 1 GB
4648 WROTE 1 GB
4671 WROTE 1 GB
4687 WROTE 1 GB
4694 WROTE 1 GB
4696 WROTE 1 GB
4716 WROTE 1 GB
4721 WROTE 1 GB
Delapan - yang lagi-lagi masuk akal, karena pada saat itu saya punya ~ 3 GB RAM gratis dan 6 GB swap.
Lihatlah log sistem Anda setelah Anda melakukan ini. Anda harus melihat skor pelaporan pembunuh OOM (antara lain); mungkin ini berhubungan dengan oom_badness
.