Programmation Système Correction de l’examen du 21 mai 2003
A - Mémoire virtuelle
Réponse 1 : Une seule table des pages par processus impose la décomposition suivante : adresse virtuelle = numéro de page virtuelle + déplacement dans la page
Le numéro de page virtuelle désigne une entrée dans la table des pages. Si le bit de présence est à 1 alors le numéro de page physique est valide : adresse physique = numéro de page physique + déplacement dans la page
Page virtuelle et page physique ont la même taille.
Si le nombre d’entrées dans la table des pages est limité à 8 alors 3 bits sont nécessaires pour coder le numéro de page et ce quel que soit le mode d’exécution. Les bits restants forment le déplacement dans la page.
Une adresse est toujours un pointeur vers un octet donc la taille d’une page s’exprime en octet et est égale à : 2^nombre_de_bits_restants (lire : 2 puissance nombre_de_bits_restants)
Numéro de page virtuelle Taille d’une page physique Taille mémoire du processus
8 bits 3 bits 2^5 = 32o 2^8 = 256o
16 bits 3 bits 2^13 = 8Ko 2^16 = 64Ko
32 bits 3 bits 2^29 = 512Mo 2^32 = 4Go
Réponse 2 : Adresses physiques correspondant aux 16 adresses virtuelles :
adresse virtuelle 0x00 0x11 0x22 0x33 0x44 0x55 0x66 0x77 0x88 0x99 0xAA 0xBB 0xCC 0xDD 0xEE 0xFF
numéro de page virtuelle 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7
bit de présence 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0
numéro de page physique 7 7 6 6 5 5 4 4 3 3 - - - - - -
Adresse physique 0xE0 0xF1 0xC2 0xD3 0xA4 0xB5 0x86 0x97 0x68 0x79 - - - - - -
Réponse 3 : L’algorithme de libération des pages est de type « premier arrivé, premier enlevé » (FIFO). Un défaut de page est représenté par le symbole *
On obtient avec 3 pages libres en mémoire :
Numéro de page défaut de page
0 * 0
1 * 0 1
4 * 0 1 4
2 * 2 1 4
0 * 2 0 4
1 * 2 0 1
3 * 3 0 1
0
3 0 1
1
3 0 1
4 * 3 4 1
2 * 3 4 2
3
3 4 2
Publicité
Nombre de défauts de pages : 9
- 1 -
Et avec 4 pages libres en mémoire :
Numéro de page défaut de page
0 * 0
1 * 0 1
4 * 0 1 4
2 * 0 1 4 2
0
0 1 4 2
1
0 1 4 2
3 * 3 1 4 2
0 * 3 0 4 2
1 * 3 0 1 2
4 * 3 0 1 4
2 * 2 0 1 4
3 * 2 3 1 4
Nombre de défauts de pages : 10
On constate que l’exécution du processus génère 1 faute de page supplémentaire alors que la mémoire physique disponible est plus grande. C’est un des problèmes de l’algorithme FIFO : anomalie de Belady.
Réponse 4 : Dans le cas d’un adressage sur 8 bits, une entrée de la table des pages peut être codée sur 6 bits : 3 bits d’état et 3 bits pour le numéro de page physique. Dans ce cas, la mémoire physique est limitée à 8 pages de 32 octets. Le système ne sera pas très efficace : même avec une mémoire privée pour l’exécution du système et dans le cas favorable de processus utilisant une seule page, la mémoire physique est saturée avec 8 processus...
Augmenter le nombre de bits pour coder le numéro de page physique dans la table des pages permet d’augmenter la mémoire physique de la machine sans rien changer à l’espace d’adressage des processus : la mémoire virtuelle d’un processus reste limitée à 256 octets. Par contre, le système est capable d’exécuter beaucoup plus de processus en mémoire sans avoir besoin d’utiliser l’espace de pagination (swap), toujours très pénalisant en terme de performances.
Le même principe s’applique aux machines 32bits actuelles : la mémoire physique n’est plus limitée à 4Go alors que les processus continuent de fonctionner avec des adresses 32bits donc avec une mémoire de 4Go maximum. Il faut par contre, pour que la machine soit performante, que le système et surtout le matériel soient adaptes.
B – Problème de création de processus
Soit le fichier /user/toto contenant la chaîne de caractères : abcdefghijklmnopqrst
On se propose d’étudier le programme suivant :
main() { int PID; int fd; char buffer[20];
fd = open("/user/toto", O_RDWR); read(fd, buffer, 10); print(buffer);
PID = fork(); if (PID == 0) { read(fd, buffer, 5); print(buffer); } else { read(fd, buffer, 5); print(buffer); } }
- 2 -
Les fonctions open() et read() sont les appels systèmes vus en cours. La fonction print() affiche sur le terminal le contenu du buffer passé en paramètre. On remarque que comme le fichier est ouvert AVANT l'appel à la fonction fork(), les structures internes au noyau utilisées pour l'accès au fichier sont PARTAGEES par le processus père et le processus fils.
Question 1 : Donnez l'enchaînement des fonctions exécutées par le processus père et l'enchaînement des fonctions exécutées par le processus fils.
processus père
processus fils
open() read() print() fork() read() read() print() print()
Publicité
Question 2 : En exécutant plusieurs fois ce programme, on constate qu'il produit 2 affichages différents. Donnez ces 2 affichages.
CAS 1 abcdefghij klmno pqrst
CAS 2 abcdefghij pqrst klmno
Question 3 : Expliquez comment ces affichages sont produits et pourquoi ils sont différents.
Le système UNIX ne garantie pas d'ordonnancement particulier entre processus Père et Fils. Il se peut que l'un des processus soit élu après que l'autre ait fait le read() mais avant le print(). Il effectuera alors sa propre séquence read()/print(), mais en ayant eu le pointeur de position du fichier affecté par le read() de l’autre processus :
Le cas 1 ci dessus correspond à :
fils
ou
père
fils
père
open() read() print() fork() read() print()
read() print()
Le cas 2 ci dessus correspond à :
open() read() print() fork()
read() print()
read() print()
père
open() read() print() fork() read()
print()
fils
ou
père
fils
open() read() print() fork()
read() print()
read()
print()
read() print()
- 3 -
Le programme précédent est modifié dans le but d'utiliser le début du fichier /usr/toto comme zone de communication entre les 2 processus. Le processus père sera en charge de lire des données sur un capteur et de les écrire au début du fichier, et le processus fils sera en charge de lire ces données en début de fichier puis de les traiter et les afficher. On sait que le traitement des données est plus long que l’acquisition des données, et que donc le processus fils ne trouvera pas 2 fois de suite les mêmes données dans le fichier.
Le programme devient :
main() { int PID; int fd;
fd = open("/user/toto", O_RDWR);
read(fd, buffer, 10); print(buffer);
Publicité
PID = fork(); if (PID == 0) { do { lseek(fd, 0, SEEK_SET); count = read(fd, buffer, 5); if (count == 5) { traiter_données(buffer); print(buffer); } } while (1) } else { ??????? } }
Question 4 : En prenant exemple sur la boucle de traitement du processus fils, écrire la boucle de code du processus père. Les données seront lues grâce à la fonction lire_capteur(buffer) qui lit 5 caractères et les place dans la variable buffer.
do { lire_capteur(buffer); lseek(fd, 0, SEEK_SET); write(fd, buffer, 5); } while (1)
Question 5 : Le code ci dessus ne fonctionne pas bien. En effet, on constate que de temps en temps, le processus fils ne lit pas le début du fichier (caractères 0 à 4) mais lit les caractères 5 à 9 du fichier.
Expliquer comment cela est possible.
De la même façon qu’à la question 3, c’est un problème d’ordre d’exécution des processus père et fils. Si le processus fils perd le processeur après avoir positionné le pointeur du fichier (lseek()), et que le père exécute à ce moment là lseek()/write(), puis perd lui même le processeur, le fils va effectuer son read() alors que le pointeur de position se trouve en 5.
- 4 -
Identifiez la ou les sections critiques.
Les sections critiques sont :
Père lseek() write()
Fils lseek() read()
A partir du moment où un processus exécute le lseek, il doit exécuter l’accès fichier sans être interrompu par l’autre processus.
Proposez une solution à ce problème à l'aide de sémaphores que vous manipulerez avec les fonctions init(semaphore), P(semaphore), V(semaphore) vues en cours.
Les sections critiques seront simplement encadrées par un couple P()/V(), en ayant initialisé le sémaphore à 1 (1 seul processus à la fois dans la section critique) :
main() { int PID; int fd;
init(semaphore, 1);
fd = open("/user/toto", O_RDWR);
read(fd, buffer, 10); print(buffer);
PID = fork(); if (PID == 0) { do { P(semaphore); lseek(fd, 0, SEEK_SET); count = read(fd, buffer, 5); V(semaphore); if (count == 5) { traiter_données(buffer); print(buffer); } } while (1) } else { do { lire_capteur(buffer); P(semaphore) ; lseek(fd, 0, SEEK_SET); write(fd, buffer, 5); V(semaphore) } while (1) } }
Question 6 : On souhaite maintenant modifier le programme ci dessus pour que les 2 boucles infinies s'arrêtent sur réception d'un signal SIG_USR1. Proposer les modifications nécessaires.
La difficulté est que nous n’avons pas 1, mais 2 processus en cours d’exécution. Il faut donc faire en sorte que les 2 processus reçoivent l’information d’arrêt. Plusieurs solutions peuvent être envisagées :
On envoie 2 signaux, Un signal est envoyé au père, et le père informe le fils,
- 5 -
Voici une solution à 1 seul signal envoyé au père :
int cont = 1; int PID;
usr1_handler() { cont = 0; if (PID) kill(PID, SIG_USR1); }
main() { int fd; char buffer[20]; int semaphore;
fd = open("/user/toto", O_RDWR);
read(fd, buffer, 10); print(buffer);
signal(SIG_USR1, usr1_handler);
init(semaphore,1);
PID = fork(); if (PID == 0) { do { P(semaphore); lseek(fd, 0, SEEK_SET); count = read(fd, buffer, 5); V(semaphore); if (count == 5) { traiter_données(buffer); print(buffer); } } while (cont) } else { do { lire_capteur(buffer); P(semaphore); lseek(fd, 0, SEEK_SET); write(fd, buffer, 5); V(semaphore); } while (cont) } }
- 6 -