Quero criar um programa que simule uma situação de falta de memória (OOM) em um servidor Unix. Eu criei este super comedor de memória super simples:
#include <stdio.h>
#include <stdlib.h>
unsigned long long memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
void *memory = NULL;
int eat_kilobyte()
{
memory = realloc(memory, (eaten_memory * 1024) + 1024);
if (memory == NULL)
{
// realloc failed here - we probably can't allocate more memory for whatever reason
return 1;
}
else
{
eaten_memory++;
return 0;
}
}
int main(int argc, char **argv)
{
printf("I will try to eat %i kb of ram\n", memory_to_eat);
int megabyte = 0;
while (memory_to_eat > 0)
{
memory_to_eat--;
if (eat_kilobyte())
{
printf("Failed to allocate more memory! Stucked at %i kb :(\n", eaten_memory);
return 200;
}
if (megabyte++ >= 1024)
{
printf("Eaten 1 MB of ram\n");
megabyte = 0;
}
}
printf("Successfully eaten requested memory!\n");
free(memory);
return 0;
}
Ele consome tanta memória quanto definida, na memory_to_eat
qual agora são exatamente 50 GB de RAM. Ele aloca memória em 1 MB e imprime exatamente o ponto em que deixa de alocar mais, para que eu saiba qual valor máximo conseguiu consumir.
O problema é que funciona. Mesmo em um sistema com 1 GB de memória física.
Quando verifico na parte superior, vejo que o processo consome 50 GB de memória virtual e apenas menos de 1 MB de memória residente. Existe uma maneira de criar um comedor de memória que realmente o consome?
Especificações do sistema: Kernel do Linux 3.16 ( Debian ) provavelmente com a confirmação excessiva ativada (não sei como fazer check-out) sem troca e virtualizada.
fonte
sysctl -w vm.overcommit_memory=2
como root; consulte mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Observe que isso pode ter outras consequências; em particular, programas muito grandes (por exemplo, seu navegador da web) podem não gerar programas auxiliares (por exemplo, o leitor de PDF).Respostas:
Quando sua
malloc()
implementação solicita memória ao kernel do sistema (por meio de uma chamadasbrk()
oummap()
sistema), o kernel apenas anota que você solicitou a memória e onde ela deve ser colocada no seu espaço de endereço. Na verdade, ele ainda não mapeia essas páginas .Quando o processo acessa a memória posteriormente na nova região, o hardware reconhece uma falha de segmentação e alerta o kernel para a condição. O kernel então consulta a página em suas próprias estruturas de dados e descobre que você deve ter uma página zero lá, para que ele mapeie em uma página zero (possivelmente expulsando uma página do cache de páginas) e retorne da interrupção. Seu processo não percebe que nada disso aconteceu, a operação dos kernels é perfeitamente transparente (exceto pelo pequeno atraso enquanto o kernel faz seu trabalho).
Essa otimização permite que a chamada do sistema retorne muito rapidamente e, o mais importante, evita que todos os recursos sejam comprometidos com o seu processo quando o mapeamento é feito. Isso permite que os processos reservem buffers bastante grandes que eles nunca precisam em circunstâncias normais, sem medo de consumir muita memória.
Portanto, se você deseja programar um comedor de memória, é absolutamente necessário fazer algo com a memória que você aloca. Para isso, você só precisa adicionar uma única linha ao seu código:
Observe que é perfeitamente suficiente gravar em um único byte em cada página (que contém 4096 bytes no X86). Isso ocorre porque toda a alocação de memória do kernel para um processo é feita na granularidade da página de memória, o que, por sua vez, ocorre devido ao hardware que não permite a paginação em granularidades menores.
fonte
mmap
eMAP_POPULATE
(embora observe que a página de manual diz " MAP_POPULATE é suportado para mapeamentos privados somente desde o Linux 2.6.23 ").mlockall(MCL_FUTURE)
. (Isso requer raiz, porqueulimit -l
é apenas 64kiB para contas de usuário em uma instalação padrão do Debian / Ubuntu.) Eu apenas tentei no Linux 3.19 com o sysctl padrãovm/overcommit_memory = 0
, e as páginas bloqueadas usam RAM física / de swap.malloc()
chamado de retornonull
. Essa é claramente a desvantagem dessa abordagem no gerenciamento de memória. No entanto, já é a existência de mapeamentos de cópia na gravação (pense em bibliotecas dinâmicas efork()
) que tornam impossível para o kernel saber quanta memória será realmente necessária. Portanto, se isso não comprometer demais a memória, você ficará sem memória mapeável muito antes de realmente usar toda a memória física.SIGSEGV
sinal deve ser entregue ao processo.Todas as páginas virtuais iniciam a cópia na gravação mapeada para a mesma página física zerada. Para usar páginas físicas, você pode sujá-las escrevendo algo em cada página virtual.
Se estiver executando como root, você pode usar
mlock(2)
ou fazermlockall(2)
com que o kernel ligue as páginas quando elas estiverem alocadas, sem precisar sujá-las. (usuários não-root normais têmulimit -l
apenas 64 KB.)Uma versão aprimorada do código, que faz o que o OP estava querendo:
Isso também corrige as incompatibilidades da cadeia de caracteres do formato printf com os tipos de memory_to_eat e eaten_memory, usando
%zi
para imprimirsize_t
números inteiros. O tamanho da memória a ser consumida, em kiB, pode opcionalmente ser especificado como um argumento de linha de comando.O design confuso usando variáveis globais e crescendo 1k em vez de 4k páginas é inalterado.
fonte
Uma otimização sensata está sendo feita aqui. O tempo de execução não adquire a memória até você usá-la.
Um simples
memcpy
será suficiente para contornar essa otimização. (Você pode achar quecalloc
ainda otimiza a alocação de memória até o ponto de uso.)fonte
malloc
aloque nos limites da página o que me parece bastante provável.calloc
aproveita o mmap (MAP_ANONYMOUS), que fornece páginas zeradas, para que não duplique o trabalho de zerar páginas do kernel.Não tenho certeza sobre este, mas a única explicação que posso fazer é que o linux é um sistema operacional de copiar em escrever. Quando se chamafork
os dois processos, aponte para a mesma memória fisicamente. A memória é copiada apenas quando um processo realmente grava na memória.Penso que aqui, a memória física real só é alocada quando se tenta escrever algo nela. Chamar
sbrk
oummap
pode muito bem atualizar apenas a manutenção de livros em memória do kernel. A RAM real só pode ser alocada quando realmente tentamos acessar a memória.fonte
fork
não tem nada a ver com isso. Você veria o mesmo comportamento se inicializasse o Linux com este programa como/sbin/init
. (ou seja, PID 1, o primeiro processo no modo de usuário). Você teve a ideia geral certa com a cópia na gravação: até que você as impeça, as páginas alocadas recentemente são todas as cópias na gravação mapeadas para a mesma página zerada.