Пожалуйста, объясните функцию exec () и ее семейство


98

Какова exec()функция и ее семейство? Почему используется эта функция и как она работает?

Пожалуйста, объясните эти функции.


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

Ответы:


245

Проще говоря, в UNIX у вас есть концепция процессов и программ. Процесс - это среда, в которой выполняется программа.

Простая идея «модели выполнения» UNIX заключается в том, что вы можете выполнять две операции.

Первый - to fork(), который создает новый процесс, содержащий копию (в основном) текущей программы, включая ее состояние. Между этими двумя процессами есть несколько различий, которые позволяют им определить, какой из них является родительским, а какой - дочерним.

Во-вторых exec(), программа в текущем процессе заменяется новой программой.

Из этих двух простых операций можно построить всю модель выполнения UNIX.


Чтобы добавить больше деталей к вышесказанному:

Использование UNIX fork()и exec()демонстрирует его дух, поскольку он обеспечивает очень простой способ запуска новых процессов.

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()которая использует то, что они называют копированием при записи. Это уловка, позволяющая отложить копирование пространства процесса 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
Спасибо за столь подробное объяснение :)
Файзан

2
Спасибо за ссылку на оболочку с программой поиска. Именно то, что мне нужно было понять.
Пользователь

Почему execэта утилита используется для перенаправления ввода-вывода текущего процесса? Каким образом "нулевой" случай, запуск exec без аргумента, был использован для этого соглашения?
Рэй

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

Я понимаю, что вы имеете в виду, если под «естественным продолжением» вы подразумеваете что-то вроде «органического роста». Кажется, что перенаправление было бы добавлено для поддержки функции замены программы, и я вижу, что это поведение сохраняется в вырожденном случае execвызова без программы. Но в этом сценарии это немного странно, поскольку первоначальная полезность перенаправления для новой программы - программы, которая фактически была бы execзапущена - исчезает, и у вас есть полезный артефакт, перенаправляющий текущую программу, которая не execзапускается или не запускается. вверх любым способом - вместо этого.
Рэй

36

Функции в семействе exec () имеют разное поведение:

  • l: аргументы передаются в виде списка строк в main ()
  • v: аргументы передаются как массив строк в main ()
  • p: путь / s для поиска новой запущенной программы
  • e: окружение может быть указано вызывающим

Вы можете смешивать их, поэтому у вас есть:

  • int execl (const char * путь, const char * arg, ...);
  • int execlp (const char * файл, const char * arg, ...);
  • int execle (const char * путь, const char * arg, ..., char * const envp []);
  • int execv (const char * путь, 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 (), он создает дублирующую копию самого себя, и эти дубликаты становятся дочерними для процесса. Fork () реализован с помощью системного вызова clone () в Linux, который дважды возвращается из ядра.

  • Родителю возвращается ненулевое значение (идентификатор процесса дочернего процесса).
  • Дочернему элементу возвращается нулевое значение.
  • В случае, если дочерний элемент не был успешно создан из-за каких-либо проблем, таких как нехватка памяти, в fork () возвращается -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 () не используется внутри дочернего процесса.

Но родительский и дочерний элементы различаются некоторыми атрибутами PCB (блока управления процессом). Эти:

  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 и т. Д. Будет ли правильным включать программный код, связанный с этими командами, в одну программу или же потомок загружать эти программы в память, когда это необходимо?

Все зависит от вашего варианта использования. У вас есть веб-сервер, которому задан вход x, который возвращает клиентам 2 ^ x. Для каждого запроса веб-сервер создает нового дочернего элемента и просит его выполнить вычисления. Вы напишете отдельную программу для расчета этого и будете использовать 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.