Comment la substitution de processus est-elle implémentée dans bash?

12

Je recherchais l' autre question , quand j'ai réalisé que je ne comprenais pas ce qui se passait sous le capot, quels étaient ces /dev/fd/*fichiers et comment les processus enfants pouvaient-ils les ouvrir.

x-yuri
la source
N'a-t-on pas répondu à cette question?
phk

Réponses:

21

Eh bien, il y a de nombreux aspects.

Descripteurs de fichiers

Pour chaque processus, le noyau maintient une table de fichiers ouverts (enfin, il peut être implémenté différemment, mais comme vous ne pouvez pas le voir de toute façon, vous pouvez simplement supposer qu'il s'agit d'une simple table). Ce tableau contient des informations sur quel fichier il s'agit / où il peut être trouvé, dans quel mode vous l'avez ouvert, à quelle position vous lisez / écrivez actuellement et tout ce qui est nécessaire pour effectuer des opérations d'E / S sur ce fichier. Maintenant, le processus ne peut jamais lire (ni même écrire) cette table. Lorsque le processus ouvre un fichier, il récupère un soi-disant descripteur de fichier. Ce qui est simplement un index dans la table.

L'annuaire /dev/fdet son contenu

Sous Linux dev/fdest en fait un lien symbolique vers /proc/self/fd. /procest un pseudo système de fichiers dans lequel le noyau mappe plusieurs structures de données internes auxquelles on peut accéder avec l'API de fichiers (de sorte qu'elles ressemblent simplement à des fichiers / répertoires / liens symboliques normaux vers les programmes). Surtout, il y a des informations sur tous les processus (c'est ce qui lui a donné son nom). Le lien symbolique /proc/selffait toujours référence au répertoire associé au processus en cours d'exécution (c'est-à-dire le processus qui le demande; différents processus verront donc différentes valeurs). Dans le répertoire du processus, il y a un sous-répertoirefd qui pour chaque fichier ouvert contient un lien symbolique dont le nom n'est que la représentation décimale du descripteur de fichier (l'index dans la table de fichiers du processus, voir section précédente), et dont la cible est le fichier auquel il correspond.

Descripteurs de fichiers lors de la création de processus enfants

Un processus enfant est créé par a fork. A forkfait une copie des descripteurs de fichiers, ce qui signifie que le processus enfant créé a la même liste de fichiers ouverts que le processus parent. Par conséquent, à moins que l'un des fichiers ouverts ne soit fermé par l'enfant, l'accès à un descripteur de fichier hérité de l'enfant accède au même fichier que l'accès au descripteur de fichier d'origine dans le processus parent.

Notez qu'après un fork, vous disposez initialement de deux copies du même processus qui ne diffèrent que par la valeur de retour de l'appel fork (le parent obtient le PID de l'enfant, l'enfant obtient 0). Normalement, un fork est suivi d'un execpour remplacer l'une des copies par un autre exécutable. Les descripteurs de fichiers ouverts survivent à cet exécutable. Notez également qu'avant l'exécution, le processus peut effectuer d'autres manipulations (comme fermer des fichiers que le nouveau processus ne devrait pas obtenir ou ouvrir d'autres fichiers).

Tuyaux sans nom

Un canal sans nom n'est qu'une paire de descripteurs de fichiers créés à la demande du noyau, de sorte que tout ce qui est écrit dans le premier descripteur de fichiers est transmis au second. L'utilisation la plus courante est pour la construction foo | barde tuyauterie de bash, où la sortie standard de fooest remplacée par la partie écriture du tuyau et l'entrée standard est remplacée par la partie lecture. L'entrée standard et la sortie standard ne sont que les deux premières entrées de la table de fichiers (l'entrée 0 et 1; 2 est une erreur standard), et donc la remplacer signifie simplement réécrire cette entrée de table avec les données correspondant à l'autre descripteur de fichier (encore une fois, le la mise en œuvre réelle peut différer). Comme le processus ne peut pas accéder directement à la table, il existe une fonction noyau pour le faire.

Substitution de processus

Maintenant, nous avons tout ensemble pour comprendre comment fonctionne la substitution de processus:

  1. Le processus bash crée un canal sans nom pour la communication entre les deux processus créés ultérieurement.
  2. Fourches bash pour le echoprocessus. Le processus enfant (qui est une copie exacte du bashprocessus d' origine ) ferme l'extrémité de lecture du tuyau et remplace sa propre sortie standard par l'extrémité d'écriture du tuyau. Étant donné qu'il echos'agit d'un shell intégré, il bashpeut s'épargner l' execappel, mais cela n'a pas d'importance de toute façon (le shell intégré peut également être désactivé, auquel cas il s'exécute /bin/echo).
  3. Bash (l'original, parent) remplace l'expression <(echo 1)par le lien de pseudo-fichier en /dev/fdfaisant référence à l'extrémité de lecture du canal sans nom.
  4. Bash execs pour le processus PHP (notez qu'après le fork, nous sommes toujours dans [une copie de] bash). Le nouveau processus ferme l'extrémité d'écriture héritée du canal sans nom (et effectue d'autres étapes préparatoires), mais laisse l'extrémité de lecture ouverte. Ensuite, il a exécuté PHP.
  5. Le programme PHP reçoit le nom en /dev/fd/. Étant donné que le descripteur de fichier correspondant est toujours ouvert, il correspond toujours à l'extrémité de lecture du canal. Par conséquent, si le programme PHP ouvre le fichier donné pour lecture, ce qu'il fait réellement est de créer un seconddescripteur de fichier pour la fin de lecture du canal sans nom. Mais ce n'est pas un problème, il pourrait lire l'un ou l'autre.
  6. Maintenant, le programme PHP peut lire l'extrémité de lecture du canal via le nouveau descripteur de fichier, et ainsi recevoir la sortie standard de la echocommande qui va à l'extrémité d'écriture du même canal.
celtschk
la source
Bien sûr, j'apprécie vos efforts. Mais je voulais souligner plusieurs problèmes. Tout d'abord, vous parlez de phpscénario, mais phpne gère pas bien les tuyaux . En outre, compte tenu de la commande cat <(echo test), la chose étrange ici est que les bashfourches une fois pour cat, mais deux fois pour echo test.
x-yuri
13

Emprunter de celtschkla réponse de, /dev/fdest un lien symbolique vers /proc/self/fd. Et /procest un pseudo-système de fichiers, qui présente des informations sur les processus et d'autres informations système dans une structure hiérarchique de type fichier. Les fichiers dans /dev/fdcorrespondent aux fichiers, ouverts par un processus et ont un descripteur de fichier comme nom et les fichiers eux-mêmes comme cible. L'ouverture du fichier /dev/fd/Néquivaut à dupliquer le descripteur N(en supposant que le descripteur Nest ouvert).

Et voici les résultats de mon enquête sur son fonctionnement (la stracesortie est débarrassée des détails inutiles et modifiée pour mieux exprimer ce qui se passe):

$ cat 1.c
#include <unistd.h>
#include <fcntl.h>

int main(int argc, char *argv[])
{
    char buf[100];
    int fd;
    fd = open(argv[1], O_RDONLY);
    read(fd, buf, 100);
    write(STDOUT_FILENO, buf, n_read);
    return 0;
}
$ gcc 1.c -o 1.out
$ cat 2.c
#include <unistd.h>
#include <string.h>

int main(void)
{
    char *p = "hello, world\n";
    write(STDOUT_FILENO, p, strlen(p));
    return 0;
}
$ gcc 2.c -o 2.out
$ strace -f -e pipe,fcntl,dup2,close,clone,close,execve,wait4,read,open,write bash -c './1.out <(./2.out)'
[bash] pipe([3, 4]) = 0
[bash] dup2(3, 63) = 63
[bash] close(3) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p2
Process p2 attached
[bash] close(4) = 0
[bash] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p1
Process p1 attached
[bash] close(63) = 0
[p2] dup2(4, 1) = 1
[p2] close(4) = 0
[p2] close(63) = 0
[bash] wait4(-1, <unfinished ...>
Process bash suspended
[p1] execve("/home/yuri/_/1.out", ["/home/yuri/_/1.out", "/dev/fd/63"], [/* 31 vars */]) = 0
[p2] clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7f7c211fb9d0) = p22
Process p22 attached
[p22] execve("/home/yuri/_/2.out", ["/home/yuri/_/2.out"], [/* 31 vars */]) = 0
[p2] wait4(-1, <unfinished ...>
Process p2 suspended
[p1] open("/dev/fd/63", O_RDONLY) = 3
[p1] read(3,  <unfinished ...>
[p22] write(1, "hello, world\n", 13) = 13
[p1] <... read resumed> "hello, world\n", 100) = 13
Process p2 resumed
Process p22 detached
[p1] write(1, "hello, world\n", 13) = 13
hello, world
[p2] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p22
[p2] --- SIGCHLD (Child exited) @ 0 (0) ---
[p2] wait4(-1, 0x7fff190f289c, WNOHANG, NULL) = -1 ECHILD (No child processes)
Process bash resumed
Process p1 detached
[bash] <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = p1
[bash] --- SIGCHLD (Child exited) @ 0 (0) ---
Process p2 detached
[bash] wait4(-1, 0x7fff190f2bdc, WNOHANG, NULL) = 0
--- SIGCHLD (Child exited) @ 0 (0) ---
[bash] wait4(-1, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], WNOHANG, NULL) = p2
[bash] wait4(-1, 0x7fff190f299c, WNOHANG, NULL) = -1 ECHILD (No child processes)

Fondamentalement, bashcrée un tube et transmet ses extrémités à ses enfants en tant que descripteurs de fichier (lecture en fin 1.outet écriture en fin 2.out). Et passe la fin de lecture en tant que paramètre de ligne de commande à 1.out( /dev/fd/63). Cette façon 1.outest capable d'ouvrir /dev/fd/63.

x-yuri
la source