>Soit le [programme](TP1/Exo1/adresses_virtuelles.c) suivant qui affiche les adresses virtuelles de certaines variables lors de l'exécution du processus.
>L'interface (pseudo-fichier) `proc/pid/smaps` permet de voir la consommation mémoire d'un processus. On peut le formater avec la commande `pmap -X`. Le but de l'exercice est de voir ce qui se passe au niveau de la mémoire d'un processus suivant les différents mode d'allocation. Le programme `null.c` permet d'avoir un point de comparaison. Dans les cas suivants, vérifiez où se trouve la memoire correspondante :
1. Compilez le programme. Avec la commande `size`, regardez les différents segments du programme. Où se trouve le tableau `t` ? Augmentez la valeur de N. La taille de l'exécutable a-t-elle changé ? pourquoi ?
>Le temps d'éxecution de ce programme est différent pour les deux versions car dans la première version, les éléments du tableau sont accédés de manière séquentielle, alors que dans la deuxième version, le cache est rempli car on lit les éléments du tableau par colonne, donc il y a plus de cache miss vu qu'on fait des incréments bien plus grands que dans la première version.
>Le programme [sum_array.c](TP1/Exo4/sum_array.c) fait la somme des éléments d'un tableau en accédant aux éléments séquentiellement (`-c` croissant, `-d` décroissant) ou de manière aléatoire (`-a`).
>La raison de pourquoi la manière aléatoire est plus lente que la manière croissante ou décroissante (qui sont aussi rapide l'une que l'autre), est que la manière aléatoire fait plus de cache miss que les deux autres, car elle accède aux éléments du tableau de manière aléatoire, donc il y a plus de chance que l'élément ne soit pas dans le cache, alors que dans les deux autres, les éléments sont accédés de manière séquentielle, donc il y a plus de chance que l'élément soit dans le cache. Ce qui force la lecture de la RAM.
## Exercice 5
>Ecrire une fonction
```c
void hexdump(void * ptr,size_t size);
```
>qui affiche sur la sortie standard le contenu de la mémoire `[ptr,ptr+size[` au format :
> Testez vos deux programmes sur un fichier volumineux (utilisez la commande `dd` le pseudo fichier `dev/urandom` pour générer un contenu aléatoire) et expliquez pourquoi le deuxième programme est beaucoup plus rapide que le premier.
>Remarque:
- pour être dans les mêmes conditions, il faut être sur que le fichier à copier n'est pas en cache en mémoire. Générez un nouveau fichier de même taille, ou utilisez le programme [fadvise.c](TP2/Exo1/fadvise.c) qui permet de vider les blocs du cache pour un fichier quelconque.
- utilisez strace pour tracer les appels systèmes de vos deux programmes.
>Le deuxième programme est beaucoup plus rapide que le premier car il utilise des buffers, alors que le premier programme lit et écrit octet par octet et fait plus d'appels systèmes.
>Ce programme montre que quand nous lisons avec stdio, le contenu du fichier est mis en cache, et quand nous lisons avec read, il n'y a pas de cache, c'est pourquoi quand nous lisons avec stdio nous avons l'ancienne version du fichier, et quand nous lisons avec read nous avons la nouvelle version du fichier.
## Exercice 4
>Le but est d'écrire en C un programme qui efface un fichier du disque de telle manière que le contenu effacé ne soit pas récupérable. Pour des raisons physiques, on procédera de la manière suivante :
- Si l'inode correspondant au fichier à effacer à plusieurs références, on efface juste l'entrée du répertoire correspondant.
- Sinon, on réécrit les blocs de données :
- une première passe avec `0xff` pour tous les octets.
- une deuxième passe avec des valeurs aléatoires (on utilisera le pseudo-fichier `/dev/urandom`)
- enfin, avant d'effacer le fichier, on le renomera de manière aléatoire.
> Compilez et exécutez [ex1-stdio](TP3/Exo1/ex1-stdio.c) et [ex1-syscall](TP3/Exo1/ex1-syscall.c). Expliquez.
> Avec `stdio`, le printf écrit "NON" dans le cache, c'est pourquoi lors de l'utilisation du `fork` le printf écrit "NON" deux fois, alors qu'avec `syscall`, le write écrit "NON" directement dans la sortie standard, c'est pourquoi lors de l'utilisation du `fork` le printf écrit "NON" une seule fois.
## Exercice 2
> Compilez et exécutez [fork_and_fd1.c](TP3/Exo2/fork_and_fd1.c) et [fork_and_fd2.c](TP3/Exo2/fork_and_fd2.c). Expliquez.
> Pour le premier programme, le `fork` est fait après l'ouverture du fichier, donc le père et le fils se partagent le même descripteur de fichier, si l'un écrit, l'offset va se mettre à jour pour que les deux écrivent à la suite. Pour le deuxième programme, le `fork` est fait avant l'ouverture du fichier, donc le père et le fils ont deux descripteurs de fichier différents, l'offset n'est pas partagé donc le contenu du fichier sera celui du dernier à écrire.
> Que fait le programme [copy1byte.c](TP3/Exo3/copy1byte.c) ? Décommentez la ligne du `fork`. Expliquer ce qui se passe.
> Le programme [copy1byte.c](TP3/Exo3/copy1byte.c) copie le fichier `source` dans le fichier `destination` en utilisant des appels systèmes. Quand on décommente la ligne du `fork`, ce dernier est effectué après l'ouverture du fichier, donc le père et le fils ont deux descripteurs de fichier différents, l'offset n'est pas partagé donc le contenu du fichier sera celui du dernier à écrire.
## Exercice 4
> Ecrire un programme qui crée un processus fils.
>Dans le fils :
- imprimer le retour de fork(), getpid(), getppid().
- bloquer 4 secondes (sleep).
- se terminer avec exit(2).
> Dans le père :
- imprimer le retour de fork(), getpid(), getppid().
- attendre la fin de son fils (wait()), et imprimer son code de retour.
- afficher alors la liste de tous les processus actifs (execl() avec ps -ef).
## Exercice 5
> Le but de l'exercice est de détecter la présence d'un zéro dans un tableau `unsigned char` de taille `SIZE` en découpant le travail entre plusieurs processus.
> Combien y-at-il de 0 au plus dans le tableau ? Complétez la fonction `search`, et testez.
1. Première version. Modifiez le programme pour que le processus crée un fils. Le fils et le père cherche chacun le zéro dans une moitié du tableau. Le fils communique le résultat à son père. Celui-ci, à l'aide de son propre travail, donnera la réponse.
2. Deuxième version. Votre programme accepte sur la ligne de commande un entier n entre 1 et 100. Le programme crée n fils qui cherche chacun dans une partie du tableau. Le père attend la fin de chacun de ses fils, récupère leur résultat et affiche la réponse.
3. Troisième version. On améliore la version précédente. Lorsque qu'un fils trouve le 0 dans le tableau, et que le père en est averti, faites en sorte que les autres fils vivants se terminent. On utilisera la primitive `kill()` qui perment d'envoyer le signal de terminaison (`SIGTERM`) à tout un groupe.
## Exercice 6
> Exécutez le programme [session.c](TP3/Exo6/session.c) et interprétez avec `ps` les informations `pid,ppid,pgid,sess,tpgid` des processus créés.
> On peut voir qu'on a bien créé un processus fils, mais vu qu'on est pas leader de session on ne peut pas créer de nouvelle session, c'est pourquoi `sess` et `tpgid` sont les mêmes que le père.
> Écrire un programme qui pour `n>0` donné sur la ligne de commande, engendre l'arbre généalogique :
```
0|
|\ 1
| \
| |\ 2
| | \
| | |\ ...
| | | \
| | | |\ n
x x x x x
```
> Chaque processus choisit un nombre au hasard entre 0 et 127. Le processus 0 affichera la plus grande valeur choisie par tous les processus.
> Remarque : on pourra modifier la séquence de nombres aléatoires en utilisant srand dans chaque processus créé. (pourquoi ?)
# TP 4 : Signaux
## Exercice 1
> Pour calculer le nombre pi, on utilise la méthode de Monte-Carlo. On tire aléatoirement des couples (x,y) de nombres de [0,1] x [0,1]. La probabilité qu'il tombe dans le disque de rayon 1 est exactement de pi/4. On procède à plusieurs tirages pour estimer la probilité correspondante.
> En utilisant les signaux, mettre en place :
- avec `SIGALRM`, toutes les 5 secondes, l'affichage de la valeur pi en cours et le nombre de tirs effectués.
- avec `SIGINT` l'arrêt du programme (après la demande d'une confirmation), avec l'affichage du temps écoulé depuis son lancement, quand on fait `ctrl+C` au terminal.
- avec `SIGQUIT` la réinitialisation du calcul avec ctrl+\ depuis le terminal. (faites en sorte que toutes les valeurs restent cohérentes)
> Le but est de protéger un morceau de code d'un éventuellement déroutement à cause de la prise en compte d'un signal.
1. Lancez le programme, et envoyez (depuis le terminal) le signal `SIGQUIT` souvent. La fonction `swap` est-elle interrompue ? comment le voyez-vous ?
> On peut voir que la fonction `swap` est interrompue car quand nous envoyons le signal `SIGQUIT` souvent dans le terminal, le programme s'arrête pour afficher soit "x=2 y=2" soit "x=3 y=3", alors que si la fonction `swap` n'était pas interrompue, le programme afficherait soit "x=2 y=3" ou "x=3 y=2".
2. Ajoutez le code nécessaire pour assurer que `swap` ne soit jamais interrompue par `SIGQUIT`.
> On va simuler un match de ping-pong entre un père et son fils, en utilisant le signal `SIGUSR1`.
- Le père commence à jouer.
- On simule 10 échanges. À chaque coup, le père affiche Ping, le fils Pong. L'envoie de la balle consiste à envoyer le signal `SIGUSR1` à son adversaire.
>La difficulté consiste à synchroniser correctement les échanges.
1. Expliquez pourquoi ce code n'est pas correct (Faites varier `N`).
> Ce code n'est pas correct car il n'y a pas de synchronisation entre le père et le fils, donc le père peut envoyer le signal `SIGUSR1` au fils alors que le fils n'est pas encore en attente, ce qui fait que parfois le fils ne va pas recevoir le signal et donc ne va pas afficher `Pong`.
> Pour synchroniser le père et le fils, on peut utiliser `sigprocmask` pour bloquer les signaux `SIGUSR1` dans le père et le fils, et les débloquer quand on veut envoyer le signal `SIGUSR1` au fils ou au père.
> Voici un programme qui calcule le nième terme de la suite de Fibonacci de manière récursive (ce n'est pas la meilleure approche comme vous le savez).
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
long int fibo (long int n)
{
return (n<=1)?n:fibo(n-1)+fibo(n-2);
}
int main(int argc, char *argv[])
{
long int n;
assert(argc > 1);
n=strtol(argv[1],NULL,0);
printf("fibo(%ld) = %ld\n",n,fibo(n));
return 0;
}
```
> L'arbre d'appel correspondant pour n=4 :
```
fibo(4)
/ \
/ \
fibo(2) fibo(3)
/ \ / \
/ \ / \
fibo(0) fibo(1) fibo(1) fibo(2)
/ \
/ \
fibo(0) fibo(1)
```
> Modifiez le programme pour qu'il crée 2 fils. Le premier calculera fibo(n-2), le deuxième fibo(n-1). Le père récupérera les résultats et les additionnera. La communication sera assurée par des tubes.
> Le but est de mettre en oeuvre le cribble d'Ératosthène à l'aide d'une chaîne de processus reliés entre eux par des pipes.
- un premier processus `P` crée un fils avec qui il est relié par un tube, et qui génére l'ensemble des nombres entre 2 et `N` (passé à la ligne de commande.)
- Lorsqu'un nombre n'a pas été cribblé par les différents filtres, et arrive jusqu'à `P`, il est premier, et `P` crée un nouveau filtre.
1. Est-ce que l’exécution de ce programme est correcte? Vous pouvez vous en assurer en l’exécutant plusieurs fois.
> L’exécution de ce programme n’est pas correcte car quand nous exécutons le programme on retrouve plusieurs des threads qui affichent la même valeur de `i`.
2. Si vous pensez (et avez constaté) que ce n’est pas le cas, expliquez pourquoi.
> Car il y a une variable `i` qui est partagée entre les threads et qui est modifié par le thread principal en même temps que les autres threads lisent la variable `i`.
3. Modifiez le code pour qu’il donne le résultat attendu.
> On veut écrire un programme calculant la somme des entiers de `1` à `N` à l’aide de `M` threads. Chaque thread calculera la somme d’un sous-ensemble de ces entiers et la somme globale sera obtenue en calculant la somme des résultats intermédiaires de chaque thread.
> Les entiers sont répartis uniformément entre les threads comme suit (exemple avec 3 threads) :
- Thread 1 : 1, 4, 7, ...
- Thread 2 : 2, 5, 8, ...
- Thread 3 : 3, 6, 9, ...
> Le programme doit lancer `M` threads, attendre qu’ils se terminent, faire la somme des résultats intermédiaires et afficher le résultat. Les valeurs `N` et `M` seront passées en ligne de commande.
> Il est important que le programme respecte les points suivants :
- L’implémentation ne doit utiliser aucune variable globale.
- Le travail à effectuer pour chaque thread créé doit être aussi équitable que possible, quelles que soient les valeurs `N` et `M` choisies par - l’utilisateur (ex : N=20, M=8).
- Évitez d’utiliser un tableau pour contenir les valeurs à additionner.
- Réaliser un test de validation automatiquement du résultat obtenu (vous devez connaître le résultat !).
> Comparez le temps d'éxecution en fonction du nombre de threads.