Je souhaite créer un programme qui simulera une situation de mémoire insuffisante sur un serveur Unix. J'ai créé ce mangeur de mémoire super simple:
#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;
}
Il mange autant de mémoire que défini dans memory_to_eat
qui représente exactement 50 Go de RAM. Il alloue 1 Mo de mémoire et imprime exactement le point où il ne parvient pas à allouer plus, de sorte que je sache quelle valeur maximale il a réussi à consommer.
Le problème est que cela fonctionne. Même sur un système avec 1 Go de mémoire physique.
Quand je vérifie en haut, je constate que le processus consomme 50 Go de mémoire virtuelle et seulement moins de 1 Mo de mémoire résidente. Y a-t-il un moyen de créer un mangeur de mémoire qui le consomme vraiment?
Spécifications du système: Noyau Linux 3.16 ( Debian ) très probablement avec la surconnexion activée (vous ne savez pas comment l'extraire) sans échange ni virtualisation.
Lorsque votre implémentation malloc()
demande de la mémoire au noyau système (via un appel système sbrk()
ou mmap()
), le noyau indique uniquement que vous avez demandé la mémoire. et où il doit être placé dans votre espace d'adressage. Il ne fait pas encore mapper ces pages.
Lorsque le processus accède ensuite à la mémoire dans la nouvelle région, le matériel reconnaît une erreur de segmentation et alerte le noyau de la situation. Le noyau recherche ensuite la page dans ses propres structures de données et découvre que vous devriez y avoir une page zéro. Il est donc mappé dans une page zéro (éventuellement en supprimant d'abord une page du cache de page) et renvoie à partir de l'interruption. Votre processus ne réalise pas que rien de tout cela ne s'est produit, l'opération des noyaux est parfaitement transparente (à l'exception du court délai pendant lequel le noyau fait son travail).
Cette optimisation permet à l'appel système de revenir très rapidement et, surtout, évite que des ressources ne soient engagées dans votre processus lorsque le mappage est effectué. Cela permet aux processus de réserver des tampons assez volumineux dont ils n’ont jamais besoin dans des circonstances normales, sans craindre d’encombrer leur mémoire.
Donc, si vous voulez programmer un mangeur de mémoire, vous devez absolument faire quelque chose avec la mémoire que vous allouez. Pour cela, il vous suffit d'ajouter une seule ligne à votre code:
int eat_kilobyte()
{
if (memory == NULL)
memory = malloc(1024);
else
memory = realloc(memory, (eaten_memory * 1024) + 1024);
if (memory == NULL)
{
return 1;
}
else
{
//Force the kernel to map the containing memory page.
((char*)memory)[1024*eaten_memory] = 42;
eaten_memory++;
return 0;
}
}
Notez qu'il est parfaitement suffisant d'écrire dans un seul octet dans chaque page (qui contient 4096 octets sur X86). En effet, toute l’allocation de mémoire du noyau à un processus est effectuée à la granularité de la page mémoire, ce qui est dû au matériel qui ne permet pas la pagination avec des granularités plus petites.
Toutes les pages virtuelles commencent par une copie sur écriture mappée sur la même page physique mise à zéro. Pour utiliser des pages physiques, vous pouvez les salir en écrivant quelque chose sur chaque page virtuelle.
Si vous utilisez root, vous pouvez utiliser mlock(2)
ou mlockall(2)
pour que le noyau connecte les pages lorsqu'elles sont allouées, sans avoir à les salir. (Les utilisateurs non root normaux ont un ulimit -l
de seulement 64 ko.)
Comme beaucoup d’autres l’ont suggéré, il semble que le noyau Linux n’alloue pas réellement la mémoire, sauf si vous y écrivez
Ceci corrige également les incohérences de chaîne de format printf avec les types memory_to_eat et eaten_memory, en utilisant %zi
imprimer size_t
entiers. La taille de la mémoire à manger, en Ko, peut éventuellement être spécifiée en tant qu'argument de ligne de commande.
La conception complexe utilisant des variables globales et augmentant de 1 ko au lieu de 4 k pages est inchangée.
#include <stdio.h>
#include <stdlib.h>
size_t memory_to_eat = 1024 * 50000;
size_t eaten_memory = 0;
char *memory = NULL;
void write_kilobyte(char *pointer, size_t offset)
{
int size = 0;
while (size < 1024)
{ // writing one byte per page is enough, this is overkill
pointer[offset + (size_t) size++] = 1;
}
}
int eat_kilobyte()
{
if (memory == NULL)
{
memory = malloc(1024);
} else
{
memory = realloc(memory, (eaten_memory * 1024) + 1024);
}
if (memory == NULL)
{
return 1;
}
else
{
write_kilobyte(memory, eaten_memory * 1024);
eaten_memory++;
return 0;
}
}
int main(int argc, char **argv)
{
if (argc >= 2)
memory_to_eat = atoll(argv[1]);
printf("I will try to eat %zi kb of ram\n", memory_to_eat);
int megabyte = 0;
int megabytes = 0;
while (memory_to_eat-- > 0)
{
if (eat_kilobyte())
{
printf("Failed to allocate more memory at %zi kb :(\n", eaten_memory);
return 200;
}
if (megabyte++ >= 1024)
{
megabytes++;
printf("Eaten %i MB of ram\n", megabytes);
megabyte = 0;
}
}
printf("Successfully eaten requested memory!\n");
free(memory);
return 0;
}
Une optimisation sensible est faite ici. Le runtime ne fait pas acquérir la mémoire tant que vous ne l’utilisez pas.
Un simple memcpy
suffira à contourner cette optimisation. (Vous constaterez peut-être que calloc
optimise toujours l'allocation de mémoire jusqu'au point d'utilisation.)
Pas sûr de celui-ci, mais la seule explication possible est que Linux est un système d’exploitation avec copie sur écriture. Quand on appelle fork
, les deux processus pointent vers la même mémoire physique. La mémoire n'est copiée qu'une fois qu'un processus enregistre réellement dans la mémoire.
Je pense qu'ici, la mémoire physique réelle n'est allouée que lorsque l'on essaie d'écrire quelque chose. Appeler sbrk
ou mmap
pourrait bien ne mettre à jour que la comptabilité du noyau. La mémoire réelle RAM ne peut être allouée que lorsque nous essayons réellement d'accéder à la mémoire.