Les processus UNIX
Introduction
Le système UNIX est un système multitâche (système capable d'exécuter plusieurs tâches à la fois) et classiquement on appelle processus l'instance d'un programme qui est en train de s'executer. Chaque processus UNIX possède donc ses ressources (espace mémoire, table des fichiers ouverts...). On voit bien que toute activité dans le système UNIX est un processus ; si c'est un processus relatif au système (swapper, crons, getty,...) on parle de processus système et si c'est une application utilisatrice i.e l'execuiton d'une commande ou d'une applicatrion on parle alors de processus utilisateur.
Classiquement un processus UNIX se crée avec la routine fork() par dédoublement d'un processus existant appelé communément processus père. Lorsque le processus fils est créé,le système lui attribue une copie des ressources du processus père et là s'ensuient deux problèmes :
1) Le premier est un problème de performance, puisque la duplication est un mécanisme coûteux
2) et le deuxième est un problème de communication entre les processus, qui ont des variables séparées.
Toutefois il existe des moyens d'atténuer ces problèmes : technique du copy-on-write dans le noyau pour ne dupliquer les pages mémoires que lorsque c'est strictement nécessaire), utilisation de segments de mémoire partagée (voir les IPC) pour mettre des données en commun.
Sur une machine uni-processeur, il n'y a qu'un seul processus qui s'exécute effectivement à un instant donné. Le noyau assure une commutation régulière entre tous les processus présents sur le système pour garantir un fonctionnement multitâche. Sur une machine multi-processeur, le principe est le même à la différence que plusieurs processus - mais rarement tous - peuvent s'exécuter réellement en parallèle.
Création d'un processus - fork()
Sous UNIX la création de processus est réalisée par l'appel système :
#include < sys/types.h >
#include <unistd.h>
int fork(void);
Tous les processus sauf le processus d'identification 0, sont créés par un appel à fork().Le processus qui appelle le fork() est appelé processus père. Le nouveau processus est appelé processus fils. Tout processus a un seul processus père. Tout processus peut avoir zéro ou plusieurs processus fils. Chaque processus est identifié par un numéro unique, son PID. Le processus de PID=0 est créé au démarrage de la machine, ce processus a toujours un rôle spécial1 pour le système, de plus pour le bon fonctionement des programmes utilisant fork() il faut que le PID zéro reste toujours utilisé. Le processus zéro crée, grâce à un appel de fork, le processus init de PID=1. Le processus de PID=1 de nom init est l'ancêtre de tous les autres processus (le processus 0 ne réalisant plus de fork()), c'est lui qui accueille tous les processus orphelins de père (ceci a fin de collecter les information à la mort de chaque processus).
Exemple de création de processus
exemple_fork.c #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <errno.h> #include <sys/wait.h> int main (void) { pid_t pid_fils; do { pid_fils = fork ( ) ; } while ((pid_fils == -1) && (errno == EAGAIN)); if (pid_fils == -1) { fprintf (stderr, "fork( ) impossible, errno=%d\n", errno); return (1); } if (pid_fils == 0) { fprintf (stdout, "Fils : PID=%d, PPID=%d\n", getpid( ) , getppid( )); return (0): } else { fprintf (stdout, "Père : PID=%d, PPID=%d\n", getpid( ) , getppid( )); wait (NULL); return(0); } }
Lors de son exécution, ce programme fournit les informations suivantes :
Père : PID=31382, PPID=30550, PID fils=31383 Fils : PID=31383, PPID=31382
Il est clair que ces numéros sont liés au système sur lequel le programme est lancé. La signification de certaine routine présentes dans c programmes seront détaillées ultérieurement notamment wait()
Etat d'un processus
Plusieurs processus peuvent sembler s'exécuter en même temps mais le nombre maximum de processus qui peuvent s'exécuter simultanément est le nombre de processeurs du système. Le système d'exploitation fait fonctionner un à un les processus pendant un court laps de temps et les met en pause, chaque processus utilise tour à tour le processeur plusieurs fois par seconde ce qui donne l'impression de la simultanéité d'exécution.
Pour un processus, on distingue les états suivants.
* En cours d'exécution (running): soit disposant du processeur, soit en attente du processeur.
* En sommeil (sleeping): en attente d'une ressource.
* Stoppé (stopped): arrêté généralement par CTRL-S, par exemple, ou par un debogueur.
* Zombie: le processus est terminé mais le noyau conserve encore des informations à son sujet qui pourront être consultées par un autre processus avant suppression définitive.
Un processus est caractérisé par
* son code (le code binaire du programme qu'il exécute)
* ses données:
* statiques: variables globales, variables statiques des fonctions
* pile: variables automatiques (locales)
* tas: zones mémoire allouées dynamiquement,
* des données conservées par le noyau pour la gestion du processus: identificateur, utilisateur(s) attaché(s) au processus, fichiers en cours d'utilisation, etc
zone u et table des processus
Tous les processus sont associés à une entrée dans la table des processus qui est interne au noyau. De plus, le noyau alloue pour chaque processus une structure appelée zone u , qui contient des données privées du processus, uniquement manipulables par le noyau. La table des processus nous permet d'accéder à la table des régions par processus qui permet d'accéder à la table des régions. Ce double niveau d'indirection permet de faire partager des régions. Dans l'organisation avec une mémoire virtuelle, la table des régions est matérialisée logiquement dans la table de pages. Les structures de régions de la table des régions contiennent des informations sur le type, les droits d'accès et la localisation (adresses en mémoire ou adresses sur disque) de la région. Seule la zone u du processus courant est manipulable par le noyau, les autres sont inaccessibles. L'adresse de la zone u est placée dans le mot d'état du processus.
Le pid
Les processus sont identifiés par un numéro unique attribué par le noyau: le pid (Processus ID). Les appels-système qui concernent les processus utilisent le numéro pid pour désigner le processus qui fait l'objet de l'appel. Un processus peut obtenir son pid par l'appel système getpid().
#include <unistd.h>
pid_t getpid(void)
Le processus fils peut aisément accéder au PID de son père (noté PPID pour Parent PID) grâce à l'appel-système getppid( ), déclaré lui aussi dans <unistd.h> : pid_t getppid (void); Cette routine se comporte comme getpid( ) , mais renvoie le PID du père du processus appelant. Par contre, le processus père ne peut connaître le numéro du nouveau processus créé qu'au moment du retour du fork( ).
uid_t getuid (void); uid_t geteuid(void);
Le type uid_t est défini dans <sys/types.h>. Selon les systèmes, il s'agit d'un unsigned int, unsigned short ou unsigned long. Nous utilisons donc la conversion %u pour fprintf( ) qui doit fonctionner dans la plupart des cas. L'UID effectif est différent de l'UID réel lorsque le fichier exécutable dispose d'un attribut particulier permettant au processus de changer d'identité au démarrage du programme. Considérons par exemple le programme suivant:
exemple_getuid.c : #include <stdio.h> #include <unistd.h> int main (void) { fprintf (stdout, " UID réel = %u, UID effectif = %u\n", getuid( ), geteuid( )); return (0); }
Quand on compile ce programme, qu'on lance l'executable on obtient parfaitement:
UID réel = 500, UID effectif = 500
Le comportement est pour l'instant parfaitement normal. Imaginons maintenant que root passe par là, s'attribue le fichier exécutable et lui ajoute le bit « Set-UID» à l'aide de la commande chmod (chmod +s exemple_getuid ). Lorsqu'un utilisateur va maintenant exécuter exemple_getuid, le système va lui fournir l'UID effectif du propriétaire du fichier, à savoir root (qui a toujours l'UID 0 par définition); maintenant si on execute ce programme dans l'espace utilisateur autre que root, on verra que
UID réel = 500, UID effectif = 0
Set-UID
Un processus avec le bit Set-UID positionné démarre donc avec un UID effectif différent de celui de l'utilisateur qui l'a invoqué. Quand il désire effectuer une opération non privilégiée, il peut demander à remplacer son UID effectif par l'UID réel. Une copie de l'UID effectif est conservée dans l'UID sauvé. Il pourra donc à tout moment demander à remplacer à nouveau son UID effectif par son UID sauvé.
Un programme utilisant uniquement seteuid( ) ou setreuid( ) sera 100 % compatible avec BSD, un programme utilisant uniquement setuid( ) sera 100 % compatible avec Posix. Bien entendu, ils seront tous deux 100 % compatibles avec Linux. Nous allons étudier le comportement d'un programme Set-UID qui abandonne temporairement ses privilèges pour disposer des permissions de l'utilisateur l'ayant invoqué, puis qui reprend à nouveau ses autorisations originales.
exemple_setreuid.c #include <stdio.h> #include <unistd.h> #include <sys/types.h> int main (void) { uid_t uid_reel; uid_t uid_eff; uid_reel = getuid( ); uid_eff = geteuid( ); fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( )); fprintf (stdout, " setreuid (-1, %d) = %d\n", uid_reel, setreuid (-1, uid_reel)); fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( )): fprintf (stdout, " setreuid (-1, %d) = %d\n", uid_eff, setreuid (-1, uid_eff)); fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( )); fprintf (stdout, " setreuid (%d, -1) = %d\n", uid_eff, setreuid (uid_eff, -1)); fprintf (stdout, " UID-R = %u, UID-E = %u\n", getuid( ), geteuid( )); return (0); }
à Venir....
Le GID
Chaque utilisateur du système appartient à un ou plusieurs groupes. Ces derniers sont définis dans le fichier /etc/groups. Un processus fait donc également partie des groupes de l'utilisateur qui l'a lancé. Comme nous l'avons vu avec les UID. un processus dispose donc de plusieurs GID (Group IDentifier) réel, effectif, sauvé, ainsi que de GID supplémentaires si l'utilisateur qui a lancé le processus appartient à plusieurs groupes. ATTENTION Il ne faut pas confondre les groupes d'utilisateurs auxquels un processus appartient, et qui dépendent de la personne qui lance le processus et éventuellement des attributs Set-GID du fichier exécutable, avec les groupes de processus, qui permettent principalement d'envoyer des signaux à des ensembles de processus. Un processus appartient donc à deux types de groupes qui n'ont rien à voir les uns avec les autres. Le GID réel correspond au groupe principal de l'utilisateur ayant lancé le programme (celui qui est mentionné dans /etc/passwd). Le GID effectif peut être différent du GID réel si le fichier exécutable dispose de l'attribut Set-GID (chmod g+s). C'est le GID effectif qui est utilisé par le noyau pour vérifier les autorisations d'accès aux fichiers. La lecture de ces GID se fait symétriquement à celle des UID avec les appels-système getgid( ) et getegid( ). La modification (sous réserve d'avoir les autorisations nécessaires) peut se faire à l'aide des appels setgid( ) , setegid( ) et setregid( ). Les fonctions getgid( ) et setgid( ) sont compatibles avec Posix.1, les autres avec BSD. Les prototypes de ces fonctions sont présents dans <unistd.h>, le type gid_t étant défini dans <sys/types.h> :
gid_t getgid (void); gid_t getegid (void); int setgid (gid_t egid); int setegid (gid_t egid); int setregid (gid_t rgid, gid_t egid);
Les deux premières fonctions renvoient le GID demandé. les deux dernières renvoient 0 si elle réussissent et -1 en cas d'échec. L'ensemble complet des groupes auxquels appartient un utilisateur est indiqué dans /etc/groups (en fait, c'est une table inversée puisqu'on y trouve la liste des utilisateurs appartenant à chaque groupe). Un processus peut obtenir cette liste en utilisant l'appelsystème getgroups( ) : int getgroups (int taille, gid_t liste []); Celui-ci prend deux arguments, une dimension et une table. Le premier argument indique la taille (en nombre d'entrées) de la table fournie en second argument. L'appel-système va remplir le tableau avec la liste des GID supplémentaires du processus. Si le tableau est trop petit, getgroups( ) échoue (renvoie -1 et remplit errno), sauf si la taille est nulle ; auquel cas,il renvoie le nombre de groupes supplémentaires du processus. La manière correcte d'utiliser getgroups ( ) est donc la suivante.
exemple_getgroups.c
#include <stdio.h> #include <stdlib.h> #include <sys/types.h> #include <unistd.h> #include <errno.h> int main (void) { int taille; gid_t * table_gid = NULL; int i; if ((taille = getgroups (0, NULL)) < 0) { fprintf (stderr, "Erreur getgroups, errno =%d\n", errno); return (1); } if ((table_gid = calloc (taille, sizeof (gid_t)))==NULL) { fprintf (stderr, "Erreur calloc, errno = %d\n", errno); return (1); } if (getgroups (taille, table_gid) < 0) { fprintf (stderr, "Erreur getgroups, errno %d\n", errno); return (1); } for (i = 0; i < taille; i ++) fprintf (stdout, "%u " , table_gid [i]); fprintf (stdout, "\n"); free (table_gid); return (0); }
Organisation mémoire
Espace adressage virtuel
à suivre ....
Primitives de bas niveau
Généralités
à suivre ....
ordonnancement des processus
Généralités
à suivre ....
Fonctionnement multitâche, priorités
Généralités
à suivre ....
Modification de la priorité d'un autre processus
à suivre ....
Exemples sur les processus
Exemples :fork et pipe
1 /* biproc.c */ 2 /* 3 * Illustration de fork() et pipe(); 4 * 5 * Exemple à deux processus reliés par un tuyau 6 - l'un envoie abcdef...z 10 fois dans le tuyau 7 - l'autre écrit ce qui lui arrive du tuyau sur la 8 - sortie standard, en le formattant. 9 */ 10 #include <unistd.h> 11 #include <stdlib.h> 12 #include <stdio.h> 13 #include <sys/types.h> 14 #include <sys/wait.h> 15 #define MAXLIGNE 30 16 void genere(int sortie) 17 { 18 char alphabet[26]; 19 int k; 20 for (k = 0; k < 26; k++) 21 alphabet[k] = 'a' + k; 22 for (k = 0; k < 10; k++) 23 if (write(sortie, alphabet, 26) != 26) { 24 perror("write"); 25 exit(EXIT_FAILURE); 26 }; 27 close(sortie); 28 } 29 int lire(int fd, char *buf, size_t count) 30 { 31 /* lecture, en insistant pour remplir le buffer */ 32 int deja_lus = 0, n; 33 while (deja_lus < count) { 34 n = read(fd, buf + deja_lus, count - deja_lus); 35 if (n == -1) 36 return (-1); 37 if (n == 0) 38 break; /* plus rien à lire */ 39 deja_lus += n; 40 }; 41 return (deja_lus); 42 } 43 void affiche(int entree) 44 { 45 char ligne[MAXLIGNE + 1]; 46 int nb, numligne = 1; 47 while ((nb = lire(entree, ligne, MAXLIGNE)) > 0) { 48 ligne[nb] = '\0'; 49 printf("%3d %s\n", numligne++, ligne); 50 }; 51 if (nb < 0) { 52 perror("read"); 53 exit(EXIT_FAILURE); 54 } 55 close(entree); 56 } 57 int main(void) 58 { 59 int fd[2], status; 60 pid_t fils; 61 if (pipe(fd) != 0) { 62 perror("pipe"); 63 exit(EXIT_FAILURE); 64 } 65 if ((fils = fork()) < 0) { 66 perror("fork"); 67 exit(EXIT_FAILURE); 68 } 69 if (fils == 0) { /* le processus fils */ 70 close(fd[0]); 71 close(1); 72 genere(fd[1]); 73 exit(EXIT_SUCCESS); 74 }; 75 /* le processus père continue ici */ 76 close(0); 77 close(fd[1]); 78 affiche(fd[0]); 79 wait(&status); 80 printf("status fils = %d\n", status); 81 exit(EXIT_SUCCESS); 82 }