Тема OS14асу. иерархия процессов; Управление иерархией процессов Множество процессов, существующих в каждый момент, образуют единую иерархическую структуру, где они связаны отношениями потомок-предок. Каждый новый процесс может быть образован (порожден) только одним из существующих процессов, который становится его предком. По отношению к предку порожденый процесс будет считаться потомком. Иного способа образования новых процессов и иных отношений между процессами в OS UNIX не предусмотрено. Следует отметить, что любой процесс-предок может иметь более одного потомка. При порождении нового процесса его образ строится путем копирования образа предка в свободное постранство адресов RAM. Конкретно, копируются стек и сегменты данных. Процедурный сегмент копируется, когда он не является разделяемым сегментом, который уже загружен в RAM. Кроме того, потомок наследует контекст предка. В частности, через контекст потомку передаются таблицы файлов и сигналов, а также текущий каталог предка. Таким образом, после порождения потомок является точной копией предка. В этот момент они различаются только личными идентификаторами и идентификаторами предков в таблице процессов. В дальнейшем тождество предка и потомка нарушается за счет индивидуального изменения их сегментов и контекстов. Например, процесс-потомок может распределять дополнительную память, расширяя сегмент данных, изменять содержание сегмента данных путем обработки его переменных, модифицировать таблицу открытых файлов в своем контексте, открывая и закрывая файлы, переопределить обработку сигналов в таблице сигналов своего контекста. Для порождения потомка процесс-предок использует системный вызов fork, который возвращает идентификатор потомка предку и 0 - потомку. Такой алгоритм возврата позволяет легко различать инструкции предка и потомка в тексте программы процесса. При аварийном завершении fork возвращает отрицательный код, например, когда превышено ограничение по общему числу процессов или числу процессов одного пользователя. Следующий фрагмент С-кода демонстрирует применение системного вызова fork для порождения нового процесса. int pid; /* идентификатор процесса-потомка */ /* инструкции процесса-предка до развилки */ ........................................... /* развилка процессов */ switch(pid = fork()) { case -1: /* обработка ошибки порождения процесса */ ......................................... break; case 0: /* инструкции процесса-потомка */ ......................................... break; default: /* инструкции процесса-предка после развилки */ ......................................... break; } /* switch */ При успешном завершении системного вызова fork процессы потомка и предка равноправно существуют в системе. Они могут функционировать параллельно, конкурируя за ресурсы на основе своих приоритетов, или выполнение предка может быть приостановлено до завершения потомка системным вызовом wait. Системный вызов wait возвращает предку идентификатор потомка, который завершился первым после последнего обращения к wait. Если предок имеет несколько потомком, то чтобы узнать о завершении каждого из них, нужно реализовать соответствующее число системных вызовов wait с проверкой их возвратов. Когда процесс не имеет потомков, wait возвращает код (-1). Следующий пример С-кода иллюстрирует применение системного вызова wait при ожидании завершения 2-х потомков. int pid1, pid2; /* идентификаторы процессов-потомков */ int status; /* статус завершения процесса-потомка */ int ret = 0; /* текущий возврат системного вызова wait */ /* инструкции порождения процессов-потомков */ .............................................. /* цикл ожидания звершения потомков в процессе-предке */ while ((ret = wait(&status)) != (-1)) { /* обработка завершения 1-го потомка */ if( ret == pid1) ..................................... /* обработка завершения 2-го потомка */ if( ret == pid2) ..................................... } /* while */ Каждый процесс процесс может завершиться по сигналу, который генерирует другой процесс, ядро или аппаратура, а также по собственной инициативе, используя системный вызов exit. Системный вызов exit освобождает память, динамически распределенную процессом, закрывает открытые процессом файлы и осуществляет очистку буферов системного пула, которые использованны для ввода-вывода в эти файлы. Статус завершения процесса может быть передан exit как целочисленный аргумент и будет известен потомку, если последний выполняет системный вызов wait. Для анализа причины завершения потомка системный вызов wait обеспечивает возврат слова статуса завершения потомка. Адрес слова статуса завершения потомка передается в wait как параметр. Байт слова статуса с маской FF00 содержит пользовательский код завершения потомка, объявленный им через аргумент системного вызова exit. Байт слова статуса с маской 00FF, если он не равен 0, содержит системный код завершения потомка по сигналу. Его младшие 7 бит сохраняют номер сигнала, по которому завершился потомок. Ненулевой старший бит системного кода завершения индицирует образование файла core, содержащего дамп памяти на момент завершения потомка. В OS UNIX приняты определенные соглашения, которые позволяют поддерживать иерархию процессов в нестандартных ситуациях. Две такие ситуации являются наиболее распространеннными. 1) Процесс-предок завершается раньше своих потомков. В этом случае его потомки автоматически становятся потомками процесса инициализации init с идентификатором 1. 2) Процесс-потомок завершается раньше 1-го предполагаемого обращения предка к системному вызову wait. Чтобы такая ситуация не стала тупиковой из-за бесконечного ожидания предком завершения уже несуществующего потомка введено состояние промежуточного завершения или состояние "зомби". В состоянии "зомби" процесс не имеет образа в RAM, но информация о нем сохраняется в таблице процессов для корректной реализации предком системного вызова wait. Дополнительно к рассмотренным средствам управления иерархией процессов, OS UNIX предоставляет возможность замены программы выполнения процесса с помощью системных вызовов execl, execv, execle и execve. При реализации системных вызовов семейства exec образ процесса заменяется сегментами специфицированного файла новой программы его выполнения. При этом новый процесс не создается, управление новому образу передается в рамках существующего процесса с тем же идентификатором. При этом контекст процесса сохраняется без изменений, исключая таблицу сигналов, в которой все установки нестандартной обработки сигналов будут игнорированы. При корректном завершении любого системного вызова семейства exec возврат в старый образ невозможен. Поэтому обычно они применяются в сочетании с порождением промежуточного потомка системным вызовом fork, исключительно для последующей замены программы его выполнения. Комбинация fork-exec позволяет продолжить старую программу выполнения предка после завершения потомка по новой программе, заданной системным вызовом семейства exec. Следующий фрагмент С-кода демонстрирует наиболее распространенную схему применения системного вызова execl из семейства exec для замены текущей программы выполнение процесса на программу обработки команды ls в длинном формате вывода (с ключем -l). int pid; /* идентификатор процесса-потомка */ int status = 0; /* статус завершения процесса-потомка */ /* развилка процессов */ switch(pid = fork()) { case -1: /* ошибка порождения процесса-потомка */ break; case 0: /* замена программы выполнения потомка */ execl("/bin/ls", "ls", "-l", (char *) 0); exit(1); default: /* ожидание предком завершения потомка */ while(wait(&status) != pid); break; } /* switch */ Рассмотренные средства управления иерархией процессов используются при инициализации OS UNIX и для интерпретации команд в сеансах работы пользователей в монопольном или многопользовательском режимах.