Programmation Système en C sous linux
![Programmation Système en C sous linux](/images/remos_downloads/coursinformatique-id3220.3220.pdf_thumb_prev.jpg)
Programmation Système (en C sous linux)
Rémy Malgouyres
LIMOS UMR 6158, IUT département info Université Clermont 1, B.P. 86 63172 AUBI
Une version PDF de ce document est téléchargeable sur , ansi que la version html.
2
Table des matières
1 Arguments d’un programme et variables d’environnement 5
1.1 atoi, sprinf et sscanf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.2 Argumentsdumain. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 Variablesd’environnement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2 Processus 10
2.1 Processus, PID, UID . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2 Lafonction fork . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.3 Terminaisond’unprocessusfils . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
3 Lancement d’un programme :exec 15
3.1 Rappels:Argumentsenlignedecommande . . . . . . . . . . . . . . . . . . . 15
3.2 L’appelsystème exec . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
3.3 Lafonction system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
3.4 Applicationssuidetproblèmesdessécuritéliés system, execlp ou exevp . . . 19
3.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
4 Communication entre processus 21
4.1 Tubeset fork . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.2 Transmettredesdonnéesbinaires . . . . . . . . . . . . . . . . . . . . . . . . . 23
4.3 Redirigerlesflotsd’entrées-sortiesversdestubes . . . . . . . . . . . . . . . . 24
4.4 Tubesnommés . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
5 Threads Posix 27
5.1 Pointeursdefonction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
5.2 ThreadPosix(souslinux) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
5.3 Donnéepartagéesetexclusionmutuelle . . . . . . . . . . . . . . . . . . . . . . 32
5.4 Sémaphores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
5.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
6 Gestion du disque dûr et des fichiers 42
6.1 Organisationdudisquedur . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
6.2 Obtenirlesinformationssurunfichieren C . . . . . . . . . . . . . . . . . . . 47
6.3 Parcourirlesrépertoiresen C . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
6.4 Descripteursdefichiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
6.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
TABLEDESMATIÈRES
7 Signaux 51
7.1 Préliminaire:Pointeursdefonctions . . . . . . . . . . . . . . . . . . . . . . . 51
7.2 Lesprincipauxsignaux . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7.3 Envoyerunsignal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
7.4 Capturerunsignal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
7.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
8 Programmation réseaux 60
8.1 AdressesIPetMAC . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
8.2 Protocoles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
8.3 Servicesetports . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
8.4 SocketsTCP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
8.5 Créeruneconnectionclient . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
8.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
A Compilation séparée 74
A.1 Variablesglobales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
A.2 Mettreducodedansplusieursfichiers. . . . . . . . . . . . . . . . . . . . . . . 75
A.3 Compilerunprojetmultifichiers . . . . . . . . . . . . . . . . . . . . . . . . . . 77
Introduction
Ce cours porte sur l’utilisation des appels système des systèmes de la famille Unix : Linux, MacOS X, AIX, LynxOS, BeOS, QNX, OpenBSD, FreeBSD,NetBSD.
Lerôled’unsystèmeestde(voislafigure1):
• Gérerlematériel:
– Masquer le matériel en permettant aux programmes d’intéragir avec le matériel à traversdespilotes;
– Partager les ressources entre les différents programmes en cours d’exécution (processus).
• Fourniruneinterfacespourlesprogrammes(unensembled’appelssystème)
• Fourniruneinterfacesbasniveaupourl’utilisateur(unensembledecommandeshell)
• Eventuellement une interface utilisateur haut niveau par un environnement graphique(kde, gnome)
Figure 1:Schémad’unsystèmed’exploitationdetypeUnix
LanormePOSIX(PortableOperatingSystemInterfaceuniX)estunensembledestandards del’IEEE(InstituteofElectricalandElectronicsEngineers).POSIXdéfinitnotamment:
• Lescommandesshelldebase(ksh,ls,man, )
TABLEDESMATIÈRES
• L’API(ApplicationProgrammingInterface)desappelssystème.
• L’APIdesthreads
Chapitre 1
Arguments d’un programme et variables d’environnement
Nous allons voir dans ce chapitre les passages d’arguments et variables d’environnement qui permettent à un shell de transmettre des informations à un programme qu’il lance. Plus généralement, ces techniques permettent à un programme de transmettre des informations aux programmesqu’illance(processusfilsoudescendants).
1.1atoi,sprinfetsscanf
Parfois,unnombrenousestdonnésousformedechaînedecaractèredontlescaractèressont des chiffres. Dans ce cas, la fonction atoi permet de réaliser la conversion d’une chaîne vers un int.
#include <stdio.h>
int main()
{ int a; char s[50];
printf("Saisissez des chiffres : "); scanf("%s", s); \com{saisie d'une chaîne de caractères} a = atoi(s); \com{conversion en entier} printf("Vous avez saisi : %d\n", a); return 0; }
Plusgénéralement,lafonction sscanf permetdeliredesdonnéesformatéesdansunechaîne de caractère (de même que scanf permet de lire des données formatées au clavier ou fscanf dansunfichiertexte). #include <stdio.h> int main()
RémyMalgouyres, http InitiationàlaProgrammationSystème
{ float x; char s[50];
printf("Saisissez des chiffres (avec un point au milieu) : "); scanf("%s", s); /* saisie d'une chaîne de caractères */ sscanf(s, "%f", &x); /* lecture dans la chaîne */ printf("Vous avez saisi : %f\n", x); return 0;
}
Inversement, la fonction sprintf permet d’écrire des données formatées dans une chaîne decaractères(demêmeque printf permetd’écriredanslaconsoleou fprintf dansunfichier texte).
#include <stdio.h>
void AfficheMessage(char *message)
{ puts(message);
}
int main()
{ float x; int a;
printf("Saisissez un entier et un réel : "); scanf("%d %f", &a, &x);
sprintf(s, "Vous avez tapé : a = %d x = %f", a, x);
AfficheMessage(s); return 0;
}
1.2 Arguments du main
La fonction main d’un programme peut prendre des arguments en ligne de commande. Par exemple,siunfichier monprog.c apermisdegénérerunexécutable monprog àlacompilation, gcc monprog.c -o monprog onpeutinvoquerleprogramme monprog avecdesarguments
./monprog argument1 argment2 argument3
Exemple. Lacommande cp dubashprenddeuxarguments:
$ cp nomfichier1 nomfichier2 Chapitre1:Argumentsd’unprogrammeetvariablesd’environnement
Pourrécupérerlesargumentsdansleprogramme C,onutiliselesparamètres argc et argv du main.L’entier argc donnelenombred’argumentsrentrésdanslalignedecommande plus 1,etleparamètre argv estuntableaudechaînesdecaractèresquicontientcommeéléments:
• Lepremierélément argv[0] estunechaînequicontientlenomdufichierexecutabledu programme;
• Lesélémentssuivants argv[1], argv[2],etc sontdeschaînesdecaractèresquicontiennent lesargumentspassésenlignedecommande.
Leprototypedelafonction main estdonc:
int main(int argc, char**argv);
Exemple.Voiciunprogrammelongeurs,quiprendenargumentdesmots,etaffichelalongueur decesmots.
#include <stdio.h> #include <string.h>
int main(int argc, char**argv)
{ int i;
printf("Vous avez entré %d mots\n", argc-1); puts("Leurs longueurs sont :");
for (i=1 ; i<argc ; i++)
{ printf("%s : %d\n", argv[i], strlen(argv[i]));
} return 0;
}
Voiciunexempledetrace:
$ gcc longueur.c -o longueur
$ ./longueur toto blabla Vous avez entré 2 mots Leurs longueurs sont :
toto : 4 blabla : 6
1.3 Variables d’environnement
1.3.1 Rappels sur les variables d’environnement
Lesvariablesd’environnementsontdesaffectationsdelaforme
NOM=VALEUR
RémyMalgouyres, http InitiationàlaProgrammationSystème
quisontdisponiblespourtouslesprocessusdusystème,ycomprislesshells.Dansunshell,on peutavoirlalistedesvarialblesd’environnementparlacommande env.Parexemple,pourla variable d’environnement PATH qui contient la liste des répertoires où le shell va chercher les commandesexécutables:
$ echo PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11 $ PATH=PATH:.
$ export PATH
$ env | grep PATH
PATH=/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:.
la commande export permet de transmettre la valeur d’une variable d’environnement aux descendantsd’unshell(programmeslancésàpartirdushell).
1.3.2 Accéder aux variables d’environnement en C
DansunprogrammeC,onpeutaccéderàlalistedesvariablesd’environnementdanslavariable environ, qui est un tableau de chaînes de caractères (terminé par un pointeur NULL pour marquerlafindelaliste). #include <stdio.h> extern char **environ;
int main(void)
{ int i;
for (i=0 ; environ[i]!=NULL ; i++) puts(environ[i]);
return 0; }
Pouraccéderàunevariabled’environnementparticulièreàpartirdesonnom,onutilisela fonction getenv, qui prend en paramètre le nom de la variable et qui retourne sa valeur sous formedechaînedecaractère.
#include <stdio.h>
#include <stdlib.h> /* pour utiliser getenv */
int main(void)
{ char *valeur; valeur = getenv("PATH"); if (valeur != NULL) printf("Le PATH vaut : %s\(\backslash\)n", valeur);
valeur = getenv("HOME"); if (valeur != NULL) Chapitre1:Argumentsd’unprogrammeetvariablesd’environnement
printf("Le home directory est dans %s\(\backslash\)n", valeur);
return 0;
}
Pour assigner une variable d’environnement, on utilise la fonction putenv, qui prend en paramètreunechaînedecaractère.Notonsquelamodificationdelavariablenevautquepour le programme lui-même et ses descendants (autres programmes lancés par le programme), et nesetransmetpasaushell(ouautre)quialancéleprogrammeencours.
#include <stdio.h>
#include <stdlib.h> /* pour utiliser getenv */
int main(void)
{ char *path, *home, *nouveaupath; char assignation[150]; path = getenv("PATH"); home = getenv("HOME");
printf("ancien PATH : %s\net HOME : %s\n", path, home);
sprintf(assignation, "PATH=%s:%s/bin", path, home); putenv(assignation); nouveaupath = getenv("PATH");
printf("nouveau PATH : \n%s\n", nouveaupath); return 0;
}
Exempledetrace:
$ gcc putenv.c -o putenv echo PATH
$ /usr/local/bin:/usr/bin:/bin:/usr/bin/X11
$ ./putenv ancien PATH : /usr/local/bin:/usr/bin:/bin:/usr/bin/X11 et HOME : /home/remy nouveau PATH :
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/home/remy/bin echo PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11
Chapitre 2
Processus
2.1 Processus, PID, UID
2.1.1 Processus et PID
Chaqueprogramme(fichierexécutableouscript shell,Perl)encoursd’exécutiondanslesystème coorespond à un (ou parfois plusieurs) processus du système. Chaque processus possède un numéro de processus (PID).
Sousunix,onpeutvoirlalistedesprocessusencoursd’exécution,ainsiqueleur PID,par lacommande ps,quicomportedifférentesoptions.
Pourvoirsespropresprocessusencoursd’exécutiononpeututiliserlecommande
$ ps x
Pourvoirl’ensembledesprocessusdusystème,onpeututiliserlacommande
$ ps -aux
Pourvoirl’ensembledesattributsdesprocessus,onpeututiliserl’option -f.Parexemple, pourl’ensembledesattributsdesespropresprocessus,onpeututiliser
$ ps -f x
UnprogrammeCpeutaccéderau PID desoninstanceencoursd’exécusionparlafonction getpid,quiretournele PID :
pid_t getpid(\void);
Nous allons voir dans ce chapitre comment un programme C en cours d’exécution peut créerunnouveauprocessus(fonction fork),puisauchapitresuivantcommentunprogramme C en cours d’exécution peut se faire remplacer par un autre programme,tout en gardant le même numéro de processus (fonction exec). L’ensemble de ces deux fonction permettra à un programme C de lancer un autre programme. Nous verrons ensuite la fonction system, qui permet directement de lancer un autre programme, ainsi que les problèmes de sécurité liés à l’utilisationdecettefonction.
Chapitre2:Processus
2.1.2 Privilèges, UID,Set-UID
Chaque processus possède aussi un User ID, noté UID, qui identifie l’utilisateur qui a lancé le processus. C’est en fonction de l’UID que le processus se voit accordé ou refuser les droits d’accèsenlecture,écritueouexécutionàcertainsfichiersouàcertainescommandes.Onficxe lesdroitsd’accèsd’unfichieraveclacommande chmod.L’utilisateur root possèdeun UID égal à0.UnprogrammeCpeutaccéderàl’UID desoninstanceencoursd’exécutionparlafonction getuid :
uid_t getuid(\void);
Ilexisteunepermissionspéciale,uniquementpourlesexécutablesbinaires,appeléelapermission Set-UID.Cettepermissionpermetàunustilisateurayantlesdroitsenexécutionsurle fichierdexécuterlefichier avec les privilège du propriétaire du fichier.Onmetlesdroits Set-UID avec chmod +s.
$ chmod +x fichier $ ls -l | |
-rwxr-xr-x 1 remy remy 7145 Sep $ chmod +s fichier | 6 14:04 fichier |
-rwsr-sr-x 1 remy remy 7145 Sep 2.2 La fonctionfork | 6 14:05 fichier |
Lafonction fork permetàunprogrammeencoursd’exécutiondecréerunnouveauprocessus. Le processus d’origine est appelé processus père, et il garde son PID, et le nouveau processus créés’appelle processus fils,etpossèdeunnouveau PID.Leprocessuspèreetleprocessusfils ontlemêmecodesource,mais lavaleurretournéeparforkpermetde savoirsion estdans le processus père ou fils. Ceci permet de faire deux choses différentes dans le processus père et dansleprocessusfils(enutilisantun if etun else ouun switch),mêmesilesdeuxprocessus onlemêmecodesource.
Lafonction fork retourne-1encasd’erreur,retourne0dansleprocessusfils,etretourne le PID dufilsdansleprocessuspère.Cecipermetaupèredeconnaîtrele PID desonfils.
#include <stdlib.h>
#include <stdio.h> #include <unistd.h>
int main(void)
{ pid_t pid_fils;
pid_fils = fork(); if (pid_fils == -1)
{ puts("Erreur de création du nouveau processus"); exit (1);
}
if (pid_fils == 0)
{ printf("Nous sommes dans le fils\n");
/* la fonction getpid permet de connaître son propre PID */ printf("Le PID du fils est \%d\n", getpid()); /* la fonction getppid permet de connaître le PPID
(PID de son père) */
printf("Le PID de mon père (PPID) est \%d", getppid());
} else
{ printf("Nous sommes dans le père\n"); printf("Le PID du fils est \%d\n", pid_fils); printf("Le PID du père est \%d\n", getpid()); printf("PID du grand-père : \%d", getppid());
} return 0;
}
2.3 Terminaison d’un processus fils
Lorsqueleprocessusfilssetermine(soitensortantdu main soitparunappelà exit)avantle processuspère,leprocessusfilsnedisparaîtpascomplètement,maisdevientun zombie.Pour permettreàunprocessusfilsàl’étatdezombiededisparaîtrecomplètement,leprocessuspère peutappelerl’instructionsuivantequisetrouvedanslabibliothèque sys/wait.h :
wait(NULL);
Cependant, il faut prendre garde l’appel de wait est bloquant, c’est à dire que lorsque la fonction wait est appelée, l’exécution du père est suspendue jusqu’à ce qu’un fils se termine. Deplus, il faut mettre autant d’appels dewait qu’il y a de fils.Lafonction wait renvoie lecoded’erreur?1 danslecasoùleprocessusn’apasdefils.
Lafonction wait estfréquementutiliséepourpermettreauprocessuspèred’attendrelafin desesfilsavntdeseterminerlui-même,parexemplepourrécupérerlerésultatproduitparun fils.
Ilestpossibledemettreleprocessuspèreenattentedelafind’unprocessusfilsparticulier parl’instruction
waitpid(pid_fils, NULL, 0);
Leparamètredelafonction wait,etdedeuxièmeparamètrede waitpid estunpassagepar adressed’unentierquidonnedesinformationssurlestatutdufilslorsqu’ilsetermine:terminaisonnormale,avortement(parexemplepar ctrl-C)ouprocessustemporairementstoppé). Exemple. Voici un exemple où le père récupère le code renvoyé par le fils dans la fonction exit.
#include <stdio.h>
#include <stdlib.h>
Chapitre2:Processus
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h> /* permet de récupérer les codes d'erreur */ pid_t pid_fils
int main(void)
{ int status;
switch (pid_fils=fork())
{ case -1 : perror("Problème dans fork()\n");
exit(errno); /* retour du code d'erreur */ break;
case 0 : puts("Je suis le fils");
puts("Je retourne le code 3"); exit(3);
default : puts("Je suis le père");
puts("Je récupère le code de retour"); wait(&status);
printf("code de sortie du fils %d : %d\n", pid_fils, WEXITSTATUS(status));
break;
} return 0;
}
Latracedeceprogrammeestlasuivante:
Je suis le fils
Je retourne le code 3
Je suis le père
Je récupère le code de retour code de sortie : 3
2.4 Exercices
Exercice 2.1 (E) crire un programme qui crée un fils. Le père doit afficher “je suis le père” etlefilsdoitafficher“jesuislefils”.
Exercice 2.2 (E) crire un programme qui crée deux fils appelés fils 1 et fils 2. Le père doit afficher “je suis le père” et le fils 1 doit afficher “je suis le fils 1”, et le fils 2 doit afficher “je suislefils2”.
Exercice 2.3 (E) crireunprogrammequicrée5filsenutilisantuneboucle for.Onremarqueraquepourquelefilsnecréepaslui-mêmeplusieursfils,ilfautinteromprelabouclepar un break danslefils.
Exercice 2.4 (E) crire un programme avec un processus père qui engendre 5 fils dans une boublefor.Lesfilssontnommésfils1àfils5.Lefils1doitafficher“jesuislefils1”etlefils2 doitafficherjesuislefils2,etainsidesuite. Indication. onpourrautiliserunevariableglobale.
Exercice 2.5 (E) crire un programme qui crée deux fils appelés fils 1 et fils 2. Chaque fils doit attendre un nombre de secondes aléatoire entre 1 et 10, en utilisant la fonction sleep. Le programme attend que le fils le plus long se termine et affiche la durée totale. On pourra utiliserlafonction time delabibliothèque time.h,quiretournelenombredesecondesdepuis lepremierjanvier1970à0h(entempsuniversel).
Chapitre 3
Lancement d’un programme : exec
3.1 Rappels : Arguments en ligne de commande
La fonction main d’un programme peut prendre des arguments en ligne de commande. Par exemple,siunfichier monprog.c apermisdegénérerunexécutable monprog àlacompilation,
$ gcc monprog.c -o monprog onpeutinvoquerleprogramme monprog avecdesarguments
$ ./monprog argument1 argment2 argument3
Exemple. Lacommande cp dubashprenddeuxarguments:
$ cp nomfichier1 nomfichier2
PourrécupérerlesargumentsdansleprogrammeC,onutiliselesparamètres argc et argv du main.L’entier argc donnelenombred’argumentsrentrésdanslalignedecommande plus 1,etleparamètre argv estuntableaudechaînesdecaractèresquicontientcommeéléments:
• Lepremierélémentprogramme; argv[0] estunechaînequicontientlenomdufichierexecutabledu
• lesargumentspasséélémentssuivants argv[1], argv[2],etc sontdeschaînesdecaractèresquicontiennent
#include <stdio.h>
int main(int argc, char *argv[])
{ int i; if (argc == 1) puts("Le programme n'a reçu aucun argument");
if (argc >= 2)
{ puts("Le programme a reçu les arguments suivants :");
for (i=1 ; i<argc ; i++) printf("Argument %d = %s\n", i, argv[i]);
} return 0;
}
3.2 L’appel système exec
3.2.1 Arguments en liste
L’appelsystème exec permetderemplacerleprogrammeencoursparunautreprogrammesans changerdenumérodeprocessus(PID).Autrementdit,unprogrammepeutsefaireremplacer par un autre code source ou un script shell en faisant appel à exec. Il y a en fait plusieurs fonctionsdelafamille exec quisontlégèrementdifférentes.
Lafonction execl prendenparamètreune liste desargumentsàpasserauprogramme(liste terminéepar NULL).
#include <stdio.h>
#include <stdlib.h> #include <unistd.h>
int main()
{
/* dernier élément NULL, OBLIGATOIRE */
execl("/usr/bin/emacs", "emacs", "fichier.c", "fichier.h", NULL);
perror("Problème : cette partie du code ne doit jamais être exécutée"); return 0; }
Le premier paramètre est une chaîne qui doit contenir le chemin d’accès complet (dans le systèmedefichiers)aufichierexécutableouauscriptshellàexécuter.Lesparamètressuivants sontdeschaînesdecaractèrequireprésententlesargumentspassésenlignedecommandeau main de ce programme. La chaîne argv[0] doit donner le nom du programme (sans chemin d’accès),etleschaînessuivantes argv[1], argv[2],etc donnentlesarguments.
Concernantlechemind’accès,ilestdonnéàpartirdurépertoiredetravail($PWD),ouà partirdurépertoireracine / s’ilcommenceparlecaractère / (exemple: /home/remy/enseignement/systeme/script1).
Lafonction execlp permetderechercherlesexécutablesdanslesrépertoiresapparaîssant dansle PATH,cequiévitesouventd’avoiràspécifierlechemincomplet.
#include <stdio.h>
#include <stdlib.h> #include <unistd.h>
int main()
{
/* dernier élément NULL, OBLIGATOIRE */
execlp("emacs", "emacs", "fichier.c", "fichier.h", NULL);
perror("Problème : cette partie du code ne doit jamais être exécutée"); return 0;
}
Chapitre3:Lancementd’unprogramme: exec
3.2.2 Arguments en vecteur
Nous allons étudier l’une d’entre elles (la fonction execv). La différence avec execl est que l’onn’apasbesoindeconaîtrelalistedesargumentsàl’avance(nimêmeleurnombre).Cette fonctionapourprototype:
int execv(const char* application, const char* argv[]);
Le mot const signifie seulement que la fonction execv ne modifie pas ses paramètres. Le premier paramètre est une chaîne qui doit contenir le chemin d’accès (dans le système de fichiers) au fichier exécutable ou au script shell à exécuter. Le deuxième paramètre est un tableaudechaînesdecaractèresdonnantlesargumentspassésauprogrammeàlancerdansun formatsimilaireauparamètre argv du main deceprogramme.Lachaîne argv[0] doitdonner lenomduprogramme(sanschemind’accès),etleschaînessuivants argv[1], argv[2],etc donnentlesarguments.
Le dernier élémentdu tableau de pointeurs argv doit être NULL pour marquer la fin du tableau. Ceci est dû au fait que l’on ne passe pas de paramètre argc donnantlenombred’argument
Concernantlechemind’accès,ilestdonnéàpartirdurépertoiredetravail($PWD),ouà partirdurépertoireracine/s’ilcommenceparlecaractère/ (exemple: /home/remy/enseignement/systeme/script1).
Exemple. Le programme suivant édite les fichiers .c et .h du répertoire de travail avec emacs.
Dansleprogramme,lechemind’accèsàlacommande emacs estdonnéàpartirdelaracine /usr/bin/emacs.
#include <stdio.h>
#include <stdlib.h> #include <unistd.h>
int main()
{ char * argv[] = {"emacs", "fichier.c", "fichier.h", NULL}
/* dernier élément NULL, obligatoire */ execv("/usr/bin/emacs", argv);
puts("Problème : cette partie du code ne doit jamais être exécutée"); return 0; }
Remarque 3.2.1Pour exécuter un script shell avecexecv, il faut que la première ligne de ce script soit
#! /bin/sh ou quelque chose d’analogue.
Enutilisant fork, puis enfaisantappel à exec dans leprocessus fils, un programmepeut lancerunautreprogrammeetcontinueràtournerdansleprocessuspère.
Il existe une fonction execvp qui lance un programme en le recherchant dans la variable d’environnement PATH. L’utilisation de cette fonction dans un programme Set-UID posedesproblèmesdesécurité(voirexplicationsplusloinpour lafonction system
3.3 La fonction system
3.3.1 La variable PATH dans unix
La variable d’environnement PATH sous unix et linux donne un certain nombre de chemins versdesrépertoiresoùsetrouvelesexécutablesetscriptsdescommandes.Leschminsdansle PATHsontséparéspardes’:’.
$ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games:/home/remy/bin:.
Lorsqu’on lance une commande dans une console, le système va cherche l’exécutable ou le script de cette commande dans les répertoires donnés dans le PATH. Chaque utilisateur peut rajouter des chemins dans son PATH (en modifiant son fichier .bashrc sous linux). En particulier,l’utilisateurpeutrajouterlerépertoire’.’(point)danslePATH,cequisignifieque le système va chercher les commandes dans le répertoire de travail donné dans la variable d’environnement PWD. La recherche des commandes dans les répertoires a lieu dans l’ordre dans lequel les répertoires apparaîssent dans le PATH. Par exemple, pour le PATH donné cidessus,lacommandeserarecherchéed’aborddanslerépertoire /usr/local/bin,puisdansle répertoire /usr/bin. Si deux commandes de même nom se trouve dans deux répertoires du PATH,c’estlapremièrecommandetrouvéequiseraexécutée.
3.3.2 La fonction system
La fonction system de la bibliothèque stdlib.h permet directement de lancer un programme dansunprogrammeCsansutiliser fork et exec.Pourcelà,onutilisel’instruction:
#include <stdlib.h> system("commande");
Exemple. La commande unix clear permet d’effacer la console. Pour effacer la console dansunprogrammeCavecdesentrées-sortiesdanslaconsole,onpeutustilser:
system("clear");
Lorsqu’on utilise la fonction system, la commande qu’on exécute est recherchée dans les répertoiresduPATHcommesil’onexécutaitlacommandedanslaconsole.
Chapitre3:Lancementd’unprogramme: exec
3.4 Applications suid et problèmes des sécurité liés system, execlp ou exevp
Dans le système unix, les utilisateurs et l’administrateur (utilisateur) on des droits (que l’on appelle privilèges), et l’accès à certaines commandes leur sont interdites. C’est ainsi que, par exemple, si le système est bien administré, un utilisateur ordinaire ne peut pas facilement endomagerlesystème.
Exemple. Imaginons que les utilisateurs aient tous les droits et qu’un utilisateur malintentionéoudistraittapelacommande
$ rm -r /
Cela supprimerait tous les fichiers du système et des autres utilisateurs et porterait un préjudice important pour tous les utilisateurs du système. En fait, beaucoup de fichiers sont interdits à l’utilisateur en écriture, ce qui fait que la commande rm sera ineffective sur ces fichiers.
Pourcelà,lorsquel’utilisateurlanceunecommandeouunscript(commelacommande rm), lesprivilègesdecetutilsateursontprisencomptelorsdel’exécutiondelacommande.
Sousunix,unutilisateurA(parexempleroot)peutmodifierlespermissionssurunfichier exécutablepourquetoutautreutilisateurBpuisseexécutercefichieravecsespropresprivilèges (lesprivilègesdeA).Celas’appellelespermissionssuid.
Exemple. Supposonsquel’utilisateur root tapelescommandessuivantes:
$ gcc monprog.c -o monprog $ ls -l | |
-rwxr-xr-x 1 root root 18687 Sep | 7 08:28 monprog |
-rw-r--r-- 1 root root 3143 Sep $ chmod +s monprog $ ls -l | 4 15:07 monprog.c |
-rwsr-sr-s 1 root root 18687 Sep | 7 08:28 monprog |
-rw-r--r-- 1 root root 3143 Sep | 4 15:07 monprog.c |
Le programme moprog est alors suid et n’importe quel utilisateur peut l’exécuter avec les privilègesdupropriétairedemonprog,c’estàdire root.
Supposonsmaintenantquedanslefichier monprog.c ilyaitl’instruction system("clear");
Considérons un utilisateur malintentionné remy. Cet utilisateur modifie son PATH pour rajouterlerépertoire’.’,(point)maismetlerépertoire’.’autoutdébutdePATH
$ PATH=.:\PATH
$ export PATH
$ echo \PATH
.:/usr/local/bin:/usr/bin:/bin:/usr/bin/X11:/usr/games:/home/remy/bin:.
Dans la recherche des commandes dans les répertoires du PATH, le système cherchera d’abordlescommandesdanslerépertoiredetravail’.’.Supposonsmaintenantquel’utilisateur remy créeunscriptappelé clear danssonrépertoiredetravail.quicontiennelaligne rm -r /
$ echo "rm -r /" > clear
$ cat clear rm -r / $ chmod +x clear
$ monprog
Lorsquel’utilisateur remy valancerl’exécutable monprog aveclesprivilègesde root,leprogrammevaexécuterlescript clear del’utilisateur(aulieudelacommande /usr/bin/clear) aveclesprivilègesde root,etvasupprimertouslesfichiersdusystème.
Il ne faut jamais utiliser la fonction system ou la fonction execvp dans une application suid, car un utilisateur malintentioné pourrait exécuter n’importe quelscriptavecvosprivilèges.
3.5 Exercices
Exercice 3.1 (?) Écrireunprogrammequiprenddeuxargumentsenlignedecommandeen supposantqucesontdesnombresentiers,etquiaffichel’additiondecesdeuxnombres.
Exercice 3.2 (?) Ecrireunprogrammequiprendenargumentuncheminversunrépertoire R,etcopielerépertoirecourantdanscerépertoireR.
Exercice 3.3 (?) Ecrireunprogrammequisaisitunnomdefichiertexteauclavieretouvrece fichierdansl’éditeur emacs,dontlefichierexécutablesetrouveàl’emplacement /usr/bin/emacs.
Exercice 3.4 (??) Ecrireunprogrammequisaisitdesnomsderépertoiresauclavieretcopie lerépertoirecourantdanstouscesrépertoires.Leprogrammedoitsepoursuivrejusqu’àceque l’utilisateurdemandedequitterleprogramme.
Exercice 3.5 (??) Ecrire un programme qui saisit des nom de fichiers texte au clavier et ouvre tous ces fichiers dans l’éditeur emacs. Le programme doit se poursuivre jusqu’à ce que l’utilisateurdemandedequitter.
Exercice 3.6 (???) Considéronslescoefficientsbinômiaux Cnk telsque
Ci0 = 1 et Cii = 1 pourtout i
Cnk = Cnk?1 + Cnk??11
ÉcrireunprogrammepourcalculerCnk quin’utiliseaucuneboucle(ni while ni for),etquin’ait comme seule fonction que la fonction main. La fonction main ne doit contenir aucun appel à elle-même.Onpourrautiliserdesfichierstextestemporairesdanslerépertoire /tmp.
Chapitre 4
Communication entre processus
Dans ce chapitre, nous voyons comment faire communiquer des processus entre eux par des tubes.Pourlemoment,lesprocessusquicommuniquentdoiventêtredesprocessusdelamême machine.Cependant,leprincipedecommunicationaveclesfonctions read et write seraréutilisé par la suite lorsque nous aborderons la programmation réseau, qui permet de faire communiquerdesprocessussetrouvantsurdesstationsdetravaildistinctes.
4.1 Tubes et fork
Untubedecommunicationestuntuyau(enanglais pipe)danslequelunprocessuspeutécrire desdonnééeuntubeparunappelàlafonction pipe, déclaréedans unistd.h :
int pipe(int descripteur[2]);
La fonction renvoie 0 si elle réussit, et elle crée alors un nouveau tube. La fonction pipe remplitletableau descripteur passéenparamètre,avec:
• descripteur[0] désignelasortiedutube(danslaquelleonpeutliredesdonnées);
• descripteur[1] désignel’entréedutube(danslaquelleonpeutécriredesdonnées);
Leprincipeestqu’unprocessusvaécriredans descripteur[1] etqu’unautreprocessusva lirelesmêmesdonnéesdans descripteur[0].Leproblèmeestqu’onnecréeletubedansun seulprocessus,etunautreprocessusnepeutpasdevinerlesvaleursdutableau descripteur. Pour faire communiquer plusieurs processus entre eux, il faut appeler la fonction pipe avant d’appeler la fonction fork. Ensuite, le processus père et le processus fils auront les mêmes descripteurs de tubes, et pourront donc communiquer entre eux. De plus, un tube ne permet ’onsouhaitequelesprocessuscommuniquentdans lesdeuxsens,ilfautcréerdeuxpipes.
Pourécriredansuntube,onutiliselafonction write :
ssize_t write(int descripteur1, const void *bloc, size_t taille);
Ledescripteurdoitcrrespondreàl’entréed’untube.Latailleestlenombred’octetsqu’on souhaiteécrire,etleblocestunpointeurverslamémoirecontenantcesoctets.
Pourliredansuntube,onutiliselafonction read :
ssize_t read(int descripteur0, void *bloc, size_t taille);
Le descripteur doit correspondre à la sortie d’un tube, le bloc pointe vers la mémoire destinée à recevoir les octets, et la taille donne le nombre d’octets qu’on souhaite lire. La fonction renvoie le nombre d’octets effectivement lus. Si cette valeur est inférieure à taille, c’estqu’uneerreurs’estproduiteencoursdelecture(parexemplelafermeturedel’entréedu tubesuiteàlaterminaisonduprocessusquiécrit).
Dans la pratique, on peut transmettre un buffer qui a une taille fixe (256 octets dans l’exempleci-dessous).L’essentielestqu’ilyaitexactementlemêmenombred’octetsenlecture etenécrituredepartetd’autredupipe.Lapartiesignificativedubufferestterminéeparun 0\00 commepourn’importequellechaînedecaractère.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h> #define BUFFER_SIZE 256
int main(void)
{ pid_t pid_fils; int tube[2];
unsigned char bufferR[256], bufferW[256];
puts("Cration d'un tube");
if (pipe(tube) != 0) {/* pipe */
{ fprintf(stderr, "Erreur dans pipe\(\backslash\)n"); exit(1);
}
pid_fils = fork(); {/* fork */
if (pid_fils == -1)
{ fprintf(stderr, "Erreur dans fork\(\backslash\)n"); exit(1);
} if (pid_fils == 0) {/* processus fils */
{ printf("Fermeture entre dans le fils (pid = %d)\(\backslash\)n", getpid());
close(tube[1]);
read(tube[0], bufferR, BUFFER_SIZE);
printf("Le fils (%d) a lu : %s\(\backslash\)n", getpid(), bufferR);
}
else {/* processus pre */
{ printf("Fermeture sortie dans le pre (pid = %d)\(\backslash\)n", getpid()); Chapitre4:Communicationentreprocessus
close(tube[0]);
sprintf(bufferW, "Message du pre (%d) au fils", getpid());
write(tube[1], bufferW, BUFFER_SIZE); wait(NULL);
} return 0;
}
Lasortiedeceprogrammeest:
Création d'un tube
Fermeture entrée dans le fils (pid = 12756) Fermeture sortie dans le père (pid = 12755)
Ecriture de 31 octets du tube dans le père
Lecture de 31 octets du tube dans le fils
Le fils (12756) a lu : Message du père (12755) au fils
Il faut noter que les fonctions read et write permettent de transmettre uniquement des tableauxdesoctets.Toutedonnée(nombreoutexte)doitêtreconvertieentableaudecaractère pour être transmise, et la taille des ces données doit être connue dans les deux processus communiquants.
4.2 Transmettre des données binaires
Voici un exemple de programme qui saisit une valeur x au clavier dans le processus père, et transmetlesinusdecenombre, en tant quedoubleauprocessusfils.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h> #include <math.h>
int main(\void)
{ pid_t pid_fils; int tube[2];
double x, valeurW, valeurR;
puts("Création d'un tube");
if (pipe(tube) != 0) /* pipe */
{ fprintf(stderr, "Erreur dans pipe\n"); exit(1);
}
switch(pid_fils = fork()) /* fork */
{
case -1 :
perror("Erreur dans fork\n"); exit(errno);
case 0 : /* processus fils */ close(tube[1]);
read(tube[0], &valeurR, sizeof(double));
printf("Le fils (%d) a lu : %.2f\(\backslash\)n", getpid(), valeurR); break;
default : /* processus père */
printf("Fermeture sortie dans le père (pid = %d)\n", getpid()); close(tube[0]); puts("Entrez x :"); scanf("%lf", &x); valeurW = sin(x);
write(tube[1], &valeurW, \sizeof(\double)); wait(NULL); break;
} return 0;
}
Compléments ?
D’unemanièregénérale,l’adressed’unevariablepeuttoujoursêtreconsidéréecommeun tableau dont le nombre d’octet est égal à la taille de cette variable. Ceci est dû au fait qu’untableauestseulementuneadressequipointeversunezonemémoireréservéepour leprogramme(statiquementoudynamiquement).
4.3 Rediriger les flots d’entrées-sorties vers des tubes
On peut lier la sortie tube[0] du tube à stdin. Par la suite, tout ce qui sort du tube arrive surleflotd’entréestandard stdin,etpeutêtreluavec scanf, fgets,etc Pourcelà,ilsuffit demettrel’instruction:
dup2(tube[0], STDIN_FILENO);
Demême,onpeutlierl’entrée tube[1] dutubeà stdout.Parlasuite,toutcequisortsur leflotdesortiestandard stdout entreansletube,etonpeutécriredansletubeavec printf, puts,etc Pourcelà,ilsuffitdemettrel’instruction:
dup2(tube[1], STDOUT_FILENO);
Compléments ? Plus généralement, la fonction dup2 copie le descripteur de fichier passé en premier argumentdansledescripteurpasséendeuxièmeargument. |
Chapitre4:Communicationentreprocessus
4.4 Tubes nommés
Onpeutfairecommuniquerdeuxprocessusàtraveruntubenommé.Letubenommépassepar unfichiersurledisque.L’intérêtestque les deux processus n’ont pas besoin d’avoir un lien de parenté.Pourcréeruntubenommé,onutiliselafonction mkfifo delabibliothèque sys/stat.h.
Exemple. Danslecodesuivant,lepremierprogrammetransmetlemot“coucou”audeuxième programme.Lesdeuxprogrammesn’ontpasbesoind’êtreliésparunliendeparenté.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h> #include <sys/types.h>
int main()
{ int fd; FILE *fp; char *nomfich=""; /* nom du fichier */ if(mkfifo(nomfich, 0644) != 0) /* création du fichier */
{ perror("Problème de création du noeud de tube"); exit(1);
}
fd = open(nomfich, O_WRONLY); /* ouverture en écriture */ fp=fdopen(fd, "w"); /* ouverture du flot */
fprintf(fp, "coucou\(\backslash\)n"); /* écriture dans le flot */ unlink(nomfich); /* fermeture du tube */ return 0;
}
Lafonction mkfifo prendenparamètre,outrelecheminverslefichier,lemasquedespermissions(lecture,écriture)surlastructure fifo.
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h> #include <sys/types.h>
int main()
{ int fd; FILE *fp;
char *nomfich="", chaine[50]; fd = open(nomfich, O_RDONLY); /* ouverture du tube */ fp=fdopen(fd, "r"); /* ouverture du flot */ fscanf(fp, "%s", chaine); /* lecture dans le flot */ puts(chaine); /* affichage */ unlink(nomfich); /* fermeture du flot */ return 0;
}
4.5 Exercices
Exercice 4.1 (exobasepipe) Écrireunprogrammequicréedeuxprocessus.Leprocessuspère ouvre un fichier texte en lecture. On suppose que le fichier est composé de mots formés de caractèresalphabétiquesséparé processuspèrerecherchelemotdanslefichier,ettransmetaufilslavaleur 1 silemotestdans lefichier,et 0 sinon.Lefilsaffichelerésultat.
Exercice 4.2 (R) eprendrelesprogrammesdel’exercice ??.Nousallonsfaireunprogramme qui fait la même chose, mais transmet les données différement. Dans le programme père, on lieralesflots stdout et stdin àuntube.
Exercice 4.3 (exotubeexec) Écrire un programme qui crée un tube, crée un processus fils, puis,danslefils,lancepar execv unautreprogramme,appeléprogrammefils.Leprogramme père transmets les descripteurs de tubes au programmes fils en argument, et transmet un messageaufilsparletube.Leprogrammefilsaffichelemessage.
Exercice 4.4 (M) ême question qu’à l’exercice ?? mais en passant les descripteurs de tube commevariablesd’environnement.
Chapitre 5
Threads Posix
5.1 Pointeurs de fonction
Unpointeurdefonctionsen C estunevariablequipermetdedésignerunefonction C.Comme n’importe quelle variable, on peut mettre un pointeur de fonctions soit en variable dans une fonction,soitenparamètredansunefonction.
On déclare un pointeur de fonction comme un prototype de fonction, mais on ajoute une étoile (?) devant le nom de la fonction. Dans l’exemple suivant, on déclare dans le main un pointeursurdesfonctionsquiprennentenparamètreun int,etunpointeursurdesfonctions quiretournentun int. #include <stdio.h>
int SaisisEntier(void)
{ int n;
printf("Veuillez entrer un entier : ");
scanf("%d", &n); return n;
}
void AfficheEntier(int n)
{ printf("L'entier n vaut %d\(\backslash\)n", n);
}
int main(void)
{ void (*foncAff)(int); {/* dclaration d'un pointeur foncAff */ int (*foncSais)(void); {/*dclaration d'un pointeur foncSais */ int entier;
foncSais = SaisisEntier; {/* affectation d'une fonction */ foncAff = AfficheEntier; {/* affectation d'une fonction */ entier = foncSais(); {/* on excute la fonction */ foncAff(entier); {/* on excute la fonction */ return 0; }
Dans l’exemple suivant, la fonction est passée en paramètre à une autre fonction, puis exécutée.
#include <stdio.h>
int SaisisEntier(void)
{ int n;
printf("Veuillez entrer un entier : ");
scanf("%d", &n); getchar(); return n;
}
void AfficheDecimal(int n)
{ printf("L'entier n vaut %d\(\backslash\)n", n);
}
void AfficheHexa(int n)
{ printf("L'entier n vaut %x\(\backslash\)n", n);
}
void ExecAffiche(void (*foncAff)(int), int n)
{ foncAff(n); {/* excution du paramtre */
}
int main(void)
{ int (*foncSais)(void); {/*dclaration d'un pointeur foncSais */ int entier; char rep;
foncSais = SaisisEntier; {/* affectation d'une fonction */ entier = foncSais(); {/* on excute la fonction */
puts("Voulez-vous afficher l'entier n en dcimal (d) ou en hexa (x) ?"); rep = getchar();
{/* passage de la fonction en paramtre : */ if (rep == 'd')
ExecAffiche(AfficheDecimal, entier); if (rep == 'x')
ExecAffiche(AfficheHexa, entier);
return 0; }
Pourprévoiruneutilisationplusgénéraledelafonction ExecAffiche,onpeututiliserdes fonctions qui prennent en paramètre un void* au lieu d’un int. Le void* peut être ensuite reconvertiend’autrestypesparun cast.
void AfficheEntierDecimal(void *arg)
{
inte n = (int)arg; /* un void* et un int sont sur 4 octets */ printf("L'entier n vaut \%d\n", n);
}
void ExecFonction(void (*foncAff)(void* arg), void *arg)
{ foncAff(arg); /* exécution du paramètre */
}
int main(void)
{ int n;
ExecFonction(AfficheEntierDecimal, (void*)n);
}
On peut utiliser la même fonction ExecFonction pour afficher tout autre chose que des entiers,parexempleuntableaude float.
typedef struct
{ int n; /* nombre d'éléments du tableau */
double *tab; /* tableau de double */ }TypeTableau;
void AfficheTableau(void *arg)
{ inte i;
TypeTableau *T = (TypeTableau*)arg; /* cast de pointeurs */
for (i=0 ; i<T->n ; i++)
{ printf("%.2f", T->tab[i]);
}
}
void ExecFonction(void (*foncAff)(void* arg), void *arg)
{ foncAff(arg); /* exécution du paramètre */
}
int main(void)
{
TypeTableau tt;
ExecFonction(AfficheTableau, (void*)&tt);
}
5.2 Thread Posix (sous linux)
5.2.1 Qu’est-ce qu’un thread?
Un thread (ou fil d’exécution enfrançais)estuneparieducoded’unprogramme(unefonction), qui se déroule parallèlement à d’autre parties du programme. Un premier interêt peut être d’effectuer un calcul qui dure un peu de temps (plusieurs secondes, minutes, ou heures) sans que l’interface soit bloquée (le programme continue à répondre aux signaux). L’utilisateur peutalorsinterveniretinterromprelecalculsanstaperun ctrl-C brutal.Unautreintérêtest d’effectueruncalculparallèlesurlesmachinesmulti-processeur.Lesfonctionsliéesauxthread sontdanslabibliothèque pthread.h,etilfautcompileraveclalibrairie libpthread.a :
$ gcc -lpthread monprog.c -o monprog
5.2.2 Création d’un thread et attente de terminaison
Pour créer un thread, il faut créer une fonction qui va s’exécuter dans le thread, qui a pour prototype:
void *ma_fonction_thread(void *arg);
Dans cette fonction, on met le code qui doit être exécuté dans le thread. On crée ensuite le thread par un appel à la fonction pthread_create, et on lui passe en argument la fonction ma_fonction_thread dans un pointeurs de fonction (et son argument arg). La fonction pthread_create apourprototype:
int pthread_create(pthread_t *thread, pthread_attr_t *attributs, void * (*fonction)(void *arg), void *arg);
Lepremierargumentestunpassageparadressedel’identifiantduthread(detype pthread_t). Lafonction pthread_create nousretourneainsil’identifiantduthread,quil’onutiliseensuite pourdésignerlethread.Ledeuxièmeargument attributs désignelesattributsduthread,et onpeutmettre NULL pouravoirlesattibutspardéfaut.Letroisièmeargumentestunpointeur sur la fonstion à exécuter dans le thread (par exemple ma_fonction_thread, et le quatrième argumentestl’argumentdelafonctiondethread.
Le processus qui exécute le main (l’équivalent du processus père) est aussi un thread et s’appellele thread principal.Lethreadprincipalpeutattendrelafondel’exécutiond’unautre threadparlafonction pthread_join (similaireàlafonction wait danslefork.Cettefonction permetaussiderécupérerlavaleurretournéeparlafonction ma_fonction_thread duthread. Leprototypedelafonction pthread_join estlesuivant:
int pthread_join(pthread_t thread, void **retour);
Le premier paramètre est l’dentifiant du thread (que l’on obtient dans pthread_create), etlesecondparamètreestun passage par adresse d'un pointeur quipermetderécupérer lavaleurretournéepar ma_fonction_thread.
5.2.3 Exemples
Lepremierexemplecréeunthreadquidortunnombredesecondespasséenargument,pendant quelethreadprincipalattendqu’ilsetermine.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #include <time.h>
void *ma_fonction_thread(void *arg)
{ int nbsec = (int)arg;
printf("Je suis un thread et j'attends %d secondes\(\backslash\)n", nbsec); sleep(nbsec);
puts("Je suis un thread et je me termine");
pthread_exit(NULL); {/* termine le thread proprement */
}
int main(void)
{ int ret; pthread_t my_thread; int nbsec; time_t t1; srand(time(NULL)); t1 = time(NULL);
nbsec = rand()%10; {/* on attend entre 0 et 9 secondes */
{/* on cre le thread */
ret = pthread_create(&my_thread, NULL, ma_fonction_thread, (void*)nbsec);
if (ret != 0)
{
fprintf(stderr, "Erreur de cration du thread"); exit (1);
} pthread_join(my_thread, NULL); {/* on attend la fin du thread */ printf("Dans le main, nbsec = %d\(\backslash\)n", nbsec); printf("Duree de l'operation = %d\(\backslash\)n", time(NULL)-t1); return 0;
}
Ledeuxièmeexemplecréeunthreadquilitunevaleurentièreetlaretourneau main.
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #include <time.h>
void *ma_fonction_thread(void *arg)
{ int resultat;
printf("Je suis un thread. Veuillez entrer un entier\(\backslash\)n"); scanf("%d", &resultat);
pthread_exit((void*)resultat); {/* termine le thread proprement */
}
int main(void)
{ int ret;
pthread_t my_thread; {/* on cre le thread */
ret = pthread_create(&my_thread, NULL,
ma_fonction_thread, (void*)NULL);
if (ret != 0)
{ fprintf(stderr, "Erreur de cration du thread"); exit (1);
} pthread_join(my_thread, (void*)&ret); {/* on attend la fin du thread */ printf("Dans le main, ret = %d\(\backslash\)n", ret); return 0;
}
5.3 Donnée partagées et exclusion mutuelle
Lorsqu’un nouveau processus est créé par un fork, toutes les données (variables globales, variableslocales,mémoireallouéedynamiquement),sontdupliquéesetcopiées,etleprocessus pèreetleprocessusfilstravaillentensuitesurdesvariablesdifférentes.
Danslecasdethreads,lamémoireest partagée,c’estàdirequelesvariablesglobalessont partagées entre les différents threads qui s’exécutent en parallèle. Cela pose des problèmes lorsquedeuxthreadsdifférentsessaientd’écrireetdelireunemêmedonnée.
Deuxtypesdeproblèmespeuventseposer:
• Deuxthreadsconcurrentsessaientenmêmetempsdemodifierunevariableglobale; • Un thread modifie une structure de donnée tandis qu’un autre thread essaie de la lire. Il est alors possible que le thread lecteur lise la structure alors que le thread écrivain a écritladonnéeàmoitié.Ladonnéeestalorsincohérente.
Pouraccéderàdesdonnéesglobales,ilfautdoncavoirrecoursàunmécanismed’exclusion mutuelle,quifaitquelesthreadsnepeuventpasaccéderenmêmetempsàunedonnée.Pour celà,onintroduitdesdonnéesappelés mutex,detype pthread_mutex_t.
Unthreadpeutverrouillerun mutex,aveclafonction pthread_mutex_lock(),pourpouvoir accéderàunedonnéeglobaleouàunflot(parexemplepourécriresurlasortie stdout).Une foisl’accèsterminé,lethreaddévérouillelemutex,aveclafonction pthread_mutex_unlock(). SiunthreadAessaiedevérouillerleunmutexalorsqu’ilestdéjàverrouilléparunautrethread B,lethreadArestebloquésurl’appelde pthread_mutex_lock() jusqu’àcequelethreadB dévérouillelemutex.UnefoislemutexdévérouilléparB,lethreadAverrouilleimmédiatement lemutexetsonexécutionsepoursuit.CelapermetauthreadBd’accédertranquillementàdes variablesglobalespendantquelethreadAattendpouraccéderauxmêmesvariables.
Pour déclarer et initialiser un mutex, on le déclare en variable globale (pour qu’il soit accessibleàtouslesthreads):
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER;
Lafonction pthread_mutex_lock(),quipermetdeverrouillerunmutex,apourprototype: int pthread_mutex_lock(pthread_mutex_t *mutex);
Ilfautéviterdeverrouillerdeuxfoisunmêmemutexdanslemêmethreadsans le déverrouiller entre temps. Il y a un risque de blocage définitif du thread. Certainesversionsdusystèmegèrentceproblèmemaisleurcomportementn’est pasportable.
Lafonction pthread_mutex_unlock(),quipermetdedéverrouillerunmutex,apourprototype:
int pthread_mutex_unlock(pthread_mutex_t *mutex);
Dansl’exemple suivant,différentsthreadsfontun travaild’une duréealéatoire. Ce travail estfaitalorsqu’unmutexestverrouillé.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
pthread_mutex_t my_mutex = PTHREAD_MUTEX_INITIALIZER; void* ma_fonction_thread(void *arg);
int main(void)
{ int i;
pthread_t thread[10]; srand(time(NULL));
for (i=0 ; i<10 ; i++)
pthread_create(&thread[i], NULL, ma_fonction_thread, (void*)i);
for (i=0 ; i<10 ; i++)
pthread_join(thread[i], NULL);
return 0;
}
void* ma_fonction_thread(void *arg)
{ int num_thread = (int)arg; int nombre_iterations, i, j, k, n; nombre_iterations = rand()%8; for (i=0 ; i<nombre_iterations ; i++)
{ n = rand()%10000; pthread_mutex_lock(&my_mutex);
printf("Le thread numro %d commence son calcul\(\backslash\)n", num_thread);
for (j=0 ; j<n ; j++)
for (k=0 ; k<n ; k++)
{} printf("Le thread numero %d a fini son calcul\(\backslash\)n", num_thread);
pthread_mutex_unlock(&my_mutex);
} pthread_exit(NULL);
}
Voiciunextraitdelasortieduprogramme.Onvoitqu’unthreadpeuttravaillertranquillementsansquelesautresn’écrivent.
Le thread numéro 9 commence son calcul Le thread numero 9 a fini son calcul
Le thread numéro 4 commence son calcul
Le thread numero 4 a fini son calcul
Le thread numéro 1 commence son calcul
Le thread numero 1 a fini son calcul
Le thread numéro 7 commence son calcul
Le thread numero 7 a fini son calcul
Le thread numéro 1 commence son calcul
Le thread numero 1 a fini son calcul
Le thread numéro 1 commence son calcul Le thread numero 1 a fini son calcul
Le thread numéro 9 commence son calcul Le thread numero 9 a fini son calcul
Le thread numéro 4 commence son calcul
Le thread numero 4 a fini son calcul
Enmettantencommentaireleslignesavec pthread_mutex_lock() et pthread_mutex_unlock(), onobtient:
Le thread numéro 9 commence son calcul Le thread numero 0 a fini son calcul
Le thread numéro 0 commence son calcul
Le thread numero 1 a fini son calcul
Le thread numéro 1 commence son calcul
Le thread numero 4 a fini son calcul
Le thread numero 8 a fini son calcul
Le thread numéro 8 commence son calcul
Le thread numero 8 a fini son calcul
Le thread numéro 8 commence son calcul
Le thread numero 1 a fini son calcul
Le thread numéro 1 commence son calcul
Le thread numero 3 a fini son calcul
Le thread numéro 3 commence son calcul
Le thread numero 3 a fini son calcul
Le thread numéro 3 commence son calcul
Le thread numero 5 a fini son calcul Le thread numero 9 a fini son calcul
Onvoitqueplusieursthreadsinterviennentpendantlecalculduthreadnuméro9et4.
5.4 Sémaphores
Engénéral,unesectioncritiqueestunepartieducodeoùunprocessusouunthreadnepeut rentrerqu’àunecertainecondition.Lorsqueleprocessus(ouunthread)entredanslasection critique,ilmodifielaconditionpourlesautresprocessus/threads.
Par exemple, si une section du code ne doit pas être exécutée simultanément par plus de n threads. Avant de rentrer dans la section critique, un thread doit vérifier qu’au plus n-1 threads y sont déjà. Lorsqu’un thread entre dans la section critique, il modifie la conditions surlenombredethreadsquisetrouventdanslasectioncritique.Ainsi,unautrethreadpeut setrouverempêchéd’entrerdanslasectioncritique.
La difficulté est qu’on ne peut pas utiliser une simple variable comme compteur. En effet, siletestsurlenombredethreadetlamodificationdunombredethreadslorsdel’entréedans lasectioncritiquesefontséquentiellementpardeuxinstructions,sil’onjouedemalchanceun autrethreadpourraittesterleconditionsurlenombredethreadsjustemententrel’exécution decesdeuxinstructions,etdeuxthreadspasseraientenmêmetempsdanslasectioncritiques. Il y a donc nécessité de tester et modifier la condition de manière atomique, c’est à dire qu’aucun autre processus/thread de peut rien exécuter entre le test et la modification. C’est uneopérationatomiqueappelée Test and Set Lock.
Lessémaphoressontuntype sem_t etuneensembledeprimitivesdebasequipermettent d’implémenterdesconditionsassezgénéémaphorepossède un compteur dont la valeur est un entier positif ou nul. On entre dans une section critique si lavaleurducompteureststrictementpositive.
Pourutiliserunesémaphore,ondoitledéclareretl’initialiseràunecertainevaleuravecla fonction sem_init.
\inte sem_init(sem_t *semaphore, \inte partage, {\bf unsigned} \inte valeur)
Le premier argument est un passage par adresse du sémaphore, le deuxième argument indiquesilesémaphorepeutêtrepartagéparplusieursprocessus,ouseulementparlesthreads duprocessusappelant(partage égale0).Enfin,letroisièmeargumentestlavaleurinitialedu sémaphore.
Aprèsutilisation,ilfautsystématiquementlibérerlesémaphoreaveclafonction sem_destroy.
int sem_destroy (sem_t *semaphore)
Lesprimitivesdebasessurlessémaphoressont:
• sem_wait :Restebloquéesilesémaphoreestnuletsinondécrémentelecompteur(opérationatomique);
• sem_post :incrémentelecompteur;
• sem_getvalue :récupèrelavaleurducompteurdansunevariablepasséeparadresse;
• sem_trywait : teste si le sémaphore est non nulet décrémente le sémaphore, mais sans bloquer. Provoque une erreur en cas de valeur nulle du sémaphore. Il faut utiliser cette fonctionavecprécautioncarelleestprompteàgénérerdesbogues.
Lesprototypesdesfonctions sem_wait, sem_post et sem_getvalue sont:
\inte sem_wait (sem_t * semaphore)
\inte sem_post(sem_t *semaphore)
\inte sem_getvalue(sem_t *semaphore, \inte *valeur)
Exemple. Leprogrammesuivantpermetauplusnsémaphoresdanslasectioncritique,oùn enpasséenargument.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h> #include <semaphore.h> sem_t semaphore; {/* variable globale : smaphore */ void* ma_fonction_thread(void *arg);
int main(int argc, char **argv)
{ int i;
pthread_t thread[10]; srand(time(NULL));
if (argc != 2)
{ printf("Usage : %s nbthreadmax\(\backslash\)n", argv[0]); exit(0);
} sem_init(&semaphore, 0, atoi(argv[1])); {/* initialisation */
{/* cration des threads */
for (i=0 ; i<10 ; i++)
pthread_create(&thread[i], NULL, ma_fonction_thread, (void*)i);
{/* attente */
for (i=0 ; i<10 ; i++)
pthread_join(thread[i], NULL);
sem_destroy(&semaphore);
return 0; }
void* ma_fonction_thread(void *arg)
{ int num_thread = (int)arg; {/* numro du thread */ int nombre_iterations, i, j, k, n; nombre_iterations = rand()%8+1; for (i=0 ; i<nombre_iterations ; i++)
{ sem_wait(&semaphore);
printf("Le thread %d entre dans la section critique\(\backslash\)n", num_thread);
sleep(rand()%9+1);
printf("Le thread %d sort de la section critique\(\backslash\)n", num_thread);
sem_post(&semaphore); sleep(rand()%9+1);
} pthread_exit(NULL);
}
Exemplesdetrace:
\ gcc -lpthread semaphore.c -o semaphore
\ ./semaphore 2
Le thread 0 entre dans la section critique
Le thread 1 entre dans la section critique
Le thread 0 sort de la section critique
Le thread 2 entre dans la section critique
Le thread 1 sort de la section critique
Le thread 3 entre dans la section critique
Le thread 2 sort de la section critique
Le thread 4 entre dans la section critique
Le thread 3 sort de la section critique
Le thread 5 entre dans la section critique Le thread 4 sort de la section critique
Le thread 6 entre dans la section critique Le thread 6 sort de la section critique
Le thread 7 entre dans la section critique
Le thread 7 sort de la section critique Le thread 8 entre dans la section critique
Autreexempleavectroisthreadsdanslasectioncritique:
\ ./semaphore 3
Le thread 0 entre dans la section critique
Le thread 1 entre dans la section critique
Le thread 2 entre dans la section critique
Le thread 1 sort de la section critique
Le thread 3 entre dans la section critique
Le thread 0 sort de la section critique
Le thread 4 entre dans la section critique
Le thread 2 sort de la section critique
Le thread 5 entre dans la section critique Le thread 3 sort de la section critique
Le thread 6 entre dans la section critique Le thread 6 sort de la section critique
Le thread 7 entre dans la section critique
Le thread 5 sort de la section critique
Le thread 8 entre dans la section critique
Le thread 4 sort de la section critique
Le thread 9 entre dans la section critique Le thread 9 sort de la section critique
Le thread 1 entre dans la section critique
Le thread 1 sort de la section critique
Le thread 2 entre dans la section critique
Le thread 7 sort de la section critique
Le thread 0 entre dans la section critique Le thread 8 sort de la section critique
Le thread 6 entre dans la section critique Le thread 2 sort de la section critique
Le thread 3 entre dans la section critique
Le thread 3 sort de la section critique
5.5 Exercices
Exercice 5.1 (?) Écrireunprogrammequicréeunthreadquiprendenparamètreuntableau d’entiersetl’affichedanslaconsole.
Exercice 5.2 (?) Écrire un programme qui crée un thread qui alloue un tableau d’entiers, initialiselesélémentspardesentiersaléatoiresentre0et99,etretourneletableaud’entiers.
Exercice 5.3 (??) Créerunestructure TypeTableau quicontient:
• Untableaud’entiers;
• Lenombred’élémentsdutableau;
• Unentierx.
Écrire un programme qui crée un thread qui initialise un TypeTableau avec des valeurs aléatoires entre 0 et 99. Le nombre d’éléments du tableau est passé en paramètre. Dans le même temps, le thread principal lit un entiers x au clavier. Lorsque le tableau est fini de générer, le programme crée un thread qui renvoie 1 si l’élément x est dans le tableau, et 0 sinon.
Exercice 5.4 (??) a) Reprendre la fonction de thread de génération d’un tableau aléatoire du1.LethreadprincipalcréeenparallèledeuxtableauxT1etT2,aveclenombred’éléments deT1pluspetitquelenombred’élemntsdeT2.
b) Lorsquelestableauxsontfinisdegénérer,lancerunthreadquidéterminesiletableauT1 estinclusdansletableauT2.Quelleestlacomplexitédel’algorithme?
c) Modifierleprogrammeprécédentpourqu’unautrethreadpuisseterminerleprogrammesi l’utilsateur appuie sur la touche ’A’ (par exit(0)). Le programme doit afficher un message en casd’annulation,etdoitafficherlerésultatducalculsinon.
Exercice 5.5 (??) Écrire un programme, avec un compteur global compt, et qui crée deux threads:
• Le premier thread itère l’opération suivante : on incrémente le compteur et attend un tempsalléatoireentre1et5secondes.
• Ledeuxièmethreadaffichelavaleurducompteurtouteslesdeuxsecondes.
Lesaccèsaucompteurserontbiensûrprotégésparunmutex.Lesdeuxthreadsseterminent lorsque le compteur atteint une valeur limite passée en argument (en ligne de commande) au programme.
Exercice 5.6 (??) Créer un programme qui a en variable globale un tableau de N double, avecN=100.
Dansle main,letableauserainitialiséavecdesvaleursréellesaléatoiresentre0et100,sauf lesvaleurs tableau[0] et tableau[99] quivallent0. Leprogrammecréedeuxthreads:
• Lepremierthreadremplacechaquevaleur tableau[i],aveci=1,2, ,98parlamoyenne
(tableau[i-1]+tableau[i]+tableau[i+1])/3 Ilattendensuiteuntempsalléatoireentre1et3secondes; • Ledeuxièmethreadafficheletableautoutesles4secondes.
Exercice 5.7 (??) Dans un programme prévu pour utiliser des threads, créer un compteur globalpourcompterlenombred’itérations,etunevariableglobaleréelleu.Dansle main,on initialiserauàlavaleur1
LeprogrammecréedeuxthreadsT1etT2.DanschaquethreadTi,onincrémentelecompteurdunombred’itération,etonappliqueuneaffectation:
u = f_i(u); pourunefonction f_i quidépendduthread.
f_1(x) = (x ? 1)2 et f_2(x) = (x ? 2)2
De plus, le thread affiche la valeur de u et attend un temps aléatoire (entre 1 et 5 secondes) entredeuxitérations.
Exercice 5.8 (??) (Problème du rendez-vous)
a) Lessémaphorespermettentderéalisersimplementdesrendez-vousDeuxthreadsT1etT2 itèrentuntraitement10fois.Onsouhaitequ’àchaqueitérationlethreadT1attendeàlafinde sontraitementquidure2secondeslethreadT2réalisantuntraitementd’uneduréealéatoire entre 4 et 9 secondes. Écrire le programme principal qui crée les deux threads, ainsi que les fonctionsdethreadsenorganisantlerendez-vousavecdessémaphores.
b) DanscetteversionNthreadsdoiventsedonnerrendez-vous,Nétantpasséenargumentau programme.Lesthreadsonttousuneduréealéatoireentre1et5secondes.
Exercice 5.9 (???) (Problème de l’émetteur et du récepteur) Un thread émetteur dépose , à intervalle variable entre 1 et 3 secondes, un octet dans une variable globale à destinationd’unprocessusréécepteurlitcetoctetàintervallevariableaussientre 1 et 3 secondes. Quelle solution proposez-vous pour que l’émetteur ne dépose pas un nouvel octetalorsquelerécepteurn’apasencoreluleprécédentetquelerécepteurnelisepasdeux foislemêmeoctet?
Exercice 5.10 (???) (Problème des producteurs et des consommateurs) Desprocessusproducteursproduisentdesobjetsetlesinsè entendudesprocessusconsommateursretirent,detempsentempslesobjets(unparun).
Résolvez le problème pour qu’aucun objet ne soit ni perdu ni consommé plusieurs fois. ÉcrireuneprogrammeavecNthreadsproducteursetMthreadsconsomateurs,lesnombresN etMétantsaisisauclavier.Lesproducteursetlesconsomateursattendentuntempsaléatoire entre1et3secondesentredeuxproduits.Lesproduitssontdesoctetsquel’onstockedansun tableaude10octetsavecgestion LIFO.S’iln’yaplusdeplace,lesproducteursrestentbloqués enattendantquedesplacesselibèrent.
Exercice 5.11 (???) (Problème des lecteurs et des rédacteurs) Ceproblèmemodélise lesaccèsàunebasededonnées.Onpeutaccepterqueplusieursprocessuslisentlabaseenmême tempsmaissiunprocessusestentraindelamodifier,aucunprocessus,pasmêmeunlecteur, nedoitêtreautoriséàyaccéder.Commentprogrammerleslecteursetlesrédacteurs?Proposer unesolutionavecfaminedesécrivains:lesécrivainsattendentqu’iln’yaitaucunlecteur.Écrire une programme avec N threads lecteurs et M threads rédacteurs, les nombres N et M étant saisisauclavier.Labasededonnéeestreprésentéeparuntableaude15octetsinitialisésà0. À chaque lecture/écriture, le lecteur/écrivain lit/modifie l’octet à un emplacement aléatoire. Entredeuxlectures,leslecteursattendentuntempsaléatoireentre1et3secondes.Entredeux écritures,lesécrivainsattendentuntempsaléatoireentre1et10secondes.
Chapitre 6
Gestion du disque dûr et des fichiers
6.1 Organisation du disque dur
6.1.1 Plateaux, cylindres, secteurs
Un disque dur possède plusieurs plateaux, chaque plateau possède deux faces, chaque face possèdeplusieurssecteursetplusieurscylindres(voirficgureci-dessous).
Figure 6.1:Organisationd’unefacedeplateau
Le disque possède un secteur de boot, qui contient des informations sur les partitions bootables et le boot-loader, qui permet de choisir le système sous lequel on souhaite booter. Un disque est divisé en partitions (voir la figure 6.2) Les données sur les partitions (telles que les cylindre de début et de fin ou les types de partition) sont stockées dans une table des partitions.Leschémad’organisationd’unepartition UNIX estmontrésurlafigure6.3
6.1.2 Gérer les partitions et périphériques de stockage
L’outil fdisk permet de modifier les partitions sur un disque dur. Par exemple, pour voir et modifierlespartitionssurledisquedur SATA /dev/sda,onlance fdisk :
# fdisk /dev/sda
The number of cylinders for this disk is set to 12161.
There is nothing wrong with that, but this is larger than 1024,
Figure 6.2:Organisationd’undisquedur
and could in certain setups cause problems with:
1) software that runs at boot time (e.g., old versions of LILO)
2) booting and partitioning software from other OSs
(e.g., DOS FDISK, OS/2 FDISK)
Command (m for help): m
Command action a toggle a bootable flag b edit bsd disklabel c toggle the dos compatibility flag d delete a partition l list known partition types m print this menu n add a new partition o create a new empty DOS partition table p print the partition table q quit without saving changes s create a new empty Sun disklabel t change a partition's system id u change display/entry units v verify the partition table w write table to disk and exit x extra functionality (experts only)
Command (m for help): p
Disk /dev/sda: 100.0 GB, 100030242816 bytes
255 heads, 63 sectors/track, 12161 cylinders
Units = cylinders of 16065 * 512 = 8225280 bytes
Figure 6.3:Organisationd’unepartition
Device Boot | Start | End | Blocks | Id | System |
/dev/sda1 * | 1 | 1459 | 11719386 | 7 | HPFS/NTFS |
/dev/sda2 | 1460 | 2638 | 9470317+ | 83 | Linux |
/dev/sda3 | 2639 | 12161 | 76493497+ | 5 | Extended |
/dev/sda5 | 11796 | 12161 | 2939863+ | 82 | Linux swap / Solaris |
/dev/sda6 | 2639 | 11795 | 73553571 | 83 | Linux |
Partition table entries are not in disk order
Command (m for help): q
#
On voit en particulier le nombre total des cylindres, la taille de chaque cylindre, et pour chaque partition, le cylindre de début et de fin. Ici, on voit que /dev/sda1 est une partition NTFS (typedepartitionpourwindows),lespartitions /dev/sda2 et /dev/sda6 sontdetype ext3 (linux),etlapartition /dev/sda5 estunespacedeswap(utiliséeparlesprogrammeslors d’undépassementdela RAM.
Lestypesdepartitionqu’ilest(actuellement)possibledecréeravec fdisc sont:
Empty | 1e | Hidden W95 FAT1 80 | Old Minix be | Solaris boot | ||
1 | FAT12 | 24 | NEC DOS | 81 | Minix / old Lin bf | Solaris |
2 | XENIX root | 39 | Plan 9 | 82 | Linux swap / So c1 | DRDOS/sec (FAT- |
3 | XENIX usr | 3c | PartitionMagic | 83 | Linux c4 | DRDOS/sec (FAT- |
4 | FAT16 <32M | 40 | Venix 80286 | 84 | OS/2 hidden C: c6 | DRDOS/sec (FAT- |
5 | Extended | 41 | PPC PReP Boot | 85 | Linux extended c7 | Syrinx |
6 | FAT16 | 42 | SFS | 86 | NTFS volume set da | Non-FS data |
7 | HPFS/NTFS | 4d | QNX4.x | 87 | NTFS volume set db | CP/M / CTOS / . |
8 AIX 4e QNX4.x 2nd part 88 Linux plaintext de Dell Utility 9 AIX bootable 4f QNX4.x 3rd part 8e Linux LVM df BootIt a OS/2 Boot Manag 50 OnTrack DM 93 Amoeba e1 DOS access b W95 FAT32 51 OnTrack DM6 Aux 94 Amoeba BBT e3 DOS R/O c W95 FAT32 (LBA) 52 CP/M 9f BSD/OS e4 SpeedStor
e W95 FAT16 (LBA) 53 OnTrack DM6 Aux a0 IBM Thinkpad hi eb BeOS fs f W95 Ext'd (LBA) 54 OnTrackDM6 a5 FreeBSD ee EFI GPT
10 OPUS 55 EZ-Drive a6 OpenBSD ef EFI (FAT-12/16/ 11 Hidden FAT12 56 Golden Bow a7 NeXTSTEP f0 Linux/PA-RISC b
12 Compaq diagnost 5c Priam Edisk a8 Darwin UFS f1 SpeedStor 14 Hidden FAT16 <3 61 SpeedStor a9 NetBSD f4 SpeedStor
16 Hidden FAT16 63 GNU HURD or Sys ab Darwin boot f2 DOS secondary
17 Hidden HPFS/NTF 64 Novell Netware b7 BSDI fs fd Linux raid auto
18 AST SmartSleep 65 Novell Netware b8 BSDI swap fe LANstep
1b Hidden W95 FAT3 70 DiskSecure Mult bb Boot Wizard hid ff BBT
1c Hidden W95 FAT3 75 PC/IX
Pourformaterunepartition,parexempleaprèsavoircréélapartition,onutilise mkfs. Exemple.
pour une partition windows : # mkfs -t ntfs /dev/sda1 pour une clef usb sur /dev/sdb1 : # mkfs -t vfat /dev/sdb1 pour une partition linux ext3 (comme le /home) :
# mkfs -t ext3 /dev/sda6
Il faut ensuite monter la partition, c’est à dire associer la partition à un répertoire dans l’arborescencedusystèmedefichiers. Exemple.
Pour monter une clef usb qui est sur /dev/sdb1
# mkdir /mnt/usb
# mount -t vfat /dev/sdb1 /mnt/usb
# ls /mnt/usb/ etc
ou encore, pour accéder à une partition windows à partir de linux :
# mkdir /mnt/windows
# mount -t ntfs /dev/sda1 /mnt/windows
# ls -l /mnt/windows
Pourdémonterunvolume,utiliser umount.Onpeutmonterautomatiquementunpériphérique enrajoutantunelignedanslefichier fstab :
# cat /etc/fstab # /etc/fstab: static file system information. # | ||||||
# <file system> <mount point> <type> <options> | <dump> | <pass> | ||||
proc /proc proc defaults | ||||||
/dev/sda2 | / | ext3 defaults,errors=remount-ro 0 | 1 | |||
/dev/sda6 | /home | ext3 defaults 0 2 | ||||
/dev/sda5 | none | swap sw 0 0 | ||||
/dev/scd0 | /media/cdrom0 | udf,iso9660 user,noauto 0 0 | ||||
/dev/sdb1 /mnt/usb vfat user,noauto 0 0
/dev/mmcblk0p1 /mnt/sdcard vfat user,noauto 0 0 /dev/sda1 /mnt/windows ntfs uid=1001,auto
Par exemple ici le lecteur de CDROM, la clef USB et la caret SD peuvent être montée par tous les utilisateurs (option user). Par contre, l’accès à la partition windows est résearvé à l’utilisateurd’UID 1001(voir /etc/passwd pourtrouverl’IUD d’unutilisateur).
Onpeutvoirlalistedespartitionsquisontmontées,ainsiquel’espacelibredanschacune d’elles,parlacommande df :
# df | |||
Filesystem | 1K-blocks | Used Available Use% Mounted on | |
/dev/sda2 | 9322492 | 8362396 | 486584 95% / |
tmpfs | 517544 | 517544 0% /lib/init/rw | |
udev | 10240 | 72 | 10168 1% /dev |
tmpfs | 517544 | 517544 0% /dev/shm | |
/dev/sda6 | 72397784 | 34359196 | 34360912 50% /home |
/dev/sda1 | 11719384 | 6920456 | 4798928 60% /mnt/windows |
/dev/sdb1 | 3991136 | 126880 | 3864256 4% /mnt/usb |
/dev/scd0 | 713064 | 713064 | 0 100% /media/cdrom0 |
/dev/mmcblk0p1 | 2010752 | 528224 | 1482528 27% /mnt/sdcard |
6.1.3 Fichiers, inodes et liens
Un inode identifieunfichieretdécritsespropritées,tellesqu’inpeutlesvoispar ls -l :
• donnéessurlepropriétaire:UID et GID ;
• Droitsd’accès;
• Datesdecréation,dedernièremodification,dedernieraccès;
• Nombredefichierayantcetinode(encasdeliensdurs);
• Tailleenoctets;
• Adresse d’unblockdedonnées.
Il existe dans un disque dur des liens, lorsque plusieurs noms de fichiers conduisent aux mêmesdonnées.Ceslienssontdedeuxtypes:
1. On parle d’un lien dur lorsquedeux noms de fichiers sont associés au même inode (les différents liens durs a exactement les mêmes propriétés mais des noms différents). En particulier,l’adressedesdonnéesestlamêmedanstouslesliensdurs.Lacommande rm décrémentelenombredeliensdurs.Lasuppressionn’esteffectivequelorsquelenombre de lien durs (visible par ls -l ou stat) devient nul. Les liens durs sont créés par la commande ln.
2. Onparled’un lien symbolique lorsqueleblockdedonnéesd’unfichiercontientl’adresse dublocdedonnéesd’unautrefichier.Leslienssymboliquessontcréésparlacommande ln -s (optin -s).
6.2 Obtenir les informations sur un fichier en C
On peut obtenir les informations sur un fichier ou un répertoire (ID du propriétaire, taille inode, )aveclafonction stat.
Exemple. L’exemple suivant affiche des informations sur le fichier dont le nom est passé en argument.
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h> #include <time.h>
int main(int argc, char**argv)
{ struct stat st; /* pour récupérer les informations sur un fichier */ struct tm *temps; /* pour traduire les dates (voir ctime(3)) */
if (argc != 2)
{ fprintf(stderr, "Usage : %s nom_de_fichier\n", argv[0]); exit(1);
} if (stat(argv[1], &st)!= 0)
{ perror("Erreur d'accès au fichier\n"); exit(1);
} if (S_ISDIR(st.st_mode)) printf("Le nom %s correspond à un répertoire\n", argv[1]);
if (S_ISREG(st.st_mode))
{ printf("%s est un fichier ordinaire\n", argv[1]); printf("La taille du fichier en octets est %d\n", st.st_size); temps = localtime(&st.st_mtime);
printf("Le jour de dernière mofification est %d/%d/%d\n", temps->tm_mday, temps->tm_mon+1, temps->tm_year+1900);
} return 0;
}
Exempledetraced’exécutiondeceprogramme:
$ gcc testStat.c -o testStat
$ ls -l
-rwxr-xr-x 1 remy remy 8277 Feb 1 13:03 testStat
-rw-r--r-- 1 remy remy 935 Feb 1 13:03 testStat.c
$ ./testStat testStat.c testStat.c est un fichier ordinaire La taille du fichier en octets est 935
Le jour de dernière mofification est 1/2/2008
Toutes les informatione renvoyées par stat sont stockées dans la structure stat, dont la déclarationestlasuivante(voir stat(2))
struct stat { | ||
dev_t | st_dev; | /* ID of device containing file */ |
ino_t | st_ino; | /* inode number */ |
mode_t | st_mode; | /* protection */ |
nlink_t | st_nlink; | /* number of hard links */ |
uid_t | st_uid; | /* user ID of owner */ |
gid_t | st_gid; | /* group ID of owner */ |
dev_t | st_rdev; | /* device ID (if special file) */ |
off_t | st_size; | /* total size, in bytes */ |
blksize_t st_blksize; /* blocksize for filesystem I/O */ blkcnt_t st_blocks; /* number of blocks allocated */ time_t st_atime; /* time of last access */ time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
6.3 Parcourir les répertoires en C
Lafonction opendir permetd’ouvrirunrépertoireetretourneunpointeurderépertoire(type DIR*). On peut alors parcourir la liste des éléments (fichiers, liens ou répertoires) qui sont contenusdanscerépertoire(ycomprislesrépertoires "." et "..").
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h> #include <stdlib.h>
int main(int argc, char**argv)
{
DIR *dir; struct dirent *ent; int i; for (i=1 ; i<argc ; i++) { dir = opendir(argv[i]); /* ouverture du répertoire */ if (dir==NULL)
{ fprintf(stderr, "Erreur d'ouverture du réperoire %s\(\backslash\)n", argv[i]);
fprintf(stderr, "Droits inssufisant ou répertoire incorrect\n"); exit(1);
} printf("Répertoire %s\n", argv[i]);
while ((ent=readdir(dir)) != NULL) /* on parcourt la liste */
printf("%s ", ent->d_name);
} printf("\n"); retu 0;
}
Exempledetraced’exécutiondeceprogramme:
$ gcc parcoursRep.c -o parcoursRep
$ ls
parcoursRep parcoursRep.c $ ./parcoursRep .
Répertoire .
parcoursRep .. parcoursRep.c .
6.4 Descripteurs de fichiers
Un descripteur de fichier est un entier qui identifie un fichier dans un programme C. Ne pas confondre un descripteur de fichier avec un pointeur de fichier. La fonction fdopen permet d’obtenirunpointeurdefichieràpartird’undescripteur.
6.4.1 Ouverture et création d’un fichier
La fonction open permet d’obtenir un descripteur de fichier à partir du nom de fichier sur le disque, de la même façon que fopen permet d’obtenir un pointeur de fichier. Une grande différence est que la fonction open offre beaucoup plus d’options pour tester les permissions. Leprototypedelafonction open estlesuivant:
int open(const char *pathname, int flags, mode_t mode);
Lafonctionretourneunevaleurstrictementnégativeencasd’erreur.
Leparamètre flags permetdeprécisersi,pourleprogramme,lefichierestenlectureseule (masque O_RDONLY), écriture seule (masque O_WRONLY), ou lecture-écriture (masque O_RDWR). Par un ou bit à bit (|), on peut aussi préciser si le fichier est ouvert en mode ajout (masque O_APPEND), ou si le fichier doit être écrasé (masque O_TRUNC) ou ouvert en mode création (si lefichiern’existepasilseracréé,masque O_CREAT).
Le mode permet de fixer les permissions lors de la création du fichier (le cas échéant), lorsqueleparamètre flag estspécifiéavec O_CREAT.Lespermissionspeuventêtredéfiniespar deschiffresoctaux(7pourrwx,0pouraucunepermission)pourl’utilisateur,legroupeetles autresustilisateur.Cependant,unetbitàbitesteffectuéaveclanégationbitàbitdevariable d’environnement umask (mode & ?mask)).(voir man umask et man open(2))
6.4.2 Lecture et écriture via un descripteur
Pourécriredesoctetsviadescripteurdefichier,onutiliselafonction write : ssize_t write(int descripteur1, const void *bloc, size_t taille);
Le fichier (ou tube ou socket ) doit être ouvert en écriture (options O_WRONLY, O_RDWR) La taille est le nombre d’octets qu’on souhaite écrire, et le bloc est un pointeur vers la mémoire contenantcesoctets.
Pourliredesoctetsviaundescripteurdefichiers,onutiliselafonction read :
ssize_t read(int descripteur0, void *bloc, size_t taille);
Lefichier(outubeousocket )doitêtreouvertenlecture(options O_RDONLY, O_RDWR)
Onpeutaussiutiliser fdopen pourobtenirun FILE* cequipermetd’utiliserdesfonctions plushautniveautellesque fprintf et fscanf ou fread et fwrite.
6.5 Exercices
Exercice 6.1 ([) sortezvoscalculettes!]Soitundiqueduayant2plateaux,chaquefaceayant 1000cylindreset60secteurs,chaquesecteurayant1024octets.
a) Calculezlacapacitétotaledudisque.
b) Caluculezlaposition(lenuméro)ded’octet300susecteur45ducylindre350delaface2 dupremierplateau.
c) Surquelsecteuretenquellepositionsetrouvel’octetnuméro78000000?
Exercice 6.2 (?) Ecrireunprogrammequiprendenargumentdesnomsderépertoireetaffiche la liste des fichiers de ces répertoires qui ont une taille supérieure à (à peu près) 1Mo avec l’UID dupropriétairedufichier.
Exercice 6.3 (a)) Écrire un programme qui saisit au clavier un tableau d’entiers et sauvegardecetableauauformatbinairedansunfichierayantpermissionenécriturepourlegroupe dufichieretenlectureseulepourlesautresutilisateurs.
b) Écrireuneprogrammequichargeenmémoireuntableaud’entierstelquegénéréau a).Le fichierd’entiersnecontientpaslenombred’éléments.Leprogrammedoitfonctionnerpourun nombrequelconquededonnéesentièresdanslefichier.
Chapitre 7
Signaux
7.1 Préliminaire : Pointeurs de fonctions
Unpointeurdefonctionsen C estunevariablequipermetdedésignerunefonction C.Comme nimporte quelle variable, on peut mettre un pointeur de fonctions soit en variable dans une fonction,soitenparamètredansunefonction.
On déclare un pointeur de fonction comme un prototype de fonction, mais on ajoute une étoile (?) devant le nom de la fonction. Dans l’exemple suivant, on déclare dans le main un pointeursurdesfonctionsquiprennentenparamètreun int,etunpointeursurdesfonctions quiretournentun int. #include <stdio.h>
int SaisisEntier(void)
{ int n;
printf("Veuillez entrer un entier : ");
scanf("%d", &n); return n;
}
void AfficheEntier(int n)
{ printf("L'entier n vaut %d\(\backslash\)n", n);
}
int main(void)
{ void (*foncAff)(int); /* déclaration d'un pointeur foncAff */ int (*foncSais)(void); \em{}/*déclaration d'un pointeur foncSais */ inte entier;
foncSais = SaisisEntier; {/* affectation d'une fonction */ foncAff = AfficheEntier; /* affectation d'une fonction */ entier = foncSais(); /* on exécute la fonction */ foncAff(entier); /* on exécute la fonction */
\retu 0;
}
Dans l’exemple suivant, la fonction est passée en paramètre à une autre fonction, puis exécutée.
#include <stdio.h>
int SaisisEntier(void)
{ int n;
printf("Veuillez entrer un entier : ");
scanf("%d", &n); getchar(); return n;
}
void AfficheDecimal(int n)
{ printf("L'entier n vaut %d\n", n);
}
void AfficheHexa(int n)
} printf("L'entier n vaut %x\(\backslash\)n", n);
}
void ExecAffiche(void (*foncAff)(int), int n)
{ foncAff(n); /* exécution du paramètre */
}
int main(void)
{ int (*foncSais)(\void); /*déclaration d'un pointeur foncSais */ int entier; char rep;
foncSais = SaisisEntier; /* affectation d'une fonction */ entier = foncSais(); /* on exécute la fonction */
puts("Voulez-vous afficher l'entier n en décimal (d) ou en hexa (x) ?"); rep = getchar();
/* passage de la fonction en paramètre : */ if (rep == 'd')
ExecAffiche(AfficheDecimal, entier); if (rep == 'x')
ExecAffiche(AfficheHexa, entier);
return 0;
}
7.2 Les principaux signaux
Unsignalpermetdeprévenirunprocessusqu’unévennementparticulierc’estproduitdansle systèmepour dans un (éventuellementautre) processus. Certains signaux sontenvoyés par le noyau (comme en cas d’erreur de violation mémoire ou division par 0), mais un programme utilisateurpeutenvoyerunsignalaveclafonctionoulacommande kill,ouencoreparcertaines combinaisondetouchesauclavier(comme Ctrl-C).Unutilisateur(àl’exceptionde root)ne peutenvoyerunsignalqu’àunprocessusdontilestpropriétaire.
Les principaux signaux (décrits dans la norme POSIX.1-1990) (faire man 7 signal pour descompléments)sontexpliquéssurletable7.1.
7.3 Envoyer un signal
La méthode la plus générale pour envoyer un signal est d’utiliser soit la commande sheel kill(1),soitlafonctionC kill(2).
7.3.1 La commande kill
Lacommande kill prenduneoption -signal etun pid. Exemple.
$ kill -SIGINT 14764 {\em# interromp processus de pid 14764}
$ kill -SIGSTOP 22765 {\em# stoppe temporairement le process 22765}
$ kill -SIGCONT 22765 {\em# reprend l'exécution du prcessus 22765}
Compléments ? Lesignalpardéfaut,utiliséencasest SIGTERM,quitermineleprocessus. ? Onpeututiliserun PID négatifpourindiquerungroupedeprocessus,telqu’ilestindiqué parle PGID enutilisantl’option -j delacommande ps.Celapermetd’envoyerunsignal àtoutungroupedeprocessus. |
7.3.2 La fonction kill
LafonctionC kill estsimilaireàlacommande kill du shell.Elleapourprototype:
int kill(pid_t pid, int signal);
Signal | Valeur | Action | Commentaire |
SIGHUP | 1 | Term | Terminaisonduleaderdesession(exemple:terminaison duterminalquialancéleprogrammeoulogout) |
SIGINT | 2 | Term | Interruptionauclavier(par Ctrl-C pardéfaut) |
SIGQUIT | 3 | Core | Quitparfrappeauclavier(par Ctr ? AltGr ?\pardéfaut) |
SIGILL | 4 | Core | Détectiond’uneinstructionillégale |
SIGABRT | 6 | Core | Avortementduprocessusparlafonctionabort(3) (généralementappeléeparleprogrammeur encasdedétectiond’uneerreur) |
SIGFPE | 8 | Core | Exceptiondecalculflottant(divisionpar0 racinecarréesd’unnombrenégatif,etc ) |
SIGKILL | 9 | Term | Processustué(kill) |
SIGSEGV | 11 | Core | Violationmémoire.Lecomportementpardéfaut termineleprocessussuruneerreurdesegmentation |
SIGPIPE | 13 | Term | Erreurdetube:tentatived’écriredansuntube quin’apasdesortie |
SIGALRM | 14 | Term | Signaldetimersuiteàunappelde alarm(2) quipermetd’envoyerunsignalàunecertainedate |
SIGTERM | 15 | Term | Signaldeterminaison |
SIGUSR1 | 30,10,16 | Term | Signalutilisateur1:permetauprogrammeur dedéfinirsonpropresignal pouruneutilisationlibre |
SIGUSR2 | 31,12,17 | Term | Signalutilisateur23:permetauprogrammeur dedéfinirsonpropresignal pouruneutilisationlibre |
SIGCHLD | 20,17,18 | Ign | L’undesprocessusfilseststoppé (par SIGSTOP outerminé) |
SIGSTOP | 17,19,23 | Stop | Stoppetemporairementleprocessus.Leprocessus sefigejusqu’àrecevoirunsignal SIGCONT |
SIGCONT | 19,18,25 | Cont | Reprendl’exécutiond’unprocessusstoppé. |
SIGTSTP | 18,20,24 | Stop | Processusstoppéàpartird’unterminal(tty) (par Ctrl-S pardéfaut) |
SIGTTIN | 21,21,26 | Stop | saisiedansunterminal(tty)pourun processusentâchedefond(lancéavec &) |
SIGTTOU | 22,22,27 | Stop | Affichagedansunterminal(tty)pourun processusentâchedefond(lancéavec &) |
Table 7.1:Listedesprincipauxsignaux Exemple. Leprogrammesuivanttueleprocessusdontle PID estpasséenargumentseulement sil’utilisateurconfirme.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> #include <signal.h>
int main(int argc, char **argv)
{ pid_t pidToSend; char rep; if (argc != 2)
{ fprintf(stderr, "Usage %s pid\n", argv[0]); exit(1);
} pidToSend = atoi(argv[1]);
printf("Etes-vous sûr de vouloir tuer le processus %d ? (o/n)", pidToSend);
rep = getchar();
if (rep == 'o') kill(pidToSend, SIGTERM);
return 0; }
7.3.3 Envoi d’un signal par combinaison de touches du clavier
Un certain nombre de signaux peuvent être envoyé à partir du terminal par une combinaison detouche.Onpeutvoircescombinaisonsdetouchespar stty -a :
stty -a
speed 38400 baud; rows 48; columns 83; line = 0;
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = M-^?; eol2 = M-^?; swtch = M-^?; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W; lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd cs8 hupcl -cstopb cread -clocal -crtscts
-ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc ixany imaxbel iutf8 opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0 isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctlechoke
7.4 Capturer un signal
7.4.1 Créer un gestionnaire de signal (signal handler)
Un gestionnaire de signal (signal handler) permet de changer le comportement du processus lorsdelaréceptiondusignal(parexemple,seterminer).
Voiciunexempledeprogrammequisauvegardedesdonnéesavantdeseterminerlorsd’une interruptionpar Ctrl-C dansleterminal.Leprincipeestdemodifierlecomportementlorsde laréceptiondusignal SIGINT.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h> #include <unistd.h> int donnees[5];
void gestionnaire(int numero)
{ FILE *fp; int i;
if (numero == SIGINT)
{ printf("\nSignal d'interruption, sauvegarde \n"); fp = fopen("", "w");
for (i=0 ; i<5 ; i++)
{ fprintf(fp, "%d ", donnees[i]);
} fclose(fp);
printf("Sauvegarde terminée, terminaison du processus\n"); exit(0);
}
}
int main(void)
{ int i; char continuer='o'; struct sigaction action;
action.sa_handler = gestionnaire; /* pointeur de fonction */ sigemptyset(&action.sa_mask); /* ensemble de signaux vide */ action.sa_flags = 0; /* options par défaut */ if (sigaction(SIGINT, &action, NULL) != 0)
{ fprintf(stderr, "Erreur sigaction\(\backslash\)n"); exit(1);
}
for (i=0 ; i<5 ; i++)
{ printf("donnees[%d] = ", i); scanf("%d", &donnees[i]); getchar(); }
while (continuer == 'o')
{ puts("zzz "); sleep(3);
for (i=0 ; i<5 ; i++) printf("donnees[%d] = %d ", i, donnees[i]);
printf("\(\backslash\)nVoules-vous continuer ? (o/n) "); continuer = getchar(); getchar();
}
}
Voiciletracedeceprogramme,quel’oninterromptavec Ctrl-C :
gcc sigint.c -o sigint
./sigint donnees[0] = 5 donnees[1] = 8 donnees[2] = 2 donnees[3] = 9 donnees[4] = 7 zzz
donnees[0] = 5 donnees[1] = 8 donnees[2] = 2 donnees[3] = 9 donnees[4] = 7
Voules-vous continuer ? (o/n)
Signal d'interruption, sauvegarde
Sauvegarde terminée, terminaison du processus cat
5 8 2 9 7
7.4.2 Gérer une exception de division par zéro
Voici un programme qui fait une division entre deux entiers y/x saisis au clavier. Si le dénominateur x vaut 0 un signal SIGFP est reçu et le gestionnaire de signal fait resaisir x. Une instruction siglongjmp permetdereveniràlaligned’avantl’erreurdedivisionpar0.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h> #include <setjmp.h>
int x,y, z; sigjmp_buf env; void gestionnaire(int numero)
{ FILE *fp; int i;
if (numero == SIGFPE)
{ printf("Signal d'erreur de calcul flottant.\n"); printf("Entrez x différent de zéro : "); scanf("%d", &x); siglongjmp(env, 1); /* retour au sigsetjmp */ }
}
int main(void)
{ int i; char continuer='o'; struct sigaction action; action.sa_handler = gestionnaire; /* pointeur de fonction */ sigemptyset(&action.sa_mask); /* ensemble de signaux vide */ action.sa_flags = 0; /* options par défaut */ if (sigaction(SIGFPE, &action, NULL) != 0)
{ fprintf(stderr, "Erreur sigaction\n"); exit(1);
}
printf("Veuillez entrer x et y : "); scanf("%d %d", &x, &y); getchar(); sigsetjmp(env, 1); /* on mémorise la ligne */ z = y/x; /* opération qui risque de provoquer une erreur */
printf("%d/%d=%d\(\backslash\)n", y, x, z); return 0;
}
Voicilatracedeceprogrammelorsqu’onsaisitundénominateurxégalà0:
Veuillez entrer x et y : 0 8
Signal d'erreur de calcul flottant.
Entrez x différent de zéro : 0 Signal d'erreur de calcul flottant.
Entrez x différent de zéro : 2
8/2=4
7.5 Exercices
Exercice 7.1 (?) Ecrireunprogrammequicréeunfilsquifaituncalculsansfin.Leprocessus pèreproposealorsunmenu:
• Lorsquel’utilisateurappuiesurlatouche 's',leprocessuspèreendortsonfils.
• Lorsquel’utilisateurappuiesurlatouche 'r',leprocessuspèreredémaresonfils.
• Lorsque l’utilisateur appuie sur la touche 'q', le processus près tue son fils avant de se terminer.
Exercice 7.2 (?) Ecrire un programme saisit.c qui saisit un int au clavier, et l’enregistre dansunfichier .Écrireunprogramme affiche.c quiattend(avec sleep)un signalutilisateurduprogramme saisit.c.Lorsquel’entieraétésaisi,leprogramme affiche.c affichelavaleurdel’entier.
Exercice 7.3 (a)) Écrire un programme qui crée 5 processus fils qui font une boucle while 1.Leprocessuspèreproposeradansunebouclesansfinunmenuàl’utilisateur:
• Endormirunfils;
• Réveillerunfils;
• Terminerunfils;
b) Modifier les fils pour qu’ils affichent un message lorsqu’ils sont tués. Le processus père afficheraunautremessages’ilesttué.
Exercice 7.4 (?) Ecrire un programme qui saisit les valeurs d’un tableau d’entier tab de n élémentsallouédynamiquement.L’entier n serasaisiauclavier.Leprogrammeaffichelavaleur d’un élément tab[i] où i est saisi au clavier. En cas d’erreur de segmentation le programme faitresaisirlavaleurde i avantdel’afficher.
Chapitre 8
Programmation réseaux
Lebutdelaprogrammationréseauestdepermettreàdesprogrammesdedialoguer(d’échanger desdonnées)avecd’autresprogrammesquisetrouventsurdesordinateursdistants,connectés par un réseau. Nous verrons tout d’abord des notions générales telles que les adresse IP ou le protocole TCP, avant d’étudier les sockets unix/linux qui permettent à des programmes d’établirunecommunicationetdedialoguer.
8.1 Adresses IP et MAC
Chaqueinterfacedechaqueordinateurseraidentifiépar
• Son adresse IP : une adresse IP (version 4, protocole IPV4) permet d’identifier un hôteet un sous-réseau. L’adresse IP est codée sur 4 octets. (les adresses IPV6, ou IP next generationserontcodéessur6octets).
• L’adressemacdesacarteréseau(carteethernetoucartewifi);
Une adresse IP permet d’identifier un hôte. Une passerelle est un ordinateur qui possède plusieursinterfacesetquitransmetlespaquetsd’uneinterfaceàl’autre.Lapasserellepeutainsi faire communiquer différents réseaux. Chaque carte réseau possède une adresse MAC unique garantie par le constructeur. Lorsqu’un ordinateur a plusieurs plusieurs interfaces, chacune possède sa propre adresse MAC et son adresse IP. On peut voir sa configuration réseau par ifconfig.
$ /sbin/ifconfig eth0
eth0 Link encap:Ethernet HWaddr 00:B2:3A:24:F3:C4 inet addr:192.168.0.2 Bcast:192.168.0.255 Mask:255.255.255.0 inet6 addr: fe80::2c0:9fff:fef9:95b0/64 Scope:Link UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:6 errors:0 dropped:0 overruns:0 frame:0 TX packets:16 errors:0 dropped:0 overruns:0 carrier:5 collisions:0 txqueuelen:1000
RX bytes:1520 (1.4 KiB) TX bytes:2024 (1.9 KiB)
Interrupt:10
Onvoitl’adresseMAC 00 :B2 :3A :24 :F3 :C4 etl’adresseIP192.168.0.2.Celasignifie quelepremieroctetdel’adresseIPestégalà192,ledeuxième168,letroisèmeoctetestnul, etlequatrièmevaut2.
DansunprogrammeC,les4octetsd’uneadresseIPpeuventêtrestockésdansun unsigned int. On peut stocker toutes les données d’adresse dans une structure in_addr. On peut traduire l’adresse IP en une chaîne de caractère (avec les octets écrits en décimal et séparés par des points,exemple:”192.168.0.2”)parlafonction inet_ntoa :
char * inet_ntoa(struct in_addr adresse);
Inversement,onpeuttraduireunechaînedecaractèrereprésentantuneadresseIPen struct in_addr,enpassantlastructureparadresseàlafonction inet_aton : int inet_aton(const char *chaine, struct in_addr *adresse);
8.2 Protocoles
Unpaquetdedonnéesàtransmettredansuneapplicationvasevoirajouter,suivantleprotocole,lesdonnéesnécessairespour
• leroutage(déterminationducheminparcouruparlesdonnéesjusqu’àdestination);
• la vérification de l’intégrité des données (c’est à dire la vérification qu’il n’y a pas eu d’erreurdanslatransmission).
Pour le routage, les données sont par exemple l’adresse IP de la machine de destiation ou l’adresseMACdelacarted’unepasserelle.Cesdonnéessontrajoutéesapaquetàtransmettre à travers différentes couches, jusqu’à la couche physique (câbles) qui transmet effectivement lesdonnéesd’unordinateuràl’autre.
8.2.1 La listes des protocles connus du systèmes
Unprotocole(IP,TCP,UDP, )estunmodedecommunicationréseau,c’estàdireunemanière d’établirlecontactentremachineetdetransférerlesdonnées.Souslinux,lalistedesprotocoles reconnusparlesystèmesetrouvedanslefichier /etc/protocols.
$ cat /etc/protocols # Internet (IP) protocols | |||
ip 0 | IP | # internet protocol, pseudo protocol number | |
#hopopt 0 | HOPOPT | # IPv6 Hop-by-Hop Option [RFC1883] | |
icmp 1 | ICMP | # internet control message protocol | |
igmp 2 | IGMP | # Internet Group Management | |
ggp 3 | GGP | # gateway-gateway protocol | |
ipencap 4 | IP-ENCAP | # IP encapsulated in IP (officially ``IP'') | |
st 5 | ST | # ST datagram mode | |
tcp 6 | TCP | # transmission control protocol | |
egp 8 | EGP | # exterior gateway protocol | |
igp 9 | IGP | # any private interior gateway (Cisco) | |
pup 12 | PUP | # PARC universal packet protocol | |
udp 17 | UDP | # user datagram protocol | |
hmp 20 | HMP | # host monitoring protocol | |
xns-idp 22 | XNS-IDP | # Xerox NS IDP | |
rdp 27 | RDP | # "reliable datagram" protocol | |
etc | etc |
A chaque protocole est associé un numéro d’identification standard. Le protocle IP est rarementutilisédirectementdansuneapplicationetonutiliselepluscourammentlesprotocoles
TCPetUDP.
8.2.2 Le protocole TCP
LeprotocoleTCPsertàétablirunecommunicationfiableentredeuxhôtes.Pourcela,ilassure lesfonctionnalitéssuivantes:
• Connexion.L’émetteuretlerécepteursemettentd’accordpourétablirunconnexion.Laconnexionresteouvertejusqu’àcequ’onlareferme.
• Fiabilité.Suiteautransfertdedonnées,destestssontfaitspourvérifierqu’iln’yapaseud’erreurdanslatransmission.Cestestsutilisentlaredondancedesdonnées,c’estàdire qu’un partie des données est envoyée plusieurs fois. De plus, les données arrivent dans l’ordreoùellesontétéémises.
• Possiblilité de communiquer sous forme de flot de données, comme dans un tube (parexempleaveclesfonctions read et write).Lespaquetsarriventàdestinationdansl’ordre oùilsontétéenvoyés.
8.2.3 Le protocole UDP
Leprotocole UDP permetseulementdetransmettrelespaquetssansassurerlafiabilité:
• Pasdeconnexionpréalable;
• Pasdecontroled’intégritédesdonnées.Lesdonnéesnesontenvoyéesqu’unfois;
• Lespaquetsarriventàdestinationdansledésordre.
8.3 Services et ports
Il peuty avoir de nombreuses applications réseau qui tournent sur la même machine. Les numéros de port permettent de préciser avec quel programme nous souhaitons dialoguer par le réseau. Chaque application qui souhaite utiliser les services de la couche IP se voit attribuer un uméro de port. Un numéro de port est un entier sur 16 bits (deux octets). Dans un programme C, on peut stocker un numéro de port dans un unsigned short. Il y a un certain nombredeportsquisontréservésàdesservicesstandards.Pourconnaîtrelenumérodeport correspondantàunservicetelque ssh,onpeutregarderdanslefichier /etc/services.
# Network services, Internet style
tcpmux | 1/tcp | # TCP port service multiplexer | |
echo | 7/tcp | ||
echo | 7/udp | ||
discard | 9/tcp | sink null | |
discard | 9/udp | sink null | |
systat | 11/tcp | users | |
daytime | 13/tcp | ||
daytime | 13/udp | ||
netstat | 15/tcp | ||
qotd | 17/tcp | quote | |
msp | 18/tcp | # message send protocol | |
msp | 18/udp | ||
chargen | 19/tcp | ttytst source | |
chargen | 19/udp | ttytst source | |
ftp-data | 20/tcp | ||
ftp | 21/tcp | ||
fsp | 21/udp | fspd | |
ssh | 22/tcp | # SSH Remote Login Protocol | |
ssh | 22/udp | ||
telnet | 23/tcp | ||
smtp | 25/tcp | | |
time | 37/tcp | timserver | |
time | 37/udp | timserver | |
rlp | 39/udp | resource | # resource location |
nameserver | 42/tcp | name | # IEN 116 |
whois | 43/tcp | nicname | |
tacacs | 49/tcp | # Login Host Protocol (TACACS) | |
tacacs | 49/udp | ||
re-mail-ck | 50/tcp | # Remote Mail Checking Protocol | |
re-mail-ck | 50/udp | ||
domain | 53/tcp | nameserver | # name-domain server |
domain | 53/udp | nameserver | |
mtp etc | 57/tcp | # deprecated |
L’administrateurdusystèmepeutdéfinirunnouveauserviceenl’ajoutantdans /etc/services etenprécisantlenumérodeport.Lesnumérosdeportinférieursà1024sontréservésauxserveurs et démons lancés par root (éventuellement au démarage de l’ordinateur), tels que le serveur d’impression /usr/sbin/cupsd sur le port ou le serveur ssh /usr/sbin/sshd sur le port22.
8.4 Sockets TCP
Dans cette partie, nous nous limitons aux sockets avec protocole TCP/IP, c’est à dire un protocole TCP (avec connexion préalable et vérification des données), fondé sur IP (c’est à direutilisantlacoucheIP).Pourutiliserd’autresprotocoles(telqueUDP),ilfaudraitmettre d’autresoptionsdanslesfonctionstellesque socket,etutiliserd’autresfonctionsque read et write pourtransmettredesdonnées.
8.4.1 Création d’une socket
Pour créer une socket, on utilise la fonction socket, qui nous retourne un identifiant (de type int) pour la socket. Cet identifiant servira ensuite à désigner la socket dans la suite du programme (comme un pointeur de fichiers de type FILE* sert à désigner un fichier). Par exemple, pour une socket destinée à être utilisée avec un protocole TCP/IP (avec connexion TCP)fondésurIP(AF_INET),onutilise int sock = socket(AF_INET, SOCK_STREAM, 0);
Cettesocketestdestinéeàpermettreàuneautremachinededialogueravecleprogramme.
On précise éventuellement l’adresse IP admissible (si l’on souhiate faire un contrôle sur l’adresse IP) de la machine distante, ainsi que le port utilisé. On lie ensuite la socket sock à l’adresse IP et au port en question avec la fonction bind. On passe par adresse l’addresse de lasocket(detype struct sockaddr_in)avecuncast,etlatailleenoctetsdecettestructure (revoyéepar sizeof).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h> #define BUFFER_SIZE 256
int cree_socket_tcp_ip()
{ int sock;
struct sockaddr_in adresse;
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{ fprintf(stderr, "Erreur socket\(\backslash\)n"); return -1; } memset(&adresse, 0, sizeof(struct sockaddr_in)); adresse.sin_family = AF_INET;
// donner un numro de port disponible quelconque
adresse.sin_port = htons(0); // aucun contrle sur l'adresse IP : adresse.sin_addr.s_addr = htons(INADDR_ANY);
// Autre exemple :
// connexion sur le port 33016 fix // adresse.sin_port = htons(33016); // depuis localhost seulement :
// inet_aton("127.0.0.1", &adresse.sin_addr);
if (bind(sock, (struct sockaddr*) &adresse, sizeof(struct sockaddr_in)) < 0)
{ close(sock);
fprintf(stderr, "Erreur bind\(\backslash\)n"); return -1;
}
return sock;
}
8.4.2 Affichage de l’adresse d’une socket
Après un appel à bind, on peut retrouver les données d’adresse et de port par la fonction getsockname.Lesparamètresdontpratiquementlesmêmesquepour bind saufquelenombre d’octetsestpasséparadresse.Onpeututiliserlesfonctions ntoa et ntohs pourafficherl’adresse IPetleportdemanièrecompréhensibleparl’utilisateur.
int affiche_adresse_socket(int sock)
{ struct sockaddr_in adresse; socklen_t longueur;
longueur = sizeof(struct sockaddr_in);
if (getsockname(sock, (struct sockaddr*)&adresse, &longueur) < 0)
{ fprintf(stderr, "Erreur getsockname\(\backslash\)n"); return -1;
} printf("IP = %s, Port = %u\(\backslash\)n", inet_ntoa(adresse.sin_addr), ntohs(adresse.sin_port));
return 0;
}
8.4.3 Implémentation d’un serveur TCP/IP
Un serveur (réseau) est une application qui va attendre que d’autres programmes (sur des machines distantes), appelés clients, entrent en contact avec lui, et dialoguent avec lui. Pour créerunserveurTCP/IPavecdessockets,oncréed’abordlasocketavec socket et indique ensuite au noyau linux/unix que l’on attend une connection sur cette socket. Pour cela,onutiliselafonction listen quiprendenparamètrel’identifiantdelasocketetlataille de la file d’attente (en général 5 et au plus 128) au cas ou plusieurs clients se présenteraient aumêmemoment.
Leserveurvaensuitebouclerdansl’attentedeclients,etattendreuneconnexionavecl’appel système accept.Lafonction accept crée une nouvelle socket pour le dialogue avec le client.Eneffet,lasocketinitialedoitresterouverteetenattentepourlaconnectiond’autres clients. Le dialogue avec le client se fera donc avec une nouvelle socket, qui est retournée par accept.
Ensuite,leserveurappelle fork etcréeunprocessusfilsquivatraîterleclient,tandisque leprocessuspèrevarbouclerànouveausur accept dansl’attenteduclientsuivant.
int main(void)
{ int sock_contact; int sock_connectee; struct sockaddr_in adresse; socklen_t longueur; pid_t pid_fils;
sock_contact = cree_socket_tcp_ip();
if (sock_contact < 0)
return -1;
listen(sock_contact, 5); printf("Mon adresse (sock contact) -> "); affiche_adresse_socket(sock_contact);
while (1)
{ longueur = sizeof(struct sockaddr_in); sock_connectee = accept(sock_contact,
(struct sockaddr*)&adresse, &longueur);
if (sock_connectee < 0)
{ fprintf(stderr, "Erreur accept\(\backslash\)n"); return -1;
} pid_fils = fork(); if (pid_fils == -1)
{ fprintf(stderr, "Erreur fork\(\backslash\)n"); return -1;
}
if (pid_fils == 0) {/* fils */
{ close(sock_contact);
traite_connection(sock_connectee);
exit(0);
} else
close(sock_connectee);
} return 0;
}
8.4.4 Traîtement d’une connexion
Une fois la connexion établie, le serveur (ou son fils) peut connaître les données d’adresse IP etdeportduclientparlafonction getpeername,quifonctioncomme getsockname (vueplus haut). Le programme dialogue ensuite avec le client avec les fonctions read et write comme danslecasd’untube.
void traite_connection(int sock)
{ struct sockaddr_in adresse; socklen_t longueur; char bufferR[BUFFER_SIZE]; char bufferW[BUFFER_SIZE]; int nb;
longueur = sizeof(struct sockaddr_in);
if (getpeername(sock, (struct sockaddr*) &adresse, &longueur) < 0)
{ fprintf(stderr, "Erreur getpeername\(\backslash\)n"); return;
} sprintf(bufferW, "IP = %s, Port = %u\(\backslash\)n",
inet_ntoa(adresse.sin_addr), ntohs(adresse.sin_port));
printf("Connexion : locale (sock_connectee) "); affiche_adresse_socket(sock); printf(" Machine distante : %s", bufferW); write(sock, "Votre adresse : ", 16); write(sock, bufferW, strlen(bufferW)+1); strcpy(bufferW, "Veuillez entrer une phrase : "); write(sock, bufferW, strlen(bufferW)+1); nb= read(sock, bufferR, BUFFER_SIZE); bufferR[nb-2] = '\(\backslash\)0';
printf("L'utilsateur distant a tap : %s\(\backslash\)n", bufferR); sprintf(bufferW, "Vous avez tap : %s\(\backslash\)n", bufferR); strcat(bufferW, "Appuyez sur entree pour terminer\(\backslash\)n"); write(sock, bufferW, strlen(bufferW)+1); read(sock, bufferR, BUFFER_SIZE);
}
8.4.5 Le client telnet
Un exemple classique de client est le programme telnet, qui affiche les données reçues sur sa sortie standard et envoie les données saisies dans son entrée standard dans la socket. Cela permetdefaireunsystèmeclient-serveuravecuneinterfaceenmodetextepourleclient.
Ci-dessousunexemple,avecàgauchelecôté,etàdroitelecôtéclient(connectélocalement). $ ./serveur
Mon adresse (sock contact) -> IP = 0.0.0.0, Port = 33140
$ telnet localhost 33140 Trying 127.0.0.1
Connected to portable1.
Escape character is '^]'.
Votre adresse : IP = 127.0.0.1, Port = 33141
Veuillez entrer une phrase :
Connexion : locale (sock_connectee) IP = 127.0.0.1, Port = 33140
Machine distante : IP = 127.0.0.1, Port = 33141
Veuillez entrer une phrase : coucou Vous avez tapé : coucou
Connection closed by foreign host.
L'utilsateur distant a tapé : coucou
Ci-dessous un autre exemple, avec à gauche le côté, et à droite le côté client (connecté à distance).
$ ./serveur
Mon adresse (sock contact) -> IP = 0.0.0.0, Port = 33140
$ telnet 192.168.0.2 33140 Trying 192.168.0.2
Connected to 192.168.0.2. Escape character is '^]'.
Votre adresse : IP = 192.168.0.5, Port = 34353 Veuillez entrer une phrase :
Connexion : locale (sock_connectee) IP = 127.0.0.1, Port = 33140
Machine distante : IP = 192.168.0.5, Port = 33141
Veuillez entrer une phrase : test Vous avez tapé : test
Connection closed by foreign host.
L'utilsateur distant a tapé : test
8.5 Créer une connection client
Jusqu’àmaintenant,nousn’avonspasprogrammédeclient.Nousavonssimplementtuiliséle clienttelnet.L’exempleci-dessousestuneclientqui,alternativement:
1. litunechaînedanslasocketetl’affichesursasortiestandard;
2. saisitunechaînesursonentréestandardetl’envoiedanslasocket.
C’estunmoyensimpledecréeruneinetrfaceclient.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/socket.h> #define BUFFER_SIZE 256
int cree_socket_tcp_client(int argc, char** argv)
{ struct sockaddr_in adresse; int sock;
if (argc != 3)
{ fprintf(stderr, "Usage : %s adresse port\(\backslash\)n", argv[0]); exit(0);
} if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{ fprintf(stderr, "Erreur socket\(\backslash\)n"); return -1; }
memset(&adresse, 0, sizeof(struct sockaddr_in)); adresse.sin_family = AF_INET; adresse.sin_port = htons(atoi(argv[2])); inet_aton(argv[1], &adresse.sin_addr);
if (connect(sock, (struct sockaddr*) &adresse, sizeof(struct sockaddr_in)) < 0)
{ close(sock);
fprintf(stderr, "Erreur connect\(\backslash\)n"); return -1;
}
return sock;
}
int affiche_adresse_socket(int sock)
{ struct sockaddr_in adresse; socklen_t longueur;
longueur = sizeof(struct sockaddr_in);
if (getsockname(sock, (struct sockaddr*)&adresse, &longueur) < 0)
{ fprintf(stderr, "Erreur getsockname\(\backslash\)n"); return -1;
} printf("IP = %s, Port = %u\(\backslash\)n", inet_ntoa(adresse.sin_addr), ntohs(adresse.sin_port));
return 0; }
int main(int argc, char**argv)
{ int sock;
char buffer[BUFFER_SIZE]; sock = cree_socket_tcp_client(argc, argv);
if (sock < 0)
{ puts("Erreur connection socket client"); exit(1);
} affiche_adresse_socket(sock);
while(1)
{ if (read(sock, buffer, BUFFER_SIZE)==0)
break;
puts(buffer);
if (fgets(buffer, BUFFER_SIZE, stdin) == NULL) break;
buffer[strlen(buffer)-1] = '\(\backslash\)0'; write(sock, buffer, BUFFER_SIZE); }
return 0; }
Le serveur correspondant est programmé comme le serveur TCP précédent, sauf que la fonction traite_connection doitêtremodifiéepourtenircomptedufonctionnementduclient (alternancedeslecturesetécrituresdanslasocket).
void traite_connection(int sock)
{ struct sockaddr_in adresse; socklen_t longueur; char bufferR[BUFFER_SIZE]; char bufferW[BUFFER_SIZE]; char tmp[50]; int nb;
longueur = sizeof(struct sockaddr_in);
if (getpeername(sock, (struct sockaddr*) &adresse, &longueur) < 0)
{ fprintf(stderr, "Erreur getpeername\(\backslash\)n"); return;
} sprintf(bufferW, "IP = %s, Port = %u\(\backslash\)n",
inet_ntoa(adresse.sin_addr), ntohs(adresse.sin_port));
printf("Connexion : locale (sock_connectee) "); affiche_adresse_socket(sock, tmp); printf(tmp);
printf(" Machine distante : %s", bufferW); strcat(bufferW, "Votre adresse : "); affiche_adresse_socket(sock, tmp); strcat(bufferW, tmp);
strcat(bufferW, "Veuillez entrer une phrase : ");
write(sock, bufferW, BUFFER_SIZE); nb= read(sock, bufferR, BUFFER_SIZE);
printf("L'utilsateur distant a tap : %s\(\backslash\)n", bufferR); sprintf(bufferW, "Vous avez tap : %s\(\backslash\)n", bufferR); write(sock, bufferW, BUFFER_SIZE);
}
8.6 Exercices
Exercice 8.1 (??) Lebutdel’exerciceestd’écrireunserveurTCP/IPavecclient telnet qui gèreunebasededonnéesdeproduitsetdesclientsquifontdescommandes.Chaqueclientse connecte au serveur, entre le nom du (ou des) produit(s) commandé(s), les quantités, et son nom.Leserveurafficheleprixdelacommande,etcréeunfichierdontlenomestunique(par exemple créé en fonction de la date) qui contient les données de la commande. La base de donnéeseststockéedansunfichiertextedontchaquelignecontientunnomdeproduit(sans espace),etunprixunitaire.
a) Définirunestructureproduitcontenantlesdonnéesd’unproduit.
b) Écrire une fonction de chargement de la base de données en mémoire dans un tableau de structures.
c) Écrireunefonctionquirenvoieunpointeursurlastructure(dansletableau)correspondant àunproduitdontlenomestpasséenparamètre.
d) Écrire le serveur qui va gérer les commandes de un clients. Le serveur saisit le nom d’un produit et les quantités via une socket, recherche le prix du produit, et affiche le prix de la commandedanslaconsoleduclientconnectépartelnet.
e) Même question en supposant que le client peut commander plusieurs produits dans une mêmecommande.
f) Modifier le serveur pour qu’il enregistre les données de la commande dans un fichier. Le serveurcréeunfichierdontlenomestunique(parexemplecrééenfonctiondeladate).
g) Quel reproche peut-on faire à ce programme concernant sa consommation mémoire? Que faudrait-ilfairepourgérerlesinformationssurlesstocksdisponiblesdesproduits?
Exercice 8.2 (??) a) ÉcrireunserveurTCP/IPquivérifiequel’adresseIPduclientsetrouve dans un fichier . Dans le fichier, les adresse IP autorisées sont écrites lignes parlignes.
b) Modifierleprogrammeprécédentpourqueleserveursouhaiteautomatiquementlabienvenue au client en l’appelant par son nom (écrit dans le fichier sur la même ligne que l’adresse
IP.
Exercice 8.3 (?) Ecrireunsystèmeclientserveurquiréaliselachosesuivante:
• Leclientprendenargumentuncheminversunfichiertextelocal;
• Leserveurcopiecefichiertextedansunrépertoire /home/save/ sousunnomquicomprendl’adresseIPduclientetladateauformat aaaa_mm_jj.
Pourlagestiondesdates,onutiliseralesfonctionssuivantesdelabibliothèque time.h
// la fonction suivante renvoie la date time_t time(time_t *t);
// la fonction suivante traduit la date struct tm *localtime(const time_t *t);
// la structure tm contenant les données de date // est la suivante :
struct tm \{
int tm_sec; | /* Secondes */ |
int tm_min; | /* Minutes */ |
int tm_hour; | /* Heures (0 - 23) */ |
int tm_mday; | /* Quantième du mois (1 - 31) */ |
int tm_mon; | /* Mois (0 - 11) */ |
int tm_year; | /* An (année calendaire - 1900) */ |
int tm_wday; | /* Jour de semaine (0 - 6 Dimanche = 0) */ |
int tm_yday; | /* Jour dans l'année (0 - 365) */ |
int tm_isdst; /* 1 si "daylight saving time" */
\};
Exercice 8.4 (??) Même question qu’à l’exercice 8.3, mais cette fois le client donne le chemin vers un répertoire contenant des fichiers texte ou binaires. On pourra créer une archive correspondantaurépertoiredans /tmp :
# coté client :
$ tar zcvf /chemin/vers/le/repertoire/client/
# coté serveur :
$ cd /chemin/vers/le/repertoire/serveur/ ; tar zxvf ; rm
Exercice 8.5 (??) Créezunserveurdemessagerie.Leserveurmetenrelationlesclientsdeux à deux à mesure qu’ils se connectent. Une fois que les clients sont en relation, chaque client peut alternativement saisir une phrase et lire une phrase écrite par l’autre client. Le serveur affichechezlesclient:
L'autre client dit : coucou Saisissez la réponse :
Exercice 8.6 (???) Créez un forum de chat. Le serveur met tous les clients connectés en relation. Chaque client demande à parler en tapant 1. Une seul client peut parler à la fois. Lorsqueleclientenvoieunemessage,leserveuraffichel’adresse IP duclientetlemessagechez touslesautresclients.Lesclientssonttraitéspardesthreadsetl’accèsauxsocketsenécriture estprotégéparunmutex.
Annexe A
Compilation séparée
A.1 Variables globales
Unevariableglobaleestunevariablequiestdéfinieendehorsdetoutefonction.Unevariable globale déclarée au début d’un fichier source peut être utilisée dans toutes les fonctions du fichier. La variable n’existe qu’en un seul exemplaire et la modification de la variable globale dansunefonctionchangelavaleurdecettevariabledanslesautresfonctions.
#include <stdio.h> inte ; /* déclaration en dehors de toute fonction */
\void ModifieDonneeGlobale(\void)/* pas de paramètre */
{
x = x+1;
}
void AfficheDonneGlobale(void) /* pas de paramètre */
{ printf("%d\n", x);
}
int main(void)
{
x = 1;
ModifieDonneeGlobale();
AfficheDonneGlobale(); /* affiche 2 */ return 0; }
Dans le cas d’un projet avec programmation multifichiers, on peut utiliser dans un fichier sourceunevariableglobaledéfiniedansunautrefichiersourceendéclarantcettevariableavec lemotclef extern (quisignifiequelavariableglobaleestdéfinieailleurs).
extern int x; /* déclaration d'une variable externe
A.2 Mettre du code dans plusieurs fichiers
Exemple. Supposonsqu’un TypeArticle regroupelesdonnéesd’unproduitdansunmagasin. Lafonction main,danslefichier main.c,appellelesfonctions SaisitProduit et AfficheProduit, quisontdéfiniesdanslefichier routines.c.Lesdeuxfichiers .c incluentlefichier typeproduit.h
/***************************************************
|******* HEADER FILE typeproduit.h ****************| **************************************************/
/* 1) Protection contre les inclusions multiples */
#ifndef MY_HEADER_FILE /* Evite la définition multiple */ #define MY_HEADER_FILE
/* 2) Définition des constantes et variables globales exportées */
#define STRING_LENGTH 100 extern int erreur;
/* 3) Définition des structures et types */
typedef struct {
int code; /* code article */
char denomination[STRING_LENGTH]; /* nom du produit */ float prix; /* prix unitaire du produit */
int stock; /* stock disponible */
}TypeArticle;
/* 4) Prototypes des fonctions qui sont */
/* définies dans un fichier mais utilisées */
/* dans un autre */
void SaisitProduit(TypeArticle *adr_prod); void AfficheProduit(TypeArticle prod);
#endif /* fin de la protection */
Laprotection #ifndef permetd’éviterquelecodedufichierheadernesoitcompiléplusieurs fois, provoquant des erreurs de définitions multiples, si plusieurs fichiers sources incluent le header.
/***************************************************\ |********** SOURCE FILE routines.c ****************| \***************************************************/
#include <stdio.h>
RémyMalgouyres, http InitiationàlaProgrammationSystème
#include "typeproduit.h" /* attention aux guillemets */ int max_stock=150;
/* La variable globale erreur est définie ici */ int erreur;
/* La fonction suivante est statique */ /* LitChaine n'est pas accessible dans main.c */
static LitChaine(const char *chaine)
{ fgets(chaine, STRING_LENGTH, stdin);
}
void SaisitProduit(TypeArticle *adr_prod)
{ printf("Code produit : "); scanf("%d", &adr_prod->code) printf("Dénomination : "); LitChaine(adr_prod->denomination); printf("Prix : "); scanf("%f", &adr_prod->prix); printf("Stock disponible : "); scanf("%d", &adr_prod->stock); if(adr_prod->stock > max_stock)
{ fprintf(stderr, "Erreur, stock trop grand\n"); erreur=1;
}
}
void AfficheProduit(TypeArticle prod)
{ printf("Code : %d\n", );
printf("Dénomination : %s\n", prod.denomination); printf("Prix : %f\n", );
printf("Stock disponible : %d\n", prod.stock);
}
/***************************************************\ |************* SOURCE FILE main.c *****************| \***************************************************/ #include "typeproduit.h" /* attention aux guillemets */ int main(void)
fichier1.c fichier1.o
exe2
fichier2.o all
exe1
fichier3.o
inclusion de header relation de dépendance
Figure A.1:Exempledecompilationséparéededeuxfichiersecécutables
{
TypeProduit prod;
erreur=0; /* variable définie dans un autre fichier */
SaisitProduit(&prod); /* fonction définie dans un autre fichier */ if (erreur==0)
AfficheProduit(prod);
}
A.3 Compiler un projet multifichiers
A.3.1 Sans makefile
Pourcompilerl’exempleprécédentsans makefile sous Linux,c’estàdirepourcréerunfichier exécutable,ilfautd’abordcréerunfichier objet pourchaquefichiersource.
$ gcc -c routines.c
$ gcc -c main.c
Cecidoitgénérerdeuxfichiersobjets routines.o et éeensuitel’exécutable(par exempleappelé )enréalisantl’éditiondesliens(link)parl’instructionsuivante: $ gcc routines.o main.o -o
C’estlorsdel’éditiondesliensquelesliensentrelesfonctionss’appelantd’unfichieràl’autre, et les variables globales extern sont faits. On peut schématiser ce processus de compilation séparée des fichiers sources par la figure A.1, dans laquelle deux fichiers exécutables exe1 et mttexe2 dépendent respectivement de certains fichiers sources, qui eux même incluent des header.
Lesrèglesdedépendancesindiquentquelorsqu’uncertainfichierestmodifié,certainsautres fichiersdoiventêtrerecompilés.
RémyMalgouyres, http InitiationàlaProgrammationSystème
Exemple. SurlafigureA.1,silefichier header1.h estmodifié,ilfautreconstruire fichier1.o et fichier2.o.Cecientraineque exe1.o et exe2.o doiventaussiêtrereconstruits.Encasde modificationde header1.h,ondoitdoncexécuterlescommandessuivantes:
$ gcc -c fichier1.c
$ gcc -c fichier2.c
$ gcc fichier1.o fihier2.o -o exe1
$ gcc fichier2.o fihier3.o -o exe2
A.3.2 Avec makefile
Un makefile estunmoyenquipermetd’automatiserlacompilationd’unprojetmultifichier. Grace au makefile, la mise à jours des fichiers objets et du fichier exécutable suite à une modificationd’unsourcesefaitenutilisantsimplementlacommande:
$ make
Pourcela,ilfautspécifierausystèmelesdépendancesentrelesdifférentsfichiersduprojet,en créantun fichier makefile.
Exemple 1. Pourl’exempledesfichiers main.c,routines.c et typeproduit.h ci-dessus,on créeunfichiertextedenom makefile contenantlecodesuivant:
: routines.o main.o gcc routines.o main.o -o routines.o : routines.c typeproduit.h gcc -c routines.c main.o: main.c typeproduit.h gcc -c main.c |
Ce fichier comprend trois parties. Chaque partie exprime une règle de dépendance et une rèèglesdereconstruction(lignes 2,4 et 6)commencentobligatoirementparunetabulation.
Lesrèglesdedépendancesontlessuivantes:
• Le fichier exécutable dépend de tous les fichiers objets. Si l’un des fichier objetsestmodifié,ilfaututiliserlarègledereconstructionpourfairel’éditiondesliens.
• Le fichier objet routine.o dépend du fichier routine.c et du fichier typearticle.h. Si l’un de ces deux fichiers est modifié, il faut utiliser la règle de reconstruction pour reconstruire routine.o
• Demême, main.o dépendde main.c et typearticle.h.
Exemple 2. PourlesfichiersdelafigureA.1,unmakefilesimpleressembleraitàceci:
CC=gcc # variable donnant le compilateur CFLAGS= -Wall -pedantic -g # options de compilation all: exe1 exe2 # règle pour tout reconstruire
fichier1.o : fichier1.c header1.h
$(CC) $(CFLAGS) -c fichier1.c
fichier2.o : fichier2.c header1.h header2.h
$(CC) $(CFLAGS) -c fichier2.c fichier3.o : fichier3.c header2.h
$(CC) $(CFLAGS) -c fichier3.c exe1 : fichier1.o fichier2.o
$(CC) $(CFLAGS) fichier1.o fichier2.o -o exe1 exe2 : fichier2.o fichier3.o
$(CC) $(CFLAGS) fichier3.o fichier3.o -o exe2 clean: rm *.o exe1 exe2
Utilisation:
$ make exe1 # reconstruit la cible exe1
$ make all # reconstruit tout
$ make clean # fait le ménage
$ make # reconstruit ma première cible (en l'occurence all)