Co może się stać, jeśli proces zostanie „zabity z powodu małej pamięci RAM”?
Czasami mówi się, że linux domyślnie nigdy nie odrzuca próśb o więcej pamięci z kodu aplikacji - np malloc()
. 1 To nie jest prawdą; domyślnie używa heurystyki
Oczywiste przekroczenie przestrzeni adresowej jest odrzucane. Używany w typowym systemie. Zapewnia to, że poważna alokacja nie powiedzie się, jednocześnie umożliwiając nadmierne zaangażowanie, aby zmniejszyć użycie swapu.
From [linux_src]/Documentation/vm/overcommit-accounting
(wszystkie cytaty pochodzą z drzewa 3.11). Dokładnie to, co liczy się jako „poważnie dziki przydział”, nie jest wyraźnie określone, więc musielibyśmy przejrzeć źródło, aby ustalić szczegóły. Możemy również użyć metody eksperymentalnej w przypisie 2 (poniżej), aby spróbować uzyskać odbicie heurystyki - na tej podstawie moja początkowa obserwacja empiryczna jest taka, że w idealnych okolicznościach (== system jest bezczynny), jeśli nie „ Jeśli masz zamianę, będziesz mógł przeznaczyć około połowy pamięci RAM, a jeśli masz zamianę, dostaniesz około połowy pamięci RAM plus całą swoją wymianę. To mniej więcej na proces (należy jednak pamiętać, że limit ten jest dynamiczny i może ulec zmianie ze względu na stan, zob. Uwagi w przypisie 5).
Połowa pamięci RAM plus swap jest wyraźnie domyślną wartością dla pola „CommitLimit” w /proc/meminfo
. Oto, co to znaczy - i zauważ, że tak naprawdę nie ma to nic wspólnego z omówionym limitem (z [src]/Documentation/filesystems/proc.txt
):
CommitLimit: w oparciu o współczynnik nadmiaru („vm.overcommit_ratio”), jest to całkowita ilość pamięci obecnie dostępnej do przydzielenia w systemie. Limit ten jest przestrzegany tylko wtedy, gdy włączone jest ścisłe rozliczanie z nadmiernym zaangażowaniem (tryb 2 w „vm.overcommit_memory”). CommitLimit jest obliczany według następującego wzoru: CommitLimit = ('vm.overcommit_ratio' * Fizyczna pamięć RAM) + Zamień Na przykład w systemie z 1G fizycznej pamięci RAM i 7G swapu z 'vm.overcommit_ratio' wynoszącym 30 dałoby to CommitLimit 7,3G.
Cytowany poprzednio dokument księgowania nadmiarowego stwierdza, że wartością domyślną vm.overcommit_ratio
jest 50. Więc jeśli możesz sysctl vm.overcommit_memory=2
, możesz dostosować vm.covercommit_ratio (with sysctl
) i zobaczyć konsekwencje. 3 Tryb domyślny, gdy CommitLimit
nie jest wymuszony i tylko „oczywiste nadmierne polecenia przestrzeni adresowej są odrzucane”, to kiedy vm.overcommit_memory=0
.
Chociaż strategia domyślna ma heurystyczny limit na proces zapobiegający „poważnie dzikiej alokacji”, pozostawia system jako całość wolną od poważnie dzikiej alokacji. 4 Oznacza to, że w pewnym momencie może zabraknąć pamięci i musi ogłosić bankructwo w niektórych procesach za pośrednictwem zabójcy OOM .
Co zabija zabójca OOM? Niekoniecznie proces, który poprosił o pamięć, gdy jej nie było, ponieważ niekoniecznie jest to naprawdę winny proces, a co ważniejsze, niekoniecznie ten, który najszybciej usunie system z problemu, w którym się znajduje.
Jest to cytowane stąd, które prawdopodobnie przytacza źródło 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)
*/
Co wydaje się być porządnym uzasadnieniem. Jednak bez uzyskania wiedzy sądowej numer 5 (który jest zbędny z numeru 1) wydaje się być trudny pod względem implementacji sprzedaży, a numer 3 jest zbędny z numeru 2. Dlatego warto rozważyć sprowadzenie tego do # 2/3 i # 4.
Przeszukałem najnowsze źródło (3.11) i zauważyłem, że ten komentarz zmienił się w międzyczasie:
/**
* 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.
*/
Jest to nieco bardziej wyraźnie na temat nr 2: „Celem jest [zabicie] zadania zajmującego najwięcej pamięci, aby uniknąć późniejszych awarii oom”, a przez domniemanie nr 4 ( „chcemy zabić minimalną liczbę procesów ( jeden ) ) .
Jeśli chcesz zobaczyć zabójcę OOM w akcji, zobacz przypis 5.
1 Złudzenie, którego Gilles na szczęście mnie pozbył, zobacz komentarze.
2 Oto prosty fragment C, który prosi o coraz większe fragmenty pamięci, aby określić, kiedy żądanie o więcej nie powiedzie się:
#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;
}
Jeśli nie znasz C, możesz to skompilować gcc virtlimitcheck.c -o virtlimitcheck
, a następnie uruchomić ./virtlimitcheck
. Jest całkowicie nieszkodliwy, ponieważ proces nie zajmuje miejsca, o które prosi - tzn. Tak naprawdę nigdy nie wykorzystuje pamięci RAM.
W systemie 3.11 x86_64 z systemem 4 GB i 6 GB wymiany nie udało mi się przy ~ 7400000 kB; liczba się zmienia, więc być może stan jest czynnikiem. To przypadkowo blisko CommitLimit
IN /proc/meminfo
, ale poprzez modyfikację tego vm.overcommit_ratio
nie robi żadnej różnicy. W 32-bitowym systemie ARM 3.6.11 z 448 MB i 64 MB wymiany, jednak nie udaje mi się przy ~ 230 MB. Jest to interesujące, ponieważ w pierwszym przypadku ilość ta jest prawie dwa razy większa niż ilość pamięci RAM, podczas gdy w drugim przypadku jest to około 1/4, co - silnie implikuje ilość wymiany. Zostało to potwierdzone przez wyłączenie swapu w pierwszym systemie, gdy próg awarii spadł do ~ 1,95 GB, co jest bardzo podobnym współczynnikiem do małego pudełka ARM.
Ale czy to naprawdę na proces? To wydaje się być. Krótki program poniżej prosi o zdefiniowaną przez użytkownika część pamięci, a jeśli się powiedzie, czeka na trafienie return - w ten sposób możesz wypróbować wiele instancji jednocześnie:
#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;
}
Uważaj jednak, że nie chodzi wyłącznie o ilość pamięci RAM i wymiany bez względu na użycie - uwagi na temat skutków stanu systemu znajdują się w przypisie 5.
3 CommitLimit
odnosi się do ilości przestrzeni adresowej dozwolonej dla systemu, gdy vm.overcommit_memory = 2. Przypuszczalnie wtedy ilość, którą można przeznaczyć, powinna być równa minus to, co już zostało zatwierdzone, co jest najwyraźniej Committed_AS
polem.
Potencjalnie interesującym eksperymentem pokazującym to jest dodanie #include <unistd.h>
na początku virtlimitcheck.c (patrz przypis 2) i fork()
tuż przed while()
pętlą. Nie gwarantuje się, że będzie działać zgodnie z opisem tutaj bez żmudnej synchronizacji, ale istnieje spora szansa, że tak, 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.
Ma to sens - patrząc szczegółowo na tmp.txt możesz zobaczyć procesy naprzemiennie ich coraz większe alokacje (jest to łatwiejsze, jeśli wrzucisz pid do wyniku), dopóki jeden, najwyraźniej, nie stwierdzi wystarczająco, że drugi zawiedzie. Zwycięzca może następnie zgarnąć wszystko do CommitLimit
minus Committed_AS
.
4 Warto w tym miejscu wspomnieć, że jeśli jeszcze nie rozumiesz adresowania wirtualnego i stronicowania na żądanie, to, co sprawia, że możliwe jest przejęcie zobowiązań, to przede wszystkim to, że to, co jądro alokuje do procesów użytkownika, wcale nie jest pamięcią fizyczną - wirtualna przestrzeń adresowa . Na przykład, jeśli proces rezerwuje na coś 10 MB, to jest to sekwencja adresów (wirtualnych), ale adresy te nie odpowiadają jeszcze pamięci fizycznej. Gdy taki adres jest dostępny, powoduje to błąd stronya następnie jądro próbuje zmapować je na prawdziwą pamięć, aby mogła przechowywać prawdziwą wartość. Procesy zwykle rezerwują znacznie więcej przestrzeni wirtualnej, niż faktycznie uzyskują dostęp, co pozwala jądru na najbardziej efektywne wykorzystanie pamięci RAM. Jednak pamięć fizyczna jest nadal zasobem skończonym, a gdy wszystko zostało zamapowane na wirtualną przestrzeń adresową, część wirtualnej przestrzeni adresowej należy wyeliminować, aby zwolnić trochę pamięci RAM.
5 Najpierw ostrzeżenie : jeśli spróbujesz tego vm.overcommit_memory=0
, pamiętaj, aby najpierw zapisać swoją pracę i zamknąć wszystkie krytyczne aplikacje, ponieważ system zostanie zamrożony na około 90 sekund, a niektóre procesy umrą!
Chodzi o to, aby uruchomić bombę wideł, która wygaśnie po 90 sekundach, a widelce przydzielają miejsce, a niektóre z nich zapisują duże ilości danych do pamięci RAM, jednocześnie raportując do 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);
}
}
}
Skompiluj to gcc forkbomb.c -o forkbomb
. Najpierw spróbuj z sysctl vm.overcommit_memory=2
- prawdopodobnie dostaniesz coś takiego:
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.
W tym środowisku taka widelecowa bomba nie dociera zbyt daleko. Zauważ, że liczba w „mówi N widelców” nie jest całkowitą liczbą procesów, jest to liczba procesów w łańcuchu / gałęzi prowadzących do tego.
Teraz spróbuj vm.overcommit_memory=0
. Jeśli przekierujesz stderr do pliku, możesz później przeprowadzić zgrubną analizę, np .:
> 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!
Tylko 15 procesom nie udało się przydzielić 1 GB - co pokazuje, że stan ma wpływ na heurystykę dla overcommit_memory = 0 . Ile tam było procesów? Patrząc na koniec tmp.txt, prawdopodobnie> 100 000. W jaki sposób można faktycznie korzystać z 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
Osiem - co znowu ma sens, ponieważ miałem wtedy ~ 3 GB wolnej pamięci RAM i 6 GB wymiany.
Po wykonaniu tej czynności przejrzyj dzienniki systemu. Powinieneś zobaczyć wyniki raportowania zabójcy OOM (między innymi); przypuszczalnie dotyczy to oom_badness
.