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