Будь ласка, поясніть функцію exec () та її сімейство


98

Яка exec()функція та її родина? Чому використовується ця функція і як вона працює?

Будь ласка, будь-хто пояснить ці функції.


4
Спробуйте прочитати Стівенса ще раз і поясніть, що саме ви не розумієте.
vlabrecque

Відповіді:


244

Спрощено, в UNIX ви маєте поняття процесів і програм. Процес - це середовище, в якому виконується програма.

Проста ідея «моделі виконання» UNIX полягає в тому, що ви можете зробити дві операції.

Перший - це fork(), який створює абсолютно новий процес, що містить дублікат (переважно) поточної програми, включаючи її стан. Існує кілька відмінностей між двома процесами, які дозволяють їм з’ясувати, хто є батьком, а хто дитиною.

Другий - до exec(), який замінює програму в поточному процесі новою програмою.

З цих двох простих операцій можна побудувати всю модель виконання UNIX.


Щоб додати дещо більше до вищезазначеного:

Використання fork()та exec()ілюструє дух UNIX, оскільки він забезпечує дуже простий спосіб для запуску нових процесів.

fork()Виклик робить майже дублікат поточного процесу, ідентичний майже в усіх відношеннях (Не все скопійовано, наприклад, обмеження використання ресурсів в деяких реалізаціях, але ідея полягає в тому, щоб створити як можна ближче копію наскільки це можливо). Лише один процес викликає, fork() але два процеси повертаються з цього дзвінка - звучить химерно, але насправді досить елегантно

Новий процес (який називається дочірнім) отримує інший ідентифікатор процесу (PID) і має PID старого процесу (батьківського) як батьківський PID (PPID).

Оскільки зараз два процеси виконують абсолютно однаковий код, вони повинні мати змогу визначити, який саме - код повернення fork()надає цю інформацію - дочірній отримує 0, батько отримує PID дочірнього (якщо fork()не вдається, ні дитина створюється, а батько отримує код помилки).

Таким чином, батько знає PID дитини і може спілкуватися з ним, вбивати його, чекати його тощо (дитина завжди може знайти свій батьківський процес із викликом getppid()).

exec()Виклик замінює весь вміст поточного процесу з новою програмою. Він завантажує програму в поточний простір процесу та запускає її з точки входу.

Отже, fork()і exec()часто використовуються послідовно для запуску нової програми як дочірнього поточного процесу. Зазвичай оболонки роблять це, коли ви намагаєтеся запустити програму, наприклад find- оболонка розгалужується, потім дочірня findпрограма завантажує в пам’ять, налаштовуючи всі аргументи командного рядка, стандартний ввід / вивід тощо.

Але їх не потрібно використовувати разом. Цілком прийнятно, щоб програма дзвонила fork()без наступного, exec()якщо, наприклад, програма містить і батьківський, і дочірній код (потрібно бути обережним, що ви робите, кожна реалізація може мати обмеження).

Це використовувалось досить багато (і дотепер) для демонів, які просто прослуховують порт TCP і розгалужують копію себе для обробки конкретного запиту, поки батько повертається до прослуховування. У цій ситуації програма містить як батьківський, так і дочірній код.

Подібним чином програми, які знають, що вони закінчили і просто хочуть запустити іншу програму, не потрібно fork(), exec()і то wait()/waitpid()для дитини. Вони можуть просто завантажити дитину безпосередньо в поточний процес exec().

Деякі реалізації UNIX мають оптимізовану версію, fork()яка використовує те, що вони називають copy-on-write. Це фокус, щоб затримати копіювання простору процесів, fork()доки програма не спробує щось змінити в цьому просторі. Це корисно для тих програм, які використовують лише, fork()а не exec()тим, що їм не потрібно копіювати весь простір процесу. У Linux, fork()лише копіюючи таблиці сторінок і нову структуру завдань, exec()буде виконувати грубу роботу "розділення" пам'яті двох процесів.

Якщо exec це називається наступне fork(і це те , що відбувається , в основному), що призводить до запису в простір процесу і потім копіюється для дочірнього процесу, перед внесенням змін допускається.

Linux також має vfork(), ще більш оптимізовану, яка поділяє майже все між двома процесами. Через це існують певні обмеження щодо того, що може робити дитина, і батько зупиняється, поки дитина не зателефонує exec()або _exit().

Батька слід зупинити (і дочірній заборонено повертатися з поточної функції), оскільки два процеси навіть мають однаковий стек. Це трохи ефективніше для класичного випадку використання, за fork()яким слід негайно exec().

Зверніть увагу , що існує ціле сімейство execвикликів ( execl, execle, execveі так далі) , але execв контексті тут означає будь-яку з них.

Наступна схема ілюструє типову fork/execоперацію, коли bashоболонка використовується для переліку каталогу за допомогою lsкоманди:

+--------+
| pid=7  |
| ppid=4 |
| bash   |
+--------+
    |
    | calls fork
    V
+--------+             +--------+
| pid=7  |    forks    | pid=22 |
| ppid=4 | ----------> | ppid=7 |
| bash   |             | bash   |
+--------+             +--------+
    |                      |
    | waits for pid 22     | calls exec to run ls
    |                      V
    |                  +--------+
    |                  | pid=22 |
    |                  | ppid=7 |
    |                  | ls     |
    V                  +--------+
+--------+                 |
| pid=7  |                 | exits
| ppid=4 | <---------------+
| bash   |
+--------+
    |
    | continues
    V

12
Дякую за таке розгорнуте пояснення :)
Faizan

2
Дякуємо за посилання на оболонку з програмою пошуку. Саме те, що мені потрібно було зрозуміти.
Користувач

Чому execутиліта використовується для перенаправлення поточного вводу-виводу поточного процесу? Як "нульовий" випадок, що виконує exec без аргументу, звик до цієї конвенції?
Рей

@Ray, я завжди думав про це як про природне продовження. Якщо ви думаєте , execяк засіб , щоб замінити поточну програму (оболонка) в цьому процесі з іншого, то НЕ вказавши , що інші програми , щоб замінити його може просто означати , що ви НЕ хочете , щоб замінити його.
paxdiablo

Я розумію, що ви маєте на увазі, якщо під "природним продовженням" ви маєте на увазі щось на зразок "органічного зростання". Здається, перенаправлення було б додано для підтримки функції заміни програми, і я бачу, що така поведінка залишається у виродженому випадку execвиклику без програми. Але це трохи дивно в цьому сценарії, оскільки первісна корисність перенаправлення на нову програму - програму, яка насправді може бути використана exec- зникає, і у вас є корисний артефакт, що перенаправляє поточну програму - яка не використовується execабо не запускається будь-яким способом - замість цього.
Рей

36

Функції в сім'ї exec () мають різну поведінку:

  • l: аргументи передаються як список рядків до main ()
  • v: аргументи передаються як масив рядків до main ()
  • p: шлях / с для пошуку нової запущеної програми
  • e: оточуюче може вказати середовище

Ви можете їх змішувати, тому у вас є:

  • int execl (const char * path, const char * arg, ...);
  • int execlp (файл const char *, const char * arg, ...);
  • int execle (const char * path, const char * arg, ..., char * const envp []);
  • int execv (const char * path, char * const argv []);
  • int execvp (файл const char *, char * const argv []);
  • int execvpe (файл const char *, char * const argv [], char * const envp []);

Для всіх них початковим аргументом є назва файлу, який має бути виконаний.

Для отримання додаткової інформації прочитайте сторінку керівництва exec (3) :

man 3 exec  # if you are running a UNIX system

1
Цікаво, що ви пропустили execve()зі свого списку, який визначений POSIX, і додали execvpe(), який не визначений POSIX (здебільшого з історичних причин; він завершує набір функцій). В іншому випадку корисне пояснення правила іменування для сім’ї - корисний додаток до paxdiablo - відповідь, яка пояснює більше про роботу функцій.
Джонатан Леффлер,

І, на ваш захист, я бачу, що сторінка керівництва Linux для execvpe()(та ін) не містить переліку execve(); він має свою окрему сторінку довідок (принаймні на Ubuntu 16.04 LTS) - різниця полягає в тому, що інші exec()функції сімейства перелічені в розділі 3 (функції), тоді execve()як перелічені в розділі 2 (системні дзвінки). В основному всі інші функції в сім’ї реалізуються з точки зору заклику до execve().
Джонатан Леффлер,

18

execСімейство функцій зробити процес виконання іншої програми, замінивши стару програму вона була запущена. Тобто, якщо ви телефонуєте

execl("/bin/ls", "ls", NULL);

тоді lsпрограма виконується з ідентифікатором процесу, поточним робочим каталогом та користувачем / групою (права доступу) процесу, який викликав execl. Згодом оригінальна програма вже не працює.

Для запуску нового процесу використовується forkсистемний виклик. Щоб виконати програму без заміни оригіналу, потрібно fork, тоді exec.


Дякую, що це було дійсно корисно. Зараз я роблю проект, який вимагає від нас використання exec (), і ваш опис закріпив моє розуміння.
TwilightSparkleTheGeek

7

що таке функція exec та її родина.

execФункція сім'я всі функції , які використовуються для виконання файлу, таких як execl, execlp, execle, execvі execvp.Оніте все оболонки для execveі забезпечують різні способи виклику його.

чому використовується ця функція

Функції Exec використовуються, коли потрібно виконати (запустити) файл (програму).

і як це працює.

Вони працюють, перезаписуючи поточний образ процесу на той, який ви запустили. Вони замінюють (закінчуючи) поточно запущений процес (той, що викликав команду exec) новим процесом, який запущений.

Детальніше: див. Це посилання .


7

execчасто використовується разом із fork, про що я бачив, про що ви також запитували, тому я обговорю це з урахуванням цього.

execперетворює поточний процес в іншу програму. Якщо ви коли-небудь спостерігали за Доктором Хто, то це як коли він відроджується - його старе тіло замінюється новим.

Спосіб, що це відбувається з вашою програмою, execполягає в тому, що багато ресурсів, які перевіряє ядро ​​ОС, перевіряє, чи execвиконується поточним користувачем (ідентифікатор користувача процесу) файл, якому ви передаєте як аргумент програми (перший аргумент) здійснення execвиклику), і якщо це так, то він замінює відображення віртуальної пам'яті поточного процесу віртуальною пам’яттю новим процесом і копіює дані argvта envpдані, передані під час execвиклику, в область цієї нової карти віртуальної пам'яті. Тут може трапитися ще кілька речей, але файли, відкриті для програми, яка викликала exec, все одно будуть відкриті для нової програми, і вони матимуть той самий ідентифікатор процесу, але програма, яка викликала exec, припиниться (якщо exec не вдалося).

Причина , по якій це робиться таким чином, що шляхом поділу роботи на нову програму в два етапи , як це ви можете зробити деякі речі між двома кроками. Найпоширеніше, що потрібно зробити, це переконатися, що нова програма відкриває певні файли як певні дескриптори файлів. (тут пам’ятайте, що дескриптори файлів - це не те саме FILE *, але це intзначення, про які знає ядро). Роблячи це, ви можете:

int X = open("./output_file.txt", O_WRONLY);

pid_t fk = fork();
if (!fk) { /* in child */
    dup2(X, 1); /* fd 1 is standard output,
                   so this makes standard out refer to the same file as X  */
    close(X);

    /* I'm using execl here rather than exec because
       it's easier to type the arguments. */
    execl("/bin/echo", "/bin/echo", "hello world");
    _exit(127); /* should not get here */
} else if (fk == -1) {
    /* An error happened and you should do something about it. */
    perror("fork"); /* print an error message */
}
close(X); /* The parent doesn't need this anymore */

Це досягає запуску:

/bin/echo "hello world" > ./output_file.txt

з командної оболонки.


5

Коли процес використовує fork (), він створює дублікат своєї копії, і цей дублікат стає дочірнім процесом. Форк () реалізований за допомогою системного виклику clone () в linux, який двічі повертається з ядра.

  • Ненульове значення (ідентифікатор процесу дочірнього) повертається батьківському елементу.
  • Значення нуля повертається дочірній організації.
  • Якщо дитина не створена успішно через будь-які проблеми, такі як недостатня пам'ять, -1 повертається у форк ().

Давайте розберемося в цьому на прикладі:

pid = fork(); 
// Both child and parent will now start execution from here.
if(pid < 0) {
    //child was not created successfully
    return 1;
}
else if(pid == 0) {
    // This is the child process
    // Child process code goes here
}
else {
    // Parent process code goes here
}
printf("This is code common to parent and child");

У цьому прикладі ми припустили, що exec () не використовується всередині дочірнього процесу.

Але батько та дочірній матеріал відрізняються за деякими атрибутами друкованої плати (блоку управління процесом). Це:

  1. PID - І дочірній, і батьківський мають різний ідентифікатор процесу.
  2. Очікуючі сигнали - дитина не успадковує очікувані сигнали батьків. При створенні він буде порожнім для дочірнього процесу.
  3. Блокування пам'яті - дитина не успадковує блокування пам'яті батьків. Блокування пам'яті - це замки, які можна використовувати для блокування області пам'яті, і тоді цю область пам'яті не можна поміняти на диск.
  4. Блокування записів - дитина не успадковує блокування записів батьків. Блокування записів пов’язане з файловим блоком або цілим файлом.
  5. Використання ресурсів процесу та витрата процесора для дитини встановлені на нуль.
  6. Дитина також не успадковує таймери від батьків.

Але як щодо пам’яті дитини? Чи створений новий адресний простір для дитини?

Відповіді у п. Після fork () і батьки, і діти дочіряють спільний адресний простір пам'яті батьків. У Linux ці адресні простори розділені на кілька сторінок. Лише коли дитина пише на одній із батьківських сторінок пам'яті, для дитини створюється дублікат цієї сторінки. Це також відомо як копіювання на запис (копіюйте батьківські сторінки лише тоді, коли дитина пише на нього).

Давайте розберемося з копією на запис на прикладі.

int x = 2;
pid = fork();
if(pid == 0) {
    x = 10;
    // child is changing the value of x or writing to a page
    // One of the parent stack page will contain this local               variable. That page will be duplicated for child and it will store the value 10 in x in duplicated page.  
}
else {
    x = 4;
}

Але чому необхідна копія на запис?

Типове створення процесу відбувається за допомогою комбінації fork () - exec (). Давайте спочатку зрозуміємо, що робить exec ().

Група функцій Exec () замінює адресний простір дитини новою програмою. Після виклику exec () всередині дочірньої дитини буде створено окремий адресний простір для дитини, який повністю відрізняється від батьківського.

Якби не було копіювання механізму запису, пов’язаного з fork (), дублікати сторінок були б створені для дитини, і всі дані були б скопійовані на сторінки дитини. Виділення нової пам'яті та копіювання даних є дуже дорогим процесом (вимагає часу процесора та інших системних ресурсів). Ми також знаємо, що в більшості випадків дитина збирається викликати exec (), і це замінить пам'ять дитини новою програмою. Отже, перша копія, яку ми зробили, була б марною, якби там не було копії на запис.

pid = fork();
if(pid == 0) {
    execlp("/bin/ls","ls",NULL);
    printf("will this line be printed"); // Think about it
    // A new memory space will be created for the child and that   memory will contain the "/bin/ls" program(text section), it's stack, data section and heap section
else {
    wait(NULL);
    // parent is waiting for the child. Once child terminates, parent will get its exit status and can then continue
}
return 1; // Both child and parent will exit with status code 1.

Чому батьки чекають на процес дитини?

  1. Батьки можуть призначити завдання своїй дитині і почекати, поки воно виконає своє завдання. Тоді це може виконувати якусь іншу роботу.
  2. Після завершення дочірнього звільнення звільняються всі ресурси, пов’язані з дочірніми, за винятком блоку управління процесом. Зараз дитина перебуває в зомбі-стані. Використовуючи функцію wait (), батько може запитати про статус дитини, а потім попросити ядро ​​звільнити друковану плату. Якщо батько не використовує очікування, дитина залишатиметься в зомбі-стані.

Чому необхідний системний виклик exec ()?

Не обов’язково використовувати exec () з fork (). Якщо код, який буде виконувати дочірній файл, знаходиться в програмі, пов'язаній з батьківським, exec () не потрібен.

Але згадайте випадки, коли дитині доводиться запускати кілька програм. Візьмемо приклад програми оболонки. Він підтримує декілька команд, таких як find, mv, cp, date і т. Д. Чи правильно буде включати програмний код, пов'язаний з цими командами, в одну програму або дочірній завантажувати ці програми в пам'ять, коли це потрібно?

Все залежить від вашого випадку використання. У вас є веб-сервер, який вводить х, який повертає клієнтам 2 ^ х. Для кожного запиту веб-сервер створює нову дочірню систему та просить її обчислити. Чи будете ви писати окрему програму для обчислення цього та використовувати exec ()? Або ви просто напишете код обчислення всередині батьківської програми?

Зазвичай створення процесу передбачає поєднання викликів fork (), exec (), wait () та exit ().


4

Ці exec(3,3p)функції замінюють поточний процес з іншим. Тобто поточний процес зупиняється , а замість нього запускається інший, беручи на себе частину ресурсів, які мала оригінальна програма.


6
Не зовсім. Він замінює поточний образ процесу новим процесом. Процес - це той самий процес з тим самим pid, тим самим середовищем і тією ж таблицею дескрипторів файлів. Змінилася вся віртуальна пам’ять і стан процесора.
JeremyP

@JeremyP "Тут був важливий той самий дескриптор файлу", він пояснює, як працює переспрямування в оболонках! Я був здивований, як може працювати переспрямування, якщо exec перезапише все! Дякуємо
FUD
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.