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.
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.
Réponses:
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/fd
et son contenu
Sous Linux dev/fd
est en fait un lien symbolique vers /proc/self/fd
. /proc
est 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/self
fait 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 fork
fait 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 exec
pour 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 | bar
de tuyauterie de bash
, où la sortie standard de foo
est 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:
echo
processus. Le processus enfant (qui est une copie exacte du bash
processus 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 echo
s'agit d'un shell intégré, il bash
peut s'épargner l' exec
appel, 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
).<(echo 1)
par le lien de pseudo-fichier en /dev/fd
faisant référence à l'extrémité de lecture du canal sans nom./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 second
descripteur 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.echo
commande qui va à l'extrémité d'écriture du même canal.php
scénario, mais php
ne gère pas bien les tuyaux . En outre, compte tenu de la commande cat <(echo test)
, la chose étrange ici est que les bash
fourches une fois pour cat
, mais deux fois pour echo test
.
Emprunter de celtschk
la réponse de, /dev/fd
est un lien symbolique vers /proc/self/fd
. Et /proc
est 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/fd
correspondent 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 N
est ouvert).
Et voici les résultats de mon enquête sur son fonctionnement (la strace
sortie 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, bash
crée un tube et transmet ses extrémités à ses enfants en tant que descripteurs de fichier (lecture en fin 1.out
et é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.out
est capable d'ouvrir /dev/fd/63
.