Comment les pipelines limitent-ils l'utilisation de la mémoire?

36

Brian Kernighan explique dans cette vidéo l'attrait des débuts des Bell Labs pour les petits langages / programmes basés sur des limitations de mémoire

Une grosse machine aurait 64 ko - K, pas M ou G - et cela signifiait que tout programme individuel ne pouvait pas être très grand, et il y avait donc une tendance naturelle à écrire de petits programmes, puis le mécanisme de canal, En gros, la redirection des sorties d’entrée permettait de relier un programme à un autre.

Mais je ne comprends pas comment cela pourrait limiter l'utilisation de la mémoire compte tenu du fait que les données doivent être stockées dans la RAM pour pouvoir être transmises entre programmes.

De Wikipedia :

Dans la plupart des systèmes de type Unix, tous les processus d'un pipeline sont démarrés en même temps., avec leurs flux correctement connectés et gérés par le planificateur ainsi que tous les autres processus en cours d'exécution sur la machine. Le concept de mise en mémoire tampon est un aspect important qui distingue les pipes Unix des autres implémentations de pipes: par exemple, un programme d’envoi peut produire 5 000 octets par seconde, et un programme récepteur ne peut accepter que 100 octets par seconde. les données sont perdues. Au lieu de cela, la sortie du programme d'envoi est conservée dans la mémoire tampon. Lorsque le programme récepteur est prêt à lire les données, le programme suivant du pipeline est lu dans la mémoire tampon. Sous Linux, la taille du tampon est de 65 536 octets (64 Ko). Un filtre tiers open source appelé bfr est disponible pour fournir des tampons plus volumineux si nécessaire.

Cela me confond encore plus, car cela va complètement à l'encontre de l'objectif des petits programmes (même s'ils seraient modulaires jusqu'à une certaine échelle).

La seule chose à laquelle je puisse penser comme une solution à ma première question (les limites de mémoire étant problématiques en fonction de la taille des données) serait que les grands ensembles de données n’étaient tout simplement pas calculés à l’époque et que le véritable problème que les pipelines étaient censés résoudre était la résolution. quantité de mémoire requise par les programmes eux-mêmes. Mais étant donné le texte en gras dans la citation de Wikipédia, même cela me perturbe: un programme n’est pas mis en œuvre à la fois.

Tout cela aurait beaucoup de sens si des fichiers temporaires étaient utilisés, mais je comprends que les tubes n’écrivent pas sur le disque (sauf si l’échange est utilisé).

Exemple:

sed 'simplesubstitution' file | sort | uniq > file2

Il est clair pour moi que sedlire dans le fichier et le cracher ligne par ligne. Mais sort, comme BK l'indique dans la vidéo liée, est un arrêt complet, de sorte que toutes les données doivent être lues en mémoire (ou le fait-il?), Puis elles sont transmises à uniq, ce qui (à mon avis) serait un programme en ligne à la fois. Mais entre le premier et le deuxième canal, toutes les données doivent être en mémoire, non?

Malan
la source
1
unless swap is usedswap est toujours utilisé quand il n'y a pas assez de RAM
edc65

Réponses:

44

Les données n'ont pas besoin d'être stockées dans la RAM. Pipes bloquent leurs écrivains si les lecteurs sont absents ou ne peuvent pas suivre le rythme; sous Linux (et la plupart des autres implémentations, j'imagine), il y a une mise en mémoire tampon mais ce n'est pas obligatoire. Comme mentionné par mtraceur et jdebp (voir la réponse de ce dernier)), les premières versions de tubes Unix tamponnés sur disque, et c’est ainsi qu’elles ont permis de limiter l’utilisation de la mémoire: un pipeline de traitement pouvait être scindé en petits programmes, chacun traitant des données dans les limites des tampons de disque. Les petits programmes utilisent moins de mémoire et l'utilisation de pipes permettait de sérialiser le traitement: le premier programme s'exécutait, remplissait son tampon de sortie, était suspendu, puis le second programme était planifié, traitait le tampon, etc. Les systèmes modernes sont des commandes d’amplitude plus grande que les anciens systèmes Unix, et peut faire fonctionner de nombreux tuyaux en parallèle; mais pour des quantités énormes de données, vous constaterez toujours un effet similaire (et des variantes de ce type de technique sont utilisées pour le traitement «big data»).

Dans votre exemple

sed 'simplesubstitution' file | sort | uniq > file2

sedlit les données à partir filede ce qui est nécessaire, puis les écrit aussi longtemps qu'il sortest prêt à les lire; si ce sortn'est pas prêt, l'écriture bloque. Les données vivent effectivement en mémoire à terme, mais elles sont spécifiques sortet sortsont prêtes à traiter tous les problèmes (il utilisera des fichiers temporaires si la quantité de données à trier est trop importante).

Vous pouvez voir le comportement de blocage en exécutant

strace seq 1000000 -1 1 | (sleep 120; sort -n)

Cela produit une quantité juste des données et des tuyaux à un processus qui n'est pas prêt à lire quoi que ce soit pour les deux premières minutes. Vous verrez un certain nombre d' writeopérations se dérouler, mais très rapidement, seqelles s'arrêteront et attendront que les deux minutes s'écoulent, bloquées par le noyau (l' writeappel système attend).

Stephen Kitt
la source
13
Cette réponse gagnerait à expliquer en outre pourquoi le fractionnement de programmes en plusieurs petits permet d’économiser de la mémoire: un programme devait pouvoir tenir en mémoire pour s'exécuter, mais uniquement le programme en cours d’exécution . Tous les autres programmes ont été permutés sur le disque au début d’Unix, un seul programme étant même échangé à la fois dans la RAM réelle. Ainsi, la CPU exécutait un programme qui écrirait dans un tuyau (qui à l’époque se trouvait sur le disque ), le remplacerait par celui qui se lirait dans le tuyau. Manière élégante de transformer une ligne d'assemblage logiquement parallèle en une exécution sérialisée incrémentielle.
mtraceur
6
@malan: Plusieurs processus peuvent être démarrés et peuvent être dans un état exécutable en même temps. Mais au plus un processus peut s’exécuter sur chaque CPU physique à tout moment, et c’est le rôle du planificateur de processus du noyau d’allouer des "tranches" de temps CPU à chaque processus exécutable. Dans les systèmes modernes, un processus exécutable mais non planifié actuellement, une tranche de temps CPU reste généralement en mémoire en attente de la tranche suivante, mais le noyau est autorisé à transférer la mémoire de tout processus sur un disque, puis à nouveau en mémoire cela me convient. (Quelques détails ici.)
Daniel Pryden
5
Les processus situés de part et d'autre d'un tuyau peuvent se comporter efficacement comme des co-routines: un côté écrit jusqu'à remplir le tampon et les blocs d'écriture; à ce moment, le processus ne peut plus rien faire avec le reste de son temps. IO mode attente. Ensuite, le système d’exploitation donne le reste de la tranche de temps (ou une autre tranche à venir) du côté lecture, qui lit jusqu’à ce qu’il ne reste plus rien dans la mémoire tampon et les blocs de lecture suivants, moment auquel le processus de lecture ne peut rien faire avec le reste de la mémoire. son temps et rend à l'OS. Les données parcourent la valeur de tampon d'un tuyau à la fois.
Daniel Pryden
6
@malan Les programmes sont lancés "en même temps" conceptuellement sur tous les systèmes Unix, uniquement sur les systèmes multiprocesseurs modernes avec suffisamment de RAM pour les contenir, ce qui signifie qu'ils sont littéralement tous conservés en même temps, alors que sur un système Ne les conservez pas tous en même temps dans la RAM, certains seront échangés sur le disque. Notez également que, dans de nombreux contextes, le terme "mémoire" désigne une mémoire virtuelle qui représente la somme de l’espace RAM et de l’espace de permutation sur disque. Wikipedia se concentre sur le concept plutôt que sur les détails de la mise en œuvre, en particulier parce que la façon dont les choses Unix à l'ancienne est vraiment moins pertinente maintenant.
mtraceur
2
@malan En outre, la contradiction que vous voyez provient des deux significations différentes de «mémoire» (RAM vs RAM + swap). Je parlais uniquement de la mémoire RAM matérielle et, dans ce contexte, seul le code en cours d'exécution par la CPU devait tenir dans la RAM (ce qui affectait les décisions prises par Kernighan), tandis que dans le contexte de tous les programmes exécutés de manière logique par le système d’exploitation à un moment donné (au niveau abstrait fourni en plus du découpage en temps), un programme doit simplement tenir dans toute la mémoire virtuelle disponible pour le système d’exploitation, ce qui inclut l’espace de permutation sur disque.
mtraceur
34

Mais je ne comprends pas comment cela pourrait limiter l'utilisation de la mémoire compte tenu du fait que les données doivent être stockées dans la RAM pour pouvoir être transmises entre programmes.

Ceci est votre erreur fondamentale. Les premières versions d'Unix ne contenaient pas de données de canal dans la RAM. Ils les ont stockés sur disque. Les pipes avaient des i-noeuds; sur un périphérique de disque qui a été noté le périphérique de canal . L'administrateur système a exécuté un programme nommé /etc/configpour spécifier (entre autres choses) quel volume sur quel disque était le périphérique de canal, quel volume était le périphérique racine et quel périphérique de vidage .

La quantité de données en attente était limitée par le fait que seuls les blocs directs du i-nœud sur disque étaient utilisés pour le stockage. Ce mécanisme simplifiait le code, car le même algorithme était utilisé pour la lecture d'un tuyau et celui d'un fichier normal, avec quelques ajustements causés par le fait que les tuyaux n'étaient pas recherchés et que la mémoire tampon était circulaire.

Ce mécanisme a été remplacé par d'autres du milieu à la fin des années 1980. SCO XENIX a obtenu le "Système de tuyaux haute performance", qui remplace les i-nœuds par des tampons internes. 4BSD a créé des pipes non nommées dans socketpairs. AT & T a réimplémenté des tuyaux en utilisant le mécanisme STREAMS.

Et bien sûr, le sortprogramme exécutait une sorte interne limitée de morceaux d’entrée de 32 Ko (ou une quantité moindre de mémoire qu’il pourrait allouer si 32 Ko n’était pas disponible), en écrivant les résultats triés dans des stmX??fichiers intermédiaires dans /usr/tmp/lesquels il était ensuite fusionné en externe et trié pour fournir le résultat final. sortie.

Lectures complémentaires

  • Steve D. Pate (1996). "Communication interprocessus". Unix Internals: Une approche pratique . Addison-Wesley. ISBN 9780201877212.
  • Maurice J. Bach (1987). "Appels système pour le système de fichiers". La conception du système d'exploitation Unix . Prentice Hall. ISBN 0132017571.
  • Steven V. Earhart (1986). " config(1M)". Manuel du programmeur Unix: 3. Fonctions d'administration du système . Holt, Rinehart et Winston. ISBN 0030093139. pp. 23–28.
JdeBP
la source
1

Vous avez partiellement raison, mais seulement par accident .

Dans votre exemple, toutes les données doivent bien avoir été lues "entre" les canaux, mais elles ne doivent pas nécessairement être résidentes en mémoire (y compris la mémoire virtuelle). Les implémentations habituelles de sortpeuvent trier des ensembles de données qui ne rentrent pas dans la RAM en effectuant des tris partiels dans des fichiers temporaires, puis en les fusionnant. Cependant, il est un fait que vous ne pouvez probablement pas sortir une séquence triée avant d'avoir lu chaque élément. C'est assez évident. Donc, oui, sortvous ne pouvez commencer à émettre sur le deuxième tuyau qu'après avoir lu (et fait n'importe quoi, éventuellement en triant partiellement les fichiers temporaires) tout du premier. Mais il n'est pas forcément nécessaire de tout garder en RAM.

Cependant, cela n’a rien à voir avec le fonctionnement des tuyaux. Les pipes peuvent être nommées (traditionnellement, elles le sont toutes), ce qui ne signifie rien de plus et rien de moins qu’elles ont un emplacement dans le système de fichiers, comme des fichiers. Et c’est ce qu’était jadis les pipes: des fichiers (avec des écritures aussi bien que la disponibilité de la mémoire physique le permettait, en guise d’optimisation).

De nos jours, les pipes sont un petit tampon de noyau de taille finie dans lequel des données sont copiées, du moins c'est ce qui se passe de manière conceptuelle . Si le noyau le permet, les copies sont résolues en jouant des tours à la VM (par exemple, la canalisation depuis un fichier ne permet généralement que de lire la même page que l’autre processus, de sorte qu’il s’agit enfin d’une opération de lecture, et non de De toute façon, de la mémoire supplémentaire est nécessaire par rapport à la mémoire tampon déjà utilisée. Dans certains cas, vous pouvez également obtenir une copie 100% zéro, ou quelque chose de très proche.

Si les canaux sont de petite taille et de taille finie, comment cela peut-il fonctionner pour une quantité de données inconnue (éventuellement importante)? C'est simple: quand rien ne va plus, l'écriture bloque jusqu'à ce qu'il y ait de la place.

La philosophie de nombreux programmes simples était particulièrement utile jadis, lorsque la mémoire était très rare. Parce que, bon, vous pouvez travailler par petites étapes, une à la fois. De nos jours, les avantages sont, mis à part une flexibilité supplémentaire, j’ose dire que ce n’est pas si bon.
Cependant, les pipes sont mises en œuvre de manière très efficace (ce devait être le cas!), Il n'y a donc aucun inconvénient, et c'est une chose bien établie qui fonctionne bien et à laquelle les gens sont habitués. Il n'est donc pas nécessaire de changer de paradigme.

Damon
la source
Quand vous dites "les tuyaux ont été nommés" (JdeBP semble indiquer qu'il y avait un "dispositif de tuyau"), cela signifie-t-il qu'il y avait une limite au nombre de tuyaux pouvant être utilisés à un moment donné (c'est-à-dire qu'il y avait une limite à combien de fois vous pouvez utiliser |dans une commande)?
Malan
2
Je n'ai jamais vu une telle limite et je ne pense pas qu'il y en ait jamais eu en théorie . En pratique, tout ce qui a un nom de fichier nécessite un inode, et le nombre d'inodes est, bien entendu, fini. De même que le nombre de pages physiques sur un système, si rien d'autre. Les systèmes modernes garantissent des écritures atomiques de 4 ko, de sorte que chaque tuyau doit posséder au moins une page complète de 4 ko, ce qui impose une limite stricte au nombre de tuyaux que vous pouvez avoir. Mais considérez avoir quelques gigaoctets de RAM ... pratiquement, c'est une limite que vous ne rencontrerez jamais. Essayez de taper quelques millions de pipes sur un terminal ... :)
Damon