Je veux créer un programme qui simulera une situation de manque de mémoire (MOO) 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 consomme autant de mémoire que défini dans memory_to_eat
lequel se trouve maintenant exactement 50 Go de RAM. Il alloue de la mémoire de 1 Mo et imprime exactement le point où il ne parvient pas à en allouer davantage, de sorte que je sache quelle valeur maximale il a réussi à manger.
Le problème est que cela fonctionne. Même sur un système avec 1 Go de mémoire physique.
Lorsque je vérifie haut, je vois que le processus consomme 50 Go de mémoire virtuelle et seulement moins de 1 Mo de mémoire résidente. Existe-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 overcommit activé (je ne sais pas comment le vérifier) sans swap et virtualisé.
la source
sysctl -w vm.overcommit_memory=2
tant que root; voir mjmwired.net/kernel/Documentation/vm/overcommit-accounting . Notez que cela peut avoir d'autres conséquences; en particulier, des programmes très volumineux (par exemple votre navigateur Web) peuvent ne pas générer de programmes d'aide (par exemple le lecteur PDF).Réponses:
Lorsque votre
malloc()
implémentation demande de la mémoire au noyau système (via un appelsbrk()
ou unmmap()
appel système), le noyau note uniquement que vous avez demandé la mémoire et où elle doit être placée dans votre espace d'adressage. Il ne mappe pas encore ces pages .Lorsque le processus accède ultérieurement à la mémoire dans la nouvelle région, le matériel reconnaît une erreur de segmentation et alerte le noyau de la condition. Le noyau recherche alors la page dans ses propres structures de données, et trouve que vous devriez y avoir une page zéro, donc il mappe dans une page zéro (peut-être d'abord expulser une page du cache de page) et revient de l'interruption. Votre processus ne se rend pas compte que rien de tout cela s'est produit, le fonctionnement du noyau est parfaitement transparent (à 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, elle évite que des ressources 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'absorber trop de 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:
Notez qu'il est parfaitement suffisant d'écrire sur un seul octet dans chaque page (qui contient 4096 octets sur X86). En effet, toutes les allocations de mémoire du noyau à un processus sont effectuées avec une granularité de page mémoire, ce qui est, à son tour, à cause du matériel qui ne permet pas la pagination à des granularités plus petites.
la source
mmap
etMAP_POPULATE
(mais notez que la page de manuel indique que " MAP_POPULATE est pris en charge pour les mappages privés uniquement depuis Linux 2.6.23 ").mlockall(MCL_FUTURE)
. (Cela nécessite la racine, car ilulimit -l
ne s'agit que de 64 ko pour les comptes d'utilisateurs sur une installation par défaut de Debian / Ubuntu.) Je viens de l'essayer sous Linux 3.19 avec le sysctl par défautvm/overcommit_memory = 0
, et les pages verrouillées utilisent la mémoire vive / swap physique.malloc()
appels retournenull
. C'est clairement l'inconvénient de cette approche de la gestion de la mémoire. Cependant, c'est déjà l'existence de mappages de copie sur écriture (pensez aux bibliothèques dynamiques etfork()
) qui empêchent le noyau de savoir combien de mémoire sera réellement nécessaire. Donc, s'il ne surchargeait pas la mémoire, vous manqueriez de mémoire mappable bien avant d'utiliser réellement toute la mémoire physique.SIGSEGV
signal doit être délivré au processus.Toutes les pages virtuelles commencent par copie sur écriture mappées sur la même page physique remise à zéro. Pour utiliser des pages physiques, vous pouvez les salir en écrivant quelque chose sur chaque page virtuelle.
Si vous exécutez en tant que root, vous pouvez utiliser
mlock(2)
oumlockall(2)
demander au noyau de câbler les pages lorsqu'elles sont allouées, sans avoir à les salir. (les utilisateurs normaux non root ontulimit -l
seulement 64 ko.)Une version améliorée du code, qui fait ce que l'OP voulait:
Cela corrige également les incompatibilités de chaîne de format printf avec les types de memory_to_eat et eaten_memory, en utilisant
%zi
pour imprimer dessize_t
entiers. La taille de la mémoire à manger, en kio, peut éventuellement être spécifiée sous forme d'argument de ligne de commande.La conception désordonnée utilisant des variables globales et augmentant de 1k au lieu de 4k pages, reste inchangée.
la source
Une optimisation sensible est en cours ici. Le moteur d' exécution ne fait pas acquérir la mémoire jusqu'à ce que vous l' utilisez.
Un simple
memcpy
suffira pour contourner cette optimisation. (Vous constaterez peut-être que celacalloc
optimise encore l'allocation de mémoire jusqu'au point d'utilisation.)la source
malloc
alloue sur les limites de la page ce qui me semble assez probable.calloc
tire parti de mmap (MAP_ANONYMOUS) donnant des pages à zéro, donc elle ne duplique pas le travail de mise à zéro de page du noyau.Je ne suis pas sûr de celui-ci, mais la seule explication dont je peux parler est que linux est un système d'exploitation de copie sur écriture. Quand on appellefork
les deux processus pointent vers la même mémoire physique. La mémoire n'est copiée qu'une fois qu'un seul processus est réellement ÉCRIT dans la mémoire.Je pense qu'ici, la mémoire physique réelle n'est allouée que lorsque l'on essaie d'y écrire quelque chose. L'appel
sbrk
oummap
peut bien ne mettre à jour que la comptabilité de la mémoire du noyau. La RAM réelle ne peut être allouée que lorsque nous essayons réellement d'accéder à la mémoire.la source
fork
n'a rien à voir avec ça. Vous verriez le même comportement si vous démarrez Linux avec ce programme en tant que/sbin/init
. (c'est-à-dire PID 1, le premier processus en mode utilisateur). Cependant, vous aviez la bonne idée générale avec la copie sur écriture: jusqu'à ce que vous les salissiez, les pages nouvellement allouées sont toutes mappées en copie sur écriture sur la même page remise à zéro.