This is an old revision of the document!


Laboratoire 4 - Processus

Documents d'aide

Agréable à lire

  • TLPI - Chapitre 6, Processus , Chapitre 26 Surveillance des processus enfants
  • WSP4 - Chapitre 6, Gestion des processus

Liens vers des sections utiles

Vue d'ensemble des concepts

Un processus est un programme en cours d'exécution. Les processus sont l'unité primitive à travers laquelle le système d'exploitation alloue des ressources aux utilisateurs. Chaque processus a un espace d'adressage et un ou plusieurs threads. Plusieurs processus peuvent exécuter le même programme, mais deux processus sont complètement indépendants.

Les informations de processus sont conservées dans une structure appelée “Bloc de contrôle de processus” (PCB), une pour chaque processus du système. Parmi les informations les plus importantes contenues par PCB, on trouve:

  • PID - identifiant du processus
  • Espace d'adressage
  • registres généraux, PC (programme de comptage), SP (indicateur de pile)
  • ouvrir la table
    • liste des bloqués, ignorés ou en attente d'envoi au processus
    • Gestionnaires de signaux
  • informations sur les systèmes de fichiers (répertoire racine, répertoire actuel)

Lors du lancement d'un programme, un processus sera créé dans le système d'exploitation pour allouer les ressources nécessaires à l'exécution du programme. Chaque système d'exploitation fournit des appels système permettant de travailler avec des processus: création, fin, attente de fin. En même temps, il y a des appels à la duplication de descripteurs de ressources entre processus, ou à la fermeture de ces descripteurs.

Les processus peuvent avoir une organisation:

// hierarchical // - par exemple sous Linux - il existe une arborescence dans laquelle la racine est le processus '' init '' (pid = 1).
* // non hiérarchique // - par exemple sous Windows.

Généralement, un processus s'exécute dans un environnement spécifié par un ensemble de variables d'environnement . Une variable d'environnement est une paire 'NOM = valeur'. Un processus peut vérifier ou définir la valeur d'une variable d'environnement via une série d'appels de bibliothèque ( Linux, Windows).

Les canaux (canaux de communication) sont des mécanismes primitifs de communication entre processus. Un canal peut contenir une quantité limitée de données. L'accès à ces données est de type FIFO (les données sont écrites à une extrémité du canal pour être lues à l'autre extrémité). Le système d'exploitation garantit la synchronisation entre les opérations de lecture et d'écriture aux deux extrémités ( Linux, Windows).

Il existe deux types de tuyaux:

  • pipes anonymous : ne peuvent être utilisés que par des processus associés (un processus parent et un ou deux enfants) car ils ne sont accessibles que par héritage. Ces canaux n'existent plus une fois que les processus ont terminé leur exécution.
  • Name pipes: ont un support physique - ils existent sous forme de fichiers de droits d'accès. Par conséquent, ils existeront indépendamment du processus qui les crée et peuvent être utilisés par des processus indépendants.

Processus sous Linux

Le lancement d'un programme implique les étapes suivantes:

  • Un nouveau processus “fork” est créé - le processus enfant dispose d'une copie des ressources du processus parent.
  • Si vous souhaitez remplacer l'image du processus enfant, vous pouvez la modifier en appelant une fonction de la famille exec* .

Créer un processus Linux

Sous UNIX, un processus est créé à l’aide de l’appel système fork:

pid_t fork(void);

L'effet crée un nouveau processus (le processus enfant), une copie du “fork” (le processus parent). Le processus enfant reçoit un nouvel identificateur de processus ( PID ) du système d'exploitation.

Cette fonction est appelée une fois et retourne (en cas de succès) deux fois:

  • Le parent retournera le pid du processus nouvellement créé (enfant).
  • Dans le processus enfant, l'appel retournera 0.

Pour connaître le «PID» du processus actuel et du processus parent, appelez les fonctions ci-dessous.

La fonction getpid renvoie le PID du processus appelant:

pid_t getpid(void);

La fonction getppid renvoie le “PID” du processus parent du processus appelant:

pid_t getppid(void);

Remplacement d'une image de processus sous Linux

La famille de fonctions exec exécutera un nouveau programme, remplaçant l'image du processus en cours par celle d'un fichier (exécutable). Cela signifie:

  • L'espace adresse de processus sera remplacé par un nouveau créé spécifiquement pour l'exécution de fichier.
  • Le 'PC' (le compteur de programme), le 'SP' (l'indicateur de pile) et les registres généraux seront réinitialisés.
  • Les blocs de signaux ignorés et verrouillés sont réglés sur les valeurs par défaut, de même que les gestionnaires de signaux.
  • PID et les descripteurs de fichier qui n'ont pas activé l'indicateur CLOSE_ON_EXEC restent inchangés (par défaut, CLOSE_ON_EXEC n'est pas défini).
int execl(const char *path, const char *arg, ...); 
int execv(const char *path, char *const argv[]);
int execlp(const char *file, const char *arg, ...);

Exemple d'utilisation des fonctions ci-dessus:

execl("/bin/ls", "ls", "-la", NULL);
 
char *const argvec[] = {"ls", "-la", NULL};
execv("/bin/ls", argvec);
 
execlp("ls", "ls", "-la", NULL);

Le premier argument est le nom du programme. Le dernier argument de la liste de paramètres doit être NULL, que la liste soit sous la forme d'un vecteur (execv *) ou en tant qu'argument variable (execl *).

execl et execv ne recherchent pas le programme donné en tant que paramètre dans PATH , il doit donc être accompagné du chemin complet. Les versions execlp et execvp recherchent le programme dans PATH .

Toutes les fonctions exec * sont implémentées par l'appel système execve.

En attente de terminer un processus sous Linux

La famille de fonctions wait suspend l'exécution du processus appelant jusqu'à ce que le ou les processus spécifiés dans les arguments se soient terminés ou aient été arrêtés (“SIGSTOP”).

pid_t waitpid(pid_t pid, int *status, int options);

L’état du processus peut être trouvé en examinant status avec des macrodefines telles que WEXITSTATUS, qui renvoie le code d'erreur avec lequel le processus attendu a été achevé, en évaluant le plus insignifiant 8 bits.

Il existe une version simplifiée qui attend la fin d’un processus enfant. Les séquences de code suivantes sont équivalentes:

wait(&status);                   |  waitpid(-1, &status, 0);

Si vous souhaitez uniquement attendre la fin du processus enfant sans examiner l'état, vous pouvez utiliser:

wait(NULL);

Terminer un processus Linux

Pour terminer le processus en cours, Linux fournit l'appel système exit.

void exit(int status);

< spoiler Apeluri exit > A partir d'un programme C , il existe trois manières d'appeler cet appel système:

1. Appelez _exit POSIX.1-2001 ):

void _exit(int status);

2. Appelez _Exit depuis la bibliothèque standard C (selon C99):

void _Exit(int status);

3. L'appel exit de la bibliothèque standard C (selon C89, C99), celle décrite ci-dessus ci-dessus.

_exit (2) et_Exit(2) sont fonctionnellement équivalents (uniquement s'ils sont définis par des normes différentes):

  • le processus d'appel se termine immédiatement
  • tous les descripteurs de processus de fichiers sont fermés
  • les enfants du procès sont “nés” par “init”
  • Un signal 'SIGCHLD' sera envoyé au processus parent. Il sera également rétabli à «statut» à la suite d'une fonction «wait» ou «waitpid».

De plus, exit (3):

  • supprimera tous les fichiers créés avec tmpfile ()
  • écrira les tampons ouverts et les fermera

</spoiler>

Selon ISO C, un programme se terminant par return x dans main() aura le même comportement que exit (x) .

Un processus dont le parent est terminé s'appelle processus orphelin . Ce processus est automatiquement adopté par le processus init , mais il porte toujours le nom orphelin car le processus qui l'a créé à l'origine n'existe plus.

Un processus terminé dont le parent n'a pas encore lu le statut de la terminaison est appelé processus zombie . Le processus entre dans un état final et les informations continuent d'exister dans la table des processus afin de donner au parent la possibilité de vérifier le code avec lequel le processus s'est terminé. Lorsque le parent appelle l'attente, les informations sur le processus disparaissent. Tout processus enfant passera par le statut de zombie à la fin. Pour terminer un autre processus dans le système, un signal sera envoyé à ce processus via l'appel système kill. Plus de détails sur 'tuer' et les signaux dans signal lab. ==== Exemple (mon_système) ==== <code c my_system.c> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> int my_system(const char *command) { pid_t pid; int status; const char *argv[] = {command, NULL}; pid = fork(); switch (pid) { case -1: return EXIT_FAILURE; case 0: execvp(command, (char *const *) argv); exit(EXIT_FAILURE); default: break; } waitpid(pid, &status, 0); if (WIFEXITED(status)) printf(“Child %d terminated normally, with code %d\n”, pid, WEXITSTATUS(status)); return status; } int main(void) { my_system(“ls”); return 0; } </code> ==== Copier les descripteurs de fichier ==== dup duplique le descripteur de fichier oldfd et renvoie le nouveau descripteur de fichier, ou -1 en cas d'erreur: <code c> int dup(int oldfd); </code> dup2 duplique le descripteur de fichier oldfd dans le descripteur de fichier 'newfd'; si newfd existe, il sera fermé en premier. Retourne le nouveau descripteur de fichier, ou -1 en cas d'erreur: <code c> int dup2(int oldfd, int newfd); </code> Les descripteurs de fichiers sont en réalité indexés dans la table de fichiers ouverte. La table est remplie avec les structures d'informations de fichier. La duplication d'un descripteur de fichier signifie la duplication de l'entrée dans la table de fichiers ouverte (c'est-à-dire que 2 pointeurs situés à des positions différentes de la table pointeront sur la même structure de fichiers associée au fichier). Pour cette raison, toutes les informations associées à un fichier (verrou, curseur, drapeau) sont partagées par les deux descripteurs de fichier. Cela signifie que les opérations qui modifient ces informations sur l'un des descripteurs de fichier (par exemple, “lseek”) sont également visibles pour l'autre fichier de descripteur (dupliqué). <note important> L'indicateur CLOSE_ON_EXEC n'est pas partagé (cet indicateur n'est pas conservé dans la structure ci-dessus). </note> === Héritage des descripteurs de fichier après les opérations fork / exec === Les descripteurs de fichier du processus parent héritent du processus enfant après l'appel 'fork'. Après un appel à “exec”, les descripteurs de fichier sont conservés, à l'exception de ceux pour lesquels l'indicateur “CLOSE_ON_EXEC ” est activé. <spoiler Détail du drapeau CLOSE_ON_EXEC> fcntl Pour définir l'indicateur CLOSE_ON_EXEC , la fonction fcntl est utilisée, avec un appel de formulaire: <code c> fcntl(file_descriptor, F_SETFD, FD_CLOEXEC); </code> O_CLOEXEC fcntl peut uniquement activer l'indicateur FD_CLOEXEC pour les descripteurs de fichiers existants. Dans les applications multithreads, entre la création d'un descripteur de fichier et un appel “fcntl”, un appel “exec” peut être placé sur un autre thread. <code C> / * THREAD 1 */ |/ * THREAD 2 */ fd = op_creare_fd() | | exec(…) fcntl(fd, F_SETFD, FD_CLOEXEC); | </code> Comment, par défaut, les descripteurs de fichier sont hérités après un appel “exec”, bien que le programmeur ait voulu être incapable d'y accéder après “exec”, ne peut empêcher un appel “exec” entre créer et fcntl. Pour résoudre cette situation critique, de nouvelles versions d'appels système ont été introduites dans Linux 2.6.27 (2008): <code C> int dup3(int oldfd, int newfd, int flags); int pipe2(int pipefd[2], int flags); int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags); </code> Ces variations d'appels système ajoutent le champ flags , qui peut spécifier O_CLOEXEC pour créer et activer CLOSE_ON_EXEC en mode atomique . Le numéro du nom de l'appel système spécifie le nombre de paramètres d'appel. Les appels système qui créent des descripteurs de fichier ayant déjà reçu un paramètre 'flags' (par exemple, open ) ont uniquement été étendus pour accepter O_CLOEXEC. </spoiler> Variables d'environnement ===== sous Linux ===== Dans un programme, il est possible d'accéder aux variables d'environnement en mettant en surbrillance le troisième paramètre (facultatif) de la fonction main, comme dans l'exemple suivant: <code c> int main(int argc, char argv, char environ) </code> Il désigne un vecteur de pointeurs sur des chaînes contenant les variables d'environnement et leurs valeurs. Les caractères sont sous la forme VARIABLE = VALUE. Le vecteur est fini avec NULL . getenv renvoie la valeur de la variable d'environnement nommée nom ou NULL s'il n'y a pas de variable d'environnement nommée: <code c> char* getenv(const char *name); </code> setenv ajoute la variable nommée nom (si elle n'existe pas déjà) et définit sa valeur sur valeur . Si la variable existe et remplace e 0 , l'action de définition de la valeur de la variable est ignorée; si remplacer est différent de 0 , la valeur de la variable devient valeur : <code c> int setenv(const char *name, const char *value, int replace); </code> unsetenv supprime la variable nommée name de l'environnement: <code c> int unsetenv(const char *name); </code> ===== Dépannage d'un processus ===== Des informations supplémentaires sur le dépannage d'un processus peuvent être trouvées ici ===== Exercices ===== Pour résoudre le labo, veuillez cloner repository. si vous en avez déjà un, lancez svp git pull . <note tip> Pour vous aider à mettre en œuvre les exercices de laboratoire, il existe un fichier utils.h avec des fonctionnalités utiles dans le répertoire utils de l'archive. </ note> ==== Exercice 1 - Système (1p) ==== Allez dans le répertoire 1-system . Le programme mon_système.c exécute une commande envoyée en tant que paramètre à l'aide de la fonction de bibliothèque system. Le fonctionnement de system est le suivant: * créer un nouveau processus avec fork * Le processus enfant s'exécute en utilisant execve, le programme 'sh' avec les arguments -c 'commande', pendant que le processus parent attend la fin de l'enfant.

Compilez (en utilisant make) et lancez le programme en donnant une commande en paramètre.

  • Exemple:
     ./my_system pwd 

Que faites-vous pour envoyer plus de paramètres à une commande? (par exemple: ls -la )

Pour voir combien d'appels système execve est terminé, exécutez:

 strace -e execve,clone -ff -o output ./my_system ls 
  • Attention! Il n'y a pas de virgule dans l'argument execve, clone
  • L'argument -ff accompagné de -o output génère un fichier de sortie pour chaque processus.
    • Voir la page de manuel strace

Consultez la section Remplacement d’une image de processus sous Linux et du execve.

Remplacez le système par execlp . La fonction reçoit une commande, une liste d'arguments (séparés par des virgules), puis NULL (le dernier paramètre correspond à la fin de la liste).

Suivez la ligne avec TODO 1.

Exercice 2 - Paramètres (2p)

Allez dans le répertoire 2-parameters .

Compilez le programme parameters.c en utilisant la commande 'make'. Que fait parameters.c?

Résolvez l'exercice dans le fichier program.c.

2a. système (1p)

Utilisez la fonction système pour exécuter paramètres avec certains paramètres.

<note> Pour exécuter un programme à partir du répertoire en cours, préfixez l'exécutable avec ./ (ex: ./parameters) </ note>

2b. execl (1p)

Remplacez la fonction system par execl . Pourquoi le texte affiché par printf après execl ?

Suivez les lignes avec TODO 2.

Exercice 3 - Exécuter (4p)

Allez dans le répertoire 3-run .

3a - fork, exec (1p)

Utilisez les fonctions fork et execl pour exécuter la commande ls dans run.c.

Suivez les lignes avec TODO 1.

3b - waitpid (1p)

Assurez-vous que “ls a été exécuté” apparaît après la fermeture du programme ls . (Indice: waitpid) Suivez les lignes avec TODO 2. === 3c - statut de sortie (1p) === Déplacez et modifiez la ligne avec TODO 3 afin que le code de sortie de ls soit affiché. (Indice: WEXITSTATUS) === 3d - waitpid (1p) === Compilez le programme exitcode.c en utilisant la commande 'make'. Exécutez le programme et affichez le code de sortie. Modifiez le programme exitcode.c afin qu'il renvoie un autre code de sortie. Suivez les lignes avec TODO 4. ==== Exercice 4 - orphelin (0.5p) ==== Allez dans le répertoire 4-orphan et inspectez la source orphan.c . Compilez le programme ( make ) puis exécutez-le en utilisant la commande suivante: <code bash> ./orphan </code> Ouvrez un autre terminal et lancez la commande: <code bash> watch -d '(ps -al | grep -e orphan -e PID)' </code> Notez que pour le processus indiqué par l'exécutable 'orphelin' (colonne CMD), le pid du processus parent (colonne 'PPID') devient 1 car le processus est adopté par init à la fin du processus. son parent. Pourquoi deux processus orphelins? ==== Exercice 5 - zombie (0.5p) ==== Allez dans le répertoire 5-zombie et inspectez la source zombie.c . Compilez le programme ( make ) puis exécutez-le en utilisant la commande suivante: <code bash> ./zombie </code> Ouvrez un autre terminal et lancez la commande: <code bash> watch -d '(ps -al | grep -e orphan -e PID)' </code> Notez que pour le processus indiqué par l'exécutable zombie, la colonne 'CMD' devient 'zombie' '<obsolète>. Qu'est-ce qui se passe vraiment? Modifiez le fichier zombie.c afin que le processus ne devienne pas un zombie (indice: waitpid). Suivez les lignes avec TODO 1. ==== Exercice 6 - Tiny-Shell (3p) ==== Allez dans le répertoire 3-minuscule . Les sous-programmes suivants sont destinés à implémenter un shell minimal prenant en charge l'exécution d'une commande single external avec plusieurs arguments et redirections. Shell doit fournir un soutien pour l'utilisation et la définition de variables environnementales. Note: Pour quitter le petit shell, utilisez exit ou CTRL + D . === 6a. Exécution d'une commande simple (1p) === Créez un nouveau processus pour exécuter une commande simple. La fonction simple_cmd reçoit en argument un vecteur chaîne contenant la commande et ses paramètres. Voir l'exemple mon_système et suivez les commentaires avec TODO 1 dans le code. Pour tester, vous pouvez utiliser les commandes: <code bash> ./tiny > pwd > ls -al > exit </code> === 6b. Ajout du support pour la définition et le développement de variables d’environnement (1p) === Vous devez remplir les fonctions 'set_var' et 'expand'; ils sont déjà appelés lorsque la ligne de commande est analysée. Les erreurs doivent être vérifiées dans ces fonctions. * Suivre les commentaires avec TODO 2 dans le code. * Lisez la variable d'environnement Variables d'environnement Linux. * Pour tester, vous pouvez utiliser les commandes suivantes: <code bash> ./tiny > echo $HOME > name=Makefile > echo $name </code> === 6c. Rediriger la sortie standard (1p) === Remplissez le champ do_redirect pour que tiny-shell prenne en charge la redirection de la sortie d'une commande (stdout) dans un fichier. Si le fichier spécifié par nomfichier n'existe pas, il sera créé. S'il existe, il doit être tronqué. Lisez la section Copie de descripteurs de fichiers et suivez les commentaires avec le mot “TODO 3” dans le code. Pour tester, vous pouvez utiliser les commandes: <code bash> ./tiny > ls -al > out > cat out > pwd > out > cat out </code>

===== Ressources utiles ===== - Fork - Wikipedia - About Fork and Exec - Fork, Exec and Process Control - YoLinux Tutorial

sde/laboratoare/04.1552411746.txt.gz · Last modified: 2019/03/12 19:29 by iuliana.marin
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0