=======================================================================
INFO 1 - Systèmes d’Exploitations - TP3 - Communication inter-processus
=======================================================================


Avant-propos
------------

Les signaux sont souvent (tout le temps) utilisés sous UNIX :

 - À la frappe de certains caractères sur le clavier du terminal d’une session,
   tous les processus dans le groupe de processus en premier plan de cette
   session en sont informés au travers de signaux particuliers (`SIGINT,
   SIGQUIT, SIGSTP`).
 - Une violation mémoire par un processus provoque l’envoi à celui-ci d’un
   signal spécifique (`SIGSEV`).

De plus, 
 - un signal *pendant* est un signal qui a été envoyé à un processus mais n’a
   pas encore été pris en compte.
 - un signal est *délivré* à un processus lorsque le processus le prend en
   compte au cours de son exécution.

La prise en compte entraîne l’exécution d’une fonction particulière : l’un des
éléments fournis par l’indicateur de comportement d’un type de signal est un
pointeur sur le *handler* correspondant. Il faut noter qu’un signal peut être
soit *bloqué* soit *masqué*.

Sur un système donné il existe `NSIG` types de signaux différents, chacun
étant identifié par un nombre compris entre 1 et `NSIG`. Cette valeur de
`NSIG` ainsi que les autres constantes et types de données nécessaires au
fonctionnement des signaux sont accessibles via le fichier `<signal.h>`.
La commande `kill -l` donne la liste des signaux du système.


Fonctions spéciales
-------------------

```c
int kill(pid_t pid, int sig);
```

Envoie le signal de numéro `sig` au(x) processus déduit(s) de la valeur de `pid`
(e.g. >0 désigne le processus d’identité `pid`, 0 désigne tous les processus
dans le même groupe que le processus).
Retour 0 en cas de succès et -1 en cas d’erreur. Si `sig` vaut 0, la fonction
`kill` n’envoie pas de signal mais sert comme fonction de test (de l’existence
d’un processus par exemple).

```c
int raise(int sig);
```
Envoie le signal de numéro `sig` au processus courant.


La manipulation des handlers
----------------------------

Le comportement général d’un signal lors de la délivrance d’un signal correspond
dans POSIX à la structure `sigaction` :
```c
struct sigaction{
	void (*sa_handler)() ; /* pointeur sur handler */
	sigset_t sa_mask ;     /* signaux supplementaires a bloquer*/
	int sa_flags ;         /* options */
}
```
La primitive `sigaction` :
```c
int sigaction(int sig,
              const struct sigaction *p_action,
              struct sigaction *p_action_anc) ;
```
constitue l’interface générale de POSIX permettant d’installer dans un processus
un handler pour un type de signal donné. L’installation d’un handler n’envoie
pas de signal au processus sauf s’il s’agit du signal SIGCHLD (terminaison d’un
fils) auquel cas un exemplaire de ce signal est envoyé, en certaines
circonstances, au processus. Si le paramètre `p_action` n’est pas le
pointeur `NULL`, il est interprété comme un pointeur sur un objet de
structure `sigaction` à installer pour le signal de numéro `sig`.
La délivrance de ce signal entraînera alors l’exécution de la fonction
pointée par `p_action->sa_handler`. Si `p_action` est le
pointeur `NULL`, le comportement n’est pas modifié.

On utilisera le plus souvent les instructions sous la forme suivante :
```c
struct sigaction action;
/* … */
void hand(int sig)
{
	/* action à effectuer */
}
/* … */
action.sa_handler = hand;
sigaction(SIGQUIT, &action, NULL);
/* … */
```


Exercices, partie 1
-------------------

1. Écrire un programme dans lequel le processus père envoie un signal à son
   processus fils après avoir testé son existence.

2. Écrire un programme dont le processus comptabilise le nombre d’exemplaires
   des signaux `SIGINT` et `SIGQUIT` qui lui sont délivrés et se termine lorsque
   l’un de ces nombres devient égal à `NMAX`. Les signaux sont générés à la
   frappe des caractères `intr` (Ctr c) et `quit` (Ctr Altgr 6 ou 8 sur linux ou
   windows et ctr \` sur mac) au clavier.

3. Expliquer le fonctionnement du programme suivant.

```c
#include <stdio.h>
#include <signal.h>

struct sigaction action;

void hand_sigpipe(int sig)
{
	printf("Signal SIGPIPE reçu\n");
}

main()
{
	int nb_ecrit, p[2];
	action.sa_handler = hand_sigpipe;
	sigaction(SIGPIPE, &action, NULL);
	pipe(p);
	close(p[0]);
	if ((nb_ecrit = write(p[1], "A", 1)) == -1)
		perror("Write");
	else
		printf("Retour du write : %d\n", nb_ecrit);
}
```

4. Le signal `SIGCHLD` est un signal qui est automatiquement envoyé par le fils
   à son père lorsque le fils se termine (par un `exit`, un `return`, ou autre).
   Ajoutez une fonction et le code nécessaire afin que le père n’attende jamais
   son fils de façon bloquante, et que le fils ne devienne pas zombie.
```c
int main(int argc, char *argv[])
{
	if (!fork()) {
		/*simule un petit calcul */
		for (int i = 0; i < 10; i++) ;
		exit(1);
	}
	/*Simule un calcul infini */
	while (1) ;
}
```

5. Considérez le programme suivant :
```c
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <signal.h>

#define N 5

int main()
{
	pid_t pid[N];
	int i;
	for (i = 0; i < N; i++)
		if ((pid[i] = fork()) == 0) {
			while (1)
				printf("ici fils %d ", i);
		}
}
```
On veut que le processus père utilise les signaux `SIGSTOP` et `SIGCONT` pour
suspendre (bloquer) et reprendre (débloquer) l’exécution de ses processus fils.
Au départ tous les processus fils créés doivent se mettre en pause. Le processus
père répète continuellement le traitement suivant en commençant par le premier
fils : il envoie le signal `SIGCONT` à un fils puis s’endort pendant 1s. À son
réveil, il envoie `SIGSTOP` au même fils et `SIGCONT` au fils suivant (le fils
suivant du dernier est le premier).  Lorsqu’un processus fils reçoit le signal
`SIGCONT`, il affiche le message indiquant qu’il a capturé le signal `SIGCONT`
avant de poursuive son exécution.


Exercices, partie 2
-------------------

6. Avec un tube nommé, on peu forcer deux processus à se donner rendez-vous en
   un point particulier de leur exécution : l’un ouvrant le tube en écriture, et
   l’autre en lecture. Le premier des deux demandant l’ouverture est bloqué,
   jusqu’à ce que le second demande à son tour l’ouverture.
   Dans le cas d’une communication dans les deux sens, il est nécessaire de
   créer deux tubes. Il faut alors se méfier de l’ordre de réalisation des
   ouvertures pour éviter un inter-blocage.
   Donner schématiquement les instructions correspondant à une telle
   réalisation.

7. On veut établir, en utilisant des tubes (anonymes), une communication de type
   *anneau unidirectionnel* entre trois processus fils, correspondant au schéma
   suivant : `P1 ==> P2 ==> P3 ==> P1`.
   Compléter (et tester) le programme afin de réaliser un tel anneau.

```c
int main()
{
	if (fork()) {
		if (fork()) {
			if (fork()) {
				while (wait(NULL) > 0) ;
			} else {
				execlp("prgm3", "prgm3", NULL);
			}
		} else {
			execlp("prgm2", "prgm2", NULL);
		}
	} else {
		execlp("prgm1", "prgm1", NULL);
	}
}
```

8. Écrire le `main` d’un processus qui permet de simuler un tube (du shell,
   `|`). Ce code doit utiliser un tube nommé et doit avoir exactement le même
   comportement qu’un tube shell.
   Exemples d’utilisation :
   ```sh
   mon_tuyau who 'wc -l'
   mon_tuyau ls 'grep "pipe"'
   ```
   équivaut à
   ```sh
   who | wc -l
   ls | grep "pipe"
   ```
