Операционные системы 2011 (1114689), страница 33
Текст из файла (страница 33)
Во-вторых, если у процесса нет сыновних процессов, то, обращаясь ксистемному вызову wait(), процесс сразу получит соответствующий код ответа (а именно,-1). В-третьих, если у процесса имеются сыновние процессы, но ни один из них незавершился, то при обращении к указанному системному вызову данный отцовскийпроцесс будет блокирован до завершения любого из своих сыновних процессов (т. е., еслипроцесс хочет получить информацию о завершении каждого из своих потомков, ондолжен несколько раз обратиться к вызову wait()).По факту завершения одного из процессов, родительский процесс при обращении ксистемному вызову wait() получает следующую информацию.
В случае успешногозавершения возвращается идентификатор PID завершившегося процесса, или же -1 — вслучае ошибки или прерывания. А через параметр status передается указатель нацелочисленную переменную, в которой система возвращает процессу информацию опричине завершения сыновнего процесса. Данный параметр содержит в старшем байтекод завершения процесса-потомка (пользовательский код завершения процесса),передаваемый в качестве параметра системному вызову _exit(), а в младшем байте —индикатор причины завершения процесса-потомка, устанавливаемый ядром ОС Unix(системный код завершения процесса). Системный код завершения хранит номерсигнала, приход которого в сыновний процесс вызвал его завершение.Необходимо сделать замечание, касающееся системного вызова wait(). Данныйсистемный вызов не всегда отрабатывает на завершении сыновнего процесса. В случаеесли отцовский процесс производит трассировку сыновнего процесса, то посредствомсистемного вызова wait() можно фиксировать факт приостановки сыновнего процесса,причем сыновний процесс после этого может быть продолжен (т.е.
не всегда он должензавершиться, чтобы отцовский процесс получил информацию о сыне). С другой стороны,113имеется возможность изменить режим работы системного вызова wait() таким образом,чтобы отцовский процесс не блокировался в ожидании завершения одного из потомков, асразу получал соответствующий код ответа.И, наконец, отметим, что после передачи информации родительскому процессу остатусе завершения все структуры, связанные с процессом-зомби, освобождаются, изапись о нем удаляется из таблицы процессов.
Таким образом, переход в состояние зомбинеобходим именно для того, чтобы процесс-предок мог получить информацию о судьбесвоего завершившегося потомка, независимо от того, вызвал он wait() до или после егозавершения.Что же происходит с процессом-потомком, если его предок вообще не обращалсяк wait() и/или завершился раньше потомка? Как уже говорилось, при завершении процессаотцом для всех его потомков становится процесс с идентификатором 1.
Он и осуществляетсистемный вызов wait(), тем самым, освобождая все структуры, связанные с потомкамизомби.Часто используется сочетание функций fork() — wait(), если процесс-сынпредназначен для выполнения некоторой программы, вызываемой посредством функцииexec(). Фактически, это предоставляет процессу-родителю возможность контролироватьокончание выполнения процессов-потомков.Рассмотрим пример использования системного вызова wait(). Ниже приводитсятекст программы, которая последовательно запускает программы, имена которых указаныпри вызове.#include<stdio.h>int main(int argc, char **argv){int i;for (i=1; i<argc; i++){int status;if(fork() > 0){/* процесс-предок ожидает сообщенияот процесса-потомка о завершении */wait(&status);printf(“process-father\n”);continue;}execlp(argv[i], argv[i], 0);exit();}}Пусть существуют три исполняемых файла print1, print2, print3, каждый изкоторых только печатает текст first, second, third соответственно, а код вышеприведенного примера находится в исполняемом файле с именем file.
Тогда результатомработы командыfile print1 print2 print3будет:first114process-fathersecondprocess-fatherthirdprocess-fatherРассмотрим еще один пример. В данном примере процесс-предок порождает двапроцесса, каждый из которых запускает команду echo. Далее процесс-предок ждетзавершения своих потомков, после чего продолжает выполнение. В данном случае wait()вызывается в цикле три раза: первые два ожидают завершения процессов-потомков,последний вызов вернет неуспех, ибо ждать более некого.int main(int argc, char **argv){if ((fork()) == 0) /*первый процесс-потомок*/{execl(“/bin/echo”,”echo”,”this is”,”string 1”,0);exit();}if ((fork()) == 0) /*второй процесс-потомок*/{execl(“/bin/echo”,”echo”,”this is”,”string 2”,0);exit();}/*процесс-предок*/printf(“process-father is waiting for children\n”);while(wait() != -1);printf(“all children terminated\n”);exit();}2.2.3 Жизненный цикл процесса.
Состояния процессаКаждая ОС характеризуется жизненным циклом процессов, которые реализуются всистеме. Рассмотрим обобщенную и несколько упрощенную схему жизненного циклапроцессов в ОС Unix (Рис. 87). Можно выделить целую совокупность состояний, вкоторых может находиться процесс.115планирование процессовfork()СозданГотов квыполнениюочередьготовыхпроцессоввнешнее событиеВыполняетсяexit()Зомбиwait()в режиме ядрав пользовательскомрежимепрерываниеБлокированожидает внешнегособытияРис.
87. Жизненный цикл процессов ОС Unix.Начальное состояние — это состояние создания процесса: после обращения ксистемному вызову fork() создается новый процесс в системе (еще раз отметим, что иныхспособов создать процесс в ОС Unix не существует). Из этого состояния процесспрактически сразу попадает в очередь процессов, готовых к выполнению. Из этой очередипо решению планировщика процесс может переходить в состояние выполнения, а затемобратно в состояние готовности к выполнению (например, в случае исчерпания квантавремени обработки на процессоре). Переходы между этими двумя состояниямиосновываются на динамических средствах планирования ОС Unix. Эти средства основанына динамическом приоритете процесса: чем дольше процесс выполняется, тем нижестановится его приоритет, а, с другой стороны, чем дольше процесс находится всостоянии готовности к выполнению, тем выше становится приоритет процесса.В состоянии выполнения процесс может работать как в пользовательском режиме,так и в режиме ядра (супервизора).
В режиме ядра процесс работает при обращении ксистемному вызову, когда ядро ОС выполняет (на своих ресурсах) некоторые действиядля конкретного процесса.Из режима выполнения процесс может перейти как в состояние готовности квыполнению, так и в состояние блокировки. В состоянии блокировки процесс находится,ожидая завершения некоторого действия – например, ожидая завершения взаимодействияс внешним окружением.
Соответственно, при возникновении соответствующего событияпроцесс переходит из состояния блокировки в состояние готовности к выполнению.Посредством обращения к системному вызову _exit() процесс переходит изсостояния выполнения в состояние «зомби» (состояние завершения существованияпроцесса), а после получения отцовским процессом информации о завершении данногопроцесса он завершает свой жизненный цикл.Итак, мы рассмотрели упрощенную модель.
Главным упрощением в ней можносчитать отсутствие свопинга — отсутствие откачки процесса во внешнюю память. Ещёодно упрощение – реальные современные Unix-системы оперируют с нитями, и это тожевносит свои особенности в жизненный цикл процесса.2.2.4 Формирование процессов 0 и 1Все механизмы взаимодействия процессов в ОС Unix унифицированы иосновываются на связке системных вызовов fork-exec.
Абсолютно все процессы в ОС Unixсоздаются по приведенной схеме, но существуют два процесса с номерами 0 и 1, которые116являются исключениями из данного правила. Эти два системных процесса существуют втечение всего времени работы системы.Рассмотрим детально, как формируются данные процессы, но для этогонеобходимо разобраться, что происходит в системе при включении компьютера (т.е. вмомент начальной загрузки Unix). Практически во всех компьютерах имеется областьпамяти, способная постоянно хранить информацию, — т.н. постоянное запоминающееустройство (ПЗУ). В этой области памяти постоянно находится программа, котораяназывается аппаратный загрузчик компьютера. При включении компьютера схемыуправления запускают работу компьютера с адреса точки входа в этот аппаратныйзагрузчик.
Данный загрузчик в общем случае имеет информацию о перечне и приоритетахсистемных устройств компьютера, которые априори могут содержать операционнуюсистему. (Системное устройство – это блок-ориентированное устройство прямого доступа,на котором может размещаться ОС.) Приоритет определяет тот порядок, в которомаппаратный загрузчик по списку осуществляет перебор устройств в поискахпрограммного загрузчика операционных систем. (Например, бывает полезно сделатьfloppy-диск наиболее приоритетным для загрузки ОС, так как в нормальном режимеаппаратный загрузчик этот диск не обнаружит, и загрузка будет осуществляться сжёсткого диска, а в случае «гибели» жёсткого диска мы как раз воспользуемсявозможностью загрузки с floppy-диска.) Аппаратный загрузчик «знает» структурусистемного устройства.
Обычно в нулевом блоке системного устройства находится т.н.программный загрузчик, который может содержать информацию о наличии вразличных разделах системного устройства различных операционных систем. Разделсистемного устройства — это последовательность подряд идущих блоков (выделенная навнешнем запоминающем устройстве), внутри которых используется виртуальнаянумерация этих блоков, т.е. каждый раздел начинается с нулевого блока.
Соответственно,если операционных систем несколько, то программный загрузчик может предложитьпользователю компьютера выбрать, какую систему загружать. После этого программныйзагрузчик обращается к соответствующему разделу данного системного устройства и изнулевого блока выбранного раздела считывает загрузчик конкретной операционнойсистемы, после чего начинает работать программный загрузчик конкретной ОС. Этотзагрузчик, в свою очередь, «знает» структуру раздела, структуру файловой системы инаходит в соответствующей файловой системе файл, который должен быть запущен вкачестве ядра операционной системы.Что касается Unix-систем, то программный загрузчик ОС осуществляет поиск,считывание в память и запуск на исполнение файла /unix, который содержит исполняемыйкод ядра ОС Unix.