TP n°2
Exercice 1.
protection des fichiers1. Dans votre répertoire courant, créer un répertoire essai_droit. Quels sont les droits par défaut de ce répertoire ?
Quelles sont les commandes (en notation symbolique et en notation octale) pour lui donner les droits suivants :
Propriétaire Groupe Autres
lect écrit exé lect écrit exé lect écrit exé
Commande 1 Oui Oui Oui Oui Non Oui Non Non Oui
Commande 2 Oui Non Oui Non Oui Non Non Non Oui
Commande 3 Non Oui Non Non Non Oui Oui Non Non
Commande 4 Non Non Oui Oui Non Oui Non Non Non
Tester ces commandes et vérifier les droits d’accès obtenus à chaque étape.
2. Créer un fichier droit dans le répertoire essai_droit, quels sont ses droits par défaut ? En partant du répertoire courant, pour chaque commande de la question précédente, essayer d’accéder au répertoire essai_droit (commande cd), de lister le contenu du répertoire essai_droit (commande ls) et de modifier le fichier droit avec un éditeur quelconque (emacs par exemple).
Exercice 2.
System monitoringAjouter au tableau construit lors de la dernière séance de TP les commandes suivantes (avec ou sans option).
vmstat, iostat, mpstat, top, ps
Pour chacune de ces commandes, étudier la page d’aide et donner une description de la commande.
Tester chacune de ces commandes sur des exemples concrets et analyser la réponse du système.
Exercice 3.
1. Primitive fork()
int fork(void) - Créer le programme fork.c suivant:
#include <stdio.h>
main() {
if (fork() == 0)
printf("processus fils %d\n", getpid());
else
printf("processus pere %d\n", getpid());
}
- Compiler et exécuter.
- Faire afficher aussi et pour chaque processus, le n° pid du processus père. Primitive getppid() - Quel est ce processus père ?
- Constater avec la commande ps, le n° pid du shell.
#include <stdio.h>
main() {
if (fork() == 0)
printf("Un processus fils %d\n", getpid());
else
/* Creer un autre fils ici. */
if (fork() == 0)
printf("Un autre processus fils %d\n", getpid());
else
printf("processus pere %d\n", getpid());
}
- Tester et commenter le programme suivant :
#include <stdio.h>
main() {
printf(“A\n”);
fork();
printf(“B\n”);
}
- Idem pour le programme suivant
#include <stdio.h>
main() {
printf("A\n");
fork();
printf("B\n");
fork();
printf("C\n");
}
2. Le Couple fork() / exec()
int execl (char *ref, char *arg0, …, char *argn, 0) int execv (char *ref, char *argv [ ]);
La création d’un nouveau processus ne peut se faire que par le « recouvrement » du code d’un processus existant. Cela se fait à l’aide d’une des fonctions de la famille exec qui permet de faire exécuter par un processus un autre programme que celui d’origine.
Lorsqu'un processus exécute un appel exec, il charge un autre programme exécutable en conservant le même environnement système :
• Du point de vue du système, il s'agit toujours du même processus : il a conservé le même pid.
• Du point de vue de l'utilisateur, le processus qui exécute exec disparaît, au profit d’un nouveau processus disposant du même environnement système et qui débute alors son exécution.
- Créer le programme prog.c suivant:
#include <stdio.h>
main(int argc, char* argv[]) {
printf("Le programme %s lancé par un autre\n", argv[0]);
}
- Compiler par:
$ cc o prog prog.c
- Exécuter par:
$ prog
- Quel est cet «autre » programme dans ce cas ? - Créer le programme ForkExec.c suivant:
#include <stdio.h>
main() {
if (fork() == 0)
execl("prog", "prog", NULL);
else
printf("Le programme normal\n");
}
- Compiler et exécuter. Constater le résultat.
- Transformer prog.c en progParam.c suivant. Compiler le vers prog.
#include <stdio.h>
main(int argc, char* argv[]) {
int i;
printf("Le programme (%s) lancé et parametres recus:\n", argv[0]);
for(i=1; i<argc; i++)
printf("%s\n",argv[i]);
}
- Modifier le programme ForkExec.c en remplaçant la ligne execl par:
execl("prog", "prog", "Jean", "Paris", "21", NULL);
- Compiler et exécuter. Constater le résultat.
- Faites exec ps. Que se passe-t-il ? Donnez une explication.
- Vérifiez votre explication en étudiant la commande shell exec sh. (Quel est le pid de ce shell ?) 4. La primitive exit()
#include <stdlib.h>
void exit (int status)
La fonction exit met fin au processus qui l’a émis, avec un code de retour status. Tous les descripteurs de fichiers ouverts sont fermés ainsi que tous les flots de la bibliothèque d’E/S standard. Si le processus à des fils lorsque exit est appelé, ils ne sont pas modifiés mais comme le processus père prend fin, le nom de leur processus père est changé en 1, qui est l’identifiant du processus init.
Ce processus init est l’ancêtre de tous les processus du système excepté le processus 1 lui-même ainsi que le processus 0 chargé de l’ordonnancement des processus. En d’autres termes, c’est l’ancêtre de tous les processus qui adopte tous les orphelins. Par convention, un code de retour égal à zéro signifie que le processus s’est terminé correctement, et un code non nul (généralement 1) signifie qu’une erreur s’est produite. Seul l’octet de droite de l’entier status est remonté au processus père. On est donc limité à 256 valeurs pour communiquer un code retour à son processus père. Le père du processus qui effectue un exit reçoit son code retour à travers un appel à wait.
3. Appel système wait() et waitpid()
#include <sys/types.h>
#include <sys/wait.h>
int wait (int * terminaison)
int waitpid (int pid, int *terminaison, int options)
Lorsque le fils se termine, si son père ne l’attend pas, le fils passe à l’état defunct (ou zombi) dans la table des processus. L’élimination d’un processus terminé de la table ne peut se faire que par son père, grâce à la fonction wait. Avec cette instruction :
• Le père se bloque en attente de la fin d’un fils.
• Elle rendra le n° ( PID) du premier fils mort trouvé.
• La valeur du code de sortie est reliée au paramètre d’exit de ce fils. On peut donc utiliser l’instruction wait pour connaître la valeur éventuelle de retour, fournie par exit(), d’un processus. Ce mode d’utilisation est analogue à celui d’une fonction. wait() rend –1 en cas d’erreur.
L’appel système waitpid permet de tester la terminaison d’un processus particulier, dont on connaît le PID.
La primitive permet de tester, en bloquant ou non le processus appelant, la terminaison d’un processus particulier ou appartenant à un groupe de processus donné et de récupérer les informations relatives à sa terminaison à l’adresse terminaison.
Plus précisément le paramètre pid permet de sélectionner le processus attendu de la manière suivante :
• <-1 tout processus fils dans le groupe | pid |
• -1 tout processus fils
• 0 tout processus fils du même groupe que l’appelant
• >0 processus fils d’identité pid
Le paramètre options est une combinaison bit à bit des valeurs suivantes. La valeur WNOHANG permet au processus appelant de ne pas être bloqué si le processus demandé n’est pas terminé.
La fonction renvoie : -1 en cas d’erreur,
0 en cas d’échec (processus demandé existant mais non terminé) en mode non bloquant,
> 0 le numéro du processus fils zombi pris en compte.
- En se reférant au deuxième code du point 1. de ce même exercice, ajouter les instructions qui permettront au processus père d’attendre la terminaison de son fils et qui afficheront le code retour de celui-ci.
Processus zombie
Lorsqu'un programme se termine, un processus spécial hérite de ses fils, le programme init, qui s'exécute toujours avec un identifiant de processus valant 1 (il s'agit du premier processus lancé lorsque Linux démarre). Le processus init libère automatiquement les ressources de tout processus zombie dont il hérite.
Exercice 5.
Les deux programmes ci-dessous proposent une première approche de synchronisation entre deux processus père et fils. Dans le second, il y a deux fils. Le père est synchronisé de telle sorte qu'il se termine après la terminaison du deuxième fils. Ecrire ces deux programmes. Les exécuter et en analyser le fonctionnement.
// premier programme
#include <stdio.h>
#include <unistd.h>
main() {
int pid;
int status;
switch(pid=fork()) {
case 1 : perror("création de processus\n");
exit(2);
case 0: printf("Je suis le fils\n");
printf("valeur de fork = %d\n", pid);
printf("je suis le processus %d de père %d\n", getpid(),getppid());
printf("fin du fils\n");
exit(0);
default : wait(&status);
printf("Je suis le père\n");
printf("valeur de fork = %d\n", pid);
printf("je suis le processus %d de père %d\n", getpid(),getppid());
printf("fin du père\n");
exit(128);
} }
// deuxième programme
#include <stdio.h>
#include <unistd.h>
main() {
int pid_fils1, pid_fils2, pid_courant;
int status; pid_fils1=fork();
pid_courant=getpid();
switch(pid_fils1) {
case 1: perror("Pb dans la création du 1er fils\n");
exit(2);
case 0: printf("%d: je suis le 1er fils du processus %d\n", pid_courant,getppid());
printf("%d: fin du processus\n",pid_courant);
exit(0);
default : switch(pid_fils2=fork()) {
case 1: perror("Pb dans la création du 2ème fils\n");
exit(3); case 0: printf("%d: Je suis le 2nd fils du processus
%d\n",getpid(),getppid());
sleep(2);
printf("%d: fin du processus\n",getpid());
exit(1);
default : printf("%d: je suis le père et j'attends mon fils de pid %d\n",pid_courant,pid_fils2);
waitpid(pid_fils2,&status,0);
printf("%d: fin du père\n",pid_courant);
exit(128);
} }
}
Exercice 6.
La primitive system()La fonction system lance l’exécution d’un processus SHELL interprétant la commande passée en argument dans la chaîne de caractères ch. Cette fonction crée pour cela un nouveau processus, qui se termine avec la fin de la commande. Le processus à l'origine de l'appel de system est suspendu jusqu'à la fin de la commande.
int system (char *ch)
- Ecrire un programme qui débute par l'affichage de BONJOUR, se poursuit par l'exécution de la commande "ls -l" et se termine par l'affichage de BONSOIR.
- Ecrire un programme qui lance la commande shell « sleep 60 » à l’aide de la primitive system.
Lancer l’exécutable correspondant en arrière plan et taper la commande ‘ps –l’. Expliquer le résultat obtenu.
- Effectuer la même opération en lançant la commande shell en arrière plan : sleep 60 &. Que se passe-t-il ?
Exercice 7.
popen()L'inconvénient majeur de la fonction system() est que le programme appelant ne contrôle ni en entrée ni en sortie, les données traîtées par la commande. La fonction popen() permet de pallier cette insuffisance. De manière transparente, popen() ouvre un pipe avant d'appeler la commande, permettant ainsi au programme appelant de communiquer avec le programme appelé grâce à ce pipe.
La syntaxe est la suivante:
FILE * popen(char * commande, char * mode)
1. où FILE * indique que la fonction popen() retourne un pointeur sur une structure de type FILE, comme le fait la fonction fopen().
2. où commande représente la chaîne de caractères associée au nom de la commande Unix, éventuellement suivie de ses arguments, exactement comme pour la fonction system().
3. où mode correspond au mode d'entrée/sortie choisi:
◦ "r" lecture
◦ "w" ecriture
Ce mode a la même signification que celui qui est généralement utilisé dans la fonction fopen(filename, mode.
Exemple de lecture de données d'une commande restituées sur l'écran:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void main() {
FILE *pp;
char buf[128];
if ((pp = popen("ps aux", "r")) == NULL) {
perror("popen");
exit(1);
}
while (fgets(buf, sizeof(buf), pp)) fputs(buf, stdout);
pclose(pp);
}
La fonction pclose() permet de fermer le pipe, avec en argument le pointeur de fichier rendu initialement par popen().
Exercice 8.
(homework)Pour chacune de ces commandes, étudier la page d’aide et donner une description de la commande.
Tester chacune de ces commandes sur des exemples concrets et analyser la réponse du système.
mount, umount, printenv, set