Як реалізується заміна процесу в bash?


12

Я досліджував інше питання , коли зрозумів, що не розумію, що відбувається під кришкою, що це за /dev/fd/*файли і як дочірні процеси можуть їх відкрити.


Чи не на це питання відповіли?
phk

Відповіді:


21

Що ж, у цьому є багато аспектів.

Дескриптори файлів

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

Каталог /dev/fdта його зміст

На Linux dev/fdнасправді символічне посилання на /proc/self/fd. /procце псевдофайлова система, в якій ядро ​​відображає кілька внутрішніх структур даних, до яких можна отримати доступ до API файлу (тому вони просто виглядають як звичайні файли / каталоги / посилання на програми). Тим більше, що є інформація про всі процеси (саме це і дало їй назву). Символічне посилання /proc/selfзавжди посилається на каталог, пов'язаний з поточно запущеним процесом (тобто процесом, який його вимагає; тому різні процеси побачать різні значення). У каталозі процесу є підкаталогfd який для кожного відкритого файлу містить символічне посилання, ім'я якого - лише десяткове представлення дескриптора файлу (індекс у таблиці файлів процесу, див. попередній розділ), а цільовим завданням є той файл, якому він відповідає.

Дескриптори файлів під час створення дочірніх процесів

Дочірній процес створюється a fork. A forkробить копію дескрипторів файлів, це означає, що створений дочірній процес має той самий список відкритих файлів, що і батьківський процес. Тому, якщо дитина не закриє один із відкритих файлів, доступ до спадкового дескриптора файлів у дитині матиме доступ до того самого файлу, що і вихідний дескриптор файлу в батьківському процесі.

Зауважте, що після вилки ви спочатку маєте дві копії одного і того ж процесу, які відрізняються лише зворотним значенням від виклику вилки (батько отримує PID дитини, дитина отримує 0). Зазвичай за вилкою слідує a, 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команди, що йде до кінця запису тієї ж труби.

Звичайно, я ціную ваші зусилля. Але я хотів вказати на кілька питань. По-перше, ви говорите про 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.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.