Примітка. Відповідь відображає моє власне розуміння цих механізмів, накопичене під час дослідження та читання відповідей однолітками на цьому веб-сайті та unix.stackexchange.com , і оновлюватиметься з часом. Не соромтеся задавати питання або запропонувати в коментарях покращення. Я також пропоную вам спробувати побачити, як функціонують систематичні виклики в оболонці з strace
командою. Крім того, будь ласка, не лякайте поняття внутрішніх або системних дзвінків - вам не потрібно знати або вміти ними користуватися, щоб зрозуміти, як оболонка робить речі, але вони, безумовно, допомагають зрозуміти.
TL; DR
|
Труби не асоціюються із записом на диску, тому не мають вхідного числа файлової системи диска (але мають inode у віртуальній файловій системі pipefs у просторі ядра), але перенаправлення часто включають файли, які мають записи диска і тому мають відповідні inode.
- pipe не
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 . Цитата:
Щоб висловити свої найсильніші проблеми:
- Ми повинні мати деякі способи з'єднання таких програм, як садовий шланг - вкрутити в інший сегмент, коли це стає необхідним, щоб масажувати дані іншим способом. Це також шлях ІО.
Що очевидно, це те, що в той час програми були здатні записувати на диск, однак це було неефективно, якщо вихід був великим. Процитуючи пояснення Брайана Керніган у відео 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()
syscall. Бо df
в нашому прикладі, скажімо, pipefd[1]
є №4, тому оболонка зробить дитину, зробіть dup2(4,1)
(пам’ятаєте мій cp
приклад?), А потім execve()
дійсно запустіть df
. Природно,df
буде успадковано дескриптор файлу №1, але він не буде знати, що він більше не вказує на термінал, а насправді fd №4, що є фактично кінцем запису труби. Природно, те ж саме відбуватиметься, за grep 'foo'
винятком різної кількості дескрипторів файлів.
Тепер цікаве запитання: чи можемо ми зробити також труби, які перенаправляють fd # 2, а не просто fd №1? Так, насправді це те, що |&
робить у баш. Стандарт 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, труби мають номер inode у відповідній файловій системі:
# 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
? Чому додатковий оператор|
?