Операционные системы. Управление ресурсами


Цикл жизни процесса



4.3. Цикл жизни процесса

Программа, готовая к выполнению, превратится в процесс только тогда, когда ОС создаст для нее блок контекста и запись в системной таблице процессов. ОС существенно различаются по тому признаку, насколько часто они создают новые процессы и сколько процессов могут одновременно существовать в системе.

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

В многозадачных ОС, осуществляющих пакетную обработку, процессов, обслуживающих пользователей, несколько, но число их либо фиксировано и устанавливается при загрузке, либо меняется оператором, и такие изменения происходят весьма редко.

Различные подходы могут применяться в интерактивных системах. Во-первых, в интерактивной системе может копироваться стратегия многозадачной системы с пакетной обработкой: с каждым терминалом связывается единственный процесс-сеанс (session). Таким образом, предельное число процессов в системе ограничивается числом терминалов. Пользователь каждого терминала работает как бы в однозадачной среде (VM). Во-вторых, для преодоления стесненности пользователя в сеансе система может позволять в ходе сеанса порождать дополнительные, фоновые (background) процессы. Такие процессы выполняют программы, не требующие в ходе выполнения взаимодействия с оператором. Фоновые процессы работают параллельно с процессом, поддерживающим интерактивную работу в сеансе. Наконец, в-третьих, система может позволять порождать любые процессы и в любом количестве. Для каждой новой выполняемой программы создается новый процесс, который уничтожается с завершением программы. Процессы могут выполняться как последовательно, так и параллельно, ограничением на количество параллельно выполняемых процессов является объем ресурсов вычислительной системы и ОС, в частности, возможные ограничения на предельный размер таблицы процессов. Подход без ограничений на число процессов иногда называют философией "дешевых" процессов. В таких системах "накладные расходы" на создание процессов минимальны и наблюдается тенденция к предельному упрощению отдельных процессов: каждый отдельный процесс реализует некоторую весьма элементарную функцию, а сложные действия реализуются как комбинации тех или иных элементарных действий.

Процессы в системе могут либо существовать как отдельные не связанные друг с другом единицы, либо образовывать какие-либо структуры. Обычно модель независимого существования процессов используется в тех ОС, которые накладывают ограничения на возможности создания процессов (такая возможность доступна только самой ОС). В тех ОС, которые разрешают процессам в свою очередь порождать новые процессы, применяется иерархическая структура связей между процессами, в которой между процессами существуют отношения "родитель-потомок". Такая структура позволяет установить четкие правила в отношении использования ресурсов и организации управления. В такой структуре все действующие процессы могут в конечном счете являться потомками (более или менее близкими) одного системного процесса.

Передача ресурсов от предка к потомку позволяет родственным процессам легко устанавливать связи друг с другом. Наиболее важными в этом отношении ресурсами являются файлы, каналы и другие средства взаимодействия (см. главу 9). В соответствии с принятыми в конкретной ОС правилами, потомок может получать либо все ресурсы родителя, либо только те, для которых установлен признак "наследуемые" (inheritance), этот признак может быть установлен либо при выделении ресурса, либо - для уже выделенного ресурса - специальным системным вызовом.

Если пользовательскому процессу дана возможность порождать потомков, то в его распоряжении должны быть соответствующие системные вызовы. Конкретные возможные вызовы мы рассмотрим ниже, но сначала остановимся на общей семантике порождающих вызовов.

Процесс-потомок может быть запущен синхронно или асинхронно с родителем. При синхронном запуске процесс-родитель блокируется до завершения процесса-потомка. Естественно, что в этом случае родитель не может влиять на выполнение потомка. При асинхронном запуске родитель и потомок продолжают выполняться параллельно и могут взаимодействовать. Если родителю теперь необходимо дождаться завершения потомка, он выдает системный вызов ожидания. Синхронный запуск не является обязательной возможностью, так как тот же эффект может быть обеспечен парой вызовов: "асинхронный запуск" - "ожидание". Для того, чтобы родитель мог воздействовать на потомка, вызов "запуск" возвращает ему идентификатор (манипулятор) порожденного процесса. Этот идентификатор используется родителем при последующих воздействиях на потомка. Поскольку потомок может в свою очередь создавать новые процессы, то есть, стать во главе целого подсемейства (поддерева) процессов, интерпретация идентификатора процесса в системных вызовах, связанных с воздействиями на него, должна быть четко определена системными соглашениями или дополнительным параметром таких вызовов. Возможные интерпретации идентификатора следующие:

  • только тот процесс, идентификатор которого задан;
  • процесс, идентификатор которого задан, и все его потомки;
  • процесс, идентификатор которого задан, или любой из его потомков.

В конкретных системах могут вводиться некоторые разумные ограничения на число порождаемых процессов, связанные с предельными размерами таблицы процессов и очереди к планировщику. Может ограничиваться, например, число потомков у процесса или число процессов, принадлежащих одному пользователю.

Процесс-потомок при запуске не знает идентификатора своего родителя, но, как правило, может его получить при помощи системного вызова.

Ниже мы приводим набор системных вызовов, обеспечивающих порождение процессов и "родственные отношения" между ними.

Порождение нового процесса и выполнение в нем программы: pid = load(filename); для нового процесса создается новая запись в системной таблице процессов и блок контекста. В блоке контекста формируется описание адресного пространства процесса - например, таблица сегментов. Выполняется формирование адресного пространства - образы некоторых частей адресного пространства (сегментов) процесса (коды и инициализированные статические данные) загружаются из файла, имя которого является параметром вызова, выделяется память для неинициализированных данных. Формирование всех сегментов не обязательно происходит сразу же при создании процесса: во-первых, если ОС придерживается "ленивой" тактики, то формирование памяти может быть отложено до обращения к соответствующему сегменту; во-вторых, в загрузочный модуль могут быть включены характеристики сегментов: предзагружаемый (preload) или загружаемый по вызову (load-on-call). Новому процессу должна быть выделена также и вторичная память - для сохранения образов сегментов/страниц при свопинге. Часть вторичной памяти для процесса уже существует: это образы неизменяемых сегментов процесса в загрузочном модуле. Для более эффективного поиска таких сегментов ОС может включать в блок контекста специальную таблицу, содержащую адреса сегментов в загрузочном модуле. При выделении вторичной памяти для изменяемых сегментов все ОС обычно следуют "ленивой" тактике. Ресурсы процесса-родителя копируются в блок контекста потомка. В вектор состояния нового процесса заносятся значения, выбор которых в регистры процессора приведет к передаче управления на стартовую точку программы. Новый процесс ставится в очередь готовых к выполнению. Вызов load возвращает идентификатор порожденного процесса.

Смена программы процесса: exec (filename);

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

Расщепление процесса: pid = fork(); порождается новый процесс - копия процесса-родителя. При копировании таблицы сегментов родителя в блок контекста потомка принимаются во внимание характеристики сегментов:

  • уникальный - сегмент может принадлежать только одному процессу, в таблицу потомка этот сегмент не копируется;
  • разделяемый - элемент таблицы сегментов потомка совершенно идентичен элементу таблицы родителя, включая базовый адрес в реальной памяти.
  • копируемый - для потомка выделяется новый сегмент в реальной памяти, в него копируется содержимое соответствующего сегмента родителя, и элемент таблицы сегментов потомка содержит базовый адрес нового сегмента.

Для копируемых сегментов часто применяется "ленивая" тактика копирования при записи (copy-on-write). В этом случае выделение сегмента и копирование данных откладывается, родитель и потомок используют один и тот же сегмент в реальной памяти, но в их таблицах этот сегмент помечается недоступным для записи. Совместное использование сегмента для чтения продолжается, пока один из процессов не попытается писать в него. Попытка записи вызовет прерывание-ловушку по нарушению доступа, обработчик этого прерывания обеспечит создание копии сегмента, изменит его базовый адрес в таблице потомка и снимет с сегмента защиту записи.

При выполнении вызова fork копируется также и счетчик команд процесса-родителя. Выполнение потомка, таким образом, начнется с возврата из системного вызова fork, и возникает проблема самоидентификации процесса. Процесс, вернувшийся из системного вызова fork, должен определиться, кто он - родитель или потомок? Семантика вызова fork такова, что он обычно применяется в таком контексте: if ( ( childpid = fork() ) == 0 ) < процесс-потомок >; else < процесс-родитель >;

То есть, вызов возвращает 0 процессу-потомку и идентификатор потомка - процессу-родителю.

В ОС Unix вызов fork является единственным средством создания нового процесса. Поэтому, как правило, ветвь, соответствующая процессу-потомку содержит вызов exec, меняющий программу, выполняемую потомком. Эффект в этом случае буде тот же, что и при выполнении вызова load. Применение именно такой схемы, видимо, объясняется легкостью передачи ресурсов. Ниже мы покажем структуру отношений системных и пользовательских процессов в Unix.

Ожидание завершения потомка: exitcode = waitchild(pid); процесс-родитель блокируется до завершения процесса-потомка, идентификатор которого является параметром вызова (или всего семейства, возглавляемого этом потомком, или любого из членов этого семейства). Вызов может возвращать код завершения процесса-потомка (задается вызовом exit) и идентификатор завершившегося потомка. Разумеется, применение этого вызова имеет смысл только при асинхронном запуске потомка.

Выход из процесса: exit(exitcode); приводит к освобождению занятых процессом ресурсов, в том числе, и ресурса памяти. Ресурсы, запрошенные процессом динамически, требуют явного освобождения процессом (например, процесс должен закрыть все открытые им файлы), но если процесс "забыл" это сделать, это сделает за него ОС при выполнении данного вызова. При выполнении exit также могут выполняться процедуры, заданные вызовами exitlist (см. ниже). Вызов exit не обязательно должен приводить к немедленному полному уничтожению процесса. Может сохраняться соответствующая ему запись в таблице процессов и часть блока контекста, но процесс помечается завершенным. Неполное удаление процесса объясняется тем, что после процесса остается еще некоторая информация, которая может быть востребована, статистические данные о его выполнении, код завершения (параметр вызова exit), который будет прочитан вызовом waitchild в родителе и т.п. Полное удаление процесса произойдет после того, как вся остаточная информация будет обработана.

Формирование списка выхода: exitlist(procaddr); при помощи этого вызова процесс может установить процедуру (адрес такой процедуры - параметр вызова), которая должна быть выполнена при его завершении. Процедуры выхода обычно используются для сохранения параметров программы и аккуратного закрытия каких-либо важных ресурсов при аварийном завершении программы. Процесс может сделать несколько вызовов exitlist, назначив несколько процедур выхода, которые будут выполняться в неопределенном порядке. Процедура выхода может также иметь параметр, через который ей будет передаваться причина завершения: нормальное / по сигналу / по программной ошибке / по аппаратной ошибке.

Принудительное завершение: kill(pid); завершает процесс-потомок или все семейство процессов, им возглавляемое. Выполнение этого вызова заключается в выдаче сигнала kill (механизм сигналов описывается в главе 9), по умолчанию обработка этого сигнала вызывает выполнение exit с установкой специального кода завершения.

Изменить приоритет: setPriority ( pid, priority ); изменяет приоритет потомка или всего его семейства. Приоритет может задаваться как абсолютный, так и в виде приращения (положительного или отрицательного) к текущему приоритету. Как правило, пользовательские процессы не могут изменять свой приоритет в сторону увеличения.

Получение идентификаторов: pid = getpid(mode); вызов возвращает процессу его собственный идентификатор и/или идентификатор процесса-родителя.

На Рисунке 4.2 приведена в качестве примера схема наследования процессов в ОС Unix. Корнем дерева процессов является процесс init, создаваемый при загрузке ОС. Процесс init порождает для каждой линии связи (терминала) при помощи пары системных вызовов fork-exec свой процесс getty и переходит в ожидание. Каждый процесс getty ожидает ввода со своего терминала. При вводе процесс getty сменяется (системный вызов exec) процессом logon, выполняющим проверку пароля. При правильном вводе пароля процесс logon сменяется (exec) процессом shell. Командный интерпретатор shell (подробнее мы рассмотрим его в главе 12) является корнем поддерева для всех процессов пользователя, выполняющихся в данном сеансе. При завершении сеанса shell завершается (exit), при этом "пробуждается" init и порождает для этого терминала новый процесс getty.









Начало  Назад  Вперед