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