Par conséquent, aujourd'hui, nous allons le faire. Un petit travail de laboratoire. Sous la forme d'un petit programme C que nous écrivons, compilons et testons en action - avec et sans échange.
Le programme fait une chose très simple - il demande une grande quantité de mémoire, y accède et travaille activement avec. Afin de ne pas souffrir du chargement de bibliothèques, nous allons simplement créer un gros fichier qui sera mappé en mémoire comme le fait le système lors du chargement des bibliothèques partagées.
Et nous émulons simplement l'appel du code de cette «bibliothèque» en lisant un tel fichier mmap.
Le programme fera plusieurs itérations, à chaque itération il accédera simultanément au «code» et à l'une des sections d'un grand segment de données.
Et, afin de ne pas écrire de code inutile, nous définirons deux constantes qui détermineront la taille du "segment de code" et la taille totale de la RAM:
- MEM_GBYTES - la taille de la RAM pour le test
- LIB_GBYTES - taille "code"
La quantité de "données" dont nous disposons est inférieure à la quantité de mémoire physique:
- DATA_GBYTES = MEM_GBYTES - 2
La quantité totale de «code» et de «données» est légèrement supérieure à la quantité de mémoire physique:
- DATA_GBYTES + LIB_GBYTES = MEM_GBYTES + 1
Pour un test sur un ordinateur portable, j'ai pris MEM_GBYTES = 16 et j'ai obtenu les caractéristiques suivantes:
- MEM_GBYTES = 16
- DATA_GBYTES = 14 - signifie que "données" sera de 14 Go, soit "suffisamment de mémoire"
- Taille de swap = 16 Go
Texte du programme
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#define GB 1073741824l
#define MEM_SIZE 16
#define LIB_GBYTES 3
#define DATA_GBYTES (MEM_SIZE - 2)
long random_read(char * code_ptr, char * data_ptr, size_t size) {
long rbt = 0;
for (unsigned long i=0 ; i<size ; i+=4096) {
rbt += code_ptr[(8l * random() % size)] + data_ptr[i];
}
return rbt;
}
int main() {
size_t libsize = LIB_GBYTES * GB;
size_t datasize = DATA_GBYTES * GB;
int fd;
char * dataptr;
char * libptr;
srandom(256);
if ((fd = open("library.bin", O_RDONLY)) < 0) {
printf("Required library.bin of size %ld\n", libsize);
return 1;
}
if ((libptr = mmap(NULL, libsize,
PROT_READ, MAP_SHARED, fd, 0)) == MAP_FAILED) {
printf("Failed build libptr due %d\n", errno);
return 1;
}
if ((dataptr = mmap(NULL, datasize,
PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS,
-1, 0)) == MAP_FAILED) {
printf("Failed build dataptr due %d\n", errno);
return 1;
}
printf("Preparing test ...\n");
memset(dataptr, 0, datasize);
printf("Doing test ...\n");
unsigned long chunk_size = GB;
unsigned long chunk_count = (DATA_GBYTES - 3) * GB / chunk_size;
for (unsigned long chunk=0 ; chunk < chunk_count; chunk++) {
printf("Iteration %d of %d\n", 1 + chunk, chunk_count);
random_read(libptr, dataptr + (chunk * chunk_size), libsize);
}
return 0;
}
Tester sans utiliser swap
Désactivez le swap en spécifiant vm.swappines = 0 et exécutez le test
$ time ./swapdemo Preparing test ... Killed real 0m6,279s user 0m0,459s sys 0m5,791s
Qu'est-il arrivé? La valeur de swappiness = 0 a désactivé le swap - les pages anonymes n'y sont plus poussées, c'est-à-dire que les données sont toujours en mémoire. Le problème est que les 2 Go restants n'étaient pas suffisants pour que Chrome et VSCode s'exécutent en arrière-plan, et le tueur OOM a tué le programme de test. Et en même temps, le manque de mémoire a enterré l'onglet Chrome dans lequel j'ai écrit cet article. Et je n'aimais pas ça - même si la sauvegarde automatique fonctionnait. Je n'aime pas que mes données soient enterrées.
Swap inclus
Set vm_swappines = 60 (par défaut)
Exécutez le test:
$ time ./swapdemo Preparing test ... Doing test ... Iteration 1 of 11 Iteration 2 of 11 Iteration 3 of 11 Iteration 4 of 11 Iteration 5 of 11 Iteration 6 of 11 Iteration 7 of 11 Iteration 8 of 11 Iteration 9 of 11 Iteration 10 of 11 Iteration 11 of 11 real 1m55,291s user 0m2,692s sys 0m20,626s
Dessus du fragment:
Tasks: 298 total, 2 running, 296 sleeping, 0 stopped, 0 zombie %Cpu(s): 0,6 us, 3,1 sy, 0,0 ni, 85,7 id, 10,1 wa, 0,5 hi, 0,0 si, 0,0 st MiB Mem : 15670,0 total, 156,0 free, 577,5 used, 14936,5 buff/cache MiB Swap: 16384,0 total, 12292,5 free, 4091,5 used. 3079,1 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 10393 viking 20 0 17,0g 14,2g 14,2g D 17,3 93,0 0:18.78 swapdemo 136 root 20 0 0 0 0 S 9,6 0,0 4:35.68 kswapd0
Mauvais, mauvais Linux !!! Il utilise près de 4 gigaoctets de swap bien qu'il ait 14 gigaoctets de cache et 3 gigaoctets disponibles! Linux a de mauvais paramètres! Mauvais outlingo, mauvais vieux administrateurs, ils ne comprennent rien, ils ont dit d'activer le swap et maintenant ils font permuter le système et fonctionnent mal pour moi. Il est nécessaire de désactiver le swap comme conseillé par des experts Internet beaucoup plus jeunes et prometteurs, car ils savent exactement quoi faire!
Eh bien ... qu'il en soit ainsi. Désactivons le swap autant que possible sur les conseils des experts?
Test avec presque aucun échange
On met vm_swappines = 1
Cette valeur conduira au fait que l'échange de pages anonymes ne sera effectué que s'il n'y a pas d'autre issue.
Je fais confiance à Chris Down car je pense que c'est un grand ingénieur et il sait ce qu'il dit quand il explique que le fichier d'échange améliore les performances du système. Par conséquent, m'attendant à ce que «quelque chose» «tourne mal» et que le système puisse fonctionner de manière terriblement inefficace, je me suis assuré à l'avance et j'ai exécuté le programme de test, en le limitant avec une minuterie pour voir au moins sa terminaison anormale.
Regardons d'abord la sortie supérieure:
Tasks: 302 total, 1 running, 301 sleeping, 0 stopped, 0 zombie %Cpu(s): 0,2 us, 4,7 sy, 0,0 ni, 84,6 id, 10,0 wa, 0,4 hi, 0,0 si, 0,0 st MiB Mem : 15670,0 total, 162,8 free, 1077,0 used, 14430,2 buff/cache MiB Swap: 20480,0 total, 18164,6 free, 2315,4 used. 690,5 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 6127 viking 20 0 17,0g 13,5g 13,5g D 20,2 87,9 0:10.24 swapdemo 136 root 20 0 0 0 0 S 17,2 0,0 2:15.50 kswapd0
Hourra?! Le swap n'est utilisé que pour 2,5 gigaoctets, ce qui est presque 2 fois moins que dans le test avec swap activé (et swappiness = 60). Swap est moins utilisé. Il y a aussi moins de mémoire libre. Et nous pouvons probablement donner la victoire en toute sécurité à de jeunes experts. Mais voici la chose étrange - notre programme n'a jamais été capable de terminer même 1 (UNE!) Itération en 2 (DEUX!) Minutes:
$ { sleep 120 ; killall swapdemo ; } & [1] 6121 $ time ./swapdemo Preparing test … Doing test … Iteration 1 of 11 [1]+ Done { sleep 120; killall swapdemo; } Terminated real 1m58,791s user 0m0,871s sys 0m23,998s
Nous répétons - le programme n'a pas pu terminer 1 itération en 2 minutes, bien que dans le test précédent, il ait fait 11 itérations en 2 minutes - c'est-à-dire qu'avec le swap presque désactivé, le programme s'exécute plus de 10 (!) Fois plus lentement.
Mais il y a un avantage: pas un seul onglet Chrome n'a été endommagé. Et c'est bien.
Tester avec la désactivation complète du swap
Mais peut-être que simplement "écraser" le swap par swappiness n'est pas suffisant, et il devrait être complètement désactivé? Naturellement, cette théorie devrait également être testée. Nous sommes venus ici pour faire des tests, ou quoi?
C'est le cas idéal:
- nous n'avons aucun swap et toutes nos données seront garanties en mémoire
- le swap ne sera pas utilisé même accidentellement, car il n'est pas là
Et maintenant, notre test se terminera à une vitesse fulgurante, les personnes âgées iront à l'endroit qu'elles méritent et changeront de cartouche - la voie pour les jeunes.
Malheureusement, le résultat de l'exécution du programme de test est similaire - aucune itération n'a été effectuée.
Sortie supérieure:
Tasks: 217 total, 1 running, 216 sleeping, 0 stopped, 0 zombie %Cpu(s): 0,0 us, 2,2 sy, 0,0 ni, 85,2 id, 12,6 wa, 0,0 hi, 0,0 si, 0,0 st MiB Mem : 15670,0 total, 175,2 free, 331,6 used, 15163,2 buff/cache MiB Swap: 0,0 total, 0,0 free, 0,0 used. 711,2 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 136 root 20 0 0 0 0 S 12,5 0,0 3:22.56 kswapd0 7430 viking 20 0 17,0g 14,5g 14,5g D 6,2 94,8 0:14.94 swapdemo
Pourquoi cela arrive-t-il
L'explication est très simple - le «segment de code» que nous connectons via mmap (libptr) est dans le cache. Par conséquent, lorsque nous interdisons (ou presque interdisons) le swap d'une manière ou d'une autre, peu importe comment - en désactivant physiquement le swap, ou via vm.swappines = 0 | 1 - cela se termine toujours par le même scénario - vider le fichier mmap depuis le cache, puis en le chargeant à partir du disque. Et les bibliothèques sont chargées exactement via mmap, et pour vérifier cela, il vous suffit de faire ls -l / proc // map_files:
$ ls -l /proc/8253/map_files/ | head -n 10 total 0 lr-------- 1 viking viking 64 7 12:58 556799983000-55679998e000 -> /usr/libexec/gnome-session-binary lr-------- 1 viking viking 64 7 12:58 55679998e000-5567999af000 -> /usr/libexec/gnome-session-binary lr-------- 1 viking viking 64 7 12:58 5567999af000-5567999bf000 -> /usr/libexec/gnome-session-binary lr-------- 1 viking viking 64 7 12:58 5567999c0000-5567999c4000 -> /usr/libexec/gnome-session-binary lr-------- 1 viking viking 64 7 12:58 5567999c4000-5567999c5000 -> /usr/libexec/gnome-session-binary lr-------- 1 viking viking 64 7 12:58 7fb22a033000-7fb22a062000 -> /usr/share/glib-2.0/schemas/gschemas.compiled lr-------- 1 viking viking 64 7 12:58 7fb22b064000-7fb238594000 -> /usr/lib/locale/locale-archive lr-------- 1 viking viking 64 7 12:58 7fb238594000-7fb2385a7000 -> /usr/lib64/gvfs/libgvfscommon.so lr-------- 1 viking viking 64 7 12:58 7fb2385a7000-7fb2385c3000 -> /usr/lib64/gvfs/libgvfscommon.so
Et, comme nous l'avons envisagé dans la première partie de l'article, le système, dans des conditions de manque réel de mémoire, lorsque l'échange de pages anonymes est désactivé, choisira la seule option qui a été laissée par le propriétaire qui a désactivé l'échange. Et cette option récupère (libère) les pages vierges occupées par les données des bibliothèques chargées par mmap.
Conclusion
L'utilisation active de la méthode de distribution logicielle «je prends tout avec moi» (flatpak, snap, image docker) conduit au fait que la quantité de code connecté via mmap augmente considérablement.
Cela peut conduire au fait que l'utilisation d '"optimisations extrêmes" associées à la définition / désactivation du swap peut conduire à des effets totalement inattendus, car un fichier d'échange est un mécanisme d'optimisation du sous-système de mémoire virtuelle dans des conditions de pression de mémoire, et la mémoire disponible est complètement pas "mémoire inutilisée" mais la somme du cache et de la mémoire libre.
En désactivant le fichier d'échange, vous ne «supprimez pas la mauvaise option», mais «ne laissez aucune option»
Vous devez être très prudent lors de l'interprétation des données de consommation de mémoire de processus - VSS et RSS. Ils représentent «l'état actuel» et non «l'état optimal».
Si vous ne souhaitez pas que le système utilise le swap, ajoutez-y de la mémoire, mais ne désactivez pas le swap . La désactivation du swap à des niveaux de seuil rendra la situation bien pire qu'elle ne l'aurait été si le système avait été un peu swap.
PS: Dans les discussions, des questions sont régulièrement posées "mais si vous activez la compression mémoire via zram ...". Je suis devenu curieux et j'ai exécuté les tests appropriés: si vous activez zram et swap, comme c'est le cas par défaut dans Fedora, le runtime s'accélère à environ 1 minute.
Mais la raison en est que les pages avec des zéros sont très bien compressées, donc en fait les données ne sont pas échangées, mais sont stockées sous une forme compressée dans la RAM. Si vous remplissez un segment de données avec des données aléatoires mal compressibles, l'image deviendra moins spectaculaire et la durée du test passera à nouveau à 2 minutes, ce qui est comparable (et même légèrement pire) que celui d'un fichier d'échange "honnête".