Table of Contents

TP 08 - IPC - Communication inter-processus

Matériaux auxiliaires

Liens vers des sections utiles

Pipes sous Linux

Pipe anonyme sous Linux

Le canal est un mécanisme de communication à sens unique entre deux processus. Dans la plupart des implémentations UNIX, un canal apparaît comme une zone mémoire d'une certaine taille dans l'espace du noyau. Les processus qui communiquent via un canal anonyme doivent avoir un certain degré de parenté; généralement, un processus qui crée un tuyau sera alors appelé une fourchette, et le tuyau sera utilisé pour la communication parent-enfant. Dans tous les cas, les processus qui communiquent via des canaux anonymes ne peuvent pas être créés par différents utilisateurs du système.

L'appel système à la création est pipe. En python, nous pouvons utiliser deux fonctions pour créer un canal: os.pipe() et os.pipe2(flags)

 Exemple d'utilisation - le processus parent transmet des données au processus enfant via des canaux

import os
(r, w) = os.pipe()

Le tuple (r, w) contient après l'exécution de la fonction 2 descripteurs de fichiers:

La sortie de w est considérée comme une entrée pour r ..

Une autre option pour écrire est

import os
p = os.pipe()
# use p[0] and p[1]

La fonction os.pipe2 (flags) reçoit en paramètre les options spécifiques au canal. Mnémotechnique : STDIN_FILENO est 0 (lecture),STDOUT_FILENO est 1 (écrit).

Remarques:

La plupart des applications qui utilisent des tuyaux ferment dans chacun des processus l'extrémité du tuyau inutilisé en communication unidirectionnelle. Si l'un des descripteurs est fermé, les règles s'appliquent:

L'erreur la plus courante , lorsque vous travaillez avec des tuyaux, vient de la négligence du fait que EOF n'est pas envoyé à travers les tuyaux (la lecture des tuyaux ne se termine pas) à moins que toutes les extrémités ne soient fermées. TOUS les processus qui ont ouvert le descripteur de tuyau (dans le cas d'une fourche, assurez-vous de fermer les extrémités du tuyau dans le processus parent).

Autres fonctions utiles: os.popen, os.pclose.

Pipes nommés sous Linux

Il élimine le besoin de relier des processus connexes. Ainsi, chaque processus peut s'ouvrir pour lire ou écrire le fichier de canal nommé (FIFO), un type de fichier spécial, qui conserve les caractéristiques d'un tube. La communication se fait dans un sens ou dans les deux. Les fichiers FIFO peuvent être identifiés par la lettre p dans le premier champ des droits d'accès ( ls -l ). L'appel de bibliothèque pour la création de canaux FIFO est os.mkfifo:

import os
os.mkfifo(path, mode);

Une fois le tube FIFO créé, il peut être appliqué à toutes les fonctions pour les opérations courantes de travail avec les fichiers: ouvrir , fermer , lire , écrire . Le comportement d'un tube FIFO après ouverture est affecté par l'indicateur O_NONBLOCK :

Détails sur le drapeau O_NONBLOCK

Détails sur le drapeau O_NONBLOCK

  • si O_NONBLOCK n'est pas spécifié (cas normal), alors une lecture ouverte sera bloquée jusqu'à ce qu'un autre processus ouvre le même FIFO pour l'écriture. De même, si l'ouverture est destinée à l'écriture, un blocage peut se produire jusqu'à ce qu'un autre processus s'ouvre pour la lecture.
  • si O_NONBLOCK est spécifié, alors l'ouverture de lecture revient immédiatement, mais une ouverture d'écriture peut retourner une erreur avec errno ayant la valeur ENXIO s'il n'y a aucun autre processus ouvrant le même FIFO pour lecture.

Lorsque vous fermez le dernier descripteur de fichier de la fin d'écriture pour une FIFO, une fin de fichier - EOF - est générée pour le processus de lecture FIFO.

Signaux sous Linux

Dans le monde réel, un processus peut connaître une multitude de situations imprévues, qui affectent son taux d'exécution normal. Si le processus ne peut pas les gérer, ils sont transmis au système d'exploitation. Comme le système d'exploitation ne peut pas savoir si le processus peut continuer à fonctionner normalement sans effets secondaires indésirables, il est obligé de mettre fin au processus de manière forcée. Une solution à ce problème est les signaux.

Un signal est une interruption logicielle dans le flux d'exécution de processus normal.

Les signaux sont un concept spécifique aux systèmes d'exploitation UNIX. Le système d'exploitation les utilise pour signaler au processus l'émergence de situations exceptionnelles donnant au processus la possibilité de réagir. Chaque signal est associé à une classe d'événements qui peuvent se produire et répondre à certains critères. Les processus peuvent traiter, bloquer, ignorer ou permettre au système d'exploitation d'effectuer l'action par défaut lors de la réception d'un signal:

L'ensemble des types de signaux est fini; le système d'exploitation conserve, pour chaque processus, un tableau d'actions choisi par lui, pour chaque type de signal. À tout moment, ces actions sont bien déterminées. Au début du processus, la table d'actions est initialisée avec les valeurs par défaut. Le mode de traitement du signal n'est pas décidé lors de la réception du signal par le processus, mais il est automatiquement choisi dans le tableau. Les signaux sont synchrones / asynchrones avec le flux d'exécution de processus qui reçoit le signal si l'événement qui provoque l'envoi du signal est synchrone / asynchrone avec le flux d'exécution de processus.

Un signal reçu par un processus peut être généré:

Si deux signaux sont trop proches dans le temps, ils peuvent être confondus avec un. Ainsi, normalement, il n'y a pas de mécanisme pour garantir à celui qui envoie le signal qu'il a atteint sa destination.

Dans certains cas, il est nécessaire de savoir avec certitude qu'un signal envoyé a atteint sa destination et, implicitement, que le processus y répondra (en effectuant l'une des actions possibles). Le système d'exploitation offre une autre façon d'envoyer un signal, qui garantit si le signal a atteint sa destination ou si cette action a échoué. Ceci est réalisé en créant une pile de signaux, d'une certaine capacité (elle doit être finie, afin de ne pas produire de situations de débordement). Lors de l'envoi d'un signal, le système d'exploitation vérifie si la pile est pleine. Dans ce cas, la demande échoue, sinon le signal est placé dans la pile et l'opération se termine avec succès. La manière classique d'envoyer des signaux lui est analogue (la pile a la dimension 1), sauf qu'aucune information n'est fournie sur la manière d'atteindre une destination. La notion de signal est utilisée pour indiquer alternativement soit un type particulier de signal, soit des objets de ce type.

Génération de signaux

En général, les événements qui génèrent des signaux se répartissent en trois grandes catégories:

Les signaux peuvent être générés de manière synchrone ou asynchrone:

Un type de signal donné est synchrone ou asynchrone. Par exemple, les signaux d'erreur sont généralement synchrones car les erreurs génèrent des signaux de manière synchrone. Cependant, tout type de signal peut être généré de manière synchrone ou asynchrone avec une demande explicite.

Emission et réception de signaux

Lorsqu'un signal est généré, il passe dans un état en attente. Normalement, il reste dans cet état pendant très peu de temps et est ensuite envoyé au processus de destination. Cependant, si ce type de signal est actuellement verrouillé, il pourrait rester indéfiniment en état de veille jusqu'à ce que les signaux de ce type soient déverrouillés. Une fois ce type de signal déverrouillé, il sera envoyé immédiatement. Lorsque le signal a été reçu, immédiatement ou tard, l'action spécifiée pour ce signal est exécutée. Pour certains signaux, tels que SIGKILL et SIGSTOP , l'action est fixe (le processus est terminé), mais, pour la plupart des signaux, le programme peut choisir de:

Le programme précise son choix à l'aide de la fonction signal.signal().Pendant que le gestionnaire est en cours d'exécution, ce type de signal est normalement verrouillé (le déverrouillage sera effectué par une demande explicite dans le gestionnaire qui gère le signal).

Exemple d'utilisation du signal

Exemple d'utilisation du signal

Dans le code ci-dessous, nous visons à capturer les signaux SIGINT et SIGUSR1 et à prendre une action si nous les recevons. SIGINT est reçu à la fois en utilisant la commande kill -SIGINT <program> et en envoyant la combinaison de touches CTRL + c au programme.

import os
import signal
 
 
pid_t child1, child2;
int child1_pid;
 
 
def signal_handler(signum, frame):
    if signum == signal.SIGINT:
        print(''CTRL+C received in {} Exiting'').format(os.getpid())
        exit(0);
    elif signum == signal.SIGUSR1:
        print(''SIGUSR1 received. Continuing execution'')
 
 
print(''Process {} started'').format(os.getpid())
 
/* Les signaux tels que SIGKILL ou SIGSTOP ne peuvent pas être capturés */
try:
    signal.signal(SIGKILL, signal_handler) == SIG_ERR:
    printf(''\nYou shall not catch SIGKILL\n'');
 
    if(signal(SIGINT, signal_handler) == SIG_ERR) {
        printf(''Unable to catch SIGINT'');
        exit(EXIT_FAILURE);
    }
 
    if(signal(SIGUSR1, signal_handler) == SIG_ERR) {
        printf(''Unable to catch SIGUSR1'');
        exit(EXIT_FAILURE);
    }
 
 
    printf(''Press CTRL+C to stop us\n'');
 
    while(1) {
        sleep(1);
    }
 
    return 0;
}

Notez que le signal SIGKILL ne peut pas être traité (kill -9 <program> ou kill -SIGKILL <program>).

Si l'action spécifiée pour un type de signal consiste à l'ignorer, alors tout signal de ce type, qui est généré pour le processus en question, est ignoré. La même chose se produit si le signal est bloqué à ce moment-là. Un signal négligé dans ce mode ne sera jamais reçu, sauf si le programme spécifie par la suite une action différente pour ce type de signal, puis le déverrouille. Si un signal est reçu pour lequel aucun type d'action n'a été spécifié, l'action par défaut est exécutée. Chaque type de signal a sa propre action par défaut. Pour la plupart des signaux, l'action par défaut est achèvement du processus. Pour certains types de signaux, qui représentent des événements sans conséquences majeures, l'action implicite est de ne rien faire.

Lorsqu'un signal force l'achèvement d'un processus, le parent du processus peut déterminer la cause de la terminaison en examinant le code de terminaison signalé par les fonctions d'attente et d'attente. Les informations que vous pouvez obtenir incluent le fait que la fin du processus a été provoquée par un signal, ainsi que le type de signal. Si un programme que vous exécutez à partir de la ligne de commande se termine par un signal, le shell affiche généralement des messages d'erreur. Les signaux qui représentent normalement des erreurs de programme ont une propriété spéciale: lorsqu'un de ces signaux termine le processus, il écrit également un fichier core dump qui enregistre l'état du processus au moment de l'achèvement. Vous pouvez examiner le fichier avec un débogueur pour découvrir la cause de l'erreur. Si vous générez un signal, qui représente une erreur de programme, par une demande explicite, et qu'il termine le processus, le fichier est généré comme si le signal était généré par une erreur.

Si un signal est envoyé au processus, alors qu'il exécute un appel système bloqueur , le processus suspendra l'appel, exécutera le gestionnaire de traitement du signal défini à l'aide de signal , puis soit l'opération il échouera (avec errno réglé sur EINTR ), ou l'opération redémarrera. Les systèmes System V se comportent comme dans le premier cas, les BSD comme dans le second. À partir de la glibc v2, le comportement est le même que sur BSD, tout dépend de la définition de la macro _BSD_SOURCE. Le comportement peut être contrôlé par le programmeur en utilisant sigaction avec le drapeau SA_RESTART .

Types de signaux standard

Cette section présente les noms des différents types de signaux standard et décrit le type d'événements qu'ils indiquent.

Chaque nom de signal est une définition de macro qui représente, en fait, un entier positif (le numéro de ce type de signal).

Un programme ne doit jamais faire d'hypothèses sur le code numérique d'un type particulier de signal, mais plutôt s'y référer toujours par son nom. En effet, un nombre pour un type de signal peut varier d'un système à l'autre, mais leurs noms sont standard. Pour la liste complète des signaux pris en charge par un système, vous pouvez exécuter sur la ligne de commande:

$ kill -l
 
     1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL
     5) SIGTRAP      6) SIGABRT      7) SIGBUS       8) SIGFPE
     9) SIGKILL     10) SIGUSR1     11) SIGSEGV     12) SIGUSR2
    13) SIGPIPE     14) SIGALRM     15) SIGTERM     17) SIGCHLD
    18) SIGCONT     19) SIGSTOP     20) SIGTSTP     21) SIGTTIN
    22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
    26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO
    30) SIGPWR      31) SIGSYS      33) SIGRTMIN    34) SIGRTMIN+1
    35) SIGRTMIN+2  36) SIGRTMIN+3  37) SIGRTMIN+4  38) SIGRTMIN+5
    39) SIGRTMIN+6  40) SIGRTMIN+7  41) SIGRTMIN+8  42) SIGRTMIN+9
    43) SIGRTMIN+10 44) SIGRTMIN+11 45) SIGRTMIN+12 46) SIGRTMIN+13
    47) SIGRTMIN+14 48) SIGRTMIN+15 49) SIGRTMAX-15 50) SIGRTMAX-14
    51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10
    55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7  58) SIGRTMAX-6
    59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
    63) SIGRTMAX-1  64) SIGRTMAX

Les noms de signaux sont définis dans l'en-tête signal.h sous Unix. En général, les signaux ont des rôles prédéfinis, mais ils peuvent être remplacés par le programmeur. Les plus connus sont les signaux suivants:

Messages pour décrire les signaux

Cel mai bun mod de a afișa un mesaj de descriere a unui semnal este utilizarea funcțiilor strsignal. Aceasta funcție foloseste un număr de semnal pentru a specifica tipul de semnal care trebuie descris. Mai jos este prezentat un exemplu de folosire a ei:

msg_signal.py
import os
import signal
 
sig = signal.strsignal(signal.SIGKILL)
print (''signal {} is {}'').format(signal.SIGKILL, sig)

Pour la compilation et l'exécution, la séquence est la suivante:

so@spook$ python3 msg_signal.py 
signal 9 is Killed

Vous avez besoin de Python 3.8 ou supérieur pour utiliser cette fonctionnalité

Masques de signaux. Blocage du signal

Afin d'effectuer des opérations de blocage / déverrouillage de signal, nous devons connaître, à chaque étape du flux d'exécution, l'état de chaque signal. Le système d'exploitation a également besoin de la même chose pour pouvoir prendre une décision sur un signal à envoyer à un processus (il a besoin de ce type d'informations pour chaque processus séparément). À cet effet, un masque de signal est utilisé pour chaque processus.

Un masque de signal a chaque bit associé à un type de signal.

Le masque de bits est utilisé par plusieurs fonctions, dont la fonction signal.pthread_sigmask, utilisé pour changer le masque de signal du processus en cours.

signal.pthread_sigmask(how, maks)

Le masque de signal est un ensemble de numéros de signal. Pour afficher le masque actuel, nous pouvons utiliser la fonction comme suit:signal.pthread_sigmask(signal.SIG_BLOCK, []).

Processus de reporting

La fonction peut être utilisée pour transmettre un signal os.kill:

import os
os.kill(pid, signo)

La fonction envoie le signal signo au processus avec l'identifiant pid . Les conditions requises pour qu'un processus soit autorisé à envoyer un signal à un autre processus sont les mêmes que dans le cas de kill. Si le signal spécifié est bloqué à ce moment, la fonction se fermera immédiatement et si l'indicateur SA_SIGINFO est défini et les ressources sont disponibles, le signal sera mis en file d'attente dans l'état en attente (un processus peut avoir une file d'attente maximale SIGQUEUE_MAX des signaux).

En attente d'un signal

<spoiler Approches pour attendre un signal > Si les signaux sont utilisés pour la communication et / ou la synchronisation, il est souvent nécessaire d'attendre qu'un certain type de signal arrive au processus en question. Un moyen facile de le faire est une boucle, dont la condition de sortie serait le réglage correct d'une variable. Par exemple:

while (!signal_has_arrived)

Le principal inconvénient de l'approche ci-dessus (type occupé-en attente) est le temps par processeur que le processus considéré perd inutilement. Une alternative serait d'utiliser la fonction time.sleep:

while (!signal_has_arrived):
    time.sleep(1)

Exercices

Pour résoudre le laboratoire, veuillez cloner repository. Si vous l'avez déjà, veuillez lancer git pull.

Exercice 1 - pipe (2p)

Entrez dans le répertoire 1-pipe , vous avez deux programmes: pipe.py et reader.py .

1a - Pipe et fork (1p)

Dans le fichier pipe.py , vous créez un tuyau puis créez une fourche. Dans le processus parent, fermez l'extrémité de lecture du tuyau et écrivez les données du buffer dans le tube. Dans le processus enfant, fermez l'extrémité d'écriture du canal, lisez les données reçues du canal dans le tampon et affichez-les à l'écran.

Suivez les lignes TODO 1.

1b - Pipe et exec

Dans le fichier reader.py , lisez au clavier un texte stocké dans la variable buffer et affichez la variable. Dans le fichier pipe.py , modifiez le processus enfant afin qu'après la fin de l'écriture dans le tube, rediriger stdin vers la fin de lecture du tube et exécuter (en utilisant l'une des fonctions exec ) le programme reader.py .

Suivez le TODO 2 mois.

Exercice 2 - hitme (2p)

Entrez dans le répertoire 2-hitme / et analysez le contenu du fichier hitme.py . Exécutez le programme.

Utilisez la commande kill -l pour répertorier tous les signaux disponibles. Quelle est la valeur du signal SIGKILL ? Dans une autre console, envoyez au programme des signaux hitme avec des valeurs entre 20 et 25 comme suit:

Le programme affichera son PID, ce sera $ PID.
kill -20 $PID
kill -21 $PID
kill -22 $PID
kill -23 $PID
kill -24 $PID
kill -25 $PID

Exercice 3 - Normal signals vs Real-Time signals (1p)

Entrez dans le répertoire 3-signaux et parcourez le contenu du fichier signaux.py . Le programme compte combien de fois le gestionnaire de signaux est appelé en cas d'envoi des signaux SIGINT et SIGRTMIN (34)

Démarrez le programme signaux.py :

python3 signals.py

Dans le cas de signaux normaux , dans une autre console, exécutez le script send_normal.sh :

./send_normal.sh

Pour les signaux en temps réel, exécutez un autre script dans send_rt.sh dans une autre console:

./send_rt.sh

Pour fermer l'exécutable signaux est envoyé le signal SIGQUIT . D'où vient la différence? Lisez la page de manuel signal man 7 Signaux en temps réel et passez en revue la section Types de signaux standard.

La différence entre le nombre de signaux reçus est due au fait que les signaux avec des indices entre SIGRTMIN et SIGRTMAX sont des signaux en temps réel, il est donc garanti qu'ils atteindront leur destination. Vue link.

Exercice 4 - askexit (2p)

Entrez dans le répertoire 4-askexit et suivez le code source. Le programme attend occupé (pendant), affichant des numéros consécutifs sur la console.

Vous devez terminer le programme pour intercepter les signaux générés par CTRL + \ , CTRL + C et SIGUSR1 (utilisez la commande kill). Le gestionnaire associé à chacun des signaux sera le ask_handler. Pour chaque signal reçu, il sera demandé à l'utilisateur s'il souhaite terminer l'exécution ou non.

Testez la fonctionnalité du programme.

Exercice 5 - noint (1p)

Entrez dans le répertoire 5-noint et créez un programme, appelé noint.py . Le programme reçoit, comme premier paramètre, le nom d'une commande à exécuter. Les autres paramètres représentent les arguments avec lesquels la commande respective doit être invoquée; la liste des arguments peut être nulle.

Le programme exécuté par noint.py ne doit pas être averti de la réception du signal SIGINT (CTRL + C). Vous devrez ignorer le signal SIGINT fourni par le shell de processus.

Pour tester, exécutez

python3 noint.py sleep 120 &

Exercice 6 - nohup (1p)

Entrez dans le répertoire 6-nohup et créez un programme, appelé nohup.py , qui simule la commande nohup. Le programme reçoit, comme premier paramètre, le nom d'une commande à exécuter. Les autres paramètres représentent les arguments avec lesquels la commande respective doit être invoquée; la liste des arguments peut être nulle.

Le programme exécuté par nohup.py ne doit pas être averti de la fermeture du terminal auquel il était connecté. Vous devrez ignorer le signal SIGHUP, délivré par le shell de processus, à la fin de la session en cours.

Si le fichier de sortie standard était lié à un terminal, il doit être redirigé vers un fichier nohup.out .

Pour tester, exécutez

python3 nohup.py sleep 120 &

Après l'exécution, fermez la session shell en cours: soit en envoyant un signal SIGHUP, soit en utilisant l'icône X sur le côté droit de la fenêtre.

Depuis une autre console, exécutez respectivement

ps -ef | grep sleep 

Qui est le nouveau parent du procès?

L'utilisation de la commande exit ou la combinaison de touches Ctrl-d n'enverra pas de signal SIGHUP au processus de sommeil; vous pouvez tester en utilisant sleep 120 & , fermer le shell actuel en utilisant l'une des 2 méthodes, puis vérifier que le processus est toujours en cours d'exécution.

Exercice 7 - zombie (1p)

Entrez dans le répertoire '7-zombie' et parcourez le contenu des fichiers zombie.py et nozombie.py . Chaque programme créera un nouveau processus enfant, qui sera uniquement appelé exit.

Implémentez zombie.py sans attendre la fin de l'enfant créé. Le processus parent attend pendant TIMEOUT secondes et se termine (suivez TODO ).

À partir d'une autre exécution de la console:

ps -eF | grep python3

Notez que le processus enfant, bien qu'il ne soit plus en cours d'exécution, apparaît dans la liste des processus sous la forme <defunct> et possède un pid (unique dans le système à ce moment-là). Notez également qu'après la mort du processus parent, le processus zombie disparaît également.

Implémentez nozombie sans utiliser les fonctions d'attente du type wait afin que le processus enfant ne passe pas à l'état zombie. nozombie attendra TIMEOUT secondes et quittera. Utilisez le signal SIGCHLD (informations trouvées dans signal() et waitpid()).Voir aussi Traitement du signal et Crearea unui proces in linux.

Si le parent ignore explicitement le signal SIGCHLD en définissant le gestionnaire sur SIG_IGN (au lieu de contourner le signal par défaut), les informations d'état de sortie enfant seront ignorées et les enfants ne deviendront pas des processus zombies.

Solutii

Solutii

1) limite globale fixée par défaut sur Linux à 4096 octets