TL; DR : Оскільки це оптимальний метод для створення нових процесів і збереження контролю в інтерактивній оболонці
fork () необхідний для процесів і труб
Щоб відповісти на конкретну частину цього питання, якби grep blabla foo
викликати його exec()
безпосередньо через батьківську систему, батьківство захопило б існувати, а його PID з усіма ресурсами буде перейнятоgrep blabla foo
.
Однак поговоримо загалом про exec()
і fork()
. Ключова причина такої поведінки полягає в тому, що fork()/exec()
це стандартний метод створення нового процесу на Unix / Linux, і це не є специфічним башком; цей метод існує з самого початку і під цим впливом впливає вже існуюча операційна система того часу. Дещо перефразовуючи відповідь goldilocks на пов’язане питання, fork()
створити новий процес простіше, оскільки ядро менше роботи, ніж виділення ресурсів, і багато властивостей (таких, як дескриптори файлів, оточення тощо) - все може успадковується від батьківського процесу (у цьому випадку відbash
).
По-друге, що стосується інтерактивних оболонок, ви не можете запустити зовнішню команду без розгортання. Для запуску виконуваного файлу, який живе на диску (наприклад, /bin/df -h
), потрібно викликати одну з exec()
сімейних функцій, наприклад execve()
, яка замінить батьківський новий процес, перейняти його PID та наявні дескриптори файлів тощо. Що стосується інтерактивної оболонки, ви хочете, щоб елемент керування повернувся до користувача та дозволив батьківській інтерактивній оболонці продовжувати працювати. Таким чином, найкращий спосіб - створити підпроцес через fork()
, і нехай цей процес буде перейнятий через execve()
. Отже, інтерактивна оболонка PID 1156 породила б дитину за fork()
допомогою PID 1157, потім зателефонувала execve("/bin/df",["df","-h"],&environment)
, що змушує /bin/df -h
запускати PID 1157. Тепер оболонці залишається лише чекати, поки процес вийде і поверне йому управління.
У випадку, коли вам доведеться створити трубу між двома або більше командами, скажімо df | grep
, вам потрібен спосіб створити два дескриптори файлів (це читати і записувати кінець труби, що надходять з pipe()
syscall), то якимось чином нехай два нові процеси успадкують їх. Це робиться для розгортання нового процесу, а потім копіювання кінця запису труби за допомогою dup2()
виклику на його stdout
ака fd 1 (так, якщо кінець запису є fd 4, ми робимо dup2(4,1)
). Коли трапляється exec()
нерест, df
дочірній процес нічого не подумає stdout
і запише до нього, не усвідомлюючи (якщо він активно не перевіряє), що його результат насправді йде на дудку. Те ж саме відбувається і з grep
, за винятком ми fork()
, взяти читання кінця труби з дескриптором 3 і dup(3,0)
перед нерестом grep
зexec()
. Весь цей час батьківський процес все ще існує, очікуючи відновити контроль, коли конвеєр буде завершено.
У випадку вбудованих команд, як правило, оболонки немає fork()
, за винятком source
команди. Абонентів потрібно fork()
.
Словом, це необхідний і корисний механізм.
Недоліки розгортання та оптимізації
Тепер це відрізняється від неінтерактивних оболонок , таких як bash -c '<simple command>'
. Незважаючи на fork()/exec()
те, що є оптимальним методом, коли вам доведеться обробляти багато команд, це марна трата ресурсів, коли у вас є лише одна команда. Щоб цитувати Стефана Шазеласа з цієї публікації :
Форкінг коштує дорого, за час процесора, пам'яті, виділених дескрипторів файлів ... Процес оболонки, що лежить просто чекати іншого процесу перед виходом, - це лише марнотрата ресурсів. Крім того, це ускладнює правильний звіт про стан виходу окремого процесу, який би виконував команду (наприклад, коли процес загинув).
Тому багато оболонок (не просто bash
) використовують exec()
для того, bash -c ''
щоб дозволити перейняти цю єдину просту команду. І саме з зазначених вище причин краще мінімізувати конвеєри в сценарії оболонок. Часто можна побачити початківців, що роблять щось подібне:
cat /etc/passwd | cut -d ':' -f 6 | grep '/home'
Звичайно, це буде fork()
3 процеси. Це простий приклад, але врахуйте великий файл у діапазоні гігабайт. Це було б набагато ефективніше з одним процесом:
awk -F':' '$6~"/home"{print $6}' /etc/passwd
Втрата ресурсів насправді може бути формою атаки відмови в службі, і, зокрема, вилкові бомби створюються за допомогою оболонок, які називають себе в конвеєрі, який розщеплює кілька копій самих себе. Сьогодні це зменшується через обмеження максимальної кількості процесів у групах на systemd , які Ubuntu також використовує з версії 15.04.
Звичайно, це не означає, що роздвоєння просто погано. Це все-таки корисний механізм, як обговорювалося раніше, але у випадку, коли ви можете піти з меншими процесами і, відповідно, меншими ресурсами і, таким чином, кращою продуктивністю, вам слід уникати, fork()
якщо це можливо.
Дивись також