Примечание. Ответ отражает мое собственное понимание этих механизмов на сегодняшний день, накопленное в ходе исследований и прочтения ответов коллегами на этом сайте и unix.stackexchange.com , и будет обновляться с течением времени. Не стесняйтесь задавать вопросы или предлагать улучшения в комментариях. Я также предлагаю вам попытаться увидеть, как системные вызовы работают в оболочке с помощью strace
команды. Также, пожалуйста, не пугайтесь понятия внутренних систем или системных вызовов - вам не нужно знать или использовать их, чтобы понять, как работает оболочка, но они определенно помогают пониманию.
TL; DR
|
каналы не связаны с записью на диске, поэтому не имеют номера инода дисковой файловой системы (но имеют иноды в виртуальной файловой системе pipefs в пространстве ядра), но перенаправления часто включают файлы, которые имеют записи на диске и, следовательно, имеют соответствующие инод.
- каналы не
lseek()
«способны», поэтому команды не могут прочитать некоторые данные и затем перемотать их назад, но когда вы перенаправляете >
или, <
как правило, это файл, который lseek()
может быть объектом, команды могут перемещаться по своему усмотрению.
- перенаправления - это манипуляции с файловыми дескрипторами, которых может быть много; у каналов есть только два файловых дескриптора - один для левой команды и один для правой команды
- перенаправление на стандартные потоки и каналы буферизируется.
- трубы почти всегда включают в себя разветвление и, следовательно, участвуют пары процессов; перенаправления - не всегда, хотя в обоих случаях результирующие файловые дескрипторы наследуются подпроцессами.
- каналы всегда соединяют файловые дескрипторы (пара), перенаправления - либо используют путь, либо файловые дескрипторы.
- каналы - это метод межпроцессного взаимодействия, а перенаправления - это просто манипуляции с открытыми файлами или файловыми объектами.
- оба используют
dup2()
системные вызовы под капотом, чтобы предоставить копии файловых дескрипторов, где происходит фактический поток данных.
- перенаправления могут быть применены «глобально» с помощью
exec
встроенной команды (см. это и это ), поэтому, если вы сделаете это, exec > output.txt
каждая команда будет писать output.txt
с этого момента. |
Каналы применяются только для текущей команды (что означает либо простую команду, либо подобную подоболочке, seq 5 | (head -n1; head -n2)
либо составную команду .
Когда перенаправление выполняется для файлов, такие вещи, как echo "TEST" > file
и echo "TEST" >> file
оба, используют open()
syscall для этого файла ( см. Также ) и получают от него дескриптор файла, чтобы передать его dup2()
. Трубы |
только использовать pipe()
и dup2()
системный вызов.
Что касается выполняемых команд, каналы и перенаправление - это не более, чем файловые дескрипторы - файловые объекты, в которые они могут писать вслепую или манипулировать ими внутренне (что может привести к непредвиденным действиям; apt
например, имеет тенденцию даже не писать в stdout). если он знает, что есть перенаправление).
Введение
Чтобы понять, как эти два механизма различаются, необходимо понять их основные свойства, историю этих двух механизмов и их корни в языке программирования Си. На самом деле, также важно знать, что такое файловые дескрипторы, как dup2()
и как pipe()
работают системные вызовы lseek()
. Оболочка предназначена для того, чтобы сделать эти механизмы абстрактными для пользователя, но копание глубже, чем абстракция, помогает понять истинную природу поведения оболочки.
Происхождение перенаправлений и труб
Согласно статье Денниса Ритча « Пророческие петроглифы» , трубы возникли из внутренней записки 1964 года Малкольма Дугласа Макилроя , когда они работали над операционной системой Multics . Цитата:
Чтобы выразить мои самые сильные опасения в двух словах:
- У нас должно быть несколько способов подключения таких программ, как садовый шланг - вкручивайте другой сегмент, когда это становится необходимым, когда необходимо массировать данные другим способом. Это также способ IO.
Очевидно, что в то время программы могли записывать на диск, однако это было неэффективно, если выходной объем был большим. Цитировать объяснение Брайана Кернигана в видео Unix Pipeline :
Во-первых, вам не нужно писать одну большую массивную программу - у вас есть более мелкие программы, которые могут уже выполнять часть работы ... Другое - то, что возможно, что объем обрабатываемой вами информации не будет соответствовать вы сохранили его в файле ... потому что помните, мы вернулись во времена, когда диски на этих вещах имели, если вам повезло, один или два мегабайта данных ... Таким образом, конвейер никогда не должен был создавать весь вывод ,
Таким образом, концептуальное различие очевидно: каналы - это механизм, заставляющий программы общаться друг с другом. Перенаправления - это способ записи в файл на базовом уровне. В обоих случаях оболочка облегчает эти две вещи, но под капотом происходит много всего.
Идем глубже: системные вызовы и внутренняя работа оболочки
Начнем с понятия файлового дескриптора . Файловые дескрипторы описывают в основном открытый файл (будь то файл на диске, или в памяти, или анонимный файл), который представлен целым числом. Два стандартных потока данных (stdin, stdout, stderr) являются файловыми дескрипторами 0,1 и 2 соответственно. Откуда они ? Что ж, в командах оболочки дескрипторы файлов наследуются от их родительской оболочки. И это в целом верно для всех процессов - дочерний процесс наследует файловые дескрипторы родителя. Для демонов обычно закрывают все унаследованные файловые дескрипторы и / или перенаправляют в другие места.
Вернуться к перенаправлению. Что это на самом деле? Это механизм, который указывает оболочке подготовить файловые дескрипторы для команды (поскольку перенаправления выполняются оболочкой до запуска команды) и указывает их там, где предложил пользователь. Стандартное определение выходного перенаправления
[n]>word
Что [n]
есть номер дескриптора файла. Когда вы делаете echo "Something" > /dev/null
число 1 подразумевается там, и echo 2> /dev/null
.
Под капотом это делается путем дублирования дескриптора файла через dup2()
системный вызов. Давайте возьмем df > /dev/null
. Оболочка создаст дочерний процесс df
, который будет запущен, но перед этим он откроется /dev/null
как дескриптор файла № 3 и dup2(3,1)
будет выпущен, что создаст копию дескриптора файла 3, а копия будет 1. Вы знаете, что у вас есть два файла file1.txt
и file2.txt
, а когда вы это сделаете, у cp file1.txt file2.txt
вас будет два одинаковых файла, но вы можете управлять ими независимо? Здесь происходит то же самое. Часто вы можете видеть, что перед запуском bash
будет dup(1,10)
создан дескриптор файла копии # 1, который stdout
(и эта копия будет fd # 10), чтобы восстановить его позже. Важно отметить, что при рассмотрении встроенных команд(которые являются частью самой оболочки и не имеют файлов внутри /bin
или где-либо еще) или простые команды в неинтерактивной оболочке , оболочка не создает дочерний процесс.
И тогда у нас есть такие вещи, как [n]>&[m]
и [n]&<[m]
. Это дублирование файловых дескрипторов, механизм которых тот же, что и dup2()
только сейчас, в синтаксисе оболочки, который удобно доступен для пользователя.
Одна из важных вещей, которые следует отметить в отношении перенаправления, заключается в том, что их порядок не фиксирован, но важен для интерпретации оболочкой того, что хочет пользователь. Сравните следующее:
# Make copy of where fd 2 points , then redirect fd 2
$ ls -l /proc/self/fd/ 3>&2 2> /dev/null
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
lrwx------ 1 runner user 64 Sep 13 00:08 3 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/29/fd
# redirect fd #2 first, then clone it
$ ls -l /proc/self/fd/ 2> /dev/null 3>&2
total 0
lrwx------ 1 user user 64 Sep 13 00:08 0 -> /dev/pts/0
lrwx------ 1 user user 64 Sep 13 00:08 1 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:08 2 -> /dev/null
l-wx------ 1 user user 64 Sep 13 00:08 3 -> /dev/null
lr-x------ 1 user user 64 Sep 13 00:08 4 -> /proc/31/fd
Их практическое использование в сценариях оболочки может быть универсальным:
и многие другие.
Сантехника с pipe()
иdup2()
Так как же создаются трубы? Через pipe()
syscall , который будет принимать в качестве входных данных массив (он же список), вызываемый pipefd
из двух элементов типа int
(целое число). Эти два целых числа являются файловыми дескрипторами. pipefd[0]
Будет прочитать конец трубы и pipefd[1]
будет конец записи. Таким образом df | grep 'foo'
, grep
получит копию pipefd[0]
и df
получит копию pipefd[1]
. Но как ? Конечно, с волшебством dup2()
системного вызова. Например, df
в нашем примере, скажем, pipefd[1]
есть # 4, поэтому оболочка создаст дочерний элемент, сделайте dup2(4,1)
(помните мой cp
пример?), А затем execve()
действительно запустите df
. Естественно,df
будет наследовать файловый дескриптор # 1, но не будет знать, что он больше не указывает на терминал, а на самом деле fd # 4, который на самом деле является концом записи канала. Естественно, то же самое произойдет, grep 'foo'
за исключением разного числа файловых дескрипторов.
Теперь интересный вопрос: можем ли мы создавать каналы, которые также перенаправляют fd # 2, а не только fd # 1? Да, на самом деле это то, что |&
делает в Bash. Стандарт POSIX требует, чтобы командный язык оболочки поддерживал df 2>&1 | grep 'foo'
синтаксис для этой цели, но также bash
делает |&
это.
Важно отметить, что каналы всегда имеют дело с файловыми дескрипторами. Существует FIFO
или именованный канал , который имеет имя файла на диске и позволяет использовать его как файл, но ведет себя как канал. Но |
типы каналов - это то, что известно как анонимный канал - у них нет имени файла, потому что на самом деле это просто два объекта, соединенных вместе. Тот факт, что мы не имеем дело с файлами, также имеет важное значение: каналы не lseek()
способны. Файлы в памяти или на диске являются статическими - программы могут использовать lseek()
системный вызов для перехода к байту 120, затем обратно к байту 10, а затем переслать весь путь до конца. Трубы не являются статичными - они последовательные, и поэтому вы не можете перематывать данные, полученные с их помощью.lseek()
, Это то, что заставляет некоторые программы знать, читают ли они из файла или из конвейера, и, таким образом, они могут вносить необходимые корректировки для эффективной производительности; другими словами, он prog
может обнаружить, если я делаю cat file.txt | prog
или prog < input.txt
. Настоящий пример работы - это хвост .
Два других очень интересных свойства конвейеров - это то, что у них есть буфер, который в Linux составляет 4096 байт , и у них фактически есть файловая система, как определено в исходном коде Linux ! Они не просто объект для передачи данных, они сами представляют собой структуру данных! Фактически, поскольку существует файловая система pipefs, которая управляет как каналами, так и FIFO, каналы имеют номер инода в соответствующей файловой системе:
# Stdout of ls is wired to pipe
$ ls -l /proc/self/fd/ | cat
lrwx------ 1 user user 64 Sep 13 00:02 0 -> /dev/pts/0
l-wx------ 1 user user 64 Sep 13 00:02 1 -> pipe:[15655630]
lrwx------ 1 user user 64 Sep 13 00:02 2 -> /dev/pts/0
lr-x------ 1 user user 64 Sep 13 00:02 3 -> /proc/22/fd
# stdin of ls is wired to pipe
$ true | ls -l /proc/self/fd/0
lr-x------ 1 user user 64 Sep 13 03:58 /proc/self/fd/0 -> 'pipe:[54741]'
В Linux каналы являются однонаправленными, как перенаправление. В некоторых Unix-подобных реализациях существуют двунаправленные каналы. Хотя, используя магию сценариев оболочки, вы можете создавать двунаправленные каналы и в Linux .
Смотрите также:
thing1 > temp_file && thing2 < temp_file
чтобы сделать проще с трубами. Но почему бы не использовать>
оператор для этого, например,thing1 > thing2
для командthing1
иthing2
? Почему дополнительный оператор|
?