Операционная система UNIX - Андрей Робачевский
Шрифт:
Интервал:
Закладка:
В общем случае вызов fork(2) выполняет следующие действия:
□ Резервирует место в области свопинга для сегмента данных и стека процесса.
□ Размещает новую запись proc в таблице процессов и присваивает процессу уникальный идентификатор PID.
□ Инициализирует структуру proc (поля структуры proc подробно рассматривались в разделе "Структуры данных процесса").
□ Размещает карты отображения, необходимые для трансляции адреса.
□ Размещает u-area процесса и копирует ее содержимое с родительского.
□ Создает соответствующие области процесса, часть из которых совпадает с родительскими.
□ Инициализирует аппаратный контекст процесса, копируя его с родительского.
□ Устанавливает в ноль возвращаемое дочернему процессу вызовом fork(2) значение.
□ Устанавливает возвращаемое родительскому процессу вызовом fork(2) значение равным PID потомка.
□ Помечает процесс готовым к запуску и помещает его в очередь на выполнение.
Системный вызов fork(2) в итоге создает для дочернего процесса отдельную копию адресного пространства родителя. Во многих случаях, вскоре после этого, дочерний процесс делает системный вызов exec(2) для запуска новой программы, при этом существующее адресное пространство уничтожается и создается новое. Таким образом создание фактической копии адресного пространства процесса, т.е. выделение оперативной памяти и создание соответствующих карт отображения, является неоправданным.
Для решения данной проблемы используются два подхода. Первый из них, предложенный в UNIX System V, называется "копирование при записи" (copy-on-write или COW). Суть этого подхода заключается в том, что сегменты данных и стека родительского процесса помечаются доступными только для чтения, а дочерний процесс, хотя и получает собственные карты отображения, разделяет эти сегменты с родительским. Другими словами, сразу после создания процесса и родитель и потомок адресуют одни и те же страницы физической памяти. Если какой-либо из двух процессов попытается модифицировать данные или стек, возникнет страничная ошибка, поскольку страница открыта только для чтения, а не для записи. При этом будет запущен обработчик ошибки ядра, который создаст для процесса копию этой страницы, доступную для записи. Таким образом, фактическому копированию подлежат только модифицируемые страницы, а не все адресное пространство процесса. Если дочерний процесс делает системный вызов exec(2) или вообще завершает свое выполнение, права доступа к страницам родителя, имеющим флаг COW, возвращаются к их прежним значениям (т.е. до создания дочернего процесса), а флаг COW очищается.
Другой подход используется в BSD UNIX. В этой версии системы был предложен новый системный вызов — vfork(2). Использование этого вызова имеет смысл, когда дочерний процесс сразу же выполняет вызов exec(2) и запускает новую программу. При вызове vfork(2) родительский процесс предоставляет свое адресное пространство дочернему и переходит в состояние сна, пока последний не вернет его обратно. Далее дочерний процесс выполняется в адресном пространстве родителя, пока не делает вызов exec(2) или exit(2), после чего ядро возвращает адресное пространство родителю и пробуждает его. С помощью vfork(2) можно добиться максимального быстродействия, т.к. в этом случае мы полностью избегаем копирования, даже для карт отображения. Вместо этого адресное пространство родительского процесса предоставляется потомку передачей нескольких аппаратных регистров, отвечающих за трансляцию адресов. Однако vfork(2) таит в себе потенциальную опасность, поскольку позволяет одному процессу использовать и даже модифицировать адресное пространство другого.
Для управления памятью процесса ядру необходимо соответствующим образом задать области. При этом структуры pregion дочернего процесса, соответствующие разделяемым областям, указывают на те же структуры region, что и для родителя. Для областей, совместное использование которых недопустимо, ядро размещает отдельные структуры region для дочернего процесса (изначально копируя их содержимое с родительского) и устанавливает соответствующие указатели. На рис. 3.15 представлена схема этих операций. Заметим, что совместная работа и дублирование областей являются отдельным механизмом, не связанным с рассмотренными выше подходами, для совместного использования адресного пространства, например COW. Так, после создания отдельной копии неразделяемой области она по-прежнему будет адресовать те же страницы памяти, что и соответствующая область родителя.
Рис. 3.15. Создание областей нового процесса
Запуск новой программы
Запуск новой программы осуществляется с помощью системного вызова exec(2). Напомним, что при этом создается не новый процесс, а новое адресное пространство процесса, которое загружается содержимым новой программы. Если процесс был создан вызовом vfork(2), старое адресное пространство возвращается родителю, в противном случае оно просто уничтожается. После возврата из вызова exec(2) процесс продолжает выполнение кода новой программы.
Операционная система UNIX обычно поддерживает несколько форматов исполняемых файлов. Старейший из них — a.out, в разделе "Форматы исполняемых файлов" главы 2 также были рассмотрены форматы COFF и ELF. В любом случае исполняемый файл содержит заголовок, позволяющий ядру правильно разместить адресное пространство процесса и загрузить в него соответствующие фрагменты исполняемого файла.
Перечислим ряд действий, которые выполняет exec(2) для запуска новой программы:
□ Производит трансляцию имени файла. В результате возвращается индексный дескриптор, с помощью которого осуществляется доступ к файлу. При этом проверяются права доступа.
□ Считывает заголовок файла и проверяет, является ли файл исполняемым. Вызов exec(2) также распознает скрипты, о которых говорилось в главе 1. При этом он анализирует первую строку скрипта, которая обычно имеет вид #!shellname. В этом случае exec(2) запускает программу, указанную shellname, передавая ей в качестве аргумента имя скрипта. Если исполняемый файл (т.е. файл с установленным атрибутом x) не является бинарным и не содержит в первой строке названия интерпретатора, exec(2) запускает интерпретатор по умолчанию (/bin/sh, /usr/bin/sh, или /usr/bin/ksh, как предписывает стандарт XPG4), передавая ему содержимое файла в качестве ввода.
□ Если исполняемый файл имеет атрибуты SUID или SGID, exec(2) соответствующим образом изменяет эффективные идентификаторы UID и GID для этого процесса.[40]
□ Сохраняет аргументы вызова exec(2) и переменные окружения в адресном пространстве ядра, поскольку адресное пространство процесса будет уничтожено.
□ Резервирует место в области свопинга для сегмента данных и стека.
□ Освобождает старые области процесса и соответствующие области свопинга. Если процесс был создан вызовом vfork(2), старое адресное пространство возвращается родителю.
□ Размещает и инициализирует карты отображения для новых сегментов кода, данных и стека. Если сегмент кода является активным, например, какой-либо процесс уже выполняет эту программу, данная область используется совместно. В противном случае область заполняется содержимым соответствующего раздела исполняемого файла или инициализируется нулями для неинициализированных данных. Поскольку управление памятью процесса построено на механизме страничного замещения по требованию, копирование происходит постранично и только тогда, когда процесс обращается к страницам, отсутствующим в памяти.
□ Копирует сохраненные аргументы и переменные окружения в новый стек процесса.
□ Устанавливает обработку всех сигналов на умалчиваемые значения, поскольку процесс теперь не имеет требуемых обработчиков. Установки для игнорируемых и заблокированных сигналов не изменяются.
□ Инициализирует аппаратный контекст процесса. В частности, после этого указатель инструкций адресует точку входа новой программы.
В случае, когда программа использует динамические библиотеки, соответствующий раздел исполняемого файла (для файла формата ELF данный раздел имеет тип INTERP) содержит имя редактора связей динамической библиотеки. В этом случае редактор связей должен быть запущен до начала выполнения основной программы для связывания с программами требуемых динамических библиотек. Таким образом точка входа в программу устанавливается на точку входа в редактор связей. После завершения своей работы редактор связей, в свою очередь, запускает программу самостоятельно, анализируя заголовок исполняемого файла. Стадии запуска новой программы проиллюстрированы на рис. 3.16.