dimanche 30 mars 2014

Tentative d'observation d'un COW! (Copy On Write)

Pourquoi? 

Dans l'article "Tribulations d'un processus chez les Jivaros" on a donc construit un petit programme avec une région de code de taille inférieure à une page, et une région de pile. Il est temps de regarder comment ce programme fonctionne, et surtout de tenter d'observer comment fonctionne (du point de vue de ce programme / processus) la gestion de mémoire virtuelle. Comme on a peu de pages, cela devrait être raisonnablement facile à interpréter... 





Ce que l'on va essayer d'observer

  • Quelle est la disposition mémoire de notre processus lors dès son démarrage ? Pour cela on va  bloquer son exécution au moyen d'une lecture sur le terminal.
  • Quelle est la disposition mémoire d'un processus  (et de son père) créé par fork, juste après le retour de fork ?
  • Si on enrichit notre programme avec une région de données, que se passe-t-il quand le fils accède à cette région, en lecture puis en écriture?

Comment?

On va compléter notre programme pour lui faire faire quasiment du pas à pas, et on va utiliser un petit shell script pour accéder aux informations disponibles sur la mémoire via les fichiers /proc/[pid]/maps, /proc/[pid]/pagemap, et /proc/kpagecount, de manière à s'éviter les calculs fastidieux d'offset dans ces fichiers.

Shell script pour observer la mémoire d'un processus

 Ce script (getpg.sh)  n'a pas la prétention d'être parfait. C'est juste un outil "vite fait", pour pouvoir obtenir :
  • le numéro de page physique associé à un adresse virtuelle d'un processus,  
  • un indicateur pour déterminer si la page est présente ou non,
  • un compteur de partage de la page physique.
En fait, cela automatise les calculs faits et utilisés dans l'article : "Linux: Mémoire occupée par un processus" .

getpg.sh prend trois arguments:
  • pid : numéro du processus à observer
  • Numéro de région (indice de la région dans /proc/[pid]/maps ou la chaine 'stack'  si on veut examiner la pile
  • Nombre de pages que l'on veut examiner en partant de l'adresse de base de la région (dans le cas de la pile on part des adresses hautes en décroissant), ou la chaine 'all' si on veut voir les informations associées à toutes les pages d'une région.
Exemple:

fa $ ps -edf | grep fatiny
francois 13885  1981  0 16:56 pts/2    00:00:00 ./fatiny

fa $ cat /proc/13885/maps
08048000-08049000 r-xp 00000000 08:01 132837     /home/fa/fatiny
b776c000-b776d000 r-xp 00000000 00:00 0          [vdso]
bfeaf000-bfed0000 rw-p 00000000 00:00 0          [stack]
fa $ ./getpg.sh 13885 1 all
13885 : Page 1 (rgn 1) present (PFN: 00028a4c)has count 1

fa $

Ici, on a analysé toutes les pages de la première région (région de code qui ne contient qu'une page), et en regardant dans /proc/13885/pagemap, et dans /proc/kpagecount, on a déterminé que l'adresse virtuelle 08048000 correspondait à une page physique de numéro (PFN  ou Page Frame Number) 2814C et que cette page physique n'était référencée qu'une seule fois.

Note: comme le script accède au fichier /proc/kpagecount, il est nécessaire de l'exécuter avec les privilèges appropriés.

Le programme à surveiller

int var = 1;

_start()
{
    int i = 0;
    char buf[8];
    int pid;

    mywrite(1, "before reading var\n", 19);
    myread(0, buf, 2);
    i = var;
    mywrite(1, "after reading var\n", 18);
    myread(0, buf, 2);
    pid = myfork();
    if (pid == 0) {
        mywrite(1, "child after fork\n", 17);
        myread(0, buf, 1);
        i= var;
        mywrite(1, "child will touch data\n", 22);
        myread(0, buf, 1);
        var = 2;
        mywrite(1, "child has touched data\n", 23);
        myread(0, buf, 1);
    } else {
        mywaitpid(pid, (char*)0, 0);
        mywrite(1, "dad out of waitpid\n", 19);
    }
    myexit();
}

Rien d'extraordinaire. Les "appels systèmes"  ont été réécrits en assembleur localement comme illustré dans l'article "Tribulations d'un processus chez les Jivaros", et ont été omis ici, par souci de simplification. 

Le programme a une seule variable globale initialisée à 1. Les messages affichés, suivis d'un lecture vont nous permettre d'examiner le(s) processus pendant leur exécution en utilisant notre script. 
  • Le processus initial référence la variable globale
    • On devrait en profiter pour voir que la page de données n'est pas chargée en mémoire par défaut mais seulement "à la demande"
  • Puis le processus fait un fork()
    • Le père attend la fin d'exécution du fils
    • Le fils accède la variable globale en lecture
    • Puis le fils modifie cette variable globale.
Voilà, tout est prêt! 

Moteur! Silence, on tourne!

fa $ gcc -s -o fatiny fatiny.c -static -nostartfiles -nostdlib
fa $ size ./fatiny
   text       data        bss        dec        hex    filename
    945          4          0        949        3b5    ./fatiny

fa $

Notre programme a bien un code qui tient dans une page, et 4 octets de données statiques qui devraient aussi tenir dans une page.

fa $ ./fatiny
before reading var

Notre processus a exécuté quelques instructions, mais n'a pas encore accédé à une donnée globale. Regardons sa cartographie mémoire.

fa $ cat /proc/14132/maps
08048000-08049000 r-xp 00000000 08:01 132837     /home/fa/fatiny
0804a000-0804b000 rw-p 00001000 08:01 132837     /home/fa/fatiny
b777e000-b777f000 r-xp 00000000 00:00 0          [vdso]
bf9dc000-bf9fd000 rw-p 00000000 00:00 0          [stack]
fa $

Les régions attendues sont définies. La région de code (la première région)  a une taille de 0x1000 : une page, la deuxième ligne indique aussi que la région de données a une taille d'une page.

Que dit notre script?

fa $ ./getpg.sh 14132 1 all
14132 : Page 1 (rgn 1) present (PFN:00003f36) has count 1

fa $ ./getpg.sh 14052 2 all
14132 : Page 1 (rgn 2)not present
fa $ ./getpg.sh 14052 stack 3
14132 : Page 1 (stack) present (PFN: 000084d8) has count 1
14132 : Page 2 (stack) present (PFN: 0003e1d7) has count 1

14132 : Page 3 (stack) not present
fa $
  • La page de code est présente! Rassurant! On vient d'exécuter une partie des instructions.
  • La page de données, n'est pas en mémoire. Personne n'a encore accédé à la variable globale.
  • La pile! Tiens la pile a deux pages! A priori, sans précaution, notre programme a hérité dune flopée de variables d'environnements. Ce qui doit expliquer ces deux pages.
Donc tout est conforme à nos attentes. Laissons notre processus, accéder à sa variable globale!

Premier accès à la variable globale

fa $ ./fatiny
before reading var

after reading var

fa $ ./getpg.sh 14132 1 all
14132 : Page 1 (rgn 1) present (PFN:00003f36) has count 1

fa $ ./getpg.sh 14052 2 all
14132 : Page 1 (rgn 2) present (PFN: 0000e520) has count 1
fa $ ./getpg.sh 14052 stack 3
14132 : Page 1 (stack) present (PFN: 000084d8) has count 1
14132 : Page 2 (stack) present (PFN: 0003e1d7) has count 1

14132 : Page 3 (stack) not present
fa $

Ouf ! Notre page de données est bien chargée en mémoire. C'est la seule différence avec l'étape précédente!

Passons au fork() ! Nous allons avoir deux processus à examiner.

 fork()

fa $ ./fatiny
before reading var

after reading var

child after fork 

Qu'obtient-on pour le père?
fa $ ./getpg.sh 14132 1 all
14132 : Page 1 (rgn 1) present (PFN:00003f36) has count 2

fa $ ./getpg.sh 14052 2 all
14132 : Page 1 (rgn 2) present (PFN: 0000e520) has count 1
fa $ ./getpg.sh 14052 stack 3
14132 : Page 1 (stack) present (PFN: 000084d8) has count 2
14132 : Page 2 (stack) present (PFN: 0000b16c) has count 1

14132 : Page 3 (stack) not present
fa $

Rien de notable? Ah, si! Le compteur d'utilisation de la page de code est passé de 1 à 2! Et celui de la première page de pile également. Pourquoi la deuxième page de pile n'a-t-elle pas son compteur à 2?
  • Nos processus en retour de fork, ont déjà touché leur pile respective (la deuxième page)? Mais ce n'est pas l'explication:  en fait, si on se débrouille pour que le code utilisateur des processus, n'utilise pas la pile, on obtient le même comportement. La page semble être copiée systématiquement lors de l'appel à fork.
  •  ET! La page physique (PFN)  qui était utilisée pour la pile du processus a changé! Au voleur!
Et pourquoi la page de données n'a-t-elle pas son compteur à 2? Serait-elle déjà copiée pour le fils?
Vérifions?
fa $ ./getpg.sh 14470 1 all
14470 : Page 1 (rgn 1)is present (PFN: 00003f36) has count 2
fa $ ./getpg.sh 14470 2 all
14470 : Page 1 (rgn 2)is not present
fa $ ./getpg.sh 14470 stack 3
14470 : Page 1 (stack) present (PFN: 000084d8) has count 2
14470 : Page 2 (stack) present (PFN: 0003e1d7) has count 1
14470 : Page 3 (stack) not present
fa $

On retrouve:
  • Le code du fils utilise la même page physique (PFN) que le père, et le compteur est à 2. Le code est donc partagé.
  • Le fils n'a pas encore de page associée à sa région de données. Le noyau n'a pas jugé utile d'établir un lien entre adresse virtuelle et page physique... dès fois que ce soit inutile. Procrastination quand tu nous tiens ! Non, sagesse efficace.
  • La deuxième page physique de pile du fils est en fait l'ancienne deuxième page de pile du père. Ce n'était pas un vol, c'était un don ou héritage!

 Le fils accède  en lecture la variable globale

fa $ ./fatiny
before reading var

after reading var

child after fork 


child will touch data

fa $ ./getpg.sh 14470 1 all
14470 : Page 1 (rgn 1)is present (PFN: 00003f36) has count 2
fa $ ./getpg.sh 14470 2 all
14470 : Page 1 (rgn 2) present (PFN:0000e520) has count 2
fa $ ./getpg.sh 14470 stack 3
14470 : Page 1 (stack) present (PFN: 000084d8) has count 2
14470 : Page 2 (stack) present (PFN: 0003e1d7) has count 1
14470 : Page 3 (stack) not present
fa $


Le processus père étant bloqué dans le waitpid, son statut ne va pas évoluer. Seuls les compteurs de ses pages physiques pourraient bouger.

Ici après avoir accédé en lecture la  variable globale, on voit que:
  • le processus a maintenant une page physique associée à sa région de données.
  • Cette page est la même (même PFN) que celle utilisée par le père.
  • Et son compteur est donc logiquement 2

Le fils écrit dans la page du père?

fa $ ./fatiny
before reading var

after reading var

child after fork 


child will touch data

child has touched data

fa $ ./getpg.sh 14470 1 all
14470 : Page 1 (rgn 1)is present (PFN: 00003f36) has count 2
fa $ ./getpg.sh 14470 2 all
14470 : Page 1 (rgn 2)is present (PFN: 0000ff12) has count 1
fa $ ./getpg.sh 14470 stack 3
14470 : Page 1 (stack) present (PFN: 000084d8) has count 2
14470 : Page 2 (stack) present (PFN: 0003e1d7) has count 1
14470 : Page 3 (stack) not present
fa $ ./getpg.sh 14132 2 all
14132 : Page 1 (rgn 2) present (PFN: 0000e520) has count 1

Après écriture de la variable par le fils, on voit que le PFN de la page de données a changé et que le compteur de cette page est de 1. Et la dernière commande permet de vérifier que le processus père a toujours accès à la page physique de données initiale dont le compteur est retombé à 1

Copy On Write : je l'ai vu!


On vient donc d'observer un phénomène de Copy On Write (ou  COW) : plutôt que de se précipiter à copier les pages qui doivent être dupliquées entre processus père et fils, partage la page d'origine entre père et fils, et "interdit" les écritures sur cette page partagée, en fait pour détecter ces écritures, et faire une copie "Just In Time".

Aucun commentaire:

Enregistrer un commentaire