Cosa potrebbe accadere se un processo viene "interrotto a causa della RAM bassa"?
Talvolta si dice che Linux per impostazione predefinita non rifiuta mai le richieste di più memoria dal codice dell'applicazione, ad es malloc()
. 1 Questo non è in realtà vero; il valore predefinito utilizza un'euristica per cui
Vengono rifiutati evidenti sovraccarichi di spazio degli indirizzi. Utilizzato per un sistema tipico. Assicura che un'allocazione seria non riesce, consentendo al contempo un sovraccarico per ridurre l'utilizzo degli swap.
Da [linux_src]/Documentation/vm/overcommit-accounting
(tutte le virgolette provengono dall'albero 3.11). Ciò che conta come "allocazione seriamente selvaggia" non è reso esplicito, quindi dovremmo esaminare la fonte per determinare i dettagli. Potremmo anche utilizzare il metodo sperimentale nella nota 2 (di seguito) per cercare di ottenere un riflesso dell'euristico - in base a ciò, la mia osservazione empirica iniziale è che in circostanze ideali (== il sistema è inattivo), se non si " Non hai alcuno swap, ti sarà permesso di allocare circa la metà della tua RAM, e se hai lo swap, otterrai circa la metà della tua RAM più tutto lo swap. Questo è più o meno per processo (ma nota che questo limite è dinamico e soggetto a cambiamenti a causa dello stato, vedi alcune osservazioni nella nota 5).
La metà della RAM più swap è esplicitamente il valore predefinito per il campo "CommitLimit" in /proc/meminfo
. Ecco cosa significa - e nota che in realtà non ha nulla a che fare con il limite appena discusso (da [src]/Documentation/filesystems/proc.txt
):
CommitLimit: basato sul rapporto di sovraccarico ('vm.overcommit_ratio'), questa è la quantità totale di memoria attualmente disponibile da allocare sul sistema. Questo limite viene rispettato solo se è abilitata la contabilità per sovraccarico rigoroso (modalità 2 in "vm.overcommit_memory"). CommitLimit viene calcolato con la seguente formula: CommitLimit = ('vm.overcommit_ratio' * RAM fisica) + Swap Ad esempio, su un sistema con 1G di RAM fisica e 7G di swap con un 'vm.overcommit_ratio' di 30 produrrebbe un CommitLimit di 7.3G.
Il documento di contabilità di sovraccarico precedentemente citato afferma che il valore predefinito vm.overcommit_ratio
è 50. Quindi, se si sysctl vm.overcommit_memory=2
, è quindi possibile regolare vm.covercommit_ratio (con sysctl
) e vedere le conseguenze. 3 La modalità predefinita, quando CommitLimit
non viene applicata e vengono rifiutati solo "evidenti sovraccarichi di spazio degli indirizzi", è quando vm.overcommit_memory=0
.
Mentre la strategia di default ha un limite euristico per processo che impedisce la "allocazione seriamente selvaggia", lascia il sistema nel suo insieme libero di diventare seriamente selvaggio, allocazione saggia. 4 Ciò significa che a un certo punto può esaurire la memoria e dover dichiarare fallimento ad alcuni processi tramite il killer OOM .
Cosa uccide il killer OOM? Non necessariamente il processo che richiedeva memoria quando non ce n'era, dal momento che non è necessariamente il processo veramente colpevole, e, cosa più importante, non necessariamente quello che risolverà il sistema più rapidamente dal problema in cui si trova.
Questo è citato da qui che probabilmente cita una fonte 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)
*/
Che sembra una logica decente. Tuttavia, senza essere forensi, il n. 5 (che è ridondante del n. 1) sembra un'implementazione di vendita dura e il n. 3 è ridondante del n. 2. Quindi potrebbe avere senso considerare questo ridotto a # 2/3 e # 4.
Ho esaminato una fonte recente (3.11) e ho notato che questo commento è cambiato nel frattempo:
/**
* 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.
*/
Questo è un po 'più esplicitamente su # 2: "L'obiettivo è quello di [uccidere] l'attività che consuma più memoria per evitare successivi errori di oom", e di conseguenza # 4 ( "vogliamo uccidere la quantità minima di processi ( uno ) ) .
Se vuoi vedere il killer OOM in azione, vedi la nota 5.
1 Un'illusione di Gilles per fortuna mi libera di, vedi commenti.
2 Ecco un semplice bit di C che richiede blocchi di memoria sempre più grandi per determinare quando una richiesta di più fallirà:
#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;
}
Se non conosci C, puoi compilare questo gcc virtlimitcheck.c -o virtlimitcheck
, quindi eseguire ./virtlimitcheck
. È completamente innocuo, poiché il processo non utilizza lo spazio richiesto, ovvero non utilizza mai RAM.
Su un sistema 3,11 x86_64 con sistema da 4 GB e 6 GB di scambio, ho fallito a ~ 7400000 kB; il numero fluttua, quindi forse lo stato è un fattore. Per coincidenza, questo è vicino a CommitLimit
in /proc/meminfo
, ma modificarlo tramite vm.overcommit_ratio
non fa alcuna differenza. Su un sistema ARM 448 MB da 3,6.11 a 32 bit con 64 MB di scambio, tuttavia, non riesco a ~ 230 MB. Questo è interessante poiché nel primo caso la quantità è quasi il doppio della quantità di RAM, mentre nel secondo è circa 1/4 che - implicando fortemente la quantità di swap è un fattore. Ciò è stato confermato disattivando lo swap sul primo sistema, quando la soglia di errore è scesa a ~ 1,95 GB, un rapporto molto simile alla piccola scatola ARM.
Ma è davvero per processo? Sembra essere. Il breve programma in basso richiede un pezzo di memoria definito dall'utente e, se riesce, attende che tu prema return - in questo modo puoi provare più istanze simultanee:
#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;
}
Attenzione, tuttavia, che non si tratta strettamente della quantità di RAM e di scambio indipendentemente dall'uso - vedere la nota 5 per osservazioni sugli effetti dello stato del sistema.
3 si CommitLimit
riferisce alla quantità di spazio di indirizzi consentita per il sistema quando vm.overcommit_memory = 2. Presumibilmente quindi, la quantità che è possibile allocare dovrebbe essere quella meno ciò che è già stato impegnato, che apparentemente è il Committed_AS
campo.
Un esperimento potenzialmente interessante che lo dimostra è quello di aggiungere #include <unistd.h>
in cima a virtlimitcheck.c (vedi nota 2), e fork()
subito prima del while()
ciclo. Non è garantito che funzioni come descritto qui senza una noiosa sincronizzazione, ma c'è una buona possibilità che lo faccia, 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.
Questo ha senso - guardando in dettaglio tmp.txt è possibile vedere i processi alternare le loro allocazioni sempre più grandi (questo è più facile se si lancia il pid nell'output) fino a quando uno, evidentemente, ha affermato abbastanza che l'altro fallisce. Il vincitore è quindi libero di prendere tutto fino a CommitLimit
meno Committed_AS
.
4 Vale la pena ricordare, a questo punto, se non si capisce già l'indirizzamento virtuale e il paging della domanda, in primo luogo ciò che rende possibile l'impegno eccessivo è che ciò che il kernel assegna ai processi della terra dell'utente non è affatto memoria fisica - è spazio di indirizzi virtuali . Ad esempio, se un processo riserva 10 MB per qualcosa, è strutturato come una sequenza di indirizzi (virtuali), ma quegli indirizzi non corrispondono ancora alla memoria fisica. Quando si accede a tale indirizzo, si verifica un errore di paginae quindi il kernel tenta di mapparlo sulla memoria reale in modo che possa memorizzare un valore reale. I processi di solito riservano molto più spazio virtuale di quanto effettivamente accedano, il che consente al kernel di utilizzare la RAM in modo più efficiente. Tuttavia, la memoria fisica è ancora una risorsa limitata e quando tutto è stato mappato allo spazio degli indirizzi virtuali, è necessario eliminare parte dello spazio degli indirizzi virtuali per liberare un po 'di RAM.
5 Prima un avvertimento : se provi questo vm.overcommit_memory=0
, assicurati di salvare prima il tuo lavoro e chiudere tutte le applicazioni critiche, perché il sistema verrà bloccato per ~ 90 secondi e alcuni processi moriranno!
L'idea è di lanciare una bomba a forcella che scade dopo 90 secondi, con le forche che allocano lo spazio e alcune di esse scrivono grandi quantità di dati nella RAM, riferendosi nel frattempo a 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);
}
}
}
Compila questo gcc forkbomb.c -o forkbomb
. Innanzitutto, provalo con sysctl vm.overcommit_memory=2
- probabilmente otterrai qualcosa del tipo:
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.
In questo ambiente, questo tipo di bomba a forcella non va molto lontano. Si noti che il numero in "dice N forcelle" non è il numero totale di processi, è il numero di processi nella catena / ramo che porta a quello.
Ora provalo con vm.overcommit_memory=0
. Se si reindirizza stderr a un file, è possibile eseguire successivamente alcune analisi di base, ad esempio:
> 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!
Solo 15 processi non sono riusciti a allocare 1 GB, dimostrando che l'euristica di overcommit_memory = 0 è influenzata dallo stato. Quanti processi c'erano? Guardando la fine di tmp.txt, probabilmente> 100.000. Ora come si può effettivamente utilizzare 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
Otto - il che ha di nuovo senso, dato che all'epoca avevo ~ 3 GB di RAM libera e 6 GB di swap.
Dai un'occhiata ai log di sistema dopo aver fatto questo. Dovresti vedere i punteggi dei rapporti del killer OOM (tra le altre cose); presumibilmente questo riguarda oom_badness
.