Я исследовал другой вопрос , когда понял, что не понимаю, что происходит под капотом, что это за /dev/fd/*
файлы и как их могут открывать дочерние процессы.
Я исследовал другой вопрос , когда понял, что не понимаю, что происходит под капотом, что это за /dev/fd/*
файлы и как их могут открывать дочерние процессы.
Ответы:
Ну, в этом есть много аспектов.
Файловые дескрипторы
Для каждого процесса ядро поддерживает таблицу открытых файлов (ну, это может быть реализовано по-разному, но, поскольку вы все равно не можете ее увидеть, вы можете просто предположить, что это простая таблица). Эта таблица содержит информацию о том, в каком файле он находится / где его можно найти, в каком режиме вы его открыли, в какой позиции вы сейчас читаете / записываете и что еще нужно для фактического выполнения операций ввода-вывода с этим файлом. Теперь процессу никогда не удается прочитать (или даже записать) эту таблицу. Когда процесс открывает файл, он возвращает так называемый дескриптор файла. Что просто указатель в таблицу.
Каталог /dev/fd
и его содержание
В Linux dev/fd
это символическая ссылка /proc/self/fd
. /proc
это псевдофайловая система, в которой ядро отображает несколько внутренних структур данных, к которым осуществляется доступ с помощью файлового API (поэтому они просто выглядят как обычные файлы / каталоги / символические ссылки на программы). Особенно есть информация обо всех процессах (именно это и дало название). Символическая ссылка /proc/self
всегда относится к каталогу, связанному с текущим запущенным процессом (то есть процессом, запрашивающим его; поэтому разные процессы будут видеть разные значения). В каталоге процесса есть подкаталогfd
который для каждого открытого файла содержит символическую ссылку, имя которой является просто десятичным представлением дескриптора файла (индекс в таблице файлов процесса, см. предыдущий раздел), и чьей целью является файл, которому он соответствует.
Файловые дескрипторы при создании дочерних процессов
Дочерний процесс создается fork
. A fork
создает копию файловых дескрипторов, что означает, что созданный дочерний процесс имеет тот же список открытых файлов, что и родительский процесс. Таким образом, пока один из открытых файлов не будет закрыт дочерним процессом, доступ к унаследованному дескриптору файла в дочернем элементе будет обращаться к тому же файлу, что и к исходному дескриптору файла в родительском процессе.
Обратите внимание, что после разветвления у вас изначально есть две копии одного и того же процесса, которые отличаются только возвращаемым значением от вызова fork (родительский элемент получает PID дочернего элемента, дочерний - 0). Обычно после разветвления следует a, exec
чтобы заменить одну из копий другим исполняемым файлом. Дескрипторы открытого файла переживают это exec. Также обратите внимание, что перед exec процесс может выполнять другие манипуляции (например, закрытие файлов, которые новый процесс не должен получить, или открытие других файлов).
Безымянные трубы
Безымянный канал - это просто пара файловых дескрипторов, созданных по запросу ядра, так что все, что записано в первый файловый дескриптор, передается второму. Наиболее часто используется для труб конструкции foo | bar
из bash
, где стандартный вывод foo
заменяется на запись части трубы, а стандартный ввод заменяет по считанной части. Стандартный ввод и стандартный вывод - это только первые две записи в таблице файлов (записи 0 и 1; 2 - стандартная ошибка), и поэтому замена их означает просто переписать эту запись таблицы с данными, соответствующими другому дескриптору файла (опять же, фактическая реализация может отличаться). Поскольку процесс не может получить доступ к таблице напрямую, для этого есть функция ядра.
Процесс замещения
Теперь у нас есть все вместе, чтобы понять, как работает процесс замены:
echo
процесса. Дочерний процесс (который является точной копией исходного bash
процесса) закрывает конец чтения канала и заменяет свой собственный стандартный вывод концом записи канала. Учитывая, что echo
это встроенная оболочка, она bash
может сэкономить exec
вызов, но в любом случае это не имеет значения (встроенная оболочка также может быть отключена, в этом случае она исполняется /bin/echo
).<(echo 1)
псевдо-файловой /dev/fd
ссылкой на конец чтения безымянного канала./dev/fd/
. Поскольку соответствующий дескриптор файла все еще открыт, он все еще соответствует концу чтения канала. Поэтому, если программа PHP открывает данный файл для чтения, она фактически создает second
дескриптор файла для конца чтения безымянного канала. Но это не проблема, это можно прочитать с любого.echo
команды, которая идет в конец записи того же канала.php
сценарии, но php
плохо обращаетесь с трубами . Также, учитывая команду cat <(echo test)
, странная вещь в том, что bash
разветвляется один раз cat
, но дважды echo test
.
Заимствование из celtschk
ответа, /dev/fd
является символической ссылкой на /proc/self/fd
. И /proc
это псевдофайловая система, которая представляет информацию о процессах и другую системную информацию в виде иерархической файловой структуры. Файлы /dev/fd
соответствуют файлам, которые открываются процессом и имеют дескриптор файла в качестве своих имен, а сами файлы - их цели. Открытие файла /dev/fd/N
эквивалентно дублированию дескриптора N
(при условии, что дескриптор N
открыт).
И вот результаты моего исследования того, как это работает ( strace
вывод избавлен от ненужных деталей и изменен, чтобы лучше выразить, что происходит):
$ 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)
По сути, bash
создает канал и передает его концы своим дочерним элементам как дескрипторы файлов (конец чтения и конец 1.out
записи 2.out
). И передает read end как параметр командной строки 1.out
( /dev/fd/63
). Этот способ 1.out
способен открыть /dev/fd/63
.