logo Debian Debian Debian-France Debian-Facile Debian-fr.org Forum-Debian.fr Debian ? Communautés logo inclusivité

Debian-facile

Bienvenue sur Debian-Facile, site d'aide pour les nouveaux utilisateurs de Debian.

Vous n'êtes pas identifié(e).


L'icône rouge permet de télécharger chaque page du wiki visitée au format PDF et la grise au format ODT → ODT PDF Export

Ceci est une ancienne révision du document !


Pipe

Un pipe permet à deux processus de s'échanger des données. Les commandes Shell permettent de créer facilement un pipe :

envoyeur | receveur

Dans cet exemple, la sortie standard de envoyeur est reliée à l'entrée standard de receveur. Le Shell est un outil très pratique pour créer un pipe, mais en aucun cas les données du pipe ne transiteront par le Shell lors de l'exécution. Un pipe est aussi appelé FIFO (First In First Out) cela signifie que les données qui sortiront en premier de envoyeur seront également les données qui rentreront en premier dans receveur.

Cependant, l'ordre est celui dans lequel les appels-système à WRITE et READ ont été invoqués, pas l'ordre dans lequel les processus tentent de les réaliser. Par défaut, les pipe sont bloquants, ce qui signifie que lorsque envoyeur écrit dans le pipe, il s'endort jusqu'à ce que le processus receveur ait terminé de TOUT lire, ce qui réveille envoyeur. Si receveur est prêt à lire dans le pipe mais qu'envoyeur n'y a encore rien écrit, receveur s'endort et se réveillera dès qu'il y aura quelque chose à lire.

Lorsque vous programmez une application, vous pouvez choisir de rendre les pipes non-bloquants, grâce au flag O_NDELAY ou O_NONBLOCK. Un pipe peut très bien être bloquant d'un côté et non-bloquant de l'autre côté. L'ordre de réception des données peut différer suivant que le pipe soit bloquant (par défaut) ou non-bloquant (flag O_NDELAY).

Testons le pipe en pratique

Afin que vous puissiez expérimenter par vous-même le fonctionnement d'un pipe, je vous ai préparé deux commandes: rd (pour READ) et wr (pour WRITE) dont voici le code source src.c:

# include <fcntl.h>
# include <stdio.h>
# include <string.h>
# include <sys/wait.h>
# include <time.h>
# include <unistd.h>

char buffer[100000000] ;

int main(int n, char **v)
{
	if( n==1 || strcmp(v[1],"--help")==0 )
	{
		fprintf( stderr, "Usage: %s [-n] <timeout>s<length>k ...\n", v[0] ) ;
		return 1 ;
	}

	int i = 1, fd = ( strcmp(v[0],"wr") == 0 ) ? 1 : 0 ;

	if( strcmp(v[i],"-n") == 0 )
	{
		fcntl( fd, F_SETFL, O_NDELAY ) ;
		i++ ;
	}

	struct timespec t0, t1 ;
	clock_gettime( CLOCK_REALTIME, &t0 ) ;

	for(  ; i<n ; i++ )  if( fork() == 0 )
	{
		char *args = v[i] ;
		long timeout, length ;
		sscanf( args, "%li", &timeout ) ;
		args = index( args, 's' ) + 1 ;
		sleep( timeout ) ;
		while( *args )
		{
			sscanf( args, "%li", &length ) ;
			args = index( args, 'k' ) + 1 ;
			long ret = fd ? write(1,buffer,length*1024) : read(0,buffer,length*1024) ;
			clock_gettime( CLOCK_REALTIME, &t1 ) ;
			long t = (t1.tv_sec - t0.tv_sec)*10 + (t1.tv_nsec - t0.tv_nsec)/100000000 ;
			fprintf( stderr, fd ? "\033[%im(%lis)%2li.%lis %5lik >\033[0m\n" : "\033[%im(%lis)%2li.%lis        >%5lik\033[0m\n", fd==0 && ret>0, timeout, t/10, t%10, (ret+1023)/1024 ) ;
		}
		return 0 ;
	}

	signal( SIGCHLD, SIG_IGN ) ;
	wait( NULL ) ;

	clock_gettime( CLOCK_REALTIME, &t1 ) ;
	long t = (t1.tv_sec - t0.tv_sec)*10 + (t1.tv_nsec - t0.tv_nsec)/100000000 ;
	fprintf( stderr, fd ? "    %2li.%lis  CLOSE >\n" : "    %2li.%lis        > CLOSE\n", t/10, t%10 ) ;

	return 0 ;
}

Compilez-le avec :

gcc -s -lrt -Wall -std=gnu99 -o rd src.c
ln -s rd wr

À présent, vous vous retrouvez avec deux commandes supplémentaires :

  • rd qui réalise des READ sur son entrée standard avec les retards (secondes) et les tailles (ko) de votre choix.
  • wr qui réalise des WRITE sur sa sortie standard avec les retards (secondes) et les tailles (ko) de votre choix.

Vous pouvez écrire rd et wr plutôt que ./rd et ./wr grâce à :

PATH="$PATH:."

Scénarios simples

Pour commencer, voici 6 scénarios simples, pour faire comprendre par l'exemple comment réagit un pipe:

Exemple 1

Un cas d'école, non-représentatif de la complexité des véritables applications :

wr 3s30k | rd 2s30k

Dans cet exemple, wr et rd sont lancés simultanément et reliés grâce à un pipe créé par le Shell. rd va attendre 2 secondes puis tenter de lire 30ko dans le pipe, mais comme il n'y a encore rien à lire, rd va être mis en sommeil. wr attend 3 secondes puis écrit 30ko dans le pipe, ce qui va réveiller rd (qui aura dormi 1 seconde) avec les données demandées. Dans cet exemple, la copie s'est faite directement de la mémoire virtuelle de wr à la mémoire virtuelle de rd sans transiter par aucun tampon intermédiaire. Les processus wr et rd se réveillent une fois que Linux a copié les 30ko de données.

Cet exemple est naïf, car les processus envoyeur et receveur ne sont pas censés utiliser “comme par hasard” des paquets de même taille (ici 30ko) pour transférer les données.

Exemple 2

Tampon automatique, wr ne se rend compte de rien :

wr 2s30k | rd 3s20k

Dans cet exemple, wr attend 2 secondes puis tente d'écrire 30ko dans le pipe. À ce moment, rd n'a pas encore fait son READ, donc où vont les 30ko? Linux les stocke provisoirement dans un tampon limité à 16 pages mémoire (sachant qu'une page fait 4ko, le quota par défaut du tampon est de 64ko) en attendant que rd veuille bien lire son entrée standard. Le processus wr se termine. Une seconde plus tard, rd va lire les 20 premiers ko du tampon, sans savoir qu'il n'y a plus personne derrière son entrée standard. Puis rd se termine, et Linux va jeter les 10ko non-récupérés.

fcntl() permet de grossir le tampon du pipe jusqu'à 1Mo (ou plus avec l'intervention du root). Cependant, dans le cas de gros transferts, il est plus efficace de réaliser de grosses copies directes avec des WRITE et READ bloquants, plutôt que de multiplier le nombre de copies pour éviter de dormir… et de toute façon, tampon ou pas, un processus dort TOUJOURS pendant une copie, que cette copie soit bloquante ou non.

Exemple 3

Comme l'exemple 2, mais avec un zéro en plus dans les tailles, ça change tout :

wr 2s300k | rd 3s200k

wr tente d'écrire 300ko dans le pipe et rd n'a pas encore fait son READ. Comme le tampon du pipe n'est que de 64ko, les données seront donc copiées en prise directe: wr s'endort jusqu'à ce que rd finisse de récupérer les 300ko. Or rd ne lit que 200ko, wr reste endormi en attendant que rd fasse un autre READ, seulement contre toute attente, rd se termine, ce qui laisse les données de wr en plan. Ne sachant pas gérer la situation, wr se tue.

Dans l'exemple 2, wr ne se rendait pas compte que rd n'allait pas récupérer toutes les données, sauf qu'ici le tampon est trop petit et wr est donc en prise directe avec rd. Grâce à signal(), il aurait été possible de faire réagir wr autrement qu'en se suicidant à la réception d'un SIGPIPE, mais cela ne change rien au problème: tout processus receveur est censé attendre la fin du flux de données avant de se terminer. Dans les exemples 2 et 3, rd est malpoli.

Exemple 4

Ça devient intéressant, et commence à ressembler à la réalité :

wr 2s300k | rd 3s200k 4s200

wr tente d'écrire 300ko dans le pipe, rd récupère 200ko sans utiliser le tampon, puis une seconde plus tard, rd tente de récupérer 200ko mais reçoit les 100ko restants, ce qui réveille wr. Les 2 processus n'ont plus rien à faire, et se terminent donc simultanément.

Dans cet exemple, aucun problème.

Exemple 5

Comme l'exemple 4, mais l'écriture est non-bloquante :

wr -n 2s300k | rd 3s200k 4s200

wr (avec l'option -n) tente d'écrire 300ko, mais personne n'attend à l'autre bout du pipe. wr va donc remplir les 64ko du tampon du pipe. Une seconde plus tard, rd tente de lire 200ko et récupère les 64ko stockés dans le tampon. Une seconde plus tard, rd tente à nouveau de lire 200ko mais ne récupère rien, car le tampon est vide. rd va donc se terminer en ayant récupéré 64ko au lieu de 300ko.

L'écriture non-bloquante a rendu cet exemple naïf, mais rassurez-vous: “écriture non-bloquante” n'est pas nécessairement synonyme de “pertes de données”, car on peut s'en sortir avec une boucle, mais, cela consommera beaucoup de ressource CPU et sera de toute façon moins efficace qu'un processus qui dort, car un processus qui dort se réveille toujours au bon moment.

Exemple 6

Comme l'exemple 1, mais la lecture est non-bloquante :

wr 3s30k | rd -n 2s30k

rd (avec l'option -n) tente de lire 30ko, mais il n'y a rien à lire. Au lieu de s'endormir en attendant que wr fasse un WRITE, rd va tout simplement passer à la suite, c'est-à-dire se terminer. Une seconde plus tard, wr va vouloir écrire 30ko, mais comme il n'y a personne à l'autre bout du pipe, wr va recevoir un SIGPIPE et se suicider.

L'exemple 1, qui fonctionnait parfaitement, échoue si l'on rend la lecture non-bloquante.

Scénario complexe

Voici comment les données se comportent suivant que les WRITE et les READ soient bloquants ou non-bloquants. On constate non seulement qu'il y a pertes de données, mais qu'en plus les données n'arrivent pas forcément dans le même ordre.

Les temps demandés par l'utilisateur (entre parenthèses) ne sont pas les mêmes que ceux auxquels les primitives système ont été exécutées. De plus, les lignes correspondant à des transferts achevés s'affichent en surbrillance dans une console. Amusez-vous bien. ;-)

WRITE > READ

wr 1s10k300k7k 2s5k 4s200k | rd 0s2k 3s5000k 5s6k
(0s) 1.0s        >    2k
(2s) 3.0s     5k >
(1s) 3.0s   300k >
(1s) 3.0s     7k >
(3s) 3.0s        >  320k
(5s) 5.0s        >    6k
     5.0s        > CLOSE
     5.0s  CLOSE >
Il se passe 2 secondes entre la dernière action de wr et la fermeture du pipe, wr a donc été tué durant son sommeil.

WRITE non-bloquant > READ

wr -n 1s10k300k7k 2s5k 4s200k | rd 0s2k 3s5000k 5s6k
(0s) 1.0s        >    2k
(1s) 1.0s    52k >
(1s) 1.0s     0k >
(2s) 2.0s     0k >
(3s) 3.0s        >   60k
(4s) 4.0s    64k >
     4.0s  CLOSE >
(5s) 5.0s        >    6k
     5.0s        > CLOSE
La majorité des données a été perdue, alors que le scénario est identique au premier, au type d'écriture près.

WRITE > READ non-blocant

wr 1s10k300k7k 2s5k 4s200k | rd -n 0s2k 3s5000k 5s6k
(0s) 0.0s        >    0k
(1s) 1.0s    10k >
(2s) 3.0s     5k >
(1s) 3.0s   300k >
(1s) 3.0s     7k >
(3s) 3.0s        >  315k
(5s) 5.0s        >    6k
     5.0s        > CLOSE
     5.0s  CLOSE >
Comme précédemment, on voit que wr a été tué durant son sommeil.

WRITE non-blocant > READ non-bloquant

wr -n 1s10k300k7k 2s5k 4s200k | rd -n 0s2k 3s5000k 5s6k
(0s) 0.0s        >    0k
(1s) 1.0s    10k >
(1s) 1.0s    52k >
(1s) 1.0s     0k >
(2s) 2.0s     0k >
(3s) 3.0s        >   62k
(4s) 4.0s    64k >
     4.0s  CLOSE >
(5s) 5.0s        >    6k
     5.0s        > CLOSE
Quasiment toutes les données ont été perdues.
doc/programmation/shell/pipe.1303356040.txt.gz · Dernière modification: 21/04/2011 05:20 par smolski

Pied de page des forums

Propulsé par FluxBB