Как в bash реализована замена процесса?

12

Я исследовал другой вопрос , когда понял, что не понимаю, что происходит под капотом, что это за /dev/fd/*файлы и как их могут открывать дочерние процессы.

х-юри
источник
Разве на этот вопрос нет ответа?
17

Ответы:

21

Ну, в этом есть много аспектов.

Файловые дескрипторы

Для каждого процесса ядро ​​поддерживает таблицу открытых файлов (ну, это может быть реализовано по-разному, но, поскольку вы все равно не можете ее увидеть, вы можете просто предположить, что это простая таблица). Эта таблица содержит информацию о том, в каком файле он находится / где его можно найти, в каком режиме вы его открыли, в какой позиции вы сейчас читаете / записываете и что еще нужно для фактического выполнения операций ввода-вывода с этим файлом. Теперь процессу никогда не удается прочитать (или даже записать) эту таблицу. Когда процесс открывает файл, он возвращает так называемый дескриптор файла. Что просто указатель в таблицу.

Каталог /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 - стандартная ошибка), и поэтому замена их означает просто переписать эту запись таблицы с данными, соответствующими другому дескриптору файла (опять же, фактическая реализация может отличаться). Поскольку процесс не может получить доступ к таблице напрямую, для этого есть функция ядра.

Процесс замещения

Теперь у нас есть все вместе, чтобы понять, как работает процесс замены:

  1. Процесс bash создает безымянный канал для связи между двумя процессами, созданными позже.
  2. Баш вилки для echoпроцесса. Дочерний процесс (который является точной копией исходного bashпроцесса) закрывает конец чтения канала и заменяет свой собственный стандартный вывод концом записи канала. Учитывая, что echoэто встроенная оболочка, она bashможет сэкономить execвызов, но в любом случае это не имеет значения (встроенная оболочка также может быть отключена, в этом случае она исполняется /bin/echo).
  3. Bash (оригинал, родитель) заменяет выражение <(echo 1)псевдо-файловой /dev/fdссылкой на конец чтения безымянного канала.
  4. Bash execs для процесса PHP (обратите внимание, что после разветвления мы все еще внутри [копия] bash). Новый процесс закрывает унаследованный конец записи безымянного канала (и выполняет некоторые другие подготовительные шаги), но оставляет конец чтения открытым. Затем он выполнил PHP.
  5. Программа PHP получает имя в /dev/fd/. Поскольку соответствующий дескриптор файла все еще открыт, он все еще соответствует концу чтения канала. Поэтому, если программа PHP открывает данный файл для чтения, она фактически создает secondдескриптор файла для конца чтения безымянного канала. Но это не проблема, это можно прочитать с любого.
  6. Теперь программа PHP может читать конец чтения канала через новый дескриптор файла и, таким образом, получать стандартный вывод echoкоманды, которая идет в конец записи того же канала.
celtschk
источник
Конечно, я ценю ваши усилия. Но я хотел указать на несколько вопросов. Во-первых, вы говорите о phpсценарии, но phpплохо обращаетесь с трубами . Также, учитывая команду cat <(echo test), странная вещь в том, что bashразветвляется один раз cat, но дважды echo test.
x-yuri
13

Заимствование из 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.

х-юри
источник