Основными элементами этой и нескольких следующих глав являются системные вызовы fork(), exit(), wait() и execve(). Каждый из них имеет свои вариации, которые мы тоже не обойдем стороной. Пока что ознакомимся с кратким описанием этих четырех вызовов и узнаем, как они обычно используются в связке друг с другом.

• Системный вызов fork() позволяет одному процессу, родителю, создавать новый, дочерний процесс. Оба этих процесса являются (почти) идентичными: потомок получает копии родительского стека, данных, кучи, копии родительских сегментов стека Х(см. раздел 6.3) и текста. Термин fork («вилка», «разветвление») стали применять потому, что родительский процесс как бы делится на две копии самого себя.

• Библиотечная функция exit(status) завершает процесс, делая все его ресурсы (память, дескрипторы открытых файлов и т. д.) доступными для последующего перераспределения ядром. Аргумент status — целое число, которое определяет код завершения процесса. Родительский процесс может извлечь этот код с помощью системного вызова wait().

Библиотечная функция exit() является оберткой вокруг системного вызова _exit(). В главе 25 вы узнаете разницу между этими двумя интерфейсами. А пока достаточно помнить о том, что обычно с помощью вызова exit() завершают работу только одного родителя или потомка, порожденного вызовом fork(); остальные процессы следует завершать с помощью вызова _exit().

• Системный вызов wait(&status) имеет два назначения. Во-первых, если работа потомка текущего процесса еще не была завершена путем вызова exit(), функция wait() приостанавливает выполнение родителя, пока не будет завершен один из его потомков. Во-вторых, код завершения потомка возвращается через аргумент функции wait().

• Системный вызов execve(pathname, argv, envp) загружает в память процесса новую программу (расположенную в pathname, с аргументами argv и списком переменных среды envp). Текст существующей программы сбрасывается, а для новой программы заново создаются сегменты со стеком, данными и кучей. Эту операцию часто называют выполнением новой программы. Позже вы познакомитесь с несколькими функциями, которые являются обертками вокруг execve(), каждая из которых предоставляет полезную разновидность этого программного интерфейса. В случаях, когда эти разновидности не имеют принципиального значения, мы будем ссылаться на них с помощью обобщенного названия exec(), но имейте в виду, что системных вызовов или библиотечных функций с таким именем не существует.

В некоторых других операционных системах возможности функций fork() и exec() объединены в одну операцию — порождение (spawn); она создает новый процесс, который выполняет заданную программу. Однако подход, используемый в системах UNIX, обычно является более простым и изящным. Разделение этих двух шагов упрощает программные интерфейсы (системный вызов fork() не принимает аргументы) и делает программу более гибкой, позволяя ей выполнять определенные действия между этими двумя этапами. Кроме того, вызов fork() часто имеет смысл делать без последующего вызова exec().

Стандарт SUSv3 предусматривает дополнительную функцию posix_spawn(), которая объединяет возможности fork() и exec(). В системе Linux она и еще несколько связанных программных интерфейсов, описанных в стандарте SUSv3, реализована в библиотеке glibc. Функция posix_spawn() позволяет создавать переносимые приложения с поддержкой аппаратных архитектур, которые не предоставляют механизм файла подкачки или блоки управления памятью (что характерно для многих встраиваемых систем). В рамках таких архитектур реализация традиционного вызова fork() является либо затруднительной, либо невозможной в принципе.

На рис. 24.1 показано, как вызовы fork(), exit(), wait() и execve() обычно используются вместе (эта диаграмма изображает пошаговые действия командной оболочки, выполняющей команду: создается непрерывный цикл, в котором оболочка считывает команду, обрабатывает ее различными способами, после чего создает дочерний процесс для ее выполнения).

Применение вызова execve(), показанное на этой диаграмме, не является обязательным. Иногда имеет смысл позволить потомку продолжить выполнение программы родителя. В любом случае выполнение дочернего процесса в конечном счете завершается вызовом exit() (или передачей сигнала) и возвращением кода завершения, доступным родителю через функцию wait(). Вызов wait() тоже необязателен. Родитель может просто игнорировать своего потомка и продолжать работу. Однако позже мы увидим, что использование функции wait() обычно является желательным и выполняется внутри обработчика сигнала SIGCHLD, который генерируется ядром для родителя, когда один из его дочерних процессов завершается (по умолчанию сигнал SIGCHLD игнорируется, поэтому в диаграмме сказано, что его доставка является опциональной).

24.2. Создание нового процесса: fork()
Перейти на страницу:

Похожие книги