O que pode estar acontecendo se um processo é "interrompido devido à pouca RAM"?
Às vezes é dito que o Linux, por padrão, nunca nega solicitações de mais memória do código do aplicativo - por exemplo malloc()
. 1 Isso não é de fato verdade; o padrão usa uma heurística pela qual
Compromissos óbvios de espaço de endereço são recusados. Usado para um sistema típico. Ele garante que uma alocação seriamente falhada falhe, permitindo que o comprometimento excessivo reduza o uso de troca.
De [linux_src]/Documentation/vm/overcommit-accounting
(todas as cotações são da árvore 3.11). Exatamente o que conta como uma "alocação seriamente selvagem" não é explicitado; portanto, teríamos que passar pela fonte para determinar os detalhes. Também poderíamos usar o método experimental na nota de rodapé 2 (abaixo) para tentar obter alguma reflexão sobre a heurística - com base nisso, minha observação empírica inicial é que, em circunstâncias ideais (== o sistema está ocioso), se você não Se não houver troca, você poderá alocar cerca de metade da sua RAM e, se houver, receberá cerca da metade da RAM mais todas as trocas. Isso é mais ou menos por processo (mas observe que esse limite é dinâmico e está sujeito a alterações devido ao estado, consulte algumas observações na nota de rodapé 5).
Metade da sua RAM mais swap é explicitamente o padrão para o campo "CommitLimit" em /proc/meminfo
. Aqui está o que isso significa - e observe que ele realmente não tem nada a ver com o limite discutido (a partir de [src]/Documentation/filesystems/proc.txt
):
CommitLimit: Com base na taxa de supercomprometimento ('vm.overcommit_ratio'), essa é a quantidade total de memória atualmente disponível para ser alocada no sistema. Esse limite é respeitado apenas se a contabilidade estrita de confirmação excessiva estiver ativada (modo 2 em 'vm.overcommit_memory'). O CommitLimit é calculado com a seguinte fórmula: CommitLimit = ('vm.overcommit_ratio' * RAM física) + Swap Por exemplo, em um sistema com 1G de RAM física e 7G de swap com um 'vm.overcommit_ratio' de 30, produziria um CommitLimit de 7.3G.
O documento de contabilidade de supercomissão anteriormente citado declara que o padrão vm.overcommit_ratio
é 50. Portanto, se sysctl vm.overcommit_memory=2
você puder ajustar vm.covercommit_ratio (with sysctl
) e ver as conseqüências. 3 O modo padrão, quando CommitLimit
não é imposto e apenas "confirmações excessivas no espaço de endereço óbvio são recusadas", é quando vm.overcommit_memory=0
.
Embora a estratégia padrão tenha um limite heurístico por processo que evite a "alocação seriamente selvagem", ela deixa o sistema como um todo livre para ficar seriamente selvagem, em termos de alocação. 4 Isso significa que em algum momento ele pode ficar sem memória e precisar declarar falência em alguns processos por meio do killer do OOM .
O que o assassino da OOM mata? Não necessariamente o processo que solicitou memória quando não havia, pois esse não é necessariamente o processo verdadeiramente culpado e, o que é mais importante, não é necessariamente o que mais rapidamente tirará o sistema do problema em que ele se encontra.
Isso é citado a partir daqui, que provavelmente cita uma 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)
*/
O que parece ser uma lógica decente. No entanto, sem ser forense, o # 5 (que é redundante de # 1) parece uma implementação de venda difícil e o # 3 é redundante de # 2. Portanto, pode fazer sentido considerar isso reduzido para # 2/3 e # 4.
Pesquisei através de uma fonte recente (3.11) e notei que este comentário mudou nesse meio tempo:
/**
* 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.
*/
Isso é um pouco mais explícito sobre o item 2: "O objetivo é [matar] a tarefa que consome mais memória para evitar falhas subseqüentes do oom" e pela implicação no 4 ( "queremos matar a quantidade mínima de processos ( um ) ) .
Se você deseja ver o assassino do OOM em ação, consulte a nota de rodapé 5.
1 Uma ilusão de que Gilles me livrou, ver comentários.
2 Aqui está um pouco simples de C que solicita pedaços de memória cada vez maiores para determinar quando uma solicitação de mais falhará:
#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 você não conhece C, pode compilar isso gcc virtlimitcheck.c -o virtlimitcheck
e executar ./virtlimitcheck
. É completamente inofensivo, pois o processo não usa o espaço que ele solicita - ou seja, nunca usa realmente nenhuma RAM.
Em um sistema de 3,11 x86_64 com sistema de 4 GB e 6 GB de troca, falhei em ~ 7400000 kB; o número flutua, então talvez o estado seja um fator. Isso é coincidentemente próximo ao CommitLimit
in /proc/meminfo
, mas a modificação dessa via vm.overcommit_ratio
não faz nenhuma diferença. No entanto, em um sistema ARM de 448 MB 3.6.11 de 32 bits e 64 MB de troca, porém, falho em ~ 230 MB. Isso é interessante, pois no primeiro caso a quantidade é quase o dobro da quantidade de RAM, enquanto no segundo é cerca de 1/4 disso - implicando fortemente que a quantidade de troca é um fator. Isso foi confirmado desativando a troca no primeiro sistema, quando o limite de falhas caiu para ~ 1,95 GB, uma proporção muito semelhante à pequena caixa ARM.
Mas isso é realmente por processo? Parece ser. O pequeno programa abaixo solicita um pedaço de memória definido pelo usuário e, se for bem-sucedido, espera que você retorne - dessa forma, você pode tentar várias instâncias simultâneas:
#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;
}
Cuidado, no entanto, que não se trata estritamente da quantidade de RAM e troca, independentemente do uso - consulte a nota de rodapé 5 para observações sobre os efeitos do estado do sistema.
3 CommitLimit
refere-se à quantidade de espaço de endereço permitido para o sistema quando vm.overcommit_memory = 2. Presumivelmente, a quantidade que você pode alocar deve ser aquela menos o que já foi confirmado, que aparentemente é o Committed_AS
campo.
Um experimento potencialmente interessante demonstrando isso é adicionar #include <unistd.h>
à parte superior do virtlimitcheck.c (consulte a nota de rodapé 2) e um pouco fork()
antes do while()
loop. Não é garantido que funcione como descrito aqui sem alguma sincronização tediosa, mas há uma chance decente de que isso funcione, 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.
Isso faz sentido - olhando tmp.txt em detalhes, você pode ver os processos alternando suas alocações cada vez maiores (isso é mais fácil se você jogar o pid na saída) até que um, evidentemente, tenha reivindicado o suficiente para que o outro falhe. O vencedor fica livre para pegar tudo até CommitLimit
menos Committed_AS
.
4 Nesse ponto, vale a pena mencionar, se você ainda não entende o endereçamento virtual e exige a paginação, que o que torna possível o comprometimento em primeiro lugar é que o que o kernel aloca para os processos da terra do usuário não é memória física - é espaço de endereço virtual . Por exemplo, se um processo reserva 10 MB para algo, isso é organizado como uma sequência de endereços (virtuais), mas esses endereços ainda não correspondem à memória física. Quando esse endereço é acessado, isso resulta em uma falha de páginae então o kernel tenta mapeá-lo na memória real para que ele possa armazenar um valor real. Os processos geralmente reservam muito mais espaço virtual do que realmente acessam, o que permite que o kernel faça o uso mais eficiente da RAM. No entanto, a memória física ainda é um recurso finito e, quando tudo foi mapeado para o espaço de endereço virtual, é necessário eliminar algum espaço de endereço virtual para liberar alguma RAM.
5 Primeiro um aviso : se você tentar fazer isso vm.overcommit_memory=0
, salve o seu trabalho primeiro e feche os aplicativos críticos, pois o sistema ficará congelado por ~ 90 segundos e algum processo terminará!
A idéia é rodar uma bomba de garfo que atinge o tempo limite após 90 segundos, com os garfos alocando espaço e alguns deles gravando grandes quantidades de dados na RAM, enquanto relatam ao 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);
}
}
}
Compile isso gcc forkbomb.c -o forkbomb
. Primeiro, tente com sysctl vm.overcommit_memory=2
- você provavelmente obterá algo como:
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.
Nesse ambiente, esse tipo de bomba de forquilha não chega muito longe. Observe que o número em "diz N forks" não é o número total de processos, é o número de processos na cadeia / filial que antecederam aquele.
Agora tente com vm.overcommit_memory=0
. Se você redirecionar o stderr para um arquivo, poderá fazer algumas análises brutas depois, por exemplo:
> 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!
Apenas 15 processos falharam ao alocar 1 GB - demonstrando que a heurística para overcommit_memory = 0 é afetada pelo estado. Quantos processos havia? Olhando para o final do tmp.txt, provavelmente> 100.000. Agora, como pode realmente usar o 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
Oito - o que novamente faz sentido, já que na época eu tinha ~ 3 GB de RAM livre e 6 GB de swap.
Dê uma olhada nos logs do sistema depois de fazer isso. Você deve ver as pontuações dos relatórios do OOM killer (entre outras coisas); presumivelmente isso se refere a oom_badness
.